diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index e68e85b40d..2ecd4ac3bd 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2255,6 +2255,7 @@ dependencies = [ "chrono", "clap", "codex-ansi-escape", + "codex-app-server", "codex-app-server-protocol", "codex-arg0", "codex-backend-client", diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index cee6e6d04b..da23be1696 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -338,6 +338,9 @@ } ] }, + "CustomPromptListParams": { + "type": "object" + }, "DynamicToolSpec": { "properties": { "description": { @@ -563,6 +566,40 @@ ], "type": "string" }, + "HistoryAppendParams": { + "properties": { + "text": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "text", + "threadId" + ], + "type": "object" + }, + "HistoryEntryReadParams": { + "properties": { + "logId": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + }, + "offset": { + "format": "uint", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "logId", + "offset" + ], + "type": "object" + }, "InitializeCapabilities": { "description": "Client-declared capabilities negotiated during initialize.", "properties": { @@ -791,6 +828,9 @@ ], "type": "object" }, + "McpToolsListParams": { + "type": "object" + }, "MergeStrategy": { "enum": [ "replace", @@ -1808,6 +1848,107 @@ ], "type": "object" }, + "ThreadConfigReloadParams": { + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "type": "object" + }, + "ThreadContextUpdateParams": { + "properties": { + "approvalPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/AskForApproval" + }, + { + "type": "null" + } + ] + }, + "cwd": { + "type": [ + "string", + "null" + ] + }, + "effort": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ] + }, + "model": { + "type": [ + "string", + "null" + ] + }, + "personality": { + "anyOf": [ + { + "$ref": "#/definitions/Personality" + }, + { + "type": "null" + } + ] + }, + "sandboxPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/SandboxPolicy" + }, + { + "type": "null" + } + ] + }, + "summary": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningSummary" + }, + { + "type": "null" + } + ] + }, + "threadId": { + "type": "string" + }, + "windowsSandboxLevel": { + "anyOf": [ + { + "$ref": "#/definitions/WindowsSandboxLevel" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "threadId" + ], + "type": "object" + }, "ThreadForkParams": { "description": "There are two ways to fork a thread: 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. 2. By path: load the thread from disk by path and fork it into a new thread.\n\nIf using path, the thread_id param will be ignored.\n\nPrefer using thread_id whenever possible.", "properties": { @@ -1965,6 +2106,28 @@ }, "type": "object" }, + "ThreadMemoriesDropParams": { + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "type": "object" + }, + "ThreadMemoriesUpdateParams": { + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "type": "object" + }, "ThreadReadParams": { "properties": { "includeTurns": { @@ -2094,6 +2257,17 @@ ], "type": "object" }, + "ThreadShutdownParams": { + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "type": "object" + }, "ThreadSortKey": { "enum": [ "created_at", @@ -2205,6 +2379,32 @@ ], "type": "object" }, + "ThreadUndoParams": { + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "type": "object" + }, + "ThreadUserShellCommandRunParams": { + "properties": { + "command": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "command", + "threadId" + ], + "type": "object" + }, "TurnInterruptParams": { "properties": { "threadId": { @@ -2554,6 +2754,14 @@ } ] }, + "WindowsSandboxLevel": { + "enum": [ + "disabled", + "restricted-token", + "elevated" + ], + "type": "string" + }, "WindowsSandboxSetupMode": { "enum": [ "elevated", @@ -2792,6 +3000,174 @@ "title": "Thread/rollbackRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/context/update" + ], + "title": "Thread/context/updateRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadContextUpdateParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/context/updateRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/shutdown" + ], + "title": "Thread/shutdownRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadShutdownParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/shutdownRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/undo" + ], + "title": "Thread/undoRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadUndoParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/undoRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/config/reload" + ], + "title": "Thread/config/reloadRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadConfigReloadParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/config/reloadRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/memories/drop" + ], + "title": "Thread/memories/dropRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadMemoriesDropParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/memories/dropRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/memories/update" + ], + "title": "Thread/memories/updateRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadMemoriesUpdateParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/memories/updateRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/userShellCommand/run" + ], + "title": "Thread/userShellCommand/runRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadUserShellCommandRunParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/userShellCommand/runRequest", + "type": "object" + }, { "properties": { "id": { @@ -2864,6 +3240,102 @@ "title": "Thread/readRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "customPrompt/list" + ], + "title": "CustomPrompt/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/CustomPromptListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "CustomPrompt/listRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "mcpTools/list" + ], + "title": "McpTools/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/McpToolsListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "McpTools/listRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "history/append" + ], + "title": "History/appendRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/HistoryAppendParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "History/appendRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "history/entry/read" + ], + "title": "History/entry/readRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/HistoryEntryReadParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "History/entry/readRequest", + "type": "object" + }, { "properties": { "id": { diff --git a/codex-rs/app-server-protocol/schema/json/McpElicitationRequestParams.json b/codex-rs/app-server-protocol/schema/json/McpElicitationRequestParams.json new file mode 100644 index 0000000000..046a36c5ba --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/McpElicitationRequestParams.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "RequestId": { + "anyOf": [ + { + "type": "string" + }, + { + "format": "int64", + "type": "integer" + } + ] + } + }, + "properties": { + "message": { + "type": "string" + }, + "requestId": { + "$ref": "#/definitions/RequestId" + }, + "serverName": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "message", + "requestId", + "serverName", + "threadId" + ], + "title": "McpElicitationRequestParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/McpElicitationRequestResponse.json b/codex-rs/app-server-protocol/schema/json/McpElicitationRequestResponse.json new file mode 100644 index 0000000000..19d9e72a02 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/McpElicitationRequestResponse.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "McpElicitationDecision": { + "enum": [ + "accept", + "decline", + "cancel" + ], + "type": "string" + } + }, + "properties": { + "decision": { + "$ref": "#/definitions/McpElicitationDecision" + } + }, + "required": [ + "decision" + ], + "title": "McpElicitationRequestResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index b2dc4c3c3e..07688a9d66 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -364,6 +364,25 @@ } ] }, + "BackgroundEventNotification": { + "properties": { + "message": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "message", + "threadId", + "turnId" + ], + "type": "object" + }, "ByteRange": { "properties": { "end": { @@ -995,6 +1014,139 @@ ], "type": "object" }, + "McpServerStartupCompletedNotification": { + "properties": { + "cancelled": { + "items": { + "type": "string" + }, + "type": "array" + }, + "failed": { + "items": { + "$ref": "#/definitions/McpServerStartupFailure" + }, + "type": "array" + }, + "ready": { + "items": { + "type": "string" + }, + "type": "array" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "cancelled", + "failed", + "ready", + "threadId" + ], + "type": "object" + }, + "McpServerStartupFailure": { + "properties": { + "error": { + "type": "string" + }, + "server": { + "type": "string" + } + }, + "required": [ + "error", + "server" + ], + "type": "object" + }, + "McpServerStartupStatus": { + "oneOf": [ + { + "properties": { + "state": { + "enum": [ + "starting" + ], + "type": "string" + } + }, + "required": [ + "state" + ], + "title": "StartingMcpServerStartupStatus", + "type": "object" + }, + { + "properties": { + "state": { + "enum": [ + "ready" + ], + "type": "string" + } + }, + "required": [ + "state" + ], + "title": "ReadyMcpServerStartupStatus", + "type": "object" + }, + { + "properties": { + "error": { + "type": "string" + }, + "state": { + "enum": [ + "failed" + ], + "type": "string" + } + }, + "required": [ + "error", + "state" + ], + "type": "object" + }, + { + "properties": { + "state": { + "enum": [ + "cancelled" + ], + "type": "string" + } + }, + "required": [ + "state" + ], + "title": "CancelledMcpServerStartupStatus", + "type": "object" + } + ] + }, + "McpServerStartupUpdatedNotification": { + "properties": { + "server": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/McpServerStartupStatus" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "server", + "status", + "threadId" + ], + "type": "object" + }, "McpToolCallError": { "properties": { "message": { @@ -1397,6 +1549,36 @@ } ] }, + "SkillsUpdatedNotification": { + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "type": "object" + }, + "StreamErrorNotification": { + "properties": { + "error": { + "$ref": "#/definitions/TurnError" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "error", + "threadId", + "turnId" + ], + "type": "object" + }, "SubAgentSource": { "oneOf": [ { @@ -2177,6 +2359,17 @@ ], "type": "object" }, + "ThreadShutdownCompletedNotification": { + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "type": "object" + }, "ThreadStartedNotification": { "properties": { "thread": { @@ -2330,6 +2523,52 @@ ], "type": "object" }, + "ThreadUndoCompletedNotification": { + "properties": { + "message": { + "type": [ + "string", + "null" + ] + }, + "success": { + "type": "boolean" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "success", + "threadId", + "turnId" + ], + "type": "object" + }, + "ThreadUndoStartedNotification": { + "properties": { + "message": { + "type": [ + "string", + "null" + ] + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "threadId", + "turnId" + ], + "type": "object" + }, "TokenUsageBreakdown": { "properties": { "cachedInputTokens": { @@ -2654,6 +2893,25 @@ } ] }, + "WarningNotification": { + "properties": { + "message": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "message", + "threadId", + "turnId" + ], + "type": "object" + }, "WebSearchAction": { "oneOf": [ { @@ -2831,6 +3089,26 @@ "title": "ErrorNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "warning" + ], + "title": "WarningNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/WarningNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "WarningNotification", + "type": "object" + }, { "properties": { "method": { @@ -2951,6 +3229,66 @@ "title": "Thread/tokenUsage/updatedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "thread/shutdown/completed" + ], + "title": "Thread/shutdown/completedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadShutdownCompletedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/shutdown/completedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/undo/started" + ], + "title": "Thread/undo/startedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadUndoStartedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/undo/startedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/undo/completed" + ], + "title": "Thread/undo/completedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/ThreadUndoCompletedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/undo/completedNotification", + "type": "object" + }, { "properties": { "method": { @@ -2991,6 +3329,26 @@ "title": "Turn/completedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "turn/streamError" + ], + "title": "Turn/streamErrorNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/StreamErrorNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Turn/streamErrorNotification", + "type": "object" + }, { "properties": { "method": { @@ -3192,6 +3550,46 @@ "title": "Item/mcpToolCall/progressNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "mcpServer/startup/updated" + ], + "title": "McpServer/startup/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/McpServerStartupUpdatedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "McpServer/startup/updatedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "mcpServer/startup/completed" + ], + "title": "McpServer/startup/completedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/McpServerStartupCompletedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "McpServer/startup/completedNotification", + "type": "object" + }, { "properties": { "method": { @@ -3212,6 +3610,26 @@ "title": "McpServer/oauthLogin/completedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "skills/updated" + ], + "title": "Skills/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/SkillsUpdatedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Skills/updatedNotification", + "type": "object" + }, { "properties": { "method": { @@ -3393,6 +3811,26 @@ "title": "DeprecationNoticeNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "backgroundEvent" + ], + "title": "BackgroundEventNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/BackgroundEventNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "BackgroundEventNotification", + "type": "object" + }, { "properties": { "method": { diff --git a/codex-rs/app-server-protocol/schema/json/ServerRequest.json b/codex-rs/app-server-protocol/schema/json/ServerRequest.json index 553f189dc6..64c3374359 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ServerRequest.json @@ -430,6 +430,29 @@ ], "type": "object" }, + "McpElicitationRequestParams": { + "properties": { + "message": { + "type": "string" + }, + "requestId": { + "$ref": "#/definitions/RequestId" + }, + "serverName": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "message", + "requestId", + "serverName", + "threadId" + ], + "type": "object" + }, "NetworkApprovalContext": { "properties": { "host": { @@ -737,6 +760,31 @@ "title": "Item/tool/requestUserInputRequest", "type": "object" }, + { + "description": "Request the user to resolve an MCP elicitation prompt.", + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "item/mcpElicitation/requestDecision" + ], + "title": "Item/mcpElicitation/requestDecisionRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/McpElicitationRequestParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Item/mcpElicitation/requestDecisionRequest", + "type": "object" + }, { "description": "Execute a dynamic tool call on the client.", "properties": { 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 d478bb2c78..ad20d9aa47 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 @@ -480,6 +480,174 @@ "title": "Thread/rollbackRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/context/update" + ], + "title": "Thread/context/updateRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ThreadContextUpdateParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/context/updateRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/shutdown" + ], + "title": "Thread/shutdownRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ThreadShutdownParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/shutdownRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/undo" + ], + "title": "Thread/undoRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ThreadUndoParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/undoRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/config/reload" + ], + "title": "Thread/config/reloadRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ThreadConfigReloadParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/config/reloadRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/memories/drop" + ], + "title": "Thread/memories/dropRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ThreadMemoriesDropParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/memories/dropRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/memories/update" + ], + "title": "Thread/memories/updateRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ThreadMemoriesUpdateParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/memories/updateRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "thread/userShellCommand/run" + ], + "title": "Thread/userShellCommand/runRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ThreadUserShellCommandRunParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Thread/userShellCommand/runRequest", + "type": "object" + }, { "properties": { "id": { @@ -552,6 +720,102 @@ "title": "Thread/readRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "customPrompt/list" + ], + "title": "CustomPrompt/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/CustomPromptListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "CustomPrompt/listRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "mcpTools/list" + ], + "title": "McpTools/listRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/McpToolsListParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "McpTools/listRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "history/append" + ], + "title": "History/appendRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/HistoryAppendParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "History/appendRequest", + "type": "object" + }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "history/entry/read" + ], + "title": "History/entry/readRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/HistoryEntryReadParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "History/entry/readRequest", + "type": "object" + }, { "properties": { "id": { @@ -1419,37 +1683,6 @@ "title": "CommandExecutionRequestApprovalResponse", "type": "object" }, - "CustomPrompt": { - "properties": { - "argument_hint": { - "type": [ - "string", - "null" - ] - }, - "content": { - "type": "string" - }, - "description": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": "string" - }, - "path": { - "type": "string" - } - }, - "required": [ - "content", - "name", - "path" - ], - "type": "object" - }, "Duration": { "properties": { "nanos": { @@ -2121,7 +2354,7 @@ "network_proxy": { "anyOf": [ { - "$ref": "#/definitions/SessionNetworkProxyRuntime" + "$ref": "#/definitions/v2/SessionNetworkProxyRuntime" }, { "type": "null" @@ -3153,7 +3386,7 @@ "entry": { "anyOf": [ { - "$ref": "#/definitions/HistoryEntry" + "$ref": "#/definitions/v2/HistoryEntry" }, { "type": "null" @@ -3247,7 +3480,7 @@ "properties": { "custom_prompts": { "items": { - "$ref": "#/definitions/CustomPrompt" + "$ref": "#/definitions/v2/CustomPrompt" }, "type": "array" }, @@ -4588,27 +4821,6 @@ "title": "FuzzyFileSearchSessionUpdatedNotification", "type": "object" }, - "HistoryEntry": { - "properties": { - "conversation_id": { - "type": "string" - }, - "text": { - "type": "string" - }, - "ts": { - "format": "uint64", - "minimum": 0.0, - "type": "integer" - } - }, - "required": [ - "conversation_id", - "text", - "ts" - ], - "type": "object" - }, "InitializeCapabilities": { "description": "Client-declared capabilities negotiated during initialize.", "properties": { @@ -4772,6 +4984,52 @@ "title": "JSONRPCResponse", "type": "object" }, + "McpElicitationDecision": { + "enum": [ + "accept", + "decline", + "cancel" + ], + "type": "string" + }, + "McpElicitationRequestParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "message": { + "type": "string" + }, + "requestId": { + "$ref": "#/definitions/RequestId" + }, + "serverName": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "message", + "requestId", + "serverName", + "threadId" + ], + "title": "McpElicitationRequestParams", + "type": "object" + }, + "McpElicitationRequestResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "decision": { + "$ref": "#/definitions/McpElicitationDecision" + } + }, + "required": [ + "decision" + ], + "title": "McpElicitationRequestResponse", + "type": "object" + }, "McpInvocation": { "properties": { "arguments": { @@ -5170,8 +5428,7 @@ "format": "int64", "type": "integer" } - ], - "description": "ID of a request, which can be either a string or an integer." + ] }, "RequestUserInputQuestion": { "properties": { @@ -5429,6 +5686,26 @@ "title": "ErrorNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "warning" + ], + "title": "WarningNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/WarningNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "WarningNotification", + "type": "object" + }, { "properties": { "method": { @@ -5549,6 +5826,66 @@ "title": "Thread/tokenUsage/updatedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "thread/shutdown/completed" + ], + "title": "Thread/shutdown/completedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ThreadShutdownCompletedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/shutdown/completedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/undo/started" + ], + "title": "Thread/undo/startedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ThreadUndoStartedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/undo/startedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "thread/undo/completed" + ], + "title": "Thread/undo/completedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/ThreadUndoCompletedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Thread/undo/completedNotification", + "type": "object" + }, { "properties": { "method": { @@ -5589,6 +5926,26 @@ "title": "Turn/completedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "turn/streamError" + ], + "title": "Turn/streamErrorNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/StreamErrorNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Turn/streamErrorNotification", + "type": "object" + }, { "properties": { "method": { @@ -5790,6 +6147,46 @@ "title": "Item/mcpToolCall/progressNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "mcpServer/startup/updated" + ], + "title": "McpServer/startup/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/McpServerStartupUpdatedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "McpServer/startup/updatedNotification", + "type": "object" + }, + { + "properties": { + "method": { + "enum": [ + "mcpServer/startup/completed" + ], + "title": "McpServer/startup/completedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/McpServerStartupCompletedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "McpServer/startup/completedNotification", + "type": "object" + }, { "properties": { "method": { @@ -5810,6 +6207,26 @@ "title": "McpServer/oauthLogin/completedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "skills/updated" + ], + "title": "Skills/updatedNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/SkillsUpdatedNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "Skills/updatedNotification", + "type": "object" + }, { "properties": { "method": { @@ -5991,6 +6408,26 @@ "title": "DeprecationNoticeNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "backgroundEvent" + ], + "title": "BackgroundEventNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/BackgroundEventNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "BackgroundEventNotification", + "type": "object" + }, { "properties": { "method": { @@ -6194,6 +6631,31 @@ "title": "Item/tool/requestUserInputRequest", "type": "object" }, + { + "description": "Request the user to resolve an MCP elicitation prompt.", + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "item/mcpElicitation/requestDecision" + ], + "title": "Item/mcpElicitation/requestDecisionRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/McpElicitationRequestParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Item/mcpElicitation/requestDecisionRequest", + "type": "object" + }, { "description": "Execute a dynamic tool call on the client.", "properties": { @@ -6296,25 +6758,6 @@ ], "title": "ServerRequest" }, - "SessionNetworkProxyRuntime": { - "properties": { - "admin_addr": { - "type": "string" - }, - "http_addr": { - "type": "string" - }, - "socks_addr": { - "type": "string" - } - }, - "required": [ - "admin_addr", - "http_addr", - "socks_addr" - ], - "type": "object" - }, "StepStatus": { "enum": [ "pending", @@ -7322,6 +7765,27 @@ } ] }, + "BackgroundEventNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "message": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "message", + "threadId", + "turnId" + ], + "title": "BackgroundEventNotification", + "type": "object" + }, "ByteRange": { "properties": { "end": { @@ -8538,6 +9002,58 @@ ], "type": "object" }, + "CustomPrompt": { + "properties": { + "argumentHint": { + "type": [ + "string", + "null" + ] + }, + "content": { + "type": "string" + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": [ + "content", + "name", + "path" + ], + "type": "object" + }, + "CustomPromptListParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CustomPromptListParams", + "type": "object" + }, + "CustomPromptListResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "data": { + "items": { + "$ref": "#/definitions/v2/CustomPrompt" + }, + "type": "array" + } + }, + "required": [ + "data" + ], + "title": "CustomPromptListResponse", + "type": "object" + }, "DeprecationNoticeNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -9043,6 +9559,101 @@ ], "type": "string" }, + "HistoryAppendParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "text": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "text", + "threadId" + ], + "title": "HistoryAppendParams", + "type": "object" + }, + "HistoryAppendResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "HistoryAppendResponse", + "type": "object" + }, + "HistoryEntry": { + "properties": { + "conversationId": { + "type": "string" + }, + "text": { + "type": "string" + }, + "ts": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "conversationId", + "text", + "ts" + ], + "type": "object" + }, + "HistoryEntryReadParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "logId": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + }, + "offset": { + "format": "uint", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "logId", + "offset" + ], + "title": "HistoryEntryReadParams", + "type": "object" + }, + "HistoryEntryReadResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "entry": { + "anyOf": [ + { + "$ref": "#/definitions/v2/HistoryEntry" + }, + { + "type": "null" + } + ] + }, + "logId": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + }, + "offset": { + "format": "uint", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "logId", + "offset" + ], + "title": "HistoryEntryReadResponse", + "type": "object" + }, "InputModality": { "description": "Canonical user-input modality tags advertised by a model.", "oneOf": [ @@ -9437,6 +10048,143 @@ "title": "McpServerRefreshResponse", "type": "object" }, + "McpServerStartupCompletedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "cancelled": { + "items": { + "type": "string" + }, + "type": "array" + }, + "failed": { + "items": { + "$ref": "#/definitions/v2/McpServerStartupFailure" + }, + "type": "array" + }, + "ready": { + "items": { + "type": "string" + }, + "type": "array" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "cancelled", + "failed", + "ready", + "threadId" + ], + "title": "McpServerStartupCompletedNotification", + "type": "object" + }, + "McpServerStartupFailure": { + "properties": { + "error": { + "type": "string" + }, + "server": { + "type": "string" + } + }, + "required": [ + "error", + "server" + ], + "type": "object" + }, + "McpServerStartupStatus": { + "oneOf": [ + { + "properties": { + "state": { + "enum": [ + "starting" + ], + "type": "string" + } + }, + "required": [ + "state" + ], + "title": "StartingMcpServerStartupStatus", + "type": "object" + }, + { + "properties": { + "state": { + "enum": [ + "ready" + ], + "type": "string" + } + }, + "required": [ + "state" + ], + "title": "ReadyMcpServerStartupStatus", + "type": "object" + }, + { + "properties": { + "error": { + "type": "string" + }, + "state": { + "enum": [ + "failed" + ], + "type": "string" + } + }, + "required": [ + "error", + "state" + ], + "type": "object" + }, + { + "properties": { + "state": { + "enum": [ + "cancelled" + ], + "type": "string" + } + }, + "required": [ + "state" + ], + "title": "CancelledMcpServerStartupStatus", + "type": "object" + } + ] + }, + "McpServerStartupUpdatedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "server": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/v2/McpServerStartupStatus" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "server", + "status", + "threadId" + ], + "title": "McpServerStartupUpdatedNotification", + "type": "object" + }, "McpServerStatus": { "properties": { "authStatus": { @@ -9530,6 +10278,58 @@ ], "type": "string" }, + "McpToolsListParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "McpToolsListParams", + "type": "object" + }, + "McpToolsListResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "authStatuses": { + "additionalProperties": { + "$ref": "#/definitions/v2/McpAuthStatus" + }, + "description": "Authentication status for each configured MCP server.", + "type": "object" + }, + "resourceTemplates": { + "additionalProperties": { + "items": { + "$ref": "#/definitions/v2/ResourceTemplate" + }, + "type": "array" + }, + "description": "Known resource templates grouped by server name.", + "type": "object" + }, + "resources": { + "additionalProperties": { + "items": { + "$ref": "#/definitions/v2/Resource" + }, + "type": "array" + }, + "description": "Known resources grouped by server name.", + "type": "object" + }, + "tools": { + "additionalProperties": { + "$ref": "#/definitions/v2/Tool" + }, + "description": "Fully qualified tool name -> tool definition.", + "type": "object" + } + }, + "required": [ + "authStatuses", + "resourceTemplates", + "resources", + "tools" + ], + "title": "McpToolsListResponse", + "type": "object" + }, "MergeStrategy": { "enum": [ "replace", @@ -11130,6 +11930,25 @@ }, "type": "object" }, + "SessionNetworkProxyRuntime": { + "properties": { + "adminAddr": { + "type": "string" + }, + "httpAddr": { + "type": "string" + }, + "socksAddr": { + "type": "string" + } + }, + "required": [ + "adminAddr", + "httpAddr", + "socksAddr" + ], + "type": "object" + }, "SessionSource": { "oneOf": [ { @@ -11548,6 +12367,40 @@ "title": "SkillsRemoteWriteResponse", "type": "object" }, + "SkillsUpdatedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "SkillsUpdatedNotification", + "type": "object" + }, + "StreamErrorNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "error": { + "$ref": "#/definitions/v2/TurnError" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "error", + "threadId", + "turnId" + ], + "title": "StreamErrorNotification", + "type": "object" + }, "SubAgentSource": { "oneOf": [ { @@ -11865,6 +12718,178 @@ "title": "ThreadCompactStartResponse", "type": "object" }, + "ThreadConfigReloadParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadConfigReloadParams", + "type": "object" + }, + "ThreadConfigReloadResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "context": { + "$ref": "#/definitions/v2/ThreadContextSnapshot" + } + }, + "required": [ + "context" + ], + "title": "ThreadConfigReloadResponse", + "type": "object" + }, + "ThreadContextSnapshot": { + "properties": { + "approvalPolicy": { + "$ref": "#/definitions/v2/AskForApproval" + }, + "cwd": { + "type": "string" + }, + "model": { + "type": "string" + }, + "modelProvider": { + "type": "string" + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ReasoningEffort" + }, + { + "type": "null" + } + ] + }, + "sandbox": { + "$ref": "#/definitions/v2/SandboxPolicy" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "approvalPolicy", + "cwd", + "model", + "modelProvider", + "sandbox", + "threadId" + ], + "type": "object" + }, + "ThreadContextUpdateParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "approvalPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/v2/AskForApproval" + }, + { + "type": "null" + } + ] + }, + "cwd": { + "type": [ + "string", + "null" + ] + }, + "effort": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/definitions/v2/ReasoningEffort" + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ] + }, + "model": { + "type": [ + "string", + "null" + ] + }, + "personality": { + "anyOf": [ + { + "$ref": "#/definitions/v2/Personality" + }, + { + "type": "null" + } + ] + }, + "sandboxPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/v2/SandboxPolicy" + }, + { + "type": "null" + } + ] + }, + "summary": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ReasoningSummary" + }, + { + "type": "null" + } + ] + }, + "threadId": { + "type": "string" + }, + "windowsSandboxLevel": { + "anyOf": [ + { + "$ref": "#/definitions/v2/WindowsSandboxLevel" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "threadId" + ], + "title": "ThreadContextUpdateParams", + "type": "object" + }, + "ThreadContextUpdateResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "context": { + "$ref": "#/definitions/v2/ThreadContextSnapshot" + } + }, + "required": [ + "context" + ], + "title": "ThreadContextUpdateResponse", + "type": "object" + }, "ThreadForkParams": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "There are two ways to fork a thread: 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. 2. By path: load the thread from disk by path and fork it into a new thread.\n\nIf using path, the thread_id param will be ignored.\n\nPrefer using thread_id whenever possible.", @@ -11946,12 +12971,38 @@ "cwd": { "type": "string" }, + "forkedFromThreadId": { + "type": [ + "string", + "null" + ] + }, + "historyEntryCount": { + "format": "uint", + "minimum": 0.0, + "type": "integer" + }, + "historyLogId": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + }, "model": { "type": "string" }, "modelProvider": { "type": "string" }, + "networkProxy": { + "anyOf": [ + { + "$ref": "#/definitions/v2/SessionNetworkProxyRuntime" + }, + { + "type": "null" + } + ] + }, "reasoningEffort": { "anyOf": [ { @@ -11972,6 +13023,8 @@ "required": [ "approvalPolicy", "cwd", + "historyEntryCount", + "historyLogId", "model", "modelProvider", "sandbox", @@ -12604,6 +13657,42 @@ "title": "ThreadLoadedListResponse", "type": "object" }, + "ThreadMemoriesDropParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadMemoriesDropParams", + "type": "object" + }, + "ThreadMemoriesDropResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ThreadMemoriesDropResponse", + "type": "object" + }, + "ThreadMemoriesUpdateParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadMemoriesUpdateParams", + "type": "object" + }, + "ThreadMemoriesUpdateResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ThreadMemoriesUpdateResponse", + "type": "object" + }, "ThreadNameUpdatedNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -12745,12 +13834,38 @@ "cwd": { "type": "string" }, + "forkedFromThreadId": { + "type": [ + "string", + "null" + ] + }, + "historyEntryCount": { + "format": "uint", + "minimum": 0.0, + "type": "integer" + }, + "historyLogId": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + }, "model": { "type": "string" }, "modelProvider": { "type": "string" }, + "networkProxy": { + "anyOf": [ + { + "$ref": "#/definitions/v2/SessionNetworkProxyRuntime" + }, + { + "type": "null" + } + ] + }, "reasoningEffort": { "anyOf": [ { @@ -12771,6 +13886,8 @@ "required": [ "approvalPolicy", "cwd", + "historyEntryCount", + "historyLogId", "model", "modelProvider", "sandbox", @@ -12839,6 +13956,37 @@ "title": "ThreadSetNameResponse", "type": "object" }, + "ThreadShutdownCompletedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadShutdownCompletedNotification", + "type": "object" + }, + "ThreadShutdownParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadShutdownParams", + "type": "object" + }, + "ThreadShutdownResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ThreadShutdownResponse", + "type": "object" + }, "ThreadSortKey": { "enum": [ "created_at", @@ -12950,12 +14098,38 @@ "cwd": { "type": "string" }, + "forkedFromThreadId": { + "type": [ + "string", + "null" + ] + }, + "historyEntryCount": { + "format": "uint", + "minimum": 0.0, + "type": "integer" + }, + "historyLogId": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + }, "model": { "type": "string" }, "modelProvider": { "type": "string" }, + "networkProxy": { + "anyOf": [ + { + "$ref": "#/definitions/v2/SessionNetworkProxyRuntime" + }, + { + "type": "null" + } + ] + }, "reasoningEffort": { "anyOf": [ { @@ -12976,6 +14150,8 @@ "required": [ "approvalPolicy", "cwd", + "historyEntryCount", + "historyLogId", "model", "modelProvider", "sandbox", @@ -13171,6 +14347,104 @@ "title": "ThreadUnarchivedNotification", "type": "object" }, + "ThreadUndoCompletedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "message": { + "type": [ + "string", + "null" + ] + }, + "success": { + "type": "boolean" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "success", + "threadId", + "turnId" + ], + "title": "ThreadUndoCompletedNotification", + "type": "object" + }, + "ThreadUndoParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadUndoParams", + "type": "object" + }, + "ThreadUndoResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ThreadUndoResponse", + "type": "object" + }, + "ThreadUndoStartedNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "message": { + "type": [ + "string", + "null" + ] + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "threadId", + "turnId" + ], + "title": "ThreadUndoStartedNotification", + "type": "object" + }, + "ThreadUserShellCommandRunParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "command": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "command", + "threadId" + ], + "title": "ThreadUserShellCommandRunParams", + "type": "object" + }, + "ThreadUserShellCommandRunResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "turnId": { + "type": "string" + } + }, + "required": [ + "turnId" + ], + "title": "ThreadUserShellCommandRunResponse", + "type": "object" + }, "TokenUsageBreakdown": { "properties": { "cachedInputTokens": { @@ -13730,6 +15004,27 @@ ], "type": "string" }, + "WarningNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "message": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "message", + "threadId", + "turnId" + ], + "title": "WarningNotification", + "type": "object" + }, "WebSearchAction": { "oneOf": [ { @@ -13839,6 +15134,14 @@ ], "type": "string" }, + "WindowsSandboxLevel": { + "enum": [ + "disabled", + "restricted-token", + "elevated" + ], + "type": "string" + }, "WindowsSandboxSetupCompletedNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/BackgroundEventNotification.json b/codex-rs/app-server-protocol/schema/json/v2/BackgroundEventNotification.json new file mode 100644 index 0000000000..a31ef646b8 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/BackgroundEventNotification.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "message": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "message", + "threadId", + "turnId" + ], + "title": "BackgroundEventNotification", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/CustomPromptListParams.json b/codex-rs/app-server-protocol/schema/json/v2/CustomPromptListParams.json new file mode 100644 index 0000000000..d67c8a4b63 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/CustomPromptListParams.json @@ -0,0 +1,5 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CustomPromptListParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/CustomPromptListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/CustomPromptListResponse.json new file mode 100644 index 0000000000..a098cd7b82 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/CustomPromptListResponse.json @@ -0,0 +1,49 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "CustomPrompt": { + "properties": { + "argumentHint": { + "type": [ + "string", + "null" + ] + }, + "content": { + "type": "string" + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": [ + "content", + "name", + "path" + ], + "type": "object" + } + }, + "properties": { + "data": { + "items": { + "$ref": "#/definitions/CustomPrompt" + }, + "type": "array" + } + }, + "required": [ + "data" + ], + "title": "CustomPromptListResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/HistoryAppendParams.json b/codex-rs/app-server-protocol/schema/json/v2/HistoryAppendParams.json new file mode 100644 index 0000000000..4a86a8a612 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/HistoryAppendParams.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "text": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "text", + "threadId" + ], + "title": "HistoryAppendParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/HistoryAppendResponse.json b/codex-rs/app-server-protocol/schema/json/v2/HistoryAppendResponse.json new file mode 100644 index 0000000000..4923614eb2 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/HistoryAppendResponse.json @@ -0,0 +1,5 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "HistoryAppendResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/HistoryEntryReadParams.json b/codex-rs/app-server-protocol/schema/json/v2/HistoryEntryReadParams.json new file mode 100644 index 0000000000..b2d56daa82 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/HistoryEntryReadParams.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "logId": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + }, + "offset": { + "format": "uint", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "logId", + "offset" + ], + "title": "HistoryEntryReadParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/HistoryEntryReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/HistoryEntryReadResponse.json new file mode 100644 index 0000000000..70a1cbbb15 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/HistoryEntryReadResponse.json @@ -0,0 +1,54 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "HistoryEntry": { + "properties": { + "conversationId": { + "type": "string" + }, + "text": { + "type": "string" + }, + "ts": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "conversationId", + "text", + "ts" + ], + "type": "object" + } + }, + "properties": { + "entry": { + "anyOf": [ + { + "$ref": "#/definitions/HistoryEntry" + }, + { + "type": "null" + } + ] + }, + "logId": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + }, + "offset": { + "format": "uint", + "minimum": 0.0, + "type": "integer" + } + }, + "required": [ + "logId", + "offset" + ], + "title": "HistoryEntryReadResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/McpServerStartupCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/McpServerStartupCompletedNotification.json new file mode 100644 index 0000000000..eaaaa56571 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/McpServerStartupCompletedNotification.json @@ -0,0 +1,51 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "McpServerStartupFailure": { + "properties": { + "error": { + "type": "string" + }, + "server": { + "type": "string" + } + }, + "required": [ + "error", + "server" + ], + "type": "object" + } + }, + "properties": { + "cancelled": { + "items": { + "type": "string" + }, + "type": "array" + }, + "failed": { + "items": { + "$ref": "#/definitions/McpServerStartupFailure" + }, + "type": "array" + }, + "ready": { + "items": { + "type": "string" + }, + "type": "array" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "cancelled", + "failed", + "ready", + "threadId" + ], + "title": "McpServerStartupCompletedNotification", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/McpServerStartupUpdatedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/McpServerStartupUpdatedNotification.json new file mode 100644 index 0000000000..76e2ca8c8a --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/McpServerStartupUpdatedNotification.json @@ -0,0 +1,90 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "McpServerStartupStatus": { + "oneOf": [ + { + "properties": { + "state": { + "enum": [ + "starting" + ], + "type": "string" + } + }, + "required": [ + "state" + ], + "title": "StartingMcpServerStartupStatus", + "type": "object" + }, + { + "properties": { + "state": { + "enum": [ + "ready" + ], + "type": "string" + } + }, + "required": [ + "state" + ], + "title": "ReadyMcpServerStartupStatus", + "type": "object" + }, + { + "properties": { + "error": { + "type": "string" + }, + "state": { + "enum": [ + "failed" + ], + "type": "string" + } + }, + "required": [ + "error", + "state" + ], + "type": "object" + }, + { + "properties": { + "state": { + "enum": [ + "cancelled" + ], + "type": "string" + } + }, + "required": [ + "state" + ], + "title": "CancelledMcpServerStartupStatus", + "type": "object" + } + ] + } + }, + "properties": { + "server": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/McpServerStartupStatus" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "server", + "status", + "threadId" + ], + "title": "McpServerStartupUpdatedNotification", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/McpToolsListParams.json b/codex-rs/app-server-protocol/schema/json/v2/McpToolsListParams.json new file mode 100644 index 0000000000..b1728fdbdd --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/McpToolsListParams.json @@ -0,0 +1,5 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "McpToolsListParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/McpToolsListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/McpToolsListResponse.json new file mode 100644 index 0000000000..a2adf08024 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/McpToolsListResponse.json @@ -0,0 +1,179 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "McpAuthStatus": { + "enum": [ + "unsupported", + "not_logged_in", + "bearer_token", + "o_auth" + ], + "type": "string" + }, + "Resource": { + "description": "A known resource that the server is capable of reading.", + "properties": { + "_meta": true, + "annotations": true, + "description": { + "type": [ + "string", + "null" + ] + }, + "icons": { + "items": true, + "type": [ + "array", + "null" + ] + }, + "mimeType": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": "string" + }, + "size": { + "format": "int64", + "type": [ + "integer", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "uri": { + "type": "string" + } + }, + "required": [ + "name", + "uri" + ], + "type": "object" + }, + "ResourceTemplate": { + "description": "A template description for resources available on the server.", + "properties": { + "annotations": true, + "description": { + "type": [ + "string", + "null" + ] + }, + "mimeType": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": "string" + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "uriTemplate": { + "type": "string" + } + }, + "required": [ + "name", + "uriTemplate" + ], + "type": "object" + }, + "Tool": { + "description": "Definition for a tool the client can call.", + "properties": { + "_meta": true, + "annotations": true, + "description": { + "type": [ + "string", + "null" + ] + }, + "icons": { + "items": true, + "type": [ + "array", + "null" + ] + }, + "inputSchema": true, + "name": { + "type": "string" + }, + "outputSchema": true, + "title": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "inputSchema", + "name" + ], + "type": "object" + } + }, + "properties": { + "authStatuses": { + "additionalProperties": { + "$ref": "#/definitions/McpAuthStatus" + }, + "description": "Authentication status for each configured MCP server.", + "type": "object" + }, + "resourceTemplates": { + "additionalProperties": { + "items": { + "$ref": "#/definitions/ResourceTemplate" + }, + "type": "array" + }, + "description": "Known resource templates grouped by server name.", + "type": "object" + }, + "resources": { + "additionalProperties": { + "items": { + "$ref": "#/definitions/Resource" + }, + "type": "array" + }, + "description": "Known resources grouped by server name.", + "type": "object" + }, + "tools": { + "additionalProperties": { + "$ref": "#/definitions/Tool" + }, + "description": "Fully qualified tool name -> tool definition.", + "type": "object" + } + }, + "required": [ + "authStatuses", + "resourceTemplates", + "resources", + "tools" + ], + "title": "McpToolsListResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/SkillsUpdatedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/SkillsUpdatedNotification.json new file mode 100644 index 0000000000..0ed37a6bc9 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/SkillsUpdatedNotification.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "SkillsUpdatedNotification", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/StreamErrorNotification.json b/codex-rs/app-server-protocol/schema/json/v2/StreamErrorNotification.json new file mode 100644 index 0000000000..6362395658 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/StreamErrorNotification.json @@ -0,0 +1,165 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "CodexErrorInfo": { + "description": "This translation layer make sure that we expose codex error code in camel case.\n\nWhen an upstream HTTP status is available (for example, from the Responses API or a provider), it is forwarded in `httpStatusCode` on the relevant `codexErrorInfo` variant.", + "oneOf": [ + { + "enum": [ + "contextWindowExceeded", + "usageLimitExceeded", + "serverOverloaded", + "internalServerError", + "unauthorized", + "badRequest", + "threadRollbackFailed", + "sandboxError", + "other" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "httpConnectionFailed": { + "properties": { + "httpStatusCode": { + "format": "uint16", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + } + }, + "required": [ + "httpConnectionFailed" + ], + "title": "HttpConnectionFailedCodexErrorInfo", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Failed to connect to the response SSE stream.", + "properties": { + "responseStreamConnectionFailed": { + "properties": { + "httpStatusCode": { + "format": "uint16", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + } + }, + "required": [ + "responseStreamConnectionFailed" + ], + "title": "ResponseStreamConnectionFailedCodexErrorInfo", + "type": "object" + }, + { + "additionalProperties": false, + "description": "The response SSE stream disconnected in the middle of a turn before completion.", + "properties": { + "responseStreamDisconnected": { + "properties": { + "httpStatusCode": { + "format": "uint16", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + } + }, + "required": [ + "responseStreamDisconnected" + ], + "title": "ResponseStreamDisconnectedCodexErrorInfo", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Reached the retry limit for responses.", + "properties": { + "responseTooManyFailedAttempts": { + "properties": { + "httpStatusCode": { + "format": "uint16", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + } + }, + "required": [ + "responseTooManyFailedAttempts" + ], + "title": "ResponseTooManyFailedAttemptsCodexErrorInfo", + "type": "object" + } + ] + }, + "TurnError": { + "properties": { + "additionalDetails": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "codexErrorInfo": { + "anyOf": [ + { + "$ref": "#/definitions/CodexErrorInfo" + }, + { + "type": "null" + } + ] + }, + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + } + }, + "properties": { + "error": { + "$ref": "#/definitions/TurnError" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "error", + "threadId", + "turnId" + ], + "title": "StreamErrorNotification", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadConfigReloadParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadConfigReloadParams.json new file mode 100644 index 0000000000..b0c98e0fb1 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadConfigReloadParams.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadConfigReloadParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadConfigReloadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadConfigReloadResponse.json new file mode 100644 index 0000000000..f86745386c --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadConfigReloadResponse.json @@ -0,0 +1,283 @@ +{ + "$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" + }, + "AskForApproval": { + "oneOf": [ + { + "enum": [ + "untrusted", + "on-failure", + "on-request", + "never" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "reject": { + "properties": { + "mcp_elicitations": { + "type": "boolean" + }, + "rules": { + "type": "boolean" + }, + "sandbox_approval": { + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + } + }, + "required": [ + "reject" + ], + "title": "RejectAskForApproval", + "type": "object" + } + ] + }, + "NetworkAccess": { + "enum": [ + "restricted", + "enabled" + ], + "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" + }, + "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" + } + }, + "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" + } + ] + }, + "ThreadContextSnapshot": { + "properties": { + "approvalPolicy": { + "$ref": "#/definitions/AskForApproval" + }, + "cwd": { + "type": "string" + }, + "model": { + "type": "string" + }, + "modelProvider": { + "type": "string" + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] + }, + "sandbox": { + "$ref": "#/definitions/SandboxPolicy" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "approvalPolicy", + "cwd", + "model", + "modelProvider", + "sandbox", + "threadId" + ], + "type": "object" + } + }, + "properties": { + "context": { + "$ref": "#/definitions/ThreadContextSnapshot" + } + }, + "required": [ + "context" + ], + "title": "ThreadConfigReloadResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadContextUpdateParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadContextUpdateParams.json new file mode 100644 index 0000000000..123de96241 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadContextUpdateParams.json @@ -0,0 +1,409 @@ +{ + "$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" + }, + "AskForApproval": { + "oneOf": [ + { + "enum": [ + "untrusted", + "on-failure", + "on-request", + "never" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "reject": { + "properties": { + "mcp_elicitations": { + "type": "boolean" + }, + "rules": { + "type": "boolean" + }, + "sandbox_approval": { + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + } + }, + "required": [ + "reject" + ], + "title": "RejectAskForApproval", + "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" + } + }, + "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" + } + ] + }, + "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" + }, + "WindowsSandboxLevel": { + "enum": [ + "disabled", + "restricted-token", + "elevated" + ], + "type": "string" + } + }, + "properties": { + "approvalPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/AskForApproval" + }, + { + "type": "null" + } + ] + }, + "cwd": { + "type": [ + "string", + "null" + ] + }, + "effort": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] + }, + { + "type": "null" + } + ] + }, + "model": { + "type": [ + "string", + "null" + ] + }, + "personality": { + "anyOf": [ + { + "$ref": "#/definitions/Personality" + }, + { + "type": "null" + } + ] + }, + "sandboxPolicy": { + "anyOf": [ + { + "$ref": "#/definitions/SandboxPolicy" + }, + { + "type": "null" + } + ] + }, + "summary": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningSummary" + }, + { + "type": "null" + } + ] + }, + "threadId": { + "type": "string" + }, + "windowsSandboxLevel": { + "anyOf": [ + { + "$ref": "#/definitions/WindowsSandboxLevel" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "threadId" + ], + "title": "ThreadContextUpdateParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadContextUpdateResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadContextUpdateResponse.json new file mode 100644 index 0000000000..94dbc5a8d3 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadContextUpdateResponse.json @@ -0,0 +1,283 @@ +{ + "$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" + }, + "AskForApproval": { + "oneOf": [ + { + "enum": [ + "untrusted", + "on-failure", + "on-request", + "never" + ], + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "reject": { + "properties": { + "mcp_elicitations": { + "type": "boolean" + }, + "rules": { + "type": "boolean" + }, + "sandbox_approval": { + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + } + }, + "required": [ + "reject" + ], + "title": "RejectAskForApproval", + "type": "object" + } + ] + }, + "NetworkAccess": { + "enum": [ + "restricted", + "enabled" + ], + "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" + }, + "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" + } + }, + "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" + } + ] + }, + "ThreadContextSnapshot": { + "properties": { + "approvalPolicy": { + "$ref": "#/definitions/AskForApproval" + }, + "cwd": { + "type": "string" + }, + "model": { + "type": "string" + }, + "modelProvider": { + "type": "string" + }, + "reasoningEffort": { + "anyOf": [ + { + "$ref": "#/definitions/ReasoningEffort" + }, + { + "type": "null" + } + ] + }, + "sandbox": { + "$ref": "#/definitions/SandboxPolicy" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "approvalPolicy", + "cwd", + "model", + "modelProvider", + "sandbox", + "threadId" + ], + "type": "object" + } + }, + "properties": { + "context": { + "$ref": "#/definitions/ThreadContextSnapshot" + } + }, + "required": [ + "context" + ], + "title": "ThreadContextUpdateResponse", + "type": "object" +} \ No newline at end of file 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 71f9da2f3b..211c824d79 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -686,6 +686,25 @@ } ] }, + "SessionNetworkProxyRuntime": { + "properties": { + "adminAddr": { + "type": "string" + }, + "httpAddr": { + "type": "string" + }, + "socksAddr": { + "type": "string" + } + }, + "required": [ + "adminAddr", + "httpAddr", + "socksAddr" + ], + "type": "object" + }, "SessionSource": { "oneOf": [ { @@ -1777,12 +1796,38 @@ "cwd": { "type": "string" }, + "forkedFromThreadId": { + "type": [ + "string", + "null" + ] + }, + "historyEntryCount": { + "format": "uint", + "minimum": 0.0, + "type": "integer" + }, + "historyLogId": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + }, "model": { "type": "string" }, "modelProvider": { "type": "string" }, + "networkProxy": { + "anyOf": [ + { + "$ref": "#/definitions/SessionNetworkProxyRuntime" + }, + { + "type": "null" + } + ] + }, "reasoningEffort": { "anyOf": [ { @@ -1803,6 +1848,8 @@ "required": [ "approvalPolicy", "cwd", + "historyEntryCount", + "historyLogId", "model", "modelProvider", "sandbox", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadMemoriesDropParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadMemoriesDropParams.json new file mode 100644 index 0000000000..fecd973775 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadMemoriesDropParams.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadMemoriesDropParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadMemoriesDropResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadMemoriesDropResponse.json new file mode 100644 index 0000000000..42ee77a7c5 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadMemoriesDropResponse.json @@ -0,0 +1,5 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ThreadMemoriesDropResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadMemoriesUpdateParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadMemoriesUpdateParams.json new file mode 100644 index 0000000000..938718941a --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadMemoriesUpdateParams.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadMemoriesUpdateParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadMemoriesUpdateResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadMemoriesUpdateResponse.json new file mode 100644 index 0000000000..c0026fc173 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadMemoriesUpdateResponse.json @@ -0,0 +1,5 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ThreadMemoriesUpdateResponse", + "type": "object" +} \ No newline at end of file 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 7778773a16..8eeac512de 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -686,6 +686,25 @@ } ] }, + "SessionNetworkProxyRuntime": { + "properties": { + "adminAddr": { + "type": "string" + }, + "httpAddr": { + "type": "string" + }, + "socksAddr": { + "type": "string" + } + }, + "required": [ + "adminAddr", + "httpAddr", + "socksAddr" + ], + "type": "object" + }, "SessionSource": { "oneOf": [ { @@ -1777,12 +1796,38 @@ "cwd": { "type": "string" }, + "forkedFromThreadId": { + "type": [ + "string", + "null" + ] + }, + "historyEntryCount": { + "format": "uint", + "minimum": 0.0, + "type": "integer" + }, + "historyLogId": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + }, "model": { "type": "string" }, "modelProvider": { "type": "string" }, + "networkProxy": { + "anyOf": [ + { + "$ref": "#/definitions/SessionNetworkProxyRuntime" + }, + { + "type": "null" + } + ] + }, "reasoningEffort": { "anyOf": [ { @@ -1803,6 +1848,8 @@ "required": [ "approvalPolicy", "cwd", + "historyEntryCount", + "historyLogId", "model", "modelProvider", "sandbox", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadShutdownCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadShutdownCompletedNotification.json new file mode 100644 index 0000000000..2c1a83a653 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadShutdownCompletedNotification.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadShutdownCompletedNotification", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadShutdownParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadShutdownParams.json new file mode 100644 index 0000000000..4826e7218c --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadShutdownParams.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadShutdownParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadShutdownResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadShutdownResponse.json new file mode 100644 index 0000000000..b9dd6a7951 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadShutdownResponse.json @@ -0,0 +1,5 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ThreadShutdownResponse", + "type": "object" +} \ No newline at end of file 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 6b3d2ebc58..01b9e1ad9f 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -686,6 +686,25 @@ } ] }, + "SessionNetworkProxyRuntime": { + "properties": { + "adminAddr": { + "type": "string" + }, + "httpAddr": { + "type": "string" + }, + "socksAddr": { + "type": "string" + } + }, + "required": [ + "adminAddr", + "httpAddr", + "socksAddr" + ], + "type": "object" + }, "SessionSource": { "oneOf": [ { @@ -1777,12 +1796,38 @@ "cwd": { "type": "string" }, + "forkedFromThreadId": { + "type": [ + "string", + "null" + ] + }, + "historyEntryCount": { + "format": "uint", + "minimum": 0.0, + "type": "integer" + }, + "historyLogId": { + "format": "uint64", + "minimum": 0.0, + "type": "integer" + }, "model": { "type": "string" }, "modelProvider": { "type": "string" }, + "networkProxy": { + "anyOf": [ + { + "$ref": "#/definitions/SessionNetworkProxyRuntime" + }, + { + "type": "null" + } + ] + }, "reasoningEffort": { "anyOf": [ { @@ -1803,6 +1848,8 @@ "required": [ "approvalPolicy", "cwd", + "historyEntryCount", + "historyLogId", "model", "modelProvider", "sandbox", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUndoCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUndoCompletedNotification.json new file mode 100644 index 0000000000..a39403155f --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUndoCompletedNotification.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "message": { + "type": [ + "string", + "null" + ] + }, + "success": { + "type": "boolean" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "success", + "threadId", + "turnId" + ], + "title": "ThreadUndoCompletedNotification", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUndoParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUndoParams.json new file mode 100644 index 0000000000..81720de8f2 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUndoParams.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "threadId": { + "type": "string" + } + }, + "required": [ + "threadId" + ], + "title": "ThreadUndoParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUndoResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUndoResponse.json new file mode 100644 index 0000000000..15f79a2207 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUndoResponse.json @@ -0,0 +1,5 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ThreadUndoResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUndoStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUndoStartedNotification.json new file mode 100644 index 0000000000..46850cdb8d --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUndoStartedNotification.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "message": { + "type": [ + "string", + "null" + ] + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "threadId", + "turnId" + ], + "title": "ThreadUndoStartedNotification", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUserShellCommandRunParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUserShellCommandRunParams.json new file mode 100644 index 0000000000..7032b15b6e --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUserShellCommandRunParams.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "command": { + "type": "string" + }, + "threadId": { + "type": "string" + } + }, + "required": [ + "command", + "threadId" + ], + "title": "ThreadUserShellCommandRunParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUserShellCommandRunResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUserShellCommandRunResponse.json new file mode 100644 index 0000000000..0f00154cf8 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUserShellCommandRunResponse.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "turnId": { + "type": "string" + } + }, + "required": [ + "turnId" + ], + "title": "ThreadUserShellCommandRunResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/WarningNotification.json b/codex-rs/app-server-protocol/schema/json/v2/WarningNotification.json new file mode 100644 index 0000000000..4994fa91f1 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/WarningNotification.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "message": { + "type": "string" + }, + "threadId": { + "type": "string" + }, + "turnId": { + "type": "string" + } + }, + "required": [ + "message", + "threadId", + "turnId" + ], + "title": "WarningNotification", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts index 8344592871..bda53b0511 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -27,12 +27,16 @@ import type { CommandExecParams } from "./v2/CommandExecParams"; import type { ConfigBatchWriteParams } from "./v2/ConfigBatchWriteParams"; import type { ConfigReadParams } from "./v2/ConfigReadParams"; import type { ConfigValueWriteParams } from "./v2/ConfigValueWriteParams"; +import type { CustomPromptListParams } from "./v2/CustomPromptListParams"; import type { ExperimentalFeatureListParams } from "./v2/ExperimentalFeatureListParams"; import type { FeedbackUploadParams } from "./v2/FeedbackUploadParams"; import type { GetAccountParams } from "./v2/GetAccountParams"; +import type { HistoryAppendParams } from "./v2/HistoryAppendParams"; +import type { HistoryEntryReadParams } from "./v2/HistoryEntryReadParams"; import type { ListMcpServerStatusParams } from "./v2/ListMcpServerStatusParams"; import type { LoginAccountParams } from "./v2/LoginAccountParams"; import type { McpServerOauthLoginParams } from "./v2/McpServerOauthLoginParams"; +import type { McpToolsListParams } from "./v2/McpToolsListParams"; import type { ModelListParams } from "./v2/ModelListParams"; import type { ReviewStartParams } from "./v2/ReviewStartParams"; import type { SkillsConfigWriteParams } from "./v2/SkillsConfigWriteParams"; @@ -41,15 +45,22 @@ import type { SkillsRemoteReadParams } from "./v2/SkillsRemoteReadParams"; import type { SkillsRemoteWriteParams } from "./v2/SkillsRemoteWriteParams"; import type { ThreadArchiveParams } from "./v2/ThreadArchiveParams"; import type { ThreadCompactStartParams } from "./v2/ThreadCompactStartParams"; +import type { ThreadConfigReloadParams } from "./v2/ThreadConfigReloadParams"; +import type { ThreadContextUpdateParams } from "./v2/ThreadContextUpdateParams"; import type { ThreadForkParams } from "./v2/ThreadForkParams"; import type { ThreadListParams } from "./v2/ThreadListParams"; import type { ThreadLoadedListParams } from "./v2/ThreadLoadedListParams"; +import type { ThreadMemoriesDropParams } from "./v2/ThreadMemoriesDropParams"; +import type { ThreadMemoriesUpdateParams } from "./v2/ThreadMemoriesUpdateParams"; import type { ThreadReadParams } from "./v2/ThreadReadParams"; import type { ThreadResumeParams } from "./v2/ThreadResumeParams"; import type { ThreadRollbackParams } from "./v2/ThreadRollbackParams"; import type { ThreadSetNameParams } from "./v2/ThreadSetNameParams"; +import type { ThreadShutdownParams } from "./v2/ThreadShutdownParams"; import type { ThreadStartParams } from "./v2/ThreadStartParams"; import type { ThreadUnarchiveParams } from "./v2/ThreadUnarchiveParams"; +import type { ThreadUndoParams } from "./v2/ThreadUndoParams"; +import type { ThreadUserShellCommandRunParams } from "./v2/ThreadUserShellCommandRunParams"; import type { TurnInterruptParams } from "./v2/TurnInterruptParams"; import type { TurnStartParams } from "./v2/TurnStartParams"; import type { TurnSteerParams } from "./v2/TurnSteerParams"; @@ -58,4 +69,4 @@ import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupSta /** * Request from the client to the server. */ -export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/remote/list", id: RequestId, params: SkillsRemoteReadParams, } | { "method": "skills/remote/export", id: RequestId, params: SkillsRemoteWriteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "newConversation", id: RequestId, params: NewConversationParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "listConversations", id: RequestId, params: ListConversationsParams, } | { "method": "resumeConversation", id: RequestId, params: ResumeConversationParams, } | { "method": "forkConversation", id: RequestId, params: ForkConversationParams, } | { "method": "archiveConversation", id: RequestId, params: ArchiveConversationParams, } | { "method": "sendUserMessage", id: RequestId, params: SendUserMessageParams, } | { "method": "sendUserTurn", id: RequestId, params: SendUserTurnParams, } | { "method": "interruptConversation", id: RequestId, params: InterruptConversationParams, } | { "method": "addConversationListener", id: RequestId, params: AddConversationListenerParams, } | { "method": "removeConversationListener", id: RequestId, params: RemoveConversationListenerParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "loginApiKey", id: RequestId, params: LoginApiKeyParams, } | { "method": "loginChatGpt", id: RequestId, params: undefined, } | { "method": "cancelLoginChatGpt", id: RequestId, params: CancelLoginChatGptParams, } | { "method": "logoutChatGpt", id: RequestId, params: undefined, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "getUserSavedConfig", id: RequestId, params: undefined, } | { "method": "setDefaultModel", id: RequestId, params: SetDefaultModelParams, } | { "method": "getUserAgent", id: RequestId, params: undefined, } | { "method": "userInfo", id: RequestId, params: undefined, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, } | { "method": "execOneOffCommand", id: RequestId, params: ExecOneOffCommandParams, }; +export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/context/update", id: RequestId, params: ThreadContextUpdateParams, } | { "method": "thread/shutdown", id: RequestId, params: ThreadShutdownParams, } | { "method": "thread/undo", id: RequestId, params: ThreadUndoParams, } | { "method": "thread/config/reload", id: RequestId, params: ThreadConfigReloadParams, } | { "method": "thread/memories/drop", id: RequestId, params: ThreadMemoriesDropParams, } | { "method": "thread/memories/update", id: RequestId, params: ThreadMemoriesUpdateParams, } | { "method": "thread/userShellCommand/run", id: RequestId, params: ThreadUserShellCommandRunParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "customPrompt/list", id: RequestId, params: CustomPromptListParams, } | { "method": "mcpTools/list", id: RequestId, params: McpToolsListParams, } | { "method": "history/append", id: RequestId, params: HistoryAppendParams, } | { "method": "history/entry/read", id: RequestId, params: HistoryEntryReadParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/remote/list", id: RequestId, params: SkillsRemoteReadParams, } | { "method": "skills/remote/export", id: RequestId, params: SkillsRemoteWriteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "newConversation", id: RequestId, params: NewConversationParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "listConversations", id: RequestId, params: ListConversationsParams, } | { "method": "resumeConversation", id: RequestId, params: ResumeConversationParams, } | { "method": "forkConversation", id: RequestId, params: ForkConversationParams, } | { "method": "archiveConversation", id: RequestId, params: ArchiveConversationParams, } | { "method": "sendUserMessage", id: RequestId, params: SendUserMessageParams, } | { "method": "sendUserTurn", id: RequestId, params: SendUserTurnParams, } | { "method": "interruptConversation", id: RequestId, params: InterruptConversationParams, } | { "method": "addConversationListener", id: RequestId, params: AddConversationListenerParams, } | { "method": "removeConversationListener", id: RequestId, params: RemoveConversationListenerParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "loginApiKey", id: RequestId, params: LoginApiKeyParams, } | { "method": "loginChatGpt", id: RequestId, params: undefined, } | { "method": "cancelLoginChatGpt", id: RequestId, params: CancelLoginChatGptParams, } | { "method": "logoutChatGpt", id: RequestId, params: undefined, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "getUserSavedConfig", id: RequestId, params: undefined, } | { "method": "setDefaultModel", id: RequestId, params: SetDefaultModelParams, } | { "method": "getUserAgent", id: RequestId, params: undefined, } | { "method": "userInfo", id: RequestId, params: undefined, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, } | { "method": "execOneOffCommand", id: RequestId, params: ExecOneOffCommandParams, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts b/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts index 20dfa846f5..cbe8286cc6 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts @@ -11,6 +11,7 @@ import type { AccountRateLimitsUpdatedNotification } from "./v2/AccountRateLimit import type { AccountUpdatedNotification } from "./v2/AccountUpdatedNotification"; import type { AgentMessageDeltaNotification } from "./v2/AgentMessageDeltaNotification"; import type { AppListUpdatedNotification } from "./v2/AppListUpdatedNotification"; +import type { BackgroundEventNotification } from "./v2/BackgroundEventNotification"; import type { CommandExecutionOutputDeltaNotification } from "./v2/CommandExecutionOutputDeltaNotification"; import type { ConfigWarningNotification } from "./v2/ConfigWarningNotification"; import type { ContextCompactedNotification } from "./v2/ContextCompactedNotification"; @@ -20,6 +21,8 @@ import type { FileChangeOutputDeltaNotification } from "./v2/FileChangeOutputDel import type { ItemCompletedNotification } from "./v2/ItemCompletedNotification"; import type { ItemStartedNotification } from "./v2/ItemStartedNotification"; import type { McpServerOauthLoginCompletedNotification } from "./v2/McpServerOauthLoginCompletedNotification"; +import type { McpServerStartupCompletedNotification } from "./v2/McpServerStartupCompletedNotification"; +import type { McpServerStartupUpdatedNotification } from "./v2/McpServerStartupUpdatedNotification"; import type { McpToolCallProgressNotification } from "./v2/McpToolCallProgressNotification"; import type { ModelReroutedNotification } from "./v2/ModelReroutedNotification"; import type { PlanDeltaNotification } from "./v2/PlanDeltaNotification"; @@ -27,21 +30,27 @@ import type { RawResponseItemCompletedNotification } from "./v2/RawResponseItemC import type { ReasoningSummaryPartAddedNotification } from "./v2/ReasoningSummaryPartAddedNotification"; import type { ReasoningSummaryTextDeltaNotification } from "./v2/ReasoningSummaryTextDeltaNotification"; import type { ReasoningTextDeltaNotification } from "./v2/ReasoningTextDeltaNotification"; +import type { SkillsUpdatedNotification } from "./v2/SkillsUpdatedNotification"; +import type { StreamErrorNotification } from "./v2/StreamErrorNotification"; import type { TerminalInteractionNotification } from "./v2/TerminalInteractionNotification"; import type { ThreadArchivedNotification } from "./v2/ThreadArchivedNotification"; import type { ThreadNameUpdatedNotification } from "./v2/ThreadNameUpdatedNotification"; +import type { ThreadShutdownCompletedNotification } from "./v2/ThreadShutdownCompletedNotification"; import type { ThreadStartedNotification } from "./v2/ThreadStartedNotification"; import type { ThreadStatusChangedNotification } from "./v2/ThreadStatusChangedNotification"; import type { ThreadTokenUsageUpdatedNotification } from "./v2/ThreadTokenUsageUpdatedNotification"; import type { ThreadUnarchivedNotification } from "./v2/ThreadUnarchivedNotification"; +import type { ThreadUndoCompletedNotification } from "./v2/ThreadUndoCompletedNotification"; +import type { ThreadUndoStartedNotification } from "./v2/ThreadUndoStartedNotification"; import type { TurnCompletedNotification } from "./v2/TurnCompletedNotification"; import type { TurnDiffUpdatedNotification } from "./v2/TurnDiffUpdatedNotification"; import type { TurnPlanUpdatedNotification } from "./v2/TurnPlanUpdatedNotification"; import type { TurnStartedNotification } from "./v2/TurnStartedNotification"; +import type { WarningNotification } from "./v2/WarningNotification"; import type { WindowsSandboxSetupCompletedNotification } from "./v2/WindowsSandboxSetupCompletedNotification"; import type { WindowsWorldWritableWarningNotification } from "./v2/WindowsWorldWritableWarningNotification"; /** * Notification sent from the server to the client. */ -export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification } | { "method": "authStatusChange", "params": AuthStatusChangeNotification } | { "method": "loginChatGptComplete", "params": LoginChatGptCompleteNotification } | { "method": "sessionConfigured", "params": SessionConfiguredNotification }; +export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "warning", "params": WarningNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "thread/shutdown/completed", "params": ThreadShutdownCompletedNotification } | { "method": "thread/undo/started", "params": ThreadUndoStartedNotification } | { "method": "thread/undo/completed", "params": ThreadUndoCompletedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "turn/streamError", "params": StreamErrorNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/startup/updated", "params": McpServerStartupUpdatedNotification } | { "method": "mcpServer/startup/completed", "params": McpServerStartupCompletedNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "skills/updated", "params": SkillsUpdatedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "backgroundEvent", "params": BackgroundEventNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification } | { "method": "authStatusChange", "params": AuthStatusChangeNotification } | { "method": "loginChatGptComplete", "params": LoginChatGptCompleteNotification } | { "method": "sessionConfigured", "params": SessionConfiguredNotification }; diff --git a/codex-rs/app-server-protocol/schema/typescript/ServerRequest.ts b/codex-rs/app-server-protocol/schema/typescript/ServerRequest.ts index 17c66959aa..fe3c4f5e0e 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ServerRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ServerRequest.ts @@ -8,9 +8,10 @@ import type { ChatgptAuthTokensRefreshParams } from "./v2/ChatgptAuthTokensRefre import type { CommandExecutionRequestApprovalParams } from "./v2/CommandExecutionRequestApprovalParams"; import type { DynamicToolCallParams } from "./v2/DynamicToolCallParams"; import type { FileChangeRequestApprovalParams } from "./v2/FileChangeRequestApprovalParams"; +import type { McpElicitationRequestParams } from "./v2/McpElicitationRequestParams"; import type { ToolRequestUserInputParams } from "./v2/ToolRequestUserInputParams"; /** * Request initiated from the server and sent to the client. */ -export type ServerRequest = { "method": "item/commandExecution/requestApproval", id: RequestId, params: CommandExecutionRequestApprovalParams, } | { "method": "item/fileChange/requestApproval", id: RequestId, params: FileChangeRequestApprovalParams, } | { "method": "item/tool/requestUserInput", id: RequestId, params: ToolRequestUserInputParams, } | { "method": "item/tool/call", id: RequestId, params: DynamicToolCallParams, } | { "method": "account/chatgptAuthTokens/refresh", id: RequestId, params: ChatgptAuthTokensRefreshParams, } | { "method": "applyPatchApproval", id: RequestId, params: ApplyPatchApprovalParams, } | { "method": "execCommandApproval", id: RequestId, params: ExecCommandApprovalParams, }; +export type ServerRequest = { "method": "item/commandExecution/requestApproval", id: RequestId, params: CommandExecutionRequestApprovalParams, } | { "method": "item/fileChange/requestApproval", id: RequestId, params: FileChangeRequestApprovalParams, } | { "method": "item/tool/requestUserInput", id: RequestId, params: ToolRequestUserInputParams, } | { "method": "item/mcpElicitation/requestDecision", id: RequestId, params: McpElicitationRequestParams, } | { "method": "item/tool/call", id: RequestId, params: DynamicToolCallParams, } | { "method": "account/chatgptAuthTokens/refresh", id: RequestId, params: ChatgptAuthTokensRefreshParams, } | { "method": "applyPatchApproval", id: RequestId, params: ApplyPatchApprovalParams, } | { "method": "execCommandApproval", id: RequestId, params: ExecCommandApprovalParams, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/WindowsSandboxLevel.ts b/codex-rs/app-server-protocol/schema/typescript/WindowsSandboxLevel.ts new file mode 100644 index 0000000000..5b4db76741 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/WindowsSandboxLevel.ts @@ -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 WindowsSandboxLevel = "disabled" | "restricted-token" | "elevated"; diff --git a/codex-rs/app-server-protocol/schema/typescript/index.ts b/codex-rs/app-server-protocol/schema/typescript/index.ts index 30197f8922..3e89372f1e 100644 --- a/codex-rs/app-server-protocol/schema/typescript/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/index.ts @@ -239,4 +239,5 @@ export type { WebSearchBeginEvent } from "./WebSearchBeginEvent"; export type { WebSearchEndEvent } from "./WebSearchEndEvent"; export type { WebSearchItem } from "./WebSearchItem"; export type { WebSearchMode } from "./WebSearchMode"; +export type { WindowsSandboxLevel } from "./WindowsSandboxLevel"; export * as v2 from "./v2"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/BackgroundEventNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/BackgroundEventNotification.ts new file mode 100644 index 0000000000..31fcae42bc --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/BackgroundEventNotification.ts @@ -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 BackgroundEventNotification = { threadId: string, turnId: string, message: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CustomPrompt.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CustomPrompt.ts new file mode 100644 index 0000000000..7d7e91a933 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CustomPrompt.ts @@ -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 CustomPrompt = { name: string, path: string, content: string, description: string | null, argumentHint: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CustomPromptListParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CustomPromptListParams.ts new file mode 100644 index 0000000000..2eecdc19fc --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CustomPromptListParams.ts @@ -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 CustomPromptListParams = Record; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CustomPromptListResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CustomPromptListResponse.ts new file mode 100644 index 0000000000..f4abe7f5a7 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CustomPromptListResponse.ts @@ -0,0 +1,6 @@ +// 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 { CustomPrompt } from "./CustomPrompt"; + +export type CustomPromptListResponse = { data: Array, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/HistoryAppendParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/HistoryAppendParams.ts new file mode 100644 index 0000000000..fd66258bf0 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/HistoryAppendParams.ts @@ -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 HistoryAppendParams = { threadId: string, text: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/HistoryAppendResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/HistoryAppendResponse.ts new file mode 100644 index 0000000000..f8ea3034a5 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/HistoryAppendResponse.ts @@ -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 HistoryAppendResponse = Record; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/HistoryEntry.ts b/codex-rs/app-server-protocol/schema/typescript/v2/HistoryEntry.ts new file mode 100644 index 0000000000..347783c674 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/HistoryEntry.ts @@ -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 HistoryEntry = { conversationId: string, ts: number, text: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/HistoryEntryReadParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/HistoryEntryReadParams.ts new file mode 100644 index 0000000000..de1be0351f --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/HistoryEntryReadParams.ts @@ -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 HistoryEntryReadParams = { logId: number, offset: number, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/HistoryEntryReadResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/HistoryEntryReadResponse.ts new file mode 100644 index 0000000000..1c985cf376 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/HistoryEntryReadResponse.ts @@ -0,0 +1,6 @@ +// 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 { HistoryEntry } from "./HistoryEntry"; + +export type HistoryEntryReadResponse = { logId: number, offset: number, entry: HistoryEntry | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpElicitationDecision.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpElicitationDecision.ts new file mode 100644 index 0000000000..7eb6f5c6f3 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpElicitationDecision.ts @@ -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 McpElicitationDecision = "accept" | "decline" | "cancel"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpElicitationRequestParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpElicitationRequestParams.ts new file mode 100644 index 0000000000..109f267605 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpElicitationRequestParams.ts @@ -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 McpElicitationRequestParams = { threadId: string, serverName: string, requestId: string | number, message: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpElicitationRequestResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpElicitationRequestResponse.ts new file mode 100644 index 0000000000..ce5e3ed9da --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpElicitationRequestResponse.ts @@ -0,0 +1,6 @@ +// 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 { McpElicitationDecision } from "./McpElicitationDecision"; + +export type McpElicitationRequestResponse = { decision: McpElicitationDecision, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStartupCompletedNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStartupCompletedNotification.ts new file mode 100644 index 0000000000..b394b1ae9b --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStartupCompletedNotification.ts @@ -0,0 +1,6 @@ +// 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 { McpServerStartupFailure } from "./McpServerStartupFailure"; + +export type McpServerStartupCompletedNotification = { threadId: string, ready: Array, failed: Array, cancelled: Array, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStartupFailure.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStartupFailure.ts new file mode 100644 index 0000000000..a3beea3c66 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStartupFailure.ts @@ -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 McpServerStartupFailure = { server: string, error: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStartupStatus.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStartupStatus.ts new file mode 100644 index 0000000000..e54fd47887 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStartupStatus.ts @@ -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 McpServerStartupStatus = { "state": "starting" } | { "state": "ready" } | { "state": "failed", error: string, } | { "state": "cancelled" }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStartupUpdatedNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStartupUpdatedNotification.ts new file mode 100644 index 0000000000..0b3e7db71b --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStartupUpdatedNotification.ts @@ -0,0 +1,6 @@ +// 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 { McpServerStartupStatus } from "./McpServerStartupStatus"; + +export type McpServerStartupUpdatedNotification = { threadId: string, server: string, status: McpServerStartupStatus, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpToolsListParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpToolsListParams.ts new file mode 100644 index 0000000000..83140f15a5 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpToolsListParams.ts @@ -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 McpToolsListParams = Record; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpToolsListResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpToolsListResponse.ts new file mode 100644 index 0000000000..a9f1298036 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpToolsListResponse.ts @@ -0,0 +1,25 @@ +// 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 { McpAuthStatus } from "../McpAuthStatus"; +import type { Resource } from "../Resource"; +import type { ResourceTemplate } from "../ResourceTemplate"; +import type { Tool } from "../Tool"; + +export type McpToolsListResponse = { +/** + * Fully qualified tool name -> tool definition. + */ +tools: { [key in string]?: Tool }, +/** + * Known resources grouped by server name. + */ +resources: { [key in string]?: Array }, +/** + * Known resource templates grouped by server name. + */ +resourceTemplates: { [key in string]?: Array }, +/** + * Authentication status for each configured MCP server. + */ +authStatuses: { [key in string]?: McpAuthStatus }, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SessionNetworkProxyRuntime.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SessionNetworkProxyRuntime.ts new file mode 100644 index 0000000000..099f6a4966 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/SessionNetworkProxyRuntime.ts @@ -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 SessionNetworkProxyRuntime = { httpAddr: string, socksAddr: string, adminAddr: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SkillsUpdatedNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsUpdatedNotification.ts new file mode 100644 index 0000000000..31dedff4bc --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsUpdatedNotification.ts @@ -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 SkillsUpdatedNotification = { threadId: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/StreamErrorNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/StreamErrorNotification.ts new file mode 100644 index 0000000000..b4caec5a2e --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/StreamErrorNotification.ts @@ -0,0 +1,6 @@ +// 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 { TurnError } from "./TurnError"; + +export type StreamErrorNotification = { threadId: string, turnId: string, error: TurnError, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadConfigReloadParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadConfigReloadParams.ts new file mode 100644 index 0000000000..d2d0bf5fa7 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadConfigReloadParams.ts @@ -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 ThreadConfigReloadParams = { threadId: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadConfigReloadResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadConfigReloadResponse.ts new file mode 100644 index 0000000000..280a812235 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadConfigReloadResponse.ts @@ -0,0 +1,6 @@ +// 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 { ThreadContextSnapshot } from "./ThreadContextSnapshot"; + +export type ThreadConfigReloadResponse = { context: ThreadContextSnapshot, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadContextSnapshot.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadContextSnapshot.ts new file mode 100644 index 0000000000..d8bc5324a8 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadContextSnapshot.ts @@ -0,0 +1,8 @@ +// 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 { ReasoningEffort } from "../ReasoningEffort"; +import type { AskForApproval } from "./AskForApproval"; +import type { SandboxPolicy } from "./SandboxPolicy"; + +export type ThreadContextSnapshot = { threadId: string, model: string, modelProvider: string, cwd: string, approvalPolicy: AskForApproval, sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadContextUpdateParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadContextUpdateParams.ts new file mode 100644 index 0000000000..ea5b8afcfd --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadContextUpdateParams.ts @@ -0,0 +1,11 @@ +// 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 { Personality } from "../Personality"; +import type { ReasoningEffort } from "../ReasoningEffort"; +import type { ReasoningSummary } from "../ReasoningSummary"; +import type { WindowsSandboxLevel } from "../WindowsSandboxLevel"; +import type { AskForApproval } from "./AskForApproval"; +import type { SandboxPolicy } from "./SandboxPolicy"; + +export type ThreadContextUpdateParams = {threadId: string, cwd?: string | null, approvalPolicy?: AskForApproval | null, sandboxPolicy?: SandboxPolicy | null, windowsSandboxLevel?: WindowsSandboxLevel | null, model?: string | null, effort?: ReasoningEffort | null | null, summary?: ReasoningSummary | null, personality?: Personality | null}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadContextUpdateResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadContextUpdateResponse.ts new file mode 100644 index 0000000000..7605374a92 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadContextUpdateResponse.ts @@ -0,0 +1,6 @@ +// 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 { ThreadContextSnapshot } from "./ThreadContextSnapshot"; + +export type ThreadContextUpdateResponse = { context: ThreadContextSnapshot, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts index a46480cb7b..d9984d50e0 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts @@ -4,6 +4,7 @@ import type { ReasoningEffort } from "../ReasoningEffort"; import type { AskForApproval } from "./AskForApproval"; import type { SandboxPolicy } from "./SandboxPolicy"; +import type { SessionNetworkProxyRuntime } from "./SessionNetworkProxyRuntime"; import type { Thread } from "./Thread"; -export type ThreadForkResponse = { thread: Thread, model: string, modelProvider: string, cwd: string, approvalPolicy: AskForApproval, sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null, }; +export type ThreadForkResponse = { thread: Thread, model: string, modelProvider: string, cwd: string, approvalPolicy: AskForApproval, sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null, historyLogId: number, historyEntryCount: number, forkedFromThreadId: string | null, networkProxy: SessionNetworkProxyRuntime | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadMemoriesDropParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadMemoriesDropParams.ts new file mode 100644 index 0000000000..4470bdc091 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadMemoriesDropParams.ts @@ -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 ThreadMemoriesDropParams = { threadId: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadMemoriesDropResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadMemoriesDropResponse.ts new file mode 100644 index 0000000000..9b774e46c7 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadMemoriesDropResponse.ts @@ -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 ThreadMemoriesDropResponse = Record; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadMemoriesUpdateParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadMemoriesUpdateParams.ts new file mode 100644 index 0000000000..f8de4c3132 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadMemoriesUpdateParams.ts @@ -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 ThreadMemoriesUpdateParams = { threadId: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadMemoriesUpdateResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadMemoriesUpdateResponse.ts new file mode 100644 index 0000000000..fcae0408f8 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadMemoriesUpdateResponse.ts @@ -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 ThreadMemoriesUpdateResponse = Record; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts index 6d7a70a6a9..d1ad28cdaf 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts @@ -4,6 +4,7 @@ import type { ReasoningEffort } from "../ReasoningEffort"; import type { AskForApproval } from "./AskForApproval"; import type { SandboxPolicy } from "./SandboxPolicy"; +import type { SessionNetworkProxyRuntime } from "./SessionNetworkProxyRuntime"; import type { Thread } from "./Thread"; -export type ThreadResumeResponse = { thread: Thread, model: string, modelProvider: string, cwd: string, approvalPolicy: AskForApproval, sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null, }; +export type ThreadResumeResponse = { thread: Thread, model: string, modelProvider: string, cwd: string, approvalPolicy: AskForApproval, sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null, historyLogId: number, historyEntryCount: number, forkedFromThreadId: string | null, networkProxy: SessionNetworkProxyRuntime | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadShutdownCompletedNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadShutdownCompletedNotification.ts new file mode 100644 index 0000000000..7a190db643 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadShutdownCompletedNotification.ts @@ -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 ThreadShutdownCompletedNotification = { threadId: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadShutdownParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadShutdownParams.ts new file mode 100644 index 0000000000..cfd1a964b0 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadShutdownParams.ts @@ -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 ThreadShutdownParams = { threadId: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadShutdownResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadShutdownResponse.ts new file mode 100644 index 0000000000..396c5e913d --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadShutdownResponse.ts @@ -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 ThreadShutdownResponse = Record; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts index 4a76f9af20..bc1507d44a 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts @@ -4,6 +4,7 @@ import type { ReasoningEffort } from "../ReasoningEffort"; import type { AskForApproval } from "./AskForApproval"; import type { SandboxPolicy } from "./SandboxPolicy"; +import type { SessionNetworkProxyRuntime } from "./SessionNetworkProxyRuntime"; import type { Thread } from "./Thread"; -export type ThreadStartResponse = { thread: Thread, model: string, modelProvider: string, cwd: string, approvalPolicy: AskForApproval, sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null, }; +export type ThreadStartResponse = { thread: Thread, model: string, modelProvider: string, cwd: string, approvalPolicy: AskForApproval, sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null, historyLogId: number, historyEntryCount: number, forkedFromThreadId: string | null, networkProxy: SessionNetworkProxyRuntime | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadUndoCompletedNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadUndoCompletedNotification.ts new file mode 100644 index 0000000000..1f53982b3f --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadUndoCompletedNotification.ts @@ -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 ThreadUndoCompletedNotification = { threadId: string, turnId: string, success: boolean, message: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadUndoParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadUndoParams.ts new file mode 100644 index 0000000000..6e8a5aa0d3 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadUndoParams.ts @@ -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 ThreadUndoParams = { threadId: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadUndoResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadUndoResponse.ts new file mode 100644 index 0000000000..114c13f7cf --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadUndoResponse.ts @@ -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 ThreadUndoResponse = Record; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadUndoStartedNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadUndoStartedNotification.ts new file mode 100644 index 0000000000..8331da4d74 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadUndoStartedNotification.ts @@ -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 ThreadUndoStartedNotification = { threadId: string, turnId: string, message: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadUserShellCommandRunParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadUserShellCommandRunParams.ts new file mode 100644 index 0000000000..5e98a7cd9c --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadUserShellCommandRunParams.ts @@ -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 ThreadUserShellCommandRunParams = { threadId: string, command: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadUserShellCommandRunResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadUserShellCommandRunResponse.ts new file mode 100644 index 0000000000..b5665af6d0 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadUserShellCommandRunResponse.ts @@ -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 ThreadUserShellCommandRunResponse = { turnId: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/WarningNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/WarningNotification.ts new file mode 100644 index 0000000000..48a51a5731 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/WarningNotification.ts @@ -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 WarningNotification = { threadId: string, turnId: string, message: string, }; 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 ce2289c382..3d232bd4ec 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -19,6 +19,7 @@ export type { AppsDefaultConfig } from "./AppsDefaultConfig"; export type { AppsListParams } from "./AppsListParams"; export type { AppsListResponse } from "./AppsListResponse"; export type { AskForApproval } from "./AskForApproval"; +export type { BackgroundEventNotification } from "./BackgroundEventNotification"; export type { ByteRange } from "./ByteRange"; export type { CancelLoginAccountParams } from "./CancelLoginAccountParams"; export type { CancelLoginAccountResponse } from "./CancelLoginAccountResponse"; @@ -54,6 +55,9 @@ export type { ConfigWarningNotification } from "./ConfigWarningNotification"; export type { ConfigWriteResponse } from "./ConfigWriteResponse"; export type { ContextCompactedNotification } from "./ContextCompactedNotification"; export type { CreditsSnapshot } from "./CreditsSnapshot"; +export type { CustomPrompt } from "./CustomPrompt"; +export type { CustomPromptListParams } from "./CustomPromptListParams"; +export type { CustomPromptListResponse } from "./CustomPromptListResponse"; export type { DeprecationNoticeNotification } from "./DeprecationNoticeNotification"; export type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem"; export type { DynamicToolCallParams } from "./DynamicToolCallParams"; @@ -77,6 +81,11 @@ export type { GetAccountRateLimitsResponse } from "./GetAccountRateLimitsRespons export type { GetAccountResponse } from "./GetAccountResponse"; export type { GitInfo } from "./GitInfo"; export type { HazelnutScope } from "./HazelnutScope"; +export type { HistoryAppendParams } from "./HistoryAppendParams"; +export type { HistoryAppendResponse } from "./HistoryAppendResponse"; +export type { HistoryEntry } from "./HistoryEntry"; +export type { HistoryEntryReadParams } from "./HistoryEntryReadParams"; +export type { HistoryEntryReadResponse } from "./HistoryEntryReadResponse"; export type { ItemCompletedNotification } from "./ItemCompletedNotification"; export type { ItemStartedNotification } from "./ItemStartedNotification"; export type { ListMcpServerStatusParams } from "./ListMcpServerStatusParams"; @@ -85,15 +94,24 @@ export type { LoginAccountParams } from "./LoginAccountParams"; export type { LoginAccountResponse } from "./LoginAccountResponse"; export type { LogoutAccountResponse } from "./LogoutAccountResponse"; export type { McpAuthStatus } from "./McpAuthStatus"; +export type { McpElicitationDecision } from "./McpElicitationDecision"; +export type { McpElicitationRequestParams } from "./McpElicitationRequestParams"; +export type { McpElicitationRequestResponse } from "./McpElicitationRequestResponse"; export type { McpServerOauthLoginCompletedNotification } from "./McpServerOauthLoginCompletedNotification"; export type { McpServerOauthLoginParams } from "./McpServerOauthLoginParams"; export type { McpServerOauthLoginResponse } from "./McpServerOauthLoginResponse"; export type { McpServerRefreshResponse } from "./McpServerRefreshResponse"; +export type { McpServerStartupCompletedNotification } from "./McpServerStartupCompletedNotification"; +export type { McpServerStartupFailure } from "./McpServerStartupFailure"; +export type { McpServerStartupStatus } from "./McpServerStartupStatus"; +export type { McpServerStartupUpdatedNotification } from "./McpServerStartupUpdatedNotification"; export type { McpServerStatus } from "./McpServerStatus"; export type { McpToolCallError } from "./McpToolCallError"; export type { McpToolCallProgressNotification } from "./McpToolCallProgressNotification"; export type { McpToolCallResult } from "./McpToolCallResult"; export type { McpToolCallStatus } from "./McpToolCallStatus"; +export type { McpToolsListParams } from "./McpToolsListParams"; +export type { McpToolsListResponse } from "./McpToolsListResponse"; export type { MergeStrategy } from "./MergeStrategy"; export type { Model } from "./Model"; export type { ModelListParams } from "./ModelListParams"; @@ -127,6 +145,7 @@ export type { ReviewTarget } from "./ReviewTarget"; export type { SandboxMode } from "./SandboxMode"; export type { SandboxPolicy } from "./SandboxPolicy"; export type { SandboxWorkspaceWrite } from "./SandboxWorkspaceWrite"; +export type { SessionNetworkProxyRuntime } from "./SessionNetworkProxyRuntime"; export type { SessionSource } from "./SessionSource"; export type { SkillDependencies } from "./SkillDependencies"; export type { SkillErrorInfo } from "./SkillErrorInfo"; @@ -144,6 +163,8 @@ export type { SkillsRemoteReadParams } from "./SkillsRemoteReadParams"; export type { SkillsRemoteReadResponse } from "./SkillsRemoteReadResponse"; export type { SkillsRemoteWriteParams } from "./SkillsRemoteWriteParams"; export type { SkillsRemoteWriteResponse } from "./SkillsRemoteWriteResponse"; +export type { SkillsUpdatedNotification } from "./SkillsUpdatedNotification"; +export type { StreamErrorNotification } from "./StreamErrorNotification"; export type { TerminalInteractionNotification } from "./TerminalInteractionNotification"; export type { TextElement } from "./TextElement"; export type { TextPosition } from "./TextPosition"; @@ -155,6 +176,11 @@ export type { ThreadArchiveResponse } from "./ThreadArchiveResponse"; export type { ThreadArchivedNotification } from "./ThreadArchivedNotification"; export type { ThreadCompactStartParams } from "./ThreadCompactStartParams"; export type { ThreadCompactStartResponse } from "./ThreadCompactStartResponse"; +export type { ThreadConfigReloadParams } from "./ThreadConfigReloadParams"; +export type { ThreadConfigReloadResponse } from "./ThreadConfigReloadResponse"; +export type { ThreadContextSnapshot } from "./ThreadContextSnapshot"; +export type { ThreadContextUpdateParams } from "./ThreadContextUpdateParams"; +export type { ThreadContextUpdateResponse } from "./ThreadContextUpdateResponse"; export type { ThreadForkParams } from "./ThreadForkParams"; export type { ThreadForkResponse } from "./ThreadForkResponse"; export type { ThreadItem } from "./ThreadItem"; @@ -162,6 +188,10 @@ export type { ThreadListParams } from "./ThreadListParams"; export type { ThreadListResponse } from "./ThreadListResponse"; export type { ThreadLoadedListParams } from "./ThreadLoadedListParams"; export type { ThreadLoadedListResponse } from "./ThreadLoadedListResponse"; +export type { ThreadMemoriesDropParams } from "./ThreadMemoriesDropParams"; +export type { ThreadMemoriesDropResponse } from "./ThreadMemoriesDropResponse"; +export type { ThreadMemoriesUpdateParams } from "./ThreadMemoriesUpdateParams"; +export type { ThreadMemoriesUpdateResponse } from "./ThreadMemoriesUpdateResponse"; export type { ThreadNameUpdatedNotification } from "./ThreadNameUpdatedNotification"; export type { ThreadReadParams } from "./ThreadReadParams"; export type { ThreadReadResponse } from "./ThreadReadResponse"; @@ -171,6 +201,9 @@ export type { ThreadRollbackParams } from "./ThreadRollbackParams"; export type { ThreadRollbackResponse } from "./ThreadRollbackResponse"; export type { ThreadSetNameParams } from "./ThreadSetNameParams"; export type { ThreadSetNameResponse } from "./ThreadSetNameResponse"; +export type { ThreadShutdownCompletedNotification } from "./ThreadShutdownCompletedNotification"; +export type { ThreadShutdownParams } from "./ThreadShutdownParams"; +export type { ThreadShutdownResponse } from "./ThreadShutdownResponse"; export type { ThreadSortKey } from "./ThreadSortKey"; export type { ThreadSourceKind } from "./ThreadSourceKind"; export type { ThreadStartParams } from "./ThreadStartParams"; @@ -183,6 +216,12 @@ export type { ThreadTokenUsageUpdatedNotification } from "./ThreadTokenUsageUpda export type { ThreadUnarchiveParams } from "./ThreadUnarchiveParams"; export type { ThreadUnarchiveResponse } from "./ThreadUnarchiveResponse"; export type { ThreadUnarchivedNotification } from "./ThreadUnarchivedNotification"; +export type { ThreadUndoCompletedNotification } from "./ThreadUndoCompletedNotification"; +export type { ThreadUndoParams } from "./ThreadUndoParams"; +export type { ThreadUndoResponse } from "./ThreadUndoResponse"; +export type { ThreadUndoStartedNotification } from "./ThreadUndoStartedNotification"; +export type { ThreadUserShellCommandRunParams } from "./ThreadUserShellCommandRunParams"; +export type { ThreadUserShellCommandRunResponse } from "./ThreadUserShellCommandRunResponse"; export type { TokenUsageBreakdown } from "./TokenUsageBreakdown"; export type { ToolRequestUserInputAnswer } from "./ToolRequestUserInputAnswer"; export type { ToolRequestUserInputOption } from "./ToolRequestUserInputOption"; @@ -206,6 +245,7 @@ export type { TurnStatus } from "./TurnStatus"; export type { TurnSteerParams } from "./TurnSteerParams"; export type { TurnSteerResponse } from "./TurnSteerResponse"; export type { UserInput } from "./UserInput"; +export type { WarningNotification } from "./WarningNotification"; export type { WebSearchAction } from "./WebSearchAction"; export type { WindowsSandboxSetupCompletedNotification } from "./WindowsSandboxSetupCompletedNotification"; export type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index a9ce27ed0d..41e40390ba 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -223,6 +223,35 @@ client_request_definitions! { params: v2::ThreadRollbackParams, response: v2::ThreadRollbackResponse, }, + ThreadContextUpdate => "thread/context/update" { + params: v2::ThreadContextUpdateParams, + inspect_params: true, + response: v2::ThreadContextUpdateResponse, + }, + ThreadShutdown => "thread/shutdown" { + params: v2::ThreadShutdownParams, + response: v2::ThreadShutdownResponse, + }, + ThreadUndo => "thread/undo" { + params: v2::ThreadUndoParams, + response: v2::ThreadUndoResponse, + }, + ThreadConfigReload => "thread/config/reload" { + params: v2::ThreadConfigReloadParams, + response: v2::ThreadConfigReloadResponse, + }, + ThreadMemoriesDrop => "thread/memories/drop" { + params: v2::ThreadMemoriesDropParams, + response: v2::ThreadMemoriesDropResponse, + }, + ThreadMemoriesUpdate => "thread/memories/update" { + params: v2::ThreadMemoriesUpdateParams, + response: v2::ThreadMemoriesUpdateResponse, + }, + ThreadUserShellCommandRun => "thread/userShellCommand/run" { + params: v2::ThreadUserShellCommandRunParams, + response: v2::ThreadUserShellCommandRunResponse, + }, ThreadList => "thread/list" { params: v2::ThreadListParams, response: v2::ThreadListResponse, @@ -235,6 +264,22 @@ client_request_definitions! { params: v2::ThreadReadParams, response: v2::ThreadReadResponse, }, + CustomPromptList => "customPrompt/list" { + params: v2::CustomPromptListParams, + response: v2::CustomPromptListResponse, + }, + McpToolsList => "mcpTools/list" { + params: v2::McpToolsListParams, + response: v2::McpToolsListResponse, + }, + HistoryAppend => "history/append" { + params: v2::HistoryAppendParams, + response: v2::HistoryAppendResponse, + }, + HistoryEntryRead => "history/entry/read" { + params: v2::HistoryEntryReadParams, + response: v2::HistoryEntryReadResponse, + }, SkillsList => "skills/list" { params: v2::SkillsListParams, response: v2::SkillsListResponse, @@ -671,6 +716,12 @@ server_request_definitions! { response: v2::ToolRequestUserInputResponse, }, + /// Request the user to resolve an MCP elicitation prompt. + McpElicitationRequest => "item/mcpElicitation/requestDecision" { + params: v2::McpElicitationRequestParams, + response: v2::McpElicitationRequestResponse, + }, + /// Execute a dynamic tool call on the client. DynamicToolCall => "item/tool/call" { params: v2::DynamicToolCallParams, @@ -773,14 +824,19 @@ pub struct FuzzyFileSearchSessionCompletedNotification { server_notification_definitions! { /// NEW NOTIFICATIONS Error => "error" (v2::ErrorNotification), + Warning => "warning" (v2::WarningNotification), ThreadStarted => "thread/started" (v2::ThreadStartedNotification), ThreadStatusChanged => "thread/status/changed" (v2::ThreadStatusChangedNotification), ThreadArchived => "thread/archived" (v2::ThreadArchivedNotification), ThreadUnarchived => "thread/unarchived" (v2::ThreadUnarchivedNotification), ThreadNameUpdated => "thread/name/updated" (v2::ThreadNameUpdatedNotification), ThreadTokenUsageUpdated => "thread/tokenUsage/updated" (v2::ThreadTokenUsageUpdatedNotification), + ThreadShutdownCompleted => "thread/shutdown/completed" (v2::ThreadShutdownCompletedNotification), + ThreadUndoStarted => "thread/undo/started" (v2::ThreadUndoStartedNotification), + ThreadUndoCompleted => "thread/undo/completed" (v2::ThreadUndoCompletedNotification), TurnStarted => "turn/started" (v2::TurnStartedNotification), TurnCompleted => "turn/completed" (v2::TurnCompletedNotification), + StreamError => "turn/streamError" (v2::StreamErrorNotification), TurnDiffUpdated => "turn/diff/updated" (v2::TurnDiffUpdatedNotification), TurnPlanUpdated => "turn/plan/updated" (v2::TurnPlanUpdatedNotification), ItemStarted => "item/started" (v2::ItemStartedNotification), @@ -794,7 +850,10 @@ server_notification_definitions! { TerminalInteraction => "item/commandExecution/terminalInteraction" (v2::TerminalInteractionNotification), FileChangeOutputDelta => "item/fileChange/outputDelta" (v2::FileChangeOutputDeltaNotification), McpToolCallProgress => "item/mcpToolCall/progress" (v2::McpToolCallProgressNotification), + McpServerStartupUpdated => "mcpServer/startup/updated" (v2::McpServerStartupUpdatedNotification), + McpServerStartupCompleted => "mcpServer/startup/completed" (v2::McpServerStartupCompletedNotification), McpServerOauthLoginCompleted => "mcpServer/oauthLogin/completed" (v2::McpServerOauthLoginCompletedNotification), + SkillsUpdated => "skills/updated" (v2::SkillsUpdatedNotification), AccountUpdated => "account/updated" (v2::AccountUpdatedNotification), AccountRateLimitsUpdated => "account/rateLimits/updated" (v2::AccountRateLimitsUpdatedNotification), AppListUpdated => "app/list/updated" (v2::AppListUpdatedNotification), @@ -805,6 +864,7 @@ server_notification_definitions! { ContextCompacted => "thread/compacted" (v2::ContextCompactedNotification), ModelRerouted => "model/rerouted" (v2::ModelReroutedNotification), DeprecationNotice => "deprecationNotice" (v2::DeprecationNoticeNotification), + BackgroundEvent => "backgroundEvent" (v2::BackgroundEventNotification), ConfigWarning => "configWarning" (v2::ConfigWarningNotification), FuzzyFileSearchSessionUpdated => "fuzzyFileSearch/sessionUpdated" (FuzzyFileSearchSessionUpdatedNotification), FuzzyFileSearchSessionCompleted => "fuzzyFileSearch/sessionCompleted" (FuzzyFileSearchSessionCompletedNotification), diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 43038d351c..500bb1a3b3 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -1,9 +1,11 @@ use std::collections::HashMap; use std::path::PathBuf; +use crate::RequestId; use crate::protocol::common::AuthMode; use codex_experimental_api_macros::ExperimentalApi; use codex_protocol::account::PlanType; +use codex_protocol::approvals::ElicitationAction as CoreElicitationAction; use codex_protocol::approvals::ExecPolicyAmendment as CoreExecPolicyAmendment; use codex_protocol::approvals::NetworkApprovalContext as CoreNetworkApprovalContext; use codex_protocol::approvals::NetworkApprovalProtocol as CoreNetworkApprovalProtocol; @@ -15,11 +17,14 @@ use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::SandboxMode as CoreSandboxMode; use codex_protocol::config_types::Verbosity; use codex_protocol::config_types::WebSearchMode; +use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::custom_prompts::CustomPrompt as CoreCustomPrompt; use codex_protocol::items::AgentMessageContent as CoreAgentMessageContent; use codex_protocol::items::TurnItem as CoreTurnItem; use codex_protocol::mcp::Resource as McpResource; use codex_protocol::mcp::ResourceTemplate as McpResourceTemplate; use codex_protocol::mcp::Tool as McpTool; +use codex_protocol::message_history::HistoryEntry as CoreHistoryEntry; use codex_protocol::models::MessagePhase; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::InputModality; @@ -33,6 +38,10 @@ use codex_protocol::protocol::AskForApproval as CoreAskForApproval; use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo; use codex_protocol::protocol::CreditsSnapshot as CoreCreditsSnapshot; use codex_protocol::protocol::ExecCommandStatus as CoreExecCommandStatus; +use codex_protocol::protocol::McpAuthStatus as CoreMcpAuthStatus; +use codex_protocol::protocol::McpListToolsResponseEvent as CoreMcpListToolsResponseEvent; +use codex_protocol::protocol::McpStartupFailure as CoreMcpStartupFailure; +use codex_protocol::protocol::McpStartupStatus as CoreMcpStartupStatus; use codex_protocol::protocol::ModelRerouteReason as CoreModelRerouteReason; use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess; use codex_protocol::protocol::PatchApplyStatus as CorePatchApplyStatus; @@ -40,6 +49,7 @@ use codex_protocol::protocol::RateLimitSnapshot as CoreRateLimitSnapshot; use codex_protocol::protocol::RateLimitWindow as CoreRateLimitWindow; use codex_protocol::protocol::ReadOnlyAccess as CoreReadOnlyAccess; use codex_protocol::protocol::RejectConfig as CoreRejectConfig; +use codex_protocol::protocol::SessionNetworkProxyRuntime as CoreSessionNetworkProxyRuntime; use codex_protocol::protocol::SessionSource as CoreSessionSource; use codex_protocol::protocol::SkillDependencies as CoreSkillDependencies; use codex_protocol::protocol::SkillErrorInfo as CoreSkillErrorInfo; @@ -1616,6 +1626,12 @@ pub struct ThreadStartResponse { pub approval_policy: AskForApproval, pub sandbox: SandboxPolicy, pub reasoning_effort: Option, + #[ts(type = "number")] + pub history_log_id: u64, + #[ts(type = "number")] + pub history_entry_count: usize, + pub forked_from_thread_id: Option, + pub network_proxy: Option, } #[derive( @@ -1685,6 +1701,12 @@ pub struct ThreadResumeResponse { pub approval_policy: AskForApproval, pub sandbox: SandboxPolicy, pub reasoning_effort: Option, + #[ts(type = "number")] + pub history_log_id: u64, + #[ts(type = "number")] + pub history_entry_count: usize, + pub forked_from_thread_id: Option, + pub network_proxy: Option, } #[derive( @@ -1743,6 +1765,12 @@ pub struct ThreadForkResponse { pub approval_policy: AskForApproval, pub sandbox: SandboxPolicy, pub reasoning_effort: Option, + #[ts(type = "number")] + pub history_log_id: u64, + #[ts(type = "number")] + pub history_entry_count: usize, + pub forked_from_thread_id: Option, + pub network_proxy: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] @@ -1962,6 +1990,269 @@ pub struct ThreadReadResponse { pub thread: Thread, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SessionNetworkProxyRuntime { + pub http_addr: String, + pub socks_addr: String, + pub admin_addr: String, +} + +impl From for SessionNetworkProxyRuntime { + fn from(value: CoreSessionNetworkProxyRuntime) -> Self { + Self { + http_addr: value.http_addr, + socks_addr: value.socks_addr, + admin_addr: value.admin_addr, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadContextSnapshot { + pub thread_id: String, + pub model: String, + pub model_provider: String, + pub cwd: PathBuf, + pub approval_policy: AskForApproval, + pub sandbox: SandboxPolicy, + pub reasoning_effort: Option, +} + +#[derive( + Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS, ExperimentalApi, +)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadContextUpdateParams { + pub thread_id: String, + #[ts(optional = nullable)] + pub cwd: Option, + #[ts(optional = nullable)] + pub approval_policy: Option, + #[ts(optional = nullable)] + pub sandbox_policy: Option, + #[ts(optional = nullable)] + pub windows_sandbox_level: Option, + #[ts(optional = nullable)] + pub model: Option, + #[ts(optional = nullable)] + pub effort: Option>, + #[ts(optional = nullable)] + pub summary: Option, + #[ts(optional = nullable)] + pub personality: Option, + #[experimental("thread/context/update.collaborationMode")] + #[ts(optional = nullable)] + pub collaboration_mode: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadContextUpdateResponse { + pub context: ThreadContextSnapshot, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadShutdownParams { + pub thread_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, Default)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadShutdownResponse {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadUndoParams { + pub thread_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, Default)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadUndoResponse {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadConfigReloadParams { + pub thread_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadConfigReloadResponse { + pub context: ThreadContextSnapshot, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadMemoriesDropParams { + pub thread_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, Default)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadMemoriesDropResponse {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadMemoriesUpdateParams { + pub thread_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, Default)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadMemoriesUpdateResponse {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadUserShellCommandRunParams { + pub thread_id: String, + pub command: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadUserShellCommandRunResponse { + pub turn_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct HistoryAppendParams { + pub thread_id: String, + pub text: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, Default)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct HistoryAppendResponse {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct HistoryEntryReadParams { + #[ts(type = "number")] + pub log_id: u64, + #[ts(type = "number")] + pub offset: usize, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct HistoryEntry { + pub conversation_id: String, + #[ts(type = "number")] + pub ts: u64, + pub text: String, +} + +impl From for HistoryEntry { + fn from(value: CoreHistoryEntry) -> Self { + Self { + conversation_id: value.conversation_id, + ts: value.ts, + text: value.text, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct HistoryEntryReadResponse { + #[ts(type = "number")] + pub log_id: u64, + #[ts(type = "number")] + pub offset: usize, + pub entry: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, Default)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct CustomPromptListParams {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct CustomPrompt { + pub name: String, + pub path: PathBuf, + pub content: String, + pub description: Option, + pub argument_hint: Option, +} + +impl From for CustomPrompt { + fn from(value: CoreCustomPrompt) -> Self { + Self { + name: value.name, + path: value.path, + content: value.content, + description: value.description, + argument_hint: value.argument_hint, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct CustomPromptListResponse { + pub data: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, Default)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct McpToolsListParams {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct McpToolsListResponse { + /// Fully qualified tool name -> tool definition. + pub tools: HashMap, + /// Known resources grouped by server name. + pub resources: HashMap>, + /// Known resource templates grouped by server name. + pub resource_templates: HashMap>, + /// Authentication status for each configured MCP server. + pub auth_statuses: HashMap, +} + +impl From for McpToolsListResponse { + fn from(value: CoreMcpListToolsResponseEvent) -> Self { + Self { + tools: value.tools, + resources: value.resources, + resource_templates: value.resource_templates, + auth_statuses: value.auth_statuses, + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -3350,6 +3641,124 @@ pub struct WindowsSandboxSetupCompletedNotification { pub error: Option, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct WarningNotification { + pub thread_id: String, + pub turn_id: String, + pub message: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct BackgroundEventNotification { + pub thread_id: String, + pub turn_id: String, + pub message: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct StreamErrorNotification { + pub thread_id: String, + pub turn_id: String, + pub error: TurnError, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadUndoStartedNotification { + pub thread_id: String, + pub turn_id: String, + pub message: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadUndoCompletedNotification { + pub thread_id: String, + pub turn_id: String, + pub success: bool, + pub message: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase", tag = "state")] +#[ts(rename_all = "camelCase", tag = "state")] +#[ts(export_to = "v2/")] +pub enum McpServerStartupStatus { + Starting, + Ready, + Failed { error: String }, + Cancelled, +} + +impl From for McpServerStartupStatus { + fn from(value: CoreMcpStartupStatus) -> Self { + match value { + CoreMcpStartupStatus::Starting => Self::Starting, + CoreMcpStartupStatus::Ready => Self::Ready, + CoreMcpStartupStatus::Failed { error } => Self::Failed { error }, + CoreMcpStartupStatus::Cancelled => Self::Cancelled, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct McpServerStartupFailure { + pub server: String, + pub error: String, +} + +impl From for McpServerStartupFailure { + fn from(value: CoreMcpStartupFailure) -> Self { + Self { + server: value.server, + error: value.error, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct McpServerStartupUpdatedNotification { + pub thread_id: String, + pub server: String, + pub status: McpServerStartupStatus, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct McpServerStartupCompletedNotification { + pub thread_id: String, + pub ready: Vec, + pub failed: Vec, + pub cancelled: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SkillsUpdatedNotification { + pub thread_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadShutdownCompletedNotification { + pub thread_id: String, +} + /// Deprecated: Use `ContextCompaction` item type instead. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] @@ -3525,6 +3934,54 @@ pub struct ToolRequestUserInputResponse { pub answers: HashMap, } +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum McpElicitationDecision { + Accept, + Decline, + Cancel, +} + +impl McpElicitationDecision { + pub fn to_core(self) -> CoreElicitationAction { + match self { + Self::Accept => CoreElicitationAction::Accept, + Self::Decline => CoreElicitationAction::Decline, + Self::Cancel => CoreElicitationAction::Cancel, + } + } +} + +impl From for McpElicitationDecision { + fn from(value: CoreElicitationAction) -> Self { + match value { + CoreElicitationAction::Accept => Self::Accept, + CoreElicitationAction::Decline => Self::Decline, + CoreElicitationAction::Cancel => Self::Cancel, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct McpElicitationRequestParams { + pub thread_id: String, + pub server_name: String, + #[ts(type = "string | number")] + pub request_id: RequestId, + pub message: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct McpElicitationRequestResponse { + pub decision: McpElicitationDecision, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index fe3eb96852..96a5ff3598 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -40,6 +40,11 @@ use codex_app_server_protocol::InterruptConversationResponse; use codex_app_server_protocol::ItemCompletedNotification; use codex_app_server_protocol::ItemStartedNotification; use codex_app_server_protocol::JSONRPCErrorError; +use codex_app_server_protocol::McpElicitationDecision; +use codex_app_server_protocol::McpElicitationRequestParams; +use codex_app_server_protocol::McpElicitationRequestResponse; +use codex_app_server_protocol::McpServerStartupCompletedNotification; +use codex_app_server_protocol::McpServerStartupUpdatedNotification; use codex_app_server_protocol::McpToolCallError; use codex_app_server_protocol::McpToolCallResult; use codex_app_server_protocol::McpToolCallStatus; @@ -51,14 +56,20 @@ use codex_app_server_protocol::RawResponseItemCompletedNotification; use codex_app_server_protocol::ReasoningSummaryPartAddedNotification; use codex_app_server_protocol::ReasoningSummaryTextDeltaNotification; use codex_app_server_protocol::ReasoningTextDeltaNotification; +use codex_app_server_protocol::RequestId as AppServerRequestId; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ServerRequestPayload; +use codex_app_server_protocol::SkillsUpdatedNotification; +use codex_app_server_protocol::StreamErrorNotification; use codex_app_server_protocol::TerminalInteractionNotification; use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::ThreadNameUpdatedNotification; use codex_app_server_protocol::ThreadRollbackResponse; +use codex_app_server_protocol::ThreadShutdownCompletedNotification; use codex_app_server_protocol::ThreadTokenUsage; use codex_app_server_protocol::ThreadTokenUsageUpdatedNotification; +use codex_app_server_protocol::ThreadUndoCompletedNotification; +use codex_app_server_protocol::ThreadUndoStartedNotification; use codex_app_server_protocol::ToolRequestUserInputOption; use codex_app_server_protocol::ToolRequestUserInputParams; use codex_app_server_protocol::ToolRequestUserInputQuestion; @@ -71,6 +82,7 @@ use codex_app_server_protocol::TurnInterruptResponse; use codex_app_server_protocol::TurnPlanStep; use codex_app_server_protocol::TurnPlanUpdatedNotification; use codex_app_server_protocol::TurnStatus; +use codex_app_server_protocol::WarningNotification; use codex_app_server_protocol::build_turns_from_rollout_items; use codex_app_server_protocol::convert_patch_changes; use codex_core::CodexThread; @@ -79,6 +91,7 @@ use codex_core::find_thread_name_by_id; use codex_core::review_format::format_review_findings_block; use codex_core::review_prompts; use codex_protocol::ThreadId; +use codex_protocol::approvals::ElicitationRequestEvent; use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem; use codex_protocol::dynamic_tools::DynamicToolResponse as CoreDynamicToolResponse; use codex_protocol::plan_tool::UpdatePlanArgs; @@ -151,7 +164,18 @@ pub(crate) async fn apply_bespoke_event_handling( .await; handle_turn_complete(conversation_id, event_turn_id, &outgoing, &thread_state).await; } - EventMsg::Warning(_warning_event) => {} + EventMsg::Warning(warning_event) => { + if let ApiVersion::V2 = api_version { + let notification = WarningNotification { + thread_id: conversation_id.to_string(), + turn_id: event_turn_id.clone(), + message: warning_event.message, + }; + outgoing + .send_server_notification(ServerNotification::Warning(notification)) + .await; + } + } EventMsg::ModelReroute(event) => { if let ApiVersion::V2 = api_version { let notification = ModelReroutedNotification { @@ -361,6 +385,43 @@ pub(crate) async fn apply_bespoke_event_handling( } } } + EventMsg::ElicitationRequest(ElicitationRequestEvent { + server_name, + id, + message, + }) => { + if matches!(api_version, ApiVersion::V2) { + let permission_guard = thread_watch_manager + .note_permission_requested(&conversation_id.to_string()) + .await; + let params = McpElicitationRequestParams { + thread_id: conversation_id.to_string(), + server_name: server_name.clone(), + request_id: match id.clone() { + codex_protocol::mcp::RequestId::String(value) => { + AppServerRequestId::String(value) + } + codex_protocol::mcp::RequestId::Integer(value) => { + AppServerRequestId::Integer(value) + } + }, + message, + }; + let rx = outgoing + .send_request(ServerRequestPayload::McpElicitationRequest(params)) + .await; + tokio::spawn(async move { + on_mcp_elicitation_response( + server_name, + id, + rx, + conversation, + permission_guard, + ) + .await; + }); + } + } EventMsg::RequestUserInput(request) => { if matches!(api_version, ApiVersion::V2) { let user_input_guard = thread_watch_manager @@ -863,12 +924,101 @@ pub(crate) async fn apply_bespoke_event_handling( }; outgoing .send_server_notification(ServerNotification::Error(ErrorNotification { - error: turn_error, + error: turn_error.clone(), will_retry: true, thread_id: conversation_id.to_string(), turn_id: event_turn_id.clone(), })) .await; + if let ApiVersion::V2 = api_version { + outgoing + .send_server_notification(ServerNotification::StreamError( + StreamErrorNotification { + thread_id: conversation_id.to_string(), + turn_id: event_turn_id.clone(), + error: turn_error, + }, + )) + .await; + } + } + EventMsg::McpStartupUpdate(ev) => { + if let ApiVersion::V2 = api_version { + outgoing + .send_server_notification(ServerNotification::McpServerStartupUpdated( + McpServerStartupUpdatedNotification { + thread_id: conversation_id.to_string(), + server: ev.server, + status: ev.status.into(), + }, + )) + .await; + } + } + EventMsg::McpStartupComplete(ev) => { + if let ApiVersion::V2 = api_version { + outgoing + .send_server_notification(ServerNotification::McpServerStartupCompleted( + McpServerStartupCompletedNotification { + thread_id: conversation_id.to_string(), + ready: ev.ready, + failed: ev.failed.into_iter().map(Into::into).collect(), + cancelled: ev.cancelled, + }, + )) + .await; + } + } + EventMsg::BackgroundEvent(ev) => { + if let ApiVersion::V2 = api_version { + outgoing + .send_server_notification(ServerNotification::BackgroundEvent( + codex_app_server_protocol::BackgroundEventNotification { + thread_id: conversation_id.to_string(), + turn_id: event_turn_id.clone(), + message: ev.message, + }, + )) + .await; + } + } + EventMsg::UndoStarted(ev) => { + if let ApiVersion::V2 = api_version { + outgoing + .send_server_notification(ServerNotification::ThreadUndoStarted( + ThreadUndoStartedNotification { + thread_id: conversation_id.to_string(), + turn_id: event_turn_id.clone(), + message: ev.message, + }, + )) + .await; + } + } + EventMsg::UndoCompleted(ev) => { + if let ApiVersion::V2 = api_version { + outgoing + .send_server_notification(ServerNotification::ThreadUndoCompleted( + ThreadUndoCompletedNotification { + thread_id: conversation_id.to_string(), + turn_id: event_turn_id.clone(), + success: ev.success, + message: ev.message, + }, + )) + .await; + } + } + EventMsg::SkillsUpdateAvailable => { + if let ApiVersion::V2 = api_version { + outgoing + .send_server_notification(ServerNotification::SkillsUpdated( + SkillsUpdatedNotification { + thread_id: conversation_id.to_string(), + }, + )) + .await; + } } EventMsg::ViewImageToolCall(view_image_event) => { let item = ThreadItem::ImageView { @@ -1306,6 +1456,15 @@ pub(crate) async fn apply_bespoke_event_handling( thread_watch_manager .note_thread_shutdown(&conversation_id.to_string()) .await; + if let ApiVersion::V2 = api_version { + outgoing + .send_server_notification(ServerNotification::ThreadShutdownCompleted( + ThreadShutdownCompletedNotification { + thread_id: conversation_id.to_string(), + }, + )) + .await; + } } _ => {} @@ -1734,6 +1893,45 @@ async fn on_request_user_input_response( } } +async fn on_mcp_elicitation_response( + server_name: String, + request_id: codex_protocol::mcp::RequestId, + receiver: oneshot::Receiver, + conversation: Arc, + permission_guard: ThreadWatchActiveGuard, +) { + let response = receiver.await; + drop(permission_guard); + + let decision = match response { + Ok(Ok(value)) => serde_json::from_value::(value) + .map(|response| response.decision) + .unwrap_or_else(|err| { + error!("failed to deserialize McpElicitationRequestResponse: {err}"); + McpElicitationDecision::Cancel + }), + Ok(Err(err)) => { + error!("elicitation request failed with client error: {err:?}"); + McpElicitationDecision::Cancel + } + Err(err) => { + error!("elicitation request failed: {err:?}"); + McpElicitationDecision::Cancel + } + }; + + if let Err(err) = conversation + .submit(Op::ResolveElicitation { + server_name, + request_id, + decision: decision.to_core(), + }) + .await + { + error!("failed to submit ResolveElicitation: {err}"); + } +} + const REVIEW_FALLBACK_MESSAGE: &str = "Reviewer failed to output a response."; fn render_review_output_text(output: &ReviewOutputEvent) -> String { diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 9e53acd636..efe0fe0ed3 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -39,6 +39,8 @@ use codex_app_server_protocol::CollaborationModeListResponse; use codex_app_server_protocol::CommandExecParams; use codex_app_server_protocol::ConversationGitInfo; use codex_app_server_protocol::ConversationSummary; +use codex_app_server_protocol::CustomPromptListParams; +use codex_app_server_protocol::CustomPromptListResponse; use codex_app_server_protocol::DynamicToolSpec as ApiDynamicToolSpec; use codex_app_server_protocol::ExecOneOffCommandResponse; use codex_app_server_protocol::ExperimentalFeature as ApiExperimentalFeature; @@ -69,6 +71,10 @@ use codex_app_server_protocol::GetUserSavedConfigResponse; use codex_app_server_protocol::GitDiffToRemoteResponse; use codex_app_server_protocol::GitInfo as ApiGitInfo; use codex_app_server_protocol::HazelnutScope as ApiHazelnutScope; +use codex_app_server_protocol::HistoryAppendParams; +use codex_app_server_protocol::HistoryAppendResponse; +use codex_app_server_protocol::HistoryEntryReadParams; +use codex_app_server_protocol::HistoryEntryReadResponse; use codex_app_server_protocol::InputItem as WireInputItem; use codex_app_server_protocol::InterruptConversationParams; use codex_app_server_protocol::JSONRPCErrorError; @@ -89,6 +95,8 @@ use codex_app_server_protocol::McpServerOauthLoginParams; use codex_app_server_protocol::McpServerOauthLoginResponse; use codex_app_server_protocol::McpServerRefreshResponse; use codex_app_server_protocol::McpServerStatus; +use codex_app_server_protocol::McpToolsListParams; +use codex_app_server_protocol::McpToolsListResponse; use codex_app_server_protocol::MockExperimentalMethodParams; use codex_app_server_protocol::MockExperimentalMethodResponse; use codex_app_server_protocol::ModelListParams; @@ -129,6 +137,11 @@ use codex_app_server_protocol::ThreadBackgroundTerminalsCleanParams; use codex_app_server_protocol::ThreadBackgroundTerminalsCleanResponse; use codex_app_server_protocol::ThreadCompactStartParams; use codex_app_server_protocol::ThreadCompactStartResponse; +use codex_app_server_protocol::ThreadConfigReloadParams; +use codex_app_server_protocol::ThreadConfigReloadResponse; +use codex_app_server_protocol::ThreadContextSnapshot; +use codex_app_server_protocol::ThreadContextUpdateParams; +use codex_app_server_protocol::ThreadContextUpdateResponse; use codex_app_server_protocol::ThreadForkParams; use codex_app_server_protocol::ThreadForkResponse; use codex_app_server_protocol::ThreadItem; @@ -143,6 +156,8 @@ use codex_app_server_protocol::ThreadResumeResponse; use codex_app_server_protocol::ThreadRollbackParams; use codex_app_server_protocol::ThreadSetNameParams; use codex_app_server_protocol::ThreadSetNameResponse; +use codex_app_server_protocol::ThreadShutdownParams; +use codex_app_server_protocol::ThreadShutdownResponse; use codex_app_server_protocol::ThreadSortKey; use codex_app_server_protocol::ThreadSourceKind; use codex_app_server_protocol::ThreadStartParams; @@ -152,6 +167,10 @@ use codex_app_server_protocol::ThreadStatus; use codex_app_server_protocol::ThreadUnarchiveParams; use codex_app_server_protocol::ThreadUnarchiveResponse; use codex_app_server_protocol::ThreadUnarchivedNotification; +use codex_app_server_protocol::ThreadUndoParams; +use codex_app_server_protocol::ThreadUndoResponse; +use codex_app_server_protocol::ThreadUserShellCommandRunParams; +use codex_app_server_protocol::ThreadUserShellCommandRunResponse; use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnInterruptParams; use codex_app_server_protocol::TurnStartParams; @@ -208,6 +227,7 @@ use codex_core::find_thread_path_by_id_str; use codex_core::git_info::git_diff_to_remote; use codex_core::mcp::collect_mcp_snapshot; use codex_core::mcp::group_tools_by_server; +use codex_core::message_history; use codex_core::parse_cursor; use codex_core::read_head_for_summary; use codex_core::read_session_meta_line; @@ -574,6 +594,34 @@ impl CodexMessageProcessor { self.thread_rollback(to_connection_request_id(request_id), params) .await; } + ClientRequest::ThreadContextUpdate { request_id, params } => { + self.thread_context_update(to_connection_request_id(request_id), params) + .await; + } + ClientRequest::ThreadShutdown { request_id, params } => { + self.thread_shutdown(to_connection_request_id(request_id), params) + .await; + } + ClientRequest::ThreadUndo { request_id, params } => { + self.thread_undo(to_connection_request_id(request_id), params) + .await; + } + ClientRequest::ThreadConfigReload { request_id, params } => { + self.thread_config_reload(to_connection_request_id(request_id), params) + .await; + } + ClientRequest::ThreadMemoriesDrop { request_id, params } => { + self.thread_memories_drop(to_connection_request_id(request_id), params) + .await; + } + ClientRequest::ThreadMemoriesUpdate { request_id, params } => { + self.thread_memories_update(to_connection_request_id(request_id), params) + .await; + } + ClientRequest::ThreadUserShellCommandRun { request_id, params } => { + self.thread_user_shell_command_run(to_connection_request_id(request_id), params) + .await; + } ClientRequest::ThreadList { request_id, params } => { self.thread_list(to_connection_request_id(request_id), params) .await; @@ -586,6 +634,22 @@ impl CodexMessageProcessor { self.thread_read(to_connection_request_id(request_id), params) .await; } + ClientRequest::CustomPromptList { request_id, params } => { + self.custom_prompt_list(to_connection_request_id(request_id), params) + .await; + } + ClientRequest::McpToolsList { request_id, params } => { + self.mcp_tools_list(to_connection_request_id(request_id), params) + .await; + } + ClientRequest::HistoryAppend { request_id, params } => { + self.history_append(to_connection_request_id(request_id), params) + .await; + } + ClientRequest::HistoryEntryRead { request_id, params } => { + self.history_entry_read(to_connection_request_id(request_id), params) + .await; + } ClientRequest::SkillsList { request_id, params } => { self.skills_list(to_connection_request_id(request_id), params) .await; @@ -2069,6 +2133,12 @@ impl CodexMessageProcessor { approval_policy: config_snapshot.approval_policy.into(), sandbox: config_snapshot.sandbox_policy.into(), reasoning_effort: config_snapshot.reasoning_effort, + history_log_id: session_configured.history_log_id, + history_entry_count: session_configured.history_entry_count, + forked_from_thread_id: session_configured + .forked_from_id + .map(|thread_id| thread_id.to_string()), + network_proxy: session_configured.network_proxy.map(Into::into), }; self.outgoing.send_response(request_id, response).await; @@ -2454,6 +2524,337 @@ impl CodexMessageProcessor { } } + async fn thread_context_update( + &self, + request_id: ConnectionRequestId, + params: ThreadContextUpdateParams, + ) { + let ThreadContextUpdateParams { + thread_id, + cwd, + approval_policy, + sandbox_policy, + windows_sandbox_level, + model, + effort, + summary, + personality, + collaboration_mode, + } = params; + let (thread_id, thread) = match self.load_thread(&thread_id).await { + Ok(v) => v, + Err(error) => { + self.outgoing.send_error(request_id, error).await; + return; + } + }; + + let collaboration_mode = collaboration_mode.map(|mode| { + // Reuse the same normalization logic as `turn/start`. + self.normalize_turn_start_collaboration_mode(mode) + }); + + let submit_result = thread + .submit(Op::OverrideTurnContext { + cwd, + approval_policy: approval_policy.map(AskForApproval::to_core), + sandbox_policy: sandbox_policy.map(|policy| policy.to_core()), + windows_sandbox_level, + model, + effort, + summary, + collaboration_mode, + personality, + }) + .await; + if let Err(err) = submit_result { + self.send_internal_error( + request_id, + format!("failed to update thread context: {err}"), + ) + .await; + return; + } + + let config_snapshot = thread.config_snapshot().await; + let ThreadConfigSnapshot { + model, + model_provider_id, + approval_policy, + sandbox_policy, + cwd, + reasoning_effort, + .. + } = config_snapshot; + let response = ThreadContextUpdateResponse { + context: ThreadContextSnapshot { + thread_id: thread_id.to_string(), + model, + model_provider: model_provider_id, + cwd, + approval_policy: approval_policy.into(), + sandbox: sandbox_policy.into(), + reasoning_effort, + }, + }; + self.outgoing.send_response(request_id, response).await; + } + + async fn thread_shutdown(&self, request_id: ConnectionRequestId, params: ThreadShutdownParams) { + let ThreadShutdownParams { thread_id } = params; + let (_, thread) = match self.load_thread(&thread_id).await { + Ok(v) => v, + Err(error) => { + self.outgoing.send_error(request_id, error).await; + return; + } + }; + + match thread.submit(Op::Shutdown).await { + Ok(_) => { + self.outgoing + .send_response(request_id, ThreadShutdownResponse {}) + .await; + } + Err(err) => { + self.send_internal_error(request_id, format!("failed to shut down thread: {err}")) + .await; + } + } + } + + async fn thread_undo(&self, request_id: ConnectionRequestId, params: ThreadUndoParams) { + let ThreadUndoParams { thread_id } = params; + let (_, thread) = match self.load_thread(&thread_id).await { + Ok(v) => v, + Err(error) => { + self.outgoing.send_error(request_id, error).await; + return; + } + }; + + match thread.submit(Op::Undo).await { + Ok(_) => { + self.outgoing + .send_response(request_id, ThreadUndoResponse {}) + .await; + } + Err(err) => { + self.send_internal_error(request_id, format!("failed to start undo: {err}")) + .await; + } + } + } + + async fn thread_config_reload( + &self, + request_id: ConnectionRequestId, + params: ThreadConfigReloadParams, + ) { + let ThreadConfigReloadParams { thread_id } = params; + let (thread_id, thread) = match self.load_thread(&thread_id).await { + Ok(v) => v, + Err(error) => { + self.outgoing.send_error(request_id, error).await; + return; + } + }; + + let submit_result = thread.submit(Op::ReloadUserConfig).await; + if let Err(err) = submit_result { + self.send_internal_error(request_id, format!("failed to reload thread config: {err}")) + .await; + return; + } + + let config_snapshot = thread.config_snapshot().await; + let ThreadConfigSnapshot { + model, + model_provider_id, + approval_policy, + sandbox_policy, + cwd, + reasoning_effort, + .. + } = config_snapshot; + let response = ThreadConfigReloadResponse { + context: ThreadContextSnapshot { + thread_id: thread_id.to_string(), + model, + model_provider: model_provider_id, + cwd, + approval_policy: approval_policy.into(), + sandbox: sandbox_policy.into(), + reasoning_effort, + }, + }; + self.outgoing.send_response(request_id, response).await; + } + + async fn thread_memories_drop( + &self, + request_id: ConnectionRequestId, + params: codex_app_server_protocol::ThreadMemoriesDropParams, + ) { + let codex_app_server_protocol::ThreadMemoriesDropParams { thread_id } = params; + let (_, thread) = match self.load_thread(&thread_id).await { + Ok(v) => v, + Err(error) => { + self.outgoing.send_error(request_id, error).await; + return; + } + }; + match thread.submit(Op::DropMemories).await { + Ok(_) => { + self.outgoing + .send_response( + request_id, + codex_app_server_protocol::ThreadMemoriesDropResponse {}, + ) + .await; + } + Err(err) => { + self.send_internal_error(request_id, format!("failed to drop memories: {err}")) + .await; + } + } + } + + async fn thread_memories_update( + &self, + request_id: ConnectionRequestId, + params: codex_app_server_protocol::ThreadMemoriesUpdateParams, + ) { + let codex_app_server_protocol::ThreadMemoriesUpdateParams { thread_id } = params; + let (_, thread) = match self.load_thread(&thread_id).await { + Ok(v) => v, + Err(error) => { + self.outgoing.send_error(request_id, error).await; + return; + } + }; + match thread.submit(Op::UpdateMemories).await { + Ok(_) => { + self.outgoing + .send_response( + request_id, + codex_app_server_protocol::ThreadMemoriesUpdateResponse {}, + ) + .await; + } + Err(err) => { + self.send_internal_error(request_id, format!("failed to update memories: {err}")) + .await; + } + } + } + + async fn thread_user_shell_command_run( + &self, + request_id: ConnectionRequestId, + params: ThreadUserShellCommandRunParams, + ) { + let ThreadUserShellCommandRunParams { thread_id, command } = params; + let (_, thread) = match self.load_thread(&thread_id).await { + Ok(v) => v, + Err(error) => { + self.outgoing.send_error(request_id, error).await; + return; + } + }; + + match thread.submit(Op::RunUserShellCommand { command }).await { + Ok(turn_id) => { + self.outgoing + .send_response(request_id, ThreadUserShellCommandRunResponse { turn_id }) + .await; + } + Err(err) => { + self.send_internal_error( + request_id, + format!("failed to run user shell command: {err}"), + ) + .await; + } + } + } + + async fn custom_prompt_list( + &self, + request_id: ConnectionRequestId, + _params: CustomPromptListParams, + ) { + let data = if let Some(dir) = codex_core::custom_prompts::default_prompts_dir() { + codex_core::custom_prompts::discover_prompts_in(&dir) + .await + .into_iter() + .map(Into::into) + .collect() + } else { + Vec::new() + }; + self.outgoing + .send_response(request_id, CustomPromptListResponse { data }) + .await; + } + + async fn mcp_tools_list(&self, request_id: ConnectionRequestId, _params: McpToolsListParams) { + let snapshot = collect_mcp_snapshot(self.config.as_ref()).await; + self.outgoing + .send_response(request_id, McpToolsListResponse::from(snapshot)) + .await; + } + + async fn history_append(&self, request_id: ConnectionRequestId, params: HistoryAppendParams) { + let HistoryAppendParams { thread_id, text } = params; + let thread_id = match ThreadId::from_string(&thread_id) { + Ok(thread_id) => thread_id, + Err(err) => { + self.send_invalid_request_error(request_id, format!("invalid thread id: {err}")) + .await; + return; + } + }; + + match message_history::append_entry(&text, &thread_id, self.config.as_ref()).await { + Ok(()) => { + self.outgoing + .send_response(request_id, HistoryAppendResponse {}) + .await; + } + Err(err) => { + self.send_internal_error(request_id, format!("failed to append history: {err}")) + .await; + } + } + } + + async fn history_entry_read( + &self, + request_id: ConnectionRequestId, + params: HistoryEntryReadParams, + ) { + let HistoryEntryReadParams { log_id, offset } = params; + + let entry = message_history::lookup(log_id, offset, self.config.as_ref()).map(|entry| { + codex_app_server_protocol::HistoryEntry { + conversation_id: entry.session_id, + ts: entry.ts, + text: entry.text, + } + }); + self.outgoing + .send_response( + request_id, + HistoryEntryReadResponse { + log_id, + offset, + entry, + }, + ) + .await; + } + async fn thread_compact_start( &self, request_id: ConnectionRequestId, @@ -2971,6 +3372,12 @@ impl CodexMessageProcessor { approval_policy: session_configured.approval_policy.into(), sandbox: session_configured.sandbox_policy.into(), reasoning_effort: session_configured.reasoning_effort, + history_log_id: session_configured.history_log_id, + history_entry_count: session_configured.history_entry_count, + forked_from_thread_id: session_configured + .forked_from_id + .map(|thread_id| thread_id.to_string()), + network_proxy: session_configured.network_proxy.map(Into::into), }; self.outgoing.send_response(request_id, response).await; @@ -3499,6 +3906,12 @@ impl CodexMessageProcessor { approval_policy: session_configured.approval_policy.into(), sandbox: session_configured.sandbox_policy.into(), reasoning_effort: session_configured.reasoning_effort, + history_log_id: session_configured.history_log_id, + history_entry_count: session_configured.history_entry_count, + forked_from_thread_id: session_configured + .forked_from_id + .map(|thread_id| thread_id.to_string()), + network_proxy: session_configured.network_proxy.map(Into::into), }; self.outgoing.send_response(request_id, response).await; @@ -5872,6 +6285,7 @@ impl CodexMessageProcessor { let fallback_model_provider = self.config.model_provider_id.clone(); let single_client_mode = self.single_client_mode; let codex_home = self.config.codex_home.clone(); + let config_for_task = self.config.clone(); tokio::spawn(async move { loop { tokio::select! { @@ -5888,30 +6302,6 @@ impl CodexMessageProcessor { } }; - // For now, we send a notification for every event, - // JSON-serializing the `Event` as-is, but these should - // be migrated to be variants of `ServerNotification` - // instead. - let event_formatted = match &event.msg { - EventMsg::TurnStarted(_) => "task_started", - EventMsg::TurnComplete(_) => "task_complete", - _ => &event.msg.to_string(), - }; - let mut params = match serde_json::to_value(event.clone()) { - Ok(serde_json::Value::Object(map)) => map, - Ok(_) => { - error!("event did not serialize to an object"); - continue; - } - Err(err) => { - error!("failed to serialize event: {err}"); - continue; - } - }; - params.insert( - "conversationId".to_string(), - conversation_id.to_string().into(), - ); let (subscribed_connection_ids, raw_events_enabled) = { let mut thread_state = thread_state.lock().await; if !single_client_mode { @@ -5927,6 +6317,26 @@ impl CodexMessageProcessor { } if !subscribed_connection_ids.is_empty() { + let event_formatted = match &event.msg { + EventMsg::TurnStarted(_) => "task_started", + EventMsg::TurnComplete(_) => "task_complete", + _ => &event.msg.to_string(), + }; + let mut params = match serde_json::to_value(event.clone()) { + Ok(serde_json::Value::Object(map)) => map, + Ok(_) => { + error!("event did not serialize to an object"); + continue; + } + Err(err) => { + error!("failed to serialize event: {err}"); + continue; + } + }; + params.insert( + "conversationId".to_string(), + conversation_id.to_string().into(), + ); outgoing_for_task .send_notification_to_connections( &subscribed_connection_ids, @@ -5966,6 +6376,7 @@ impl CodexMessageProcessor { ) => { handle_pending_thread_resume_request( conversation_id, + config_for_task.as_ref(), codex_home.as_path(), &thread_state, &thread_watch_manager, @@ -6286,6 +6697,7 @@ impl CodexMessageProcessor { async fn handle_pending_thread_resume_request( conversation_id: ThreadId, + config: &Config, codex_home: &Path, thread_state: &Arc>, thread_watch_manager: &ThreadWatchManager, @@ -6352,6 +6764,7 @@ async fn handle_pending_thread_resume_request( Ok(thread_name) => thread.name = thread_name, Err(err) => warn!("Failed to read thread name for {conversation_id}: {err}"), } + let (history_log_id, history_entry_count) = message_history::history_metadata(config).await; let ThreadConfigSnapshot { model, @@ -6370,6 +6783,10 @@ async fn handle_pending_thread_resume_request( approval_policy: approval_policy.into(), sandbox: sandbox_policy.into(), reasoning_effort, + history_log_id, + history_entry_count, + forked_from_thread_id: None, + network_proxy: None, }; outgoing.send_response(request_id, response).await; thread_state.lock().await.add_connection(connection_id); diff --git a/codex-rs/app-server/src/embedded_session.rs b/codex-rs/app-server/src/embedded_session.rs new file mode 100644 index 0000000000..03dc8daf71 --- /dev/null +++ b/codex-rs/app-server/src/embedded_session.rs @@ -0,0 +1,330 @@ +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::RwLock; +use std::sync::atomic::AtomicI64; +use std::sync::atomic::Ordering; + +use codex_app_server_protocol::ClientRequest; +use codex_app_server_protocol::JSONRPCErrorError; +use codex_app_server_protocol::RequestId; +use codex_app_server_protocol::ServerNotification; +use codex_app_server_protocol::ServerRequest; +use codex_app_server_protocol::ThreadForkParams; +use codex_app_server_protocol::ThreadForkResponse; +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_cloud_requirements::cloud_requirements_loader; +use codex_core::AuthManager; +use codex_core::ThreadManager; +use codex_core::config::Config; +use codex_feedback::CodexFeedback; +use serde::Serialize; +use serde::de::DeserializeOwned; +use tokio::sync::broadcast; +use tokio::sync::mpsc; +use tokio::sync::oneshot; +use toml::Value as TomlValue; +use tracing::warn; + +use crate::codex_message_processor::CodexMessageProcessor; +use crate::codex_message_processor::CodexMessageProcessorArgs; +use crate::error_code::INTERNAL_ERROR_CODE; +use crate::outgoing_message::ConnectionId; +use crate::outgoing_message::OutgoingEnvelope; +use crate::outgoing_message::OutgoingMessage; +use crate::outgoing_message::OutgoingMessageSender; +use crate::transport::CHANNEL_CAPACITY; + +const EMBEDDED_CONNECTION_ID: ConnectionId = ConnectionId(1); + +#[derive(Debug, Clone)] +pub struct EmbeddedServerRequest { + pub request_id: RequestId, + pub request: ServerRequest, +} + +#[derive(Debug, Clone)] +pub enum EmbeddedSessionMessage { + Notification(ServerNotification), + Request(EmbeddedServerRequest), +} + +#[derive(Clone)] +pub struct EmbeddedSessionClient { + inbound_tx: mpsc::Sender, + messages_tx: broadcast::Sender, + next_request_id: Arc, +} + +pub struct EmbeddedSessionClientArgs { + pub auth_manager: Arc, + pub thread_manager: Arc, + pub config: Config, + pub cli_overrides: Vec<(String, TomlValue)>, + pub feedback: CodexFeedback, +} + +enum EmbeddedInbound { + ClientRequest { + request: ClientRequest, + reply_tx: oneshot::Sender>, + }, + ServerResponse { + request_id: RequestId, + result: serde_json::Value, + }, + ServerError { + request_id: RequestId, + error: JSONRPCErrorError, + }, +} + +impl EmbeddedSessionClient { + pub fn new(args: EmbeddedSessionClientArgs) -> Self { + let EmbeddedSessionClientArgs { + auth_manager, + thread_manager, + config, + cli_overrides, + feedback, + } = args; + let config = Arc::new(config); + let cloud_requirements = cloud_requirements_loader( + auth_manager.clone(), + config.chatgpt_base_url.clone(), + config.codex_home.clone(), + ); + + let (outgoing_tx, outgoing_rx) = mpsc::channel(CHANNEL_CAPACITY); + let outgoing = Arc::new(OutgoingMessageSender::new(outgoing_tx)); + let processor = CodexMessageProcessor::new(CodexMessageProcessorArgs { + auth_manager, + thread_manager, + outgoing: outgoing.clone(), + codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(), + config: config.clone(), + cli_overrides, + cloud_requirements: Arc::new(RwLock::new(cloud_requirements)), + single_client_mode: true, + feedback, + }); + + let (inbound_tx, inbound_rx) = mpsc::channel(CHANNEL_CAPACITY); + let (messages_tx, _) = broadcast::channel(CHANNEL_CAPACITY); + + tokio::spawn(run_embedded_session_loop( + processor, + outgoing, + outgoing_rx, + inbound_rx, + messages_tx.clone(), + )); + + Self { + inbound_tx, + messages_tx, + next_request_id: Arc::new(AtomicI64::new(1)), + } + } + + pub fn subscribe(&self) -> broadcast::Receiver { + self.messages_tx.subscribe() + } + + pub async fn thread_start( + &self, + params: ThreadStartParams, + ) -> std::result::Result { + self.request(|request_id| ClientRequest::ThreadStart { request_id, params }) + .await + } + + pub async fn thread_resume( + &self, + params: ThreadResumeParams, + ) -> std::result::Result { + self.request(|request_id| ClientRequest::ThreadResume { request_id, params }) + .await + } + + pub async fn thread_fork( + &self, + params: ThreadForkParams, + ) -> std::result::Result { + self.request(|request_id| ClientRequest::ThreadFork { request_id, params }) + .await + } + + pub async fn respond( + &self, + request_id: RequestId, + response: T, + ) -> std::result::Result<(), JSONRPCErrorError> { + let result = serde_json::to_value(response).map_err(serialize_error)?; + self.inbound_tx + .send(EmbeddedInbound::ServerResponse { request_id, result }) + .await + .map_err(channel_closed_error)?; + Ok(()) + } + + pub async fn respond_error( + &self, + request_id: RequestId, + error: JSONRPCErrorError, + ) -> std::result::Result<(), JSONRPCErrorError> { + self.inbound_tx + .send(EmbeddedInbound::ServerError { request_id, error }) + .await + .map_err(channel_closed_error)?; + Ok(()) + } + + pub async fn request(&self, build: F) -> std::result::Result + where + T: DeserializeOwned, + F: FnOnce(RequestId) -> ClientRequest, + { + let request_id = RequestId::Integer(self.next_request_id.fetch_add(1, Ordering::Relaxed)); + let request = build(request_id); + let (reply_tx, reply_rx) = oneshot::channel(); + self.inbound_tx + .send(EmbeddedInbound::ClientRequest { request, reply_tx }) + .await + .map_err(channel_closed_error)?; + + let response = reply_rx.await.map_err(channel_closed_error)??; + serde_json::from_value(response).map_err(serialize_error) + } +} + +async fn run_embedded_session_loop( + mut processor: CodexMessageProcessor, + outgoing: Arc, + mut outgoing_rx: mpsc::Receiver, + mut inbound_rx: mpsc::Receiver, + messages_tx: broadcast::Sender, +) { + let mut pending_client_requests: HashMap< + RequestId, + oneshot::Sender>, + > = HashMap::new(); + + loop { + tokio::select! { + maybe_inbound = inbound_rx.recv() => { + let Some(inbound) = maybe_inbound else { + break; + }; + match inbound { + EmbeddedInbound::ClientRequest { request, reply_tx } => { + let request_id = match extract_client_request_id(&request) { + Ok(request_id) => request_id, + Err(err) => { + let _ = reply_tx.send(Err(err)); + continue; + } + }; + pending_client_requests.insert(request_id, reply_tx); + processor.process_request(EMBEDDED_CONNECTION_ID, request).await; + } + EmbeddedInbound::ServerResponse { request_id, result } => { + outgoing.notify_client_response(request_id, result).await; + } + EmbeddedInbound::ServerError { request_id, error } => { + outgoing.notify_client_error(request_id, error).await; + } + } + } + maybe_outgoing = outgoing_rx.recv() => { + let Some(envelope) = maybe_outgoing else { + break; + }; + let message = match envelope { + OutgoingEnvelope::ToConnection { connection_id, message } => { + if connection_id != EMBEDDED_CONNECTION_ID { + continue; + } + message + } + OutgoingEnvelope::Broadcast { message } => message, + }; + match message { + OutgoingMessage::Response(response) => { + if let Some(reply_tx) = pending_client_requests.remove(&response.id) { + let _ = reply_tx.send(Ok(response.result)); + } else { + warn!("embedded session dropped unmatched response {:?}", response.id); + } + } + OutgoingMessage::Error(error) => { + if let Some(reply_tx) = pending_client_requests.remove(&error.id) { + let _ = reply_tx.send(Err(error.error)); + } else { + warn!("embedded session dropped unmatched error {:?}", error.id); + } + } + OutgoingMessage::AppServerNotification(notification) => { + let _ = messages_tx.send(EmbeddedSessionMessage::Notification(notification)); + } + OutgoingMessage::Request(request) => match extract_server_request_id(&request) { + Ok(request_id) => { + let _ = messages_tx.send(EmbeddedSessionMessage::Request( + EmbeddedServerRequest { request_id, request }, + )); + } + Err(err) => { + warn!("failed to extract embedded server request id: {err:?}"); + } + }, + OutgoingMessage::Notification(_legacy_notification) => { + // Embedded TUI clients should rely on typed app-server notifications. + } + } + } + } + } +} + +fn extract_client_request_id( + request: &ClientRequest, +) -> std::result::Result { + extract_request_id(serde_json::to_value(request).map_err(serialize_error)?) +} + +fn extract_server_request_id( + request: &ServerRequest, +) -> std::result::Result { + extract_request_id(serde_json::to_value(request).map_err(serialize_error)?) +} + +fn extract_request_id( + value: serde_json::Value, +) -> std::result::Result { + let id = value + .get("id") + .cloned() + .ok_or_else(|| internal_error("missing id on request".to_string()))?; + serde_json::from_value(id).map_err(serialize_error) +} + +fn serialize_error(err: serde_json::Error) -> JSONRPCErrorError { + internal_error(format!("serialization error: {err}")) +} + +fn channel_closed_error(err: T) -> JSONRPCErrorError +where + T: std::fmt::Display, +{ + internal_error(format!("embedded session channel closed: {err}")) +} + +fn internal_error(message: String) -> JSONRPCErrorError { + JSONRPCErrorError { + code: INTERNAL_ERROR_CODE, + message, + data: None, + } +} diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index 1bdbf97a13..9ce36530b0 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -56,6 +56,7 @@ mod bespoke_event_handling; mod codex_message_processor; mod config_api; mod dynamic_tools; +mod embedded_session; mod error_code; mod filters; mod fuzzy_file_search; @@ -67,6 +68,9 @@ mod thread_status; mod transport; pub use crate::transport::AppServerTransport; +pub use embedded_session::EmbeddedSessionClient; +pub use embedded_session::EmbeddedSessionClientArgs; +pub use embedded_session::EmbeddedSessionMessage; const LOG_FORMAT_ENV_VAR: &str = "LOG_FORMAT"; diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index e40905e314..783514782e 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -51,7 +51,7 @@ pub use mcp_connection_manager::SandboxState; mod mcp_tool_call; mod memories; mod mentions; -mod message_history; +pub mod message_history; mod model_provider_info; pub mod path_utils; pub mod personality_migration; diff --git a/codex-rs/core/src/message_history.rs b/codex-rs/core/src/message_history.rs index cb3b10098c..802acaf7e8 100644 --- a/codex-rs/core/src/message_history.rs +++ b/codex-rs/core/src/message_history.rs @@ -69,11 +69,7 @@ fn history_filepath(config: &Config) -> PathBuf { /// Append a `text` entry associated with `conversation_id` to the history file. Uses /// advisory file locking to ensure that concurrent writes do not interleave, /// which entails a small amount of blocking I/O internally. -pub(crate) async fn append_entry( - text: &str, - conversation_id: &ThreadId, - config: &Config, -) -> Result<()> { +pub async fn append_entry(text: &str, conversation_id: &ThreadId, config: &Config) -> Result<()> { match config.history.persistence { HistoryPersistence::SaveAll => { // Save everything: proceed. @@ -245,7 +241,7 @@ fn trim_target_bytes(max_bytes: u64, newest_entry_len: u64) -> u64 { /// Asynchronously fetch the history file's *identifier* (inode on Unix) and /// the current number of entries by counting newline characters. -pub(crate) async fn history_metadata(config: &Config) -> (u64, usize) { +pub async fn history_metadata(config: &Config) -> (u64, usize) { let path = history_filepath(config); history_metadata_for_file(&path).await } @@ -258,7 +254,7 @@ pub(crate) async fn history_metadata(config: &Config) -> (u64, usize) { /// /// Note this function is not async because it uses a sync advisory file /// locking API. -pub(crate) fn lookup(log_id: u64, offset: usize, config: &Config) -> Option { +pub fn lookup(log_id: u64, offset: usize, config: &Config) -> Option { let path = history_filepath(config); lookup_history_entry(&path, log_id, offset) } diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index 1b0ed9771e..d3b5162188 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -27,6 +27,7 @@ base64 = { workspace = true } chrono = { workspace = true, features = ["serde"] } clap = { workspace = true, features = ["derive"] } codex-ansi-escape = { workspace = true } +codex-app-server = { workspace = true } codex-app-server-protocol = { workspace = true } codex-arg0 = { workspace = true } codex-backend-client = { workspace = true } diff --git a/codex-rs/tui/src/agent_command.rs b/codex-rs/tui/src/agent_command.rs new file mode 100644 index 0000000000..3aa2ee9699 --- /dev/null +++ b/codex-rs/tui/src/agent_command.rs @@ -0,0 +1,105 @@ +use std::path::PathBuf; + +use codex_protocol::approvals::ElicitationAction; +use codex_protocol::config_types::CollaborationMode; +use codex_protocol::config_types::Personality; +use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig; +use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::dynamic_tools::DynamicToolResponse; +use codex_protocol::mcp::RequestId as McpRequestId; +use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; +use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::ReviewDecision; +use codex_protocol::protocol::ReviewRequest; +use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::request_user_input::RequestUserInputResponse; +use codex_protocol::user_input::UserInput; +use serde::Deserialize; +use serde::Serialize; +use serde_json::Value; + +/// TUI-side agent commands routed over the in-process app-server. +/// +/// This replaces direct `codex_protocol::protocol::Op` usage in the TUI. +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +#[allow(clippy::large_enum_variant)] +pub(crate) enum AgentCommand { + Interrupt, + CleanBackgroundTerminals, + UserTurn { + items: Vec, + cwd: PathBuf, + approval_policy: AskForApproval, + sandbox_policy: SandboxPolicy, + model: String, + effort: Option, + summary: ReasoningSummaryConfig, + final_output_json_schema: Option, + collaboration_mode: Option, + personality: Option, + }, + OverrideTurnContext { + cwd: Option, + approval_policy: Option, + sandbox_policy: Option, + windows_sandbox_level: Option, + model: Option, + effort: Option>, + summary: Option, + collaboration_mode: Option, + personality: Option, + }, + ExecApproval { + id: String, + turn_id: Option, + decision: ReviewDecision, + }, + PatchApproval { + id: String, + decision: ReviewDecision, + }, + ResolveElicitation { + server_name: String, + request_id: McpRequestId, + decision: ElicitationAction, + }, + UserInputAnswer { + id: String, + response: RequestUserInputResponse, + }, + DynamicToolResponse { + id: String, + response: DynamicToolResponse, + }, + AddToHistory { + text: String, + }, + GetHistoryEntryRequest { + offset: usize, + log_id: u64, + }, + ListMcpTools, + ReloadUserConfig, + ListCustomPrompts, + ListSkills { + cwds: Vec, + force_reload: bool, + }, + Compact, + DropMemories, + UpdateMemories, + SetThreadName { + name: String, + }, + Undo, + ThreadRollback { + num_turns: u32, + }, + Review { + review_request: ReviewRequest, + }, + Shutdown, + RunUserShellCommand { + command: String, + }, +} diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index e080b8cde0..cfa8777522 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -1,6 +1,8 @@ +use crate::agent_command::AgentCommand; use crate::app_backtrack::BacktrackState; use crate::app_event::AppEvent; use crate::app_event::ExitMode; +use crate::app_event::ThreadBootstrap; #[cfg(target_os = "windows")] use crate::app_event::WindowsSandboxEnableMode; use crate::app_event_sender::AppEventSender; @@ -32,6 +34,10 @@ use crate::tui::TuiEvent; use crate::update_action::UpdateAction; use crate::version::CODEX_CLI_VERSION; use codex_ansi_escape::ansi_escape_line; +use codex_app_server::EmbeddedSessionClient; +use codex_app_server::EmbeddedSessionClientArgs; +use codex_app_server::EmbeddedSessionMessage; +use codex_app_server_protocol as app_proto; use codex_app_server_protocol::ConfigLayerSource; use codex_core::AuthManager; use codex_core::CodexAuth; @@ -54,18 +60,12 @@ use codex_protocol::ThreadId; use codex_protocol::config_types::Personality; #[cfg(target_os = "windows")] use codex_protocol::config_types::WindowsSandboxLevel; -use codex_protocol::items::TurnItem; use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::ModelUpgrade; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; use codex_protocol::protocol::AskForApproval; -use codex_protocol::protocol::Event; -use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::FinalOutput; -use codex_protocol::protocol::ListSkillsResponseEvent; -use codex_protocol::protocol::Op; use codex_protocol::protocol::SandboxPolicy; -use codex_protocol::protocol::SessionConfiguredEvent; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SkillErrorInfo; use codex_protocol::protocol::TokenUsage; @@ -81,7 +81,6 @@ use ratatui::widgets::Paragraph; use ratatui::widgets::Wrap; use std::collections::BTreeMap; use std::collections::HashMap; -use std::collections::HashSet; use std::collections::VecDeque; use std::path::Path; use std::path::PathBuf; @@ -158,15 +157,250 @@ fn session_summary( }) } -fn errors_for_cwd(cwd: &Path, response: &ListSkillsResponseEvent) -> Vec { +fn errors_for_cwd_v2(cwd: &Path, response: &app_proto::SkillsListResponse) -> Vec { response - .skills + .data .iter() .find(|entry| entry.cwd.as_path() == cwd) - .map(|entry| entry.errors.clone()) + .and_then(|entry| convert_via_serde(entry.errors.clone()).ok()) .unwrap_or_default() } +fn convert_via_serde(value: T) -> std::result::Result +where + T: serde::Serialize, + U: serde::de::DeserializeOwned, +{ + serde_json::from_value(serde_json::to_value(value)?) +} + +fn sandbox_mode_from_policy(policy: &SandboxPolicy) -> Option { + match policy { + SandboxPolicy::ReadOnly { .. } => Some(app_proto::SandboxMode::ReadOnly), + SandboxPolicy::WorkspaceWrite { .. } => Some(app_proto::SandboxMode::WorkspaceWrite), + SandboxPolicy::DangerFullAccess => Some(app_proto::SandboxMode::DangerFullAccess), + SandboxPolicy::ExternalSandbox { .. } => None, + } +} + +fn network_proxy_from_app_proto( + network_proxy: Option, +) -> Option { + network_proxy.map( + |network_proxy| codex_protocol::protocol::SessionNetworkProxyRuntime { + http_addr: network_proxy.http_addr, + socks_addr: network_proxy.socks_addr, + admin_addr: network_proxy.admin_addr, + }, + ) +} + +struct ThreadOpenParamsCommon { + path: PathBuf, + model: Option, + model_provider: String, + cwd: String, + approval_policy: app_proto::AskForApproval, + sandbox: Option, +} + +fn thread_open_params_common(path: PathBuf, config: &Config) -> ThreadOpenParamsCommon { + ThreadOpenParamsCommon { + path, + model: config.model.clone(), + model_provider: config.model_provider_id.clone(), + cwd: config.cwd.to_string_lossy().to_string(), + approval_policy: app_proto::AskForApproval::from(*config.permissions.approval_policy.get()), + sandbox: sandbox_mode_from_policy(config.permissions.sandbox_policy.get()), + } +} + +fn thread_bootstrap_from_response_parts( + thread: app_proto::Thread, + model: String, + model_provider: String, + cwd: PathBuf, + approval_policy: app_proto::AskForApproval, + sandbox: app_proto::SandboxPolicy, + reasoning_effort: Option, + history_log_id: u64, + history_entry_count: usize, + forked_from_thread_id: Option, + network_proxy: Option, +) -> Result { + let thread_id = ThreadId::try_from(thread.id.clone()) + .map_err(|err| color_eyre::eyre::eyre!("invalid thread id {}: {err}", thread.id))?; + let forked_from_id = match forked_from_thread_id { + Some(thread_id) => Some(ThreadId::try_from(thread_id.clone()).map_err(|err| { + color_eyre::eyre::eyre!("invalid forked thread id {thread_id}: {err}") + })?), + None => None, + }; + Ok(ThreadBootstrap { + rollout_path: thread.path.clone(), + thread, + thread_id, + model, + model_provider_id: model_provider, + approval_policy: approval_policy.to_core(), + sandbox_policy: sandbox.to_core(), + cwd, + reasoning_effort, + history_log_id, + history_entry_count, + forked_from_id, + network_proxy: network_proxy_from_app_proto(network_proxy), + }) +} + +fn thread_bootstrap_from_resume_response( + response: app_proto::ThreadResumeResponse, +) -> Result { + let app_proto::ThreadResumeResponse { + thread, + model, + model_provider, + cwd, + approval_policy, + sandbox, + reasoning_effort, + history_log_id, + history_entry_count, + forked_from_thread_id, + network_proxy, + } = response; + thread_bootstrap_from_response_parts( + thread, + model, + model_provider, + cwd, + approval_policy, + sandbox, + reasoning_effort, + history_log_id, + history_entry_count, + forked_from_thread_id, + network_proxy, + ) +} + +fn thread_bootstrap_from_fork_response( + response: app_proto::ThreadForkResponse, +) -> Result { + let app_proto::ThreadForkResponse { + thread, + model, + model_provider, + cwd, + approval_policy, + sandbox, + reasoning_effort, + history_log_id, + history_entry_count, + forked_from_thread_id, + network_proxy, + } = response; + thread_bootstrap_from_response_parts( + thread, + model, + model_provider, + cwd, + approval_policy, + sandbox, + reasoning_effort, + history_log_id, + history_entry_count, + forked_from_thread_id, + network_proxy, + ) +} + +fn thread_resume_params_from_config( + path: PathBuf, + config: &Config, +) -> app_proto::ThreadResumeParams { + let common = thread_open_params_common(path, config); + app_proto::ThreadResumeParams { + thread_id: String::new(), + path: Some(common.path), + model: common.model, + model_provider: Some(common.model_provider), + cwd: Some(common.cwd), + approval_policy: Some(common.approval_policy), + sandbox: common.sandbox, + personality: config.personality, + ..Default::default() + } +} + +fn thread_fork_params_from_config(path: PathBuf, config: &Config) -> app_proto::ThreadForkParams { + let common = thread_open_params_common(path, config); + app_proto::ThreadForkParams { + thread_id: String::new(), + path: Some(common.path), + model: common.model, + model_provider: Some(common.model_provider), + cwd: Some(common.cwd), + approval_policy: Some(common.approval_policy), + sandbox: common.sandbox, + ..Default::default() + } +} + +fn notification_thread_id(notification: &app_proto::ServerNotification) -> Option { + let thread_id = match notification { + app_proto::ServerNotification::ThreadStatusChanged(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::ThreadArchived(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::ThreadUnarchived(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::ThreadNameUpdated(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::ThreadTokenUsageUpdated(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::ThreadShutdownCompleted(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::ThreadUndoStarted(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::ThreadUndoCompleted(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::TurnStarted(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::TurnCompleted(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::StreamError(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::TurnDiffUpdated(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::TurnPlanUpdated(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::ItemStarted(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::ItemCompleted(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::RawResponseItemCompleted(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::AgentMessageDelta(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::PlanDelta(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::CommandExecutionOutputDelta(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::TerminalInteraction(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::FileChangeOutputDelta(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::McpToolCallProgress(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::McpServerStartupUpdated(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::McpServerStartupCompleted(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::SkillsUpdated(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::ReasoningSummaryTextDelta(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::ReasoningSummaryPartAdded(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::ReasoningTextDelta(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::ContextCompacted(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::ModelRerouted(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::Warning(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::Error(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::BackgroundEvent(ev) => ev.thread_id.as_str(), + app_proto::ServerNotification::ThreadStarted(ev) => ev.thread.id.as_str(), + app_proto::ServerNotification::McpServerOauthLoginCompleted(_) + | app_proto::ServerNotification::AccountUpdated(_) + | app_proto::ServerNotification::AccountRateLimitsUpdated(_) + | app_proto::ServerNotification::AppListUpdated(_) + | app_proto::ServerNotification::DeprecationNotice(_) + | app_proto::ServerNotification::ConfigWarning(_) + | app_proto::ServerNotification::FuzzyFileSearchSessionUpdated(_) + | app_proto::ServerNotification::FuzzyFileSearchSessionCompleted(_) + | app_proto::ServerNotification::WindowsWorldWritableWarning(_) + | app_proto::ServerNotification::WindowsSandboxSetupCompleted(_) + | app_proto::ServerNotification::AuthStatusChange(_) + | app_proto::ServerNotification::LoginChatGptComplete(_) + | app_proto::ServerNotification::SessionConfigured(_) + | app_proto::ServerNotification::AccountLoginCompleted(_) => return None, + }; + ThreadId::try_from(thread_id.to_string()).ok() +} + fn emit_skill_load_warnings(app_event_tx: &AppEventSender, errors: &[SkillErrorInfo]) { if errors.is_empty() { return; @@ -239,15 +473,21 @@ struct SessionSummary { #[derive(Debug, Clone)] struct ThreadEventSnapshot { - session_configured: Option, - events: Vec, + bootstrap: Option, + notifications: Vec, +} + +#[derive(Debug, Clone)] +enum ThreadUiEvent { + Bootstrap(ThreadBootstrap), + Notification(app_proto::ServerNotification), + ServerRequest(app_proto::ServerRequest), } #[derive(Debug)] struct ThreadEventStore { - session_configured: Option, - buffer: VecDeque, - user_message_ids: HashSet, + bootstrap: Option, + buffer: VecDeque, capacity: usize, active: bool, } @@ -255,74 +495,42 @@ struct ThreadEventStore { impl ThreadEventStore { fn new(capacity: usize) -> Self { Self { - session_configured: None, + bootstrap: None, buffer: VecDeque::new(), - user_message_ids: HashSet::new(), capacity, active: false, } } - fn new_with_session_configured(capacity: usize, event: Event) -> Self { + fn new_with_bootstrap(capacity: usize, bootstrap: ThreadBootstrap) -> Self { let mut store = Self::new(capacity); - store.session_configured = Some(event); + store.bootstrap = Some(bootstrap); store } - fn push_event(&mut self, event: Event) { - match &event.msg { - EventMsg::SessionConfigured(_) => { - self.session_configured = Some(event); - return; - } - EventMsg::ItemCompleted(completed) => { - if let TurnItem::UserMessage(item) = &completed.item { - if !event.id.is_empty() && self.user_message_ids.contains(&event.id) { - return; - } - let legacy = Event { - id: event.id, - msg: item.as_legacy_event(), - }; - self.push_legacy_event(legacy); - return; - } - } - _ => {} - } - - self.push_legacy_event(event); + fn set_bootstrap(&mut self, bootstrap: ThreadBootstrap) { + self.bootstrap = Some(bootstrap); } - fn push_legacy_event(&mut self, event: Event) { - if let EventMsg::UserMessage(_) = &event.msg - && !event.id.is_empty() - && !self.user_message_ids.insert(event.id.clone()) - { - return; - } - self.buffer.push_back(event); - if self.buffer.len() > self.capacity - && let Some(removed) = self.buffer.pop_front() - && matches!(removed.msg, EventMsg::UserMessage(_)) - && !removed.id.is_empty() - { - self.user_message_ids.remove(&removed.id); + fn push_notification(&mut self, notification: app_proto::ServerNotification) { + self.buffer.push_back(notification); + if self.buffer.len() > self.capacity { + let _ = self.buffer.pop_front(); } } fn snapshot(&self) -> ThreadEventSnapshot { ThreadEventSnapshot { - session_configured: self.session_configured.clone(), - events: self.buffer.iter().cloned().collect(), + bootstrap: self.bootstrap.clone(), + notifications: self.buffer.iter().cloned().collect(), } } } #[derive(Debug)] struct ThreadEventChannel { - sender: mpsc::Sender, - receiver: Option>, + sender: mpsc::Sender, + receiver: Option>, store: Arc>, } @@ -336,13 +544,13 @@ impl ThreadEventChannel { } } - fn new_with_session_configured(capacity: usize, event: Event) -> Self { + fn new_with_bootstrap(capacity: usize, bootstrap: ThreadBootstrap) -> Self { let (sender, receiver) = mpsc::channel(capacity); Self { sender, receiver: Some(receiver), - store: Arc::new(Mutex::new(ThreadEventStore::new_with_session_configured( - capacity, event, + store: Arc::new(Mutex::new(ThreadEventStore::new_with_bootstrap( + capacity, bootstrap, ))), } } @@ -517,6 +725,7 @@ async fn handle_model_migration_prompt_if_needed( pub(crate) struct App { pub(crate) server: Arc, + app_server_client: Arc, pub(crate) otel_manager: OtelManager, pub(crate) app_event_tx: AppEventSender, pub(crate) chat_widget: ChatWidget, @@ -577,10 +786,9 @@ pub(crate) struct App { thread_event_channels: HashMap, active_thread_id: Option, - active_thread_rx: Option>, + active_thread_rx: Option>, primary_thread_id: Option, - primary_session_configured: Option, - pending_primary_events: VecDeque, + pending_primary_events: VecDeque, } #[derive(Default)] @@ -631,6 +839,25 @@ impl App { } } + fn append_session_summary_lines(&mut self, summary: Option) { + let Some(summary) = summary else { + return; + }; + let mut lines: Vec> = vec![summary.usage_line.into()]; + if let Some(command) = summary.resume_command { + lines.push(vec!["To continue this session, run ".into(), command.cyan()].into()); + } + self.chat_widget.add_plain_history_lines(lines); + } + + async fn switch_to_existing_thread(&mut self, tui: &mut tui::Tui, bootstrap: ThreadBootstrap) { + self.shutdown_current_thread().await; + let init = self.chatwidget_init_for_forked_or_resumed_thread(tui, self.config.clone()); + self.chat_widget = + ChatWidget::new_from_existing(init, bootstrap, self.app_server_client.clone()); + self.reset_thread_event_state(); + } + async fn rebuild_config_for_cwd(&self, cwd: PathBuf) -> Result { let mut overrides = self.harness_overrides.clone(); overrides.cwd = Some(cwd.clone()); @@ -745,7 +972,7 @@ impl App { // Clear any in-flight rollback guard when switching threads. self.backtrack.pending_rollback = None; self.suppress_shutdown_complete = true; - self.chat_widget.submit_op(Op::Shutdown); + self.chat_widget.submit_op(AgentCommand::Shutdown); self.server.remove_thread(&thread_id).await; } } @@ -794,7 +1021,7 @@ impl App { async fn activate_thread_for_replay( &mut self, thread_id: ThreadId, - ) -> Option<(mpsc::Receiver, ThreadEventSnapshot)> { + ) -> Option<(mpsc::Receiver, ThreadEventSnapshot)> { let channel = self.thread_event_channels.get_mut(&thread_id)?; let receiver = channel.receiver.take()?; let mut store = channel.store.lock().await; @@ -810,7 +1037,11 @@ impl App { self.active_thread_rx = None; } - async fn enqueue_thread_event(&mut self, thread_id: ThreadId, event: Event) -> Result<()> { + async fn enqueue_thread_ui_event( + &mut self, + thread_id: ThreadId, + event: ThreadUiEvent, + ) -> Result<()> { let (sender, store) = { let channel = self.ensure_thread_channel(thread_id); (channel.sender.clone(), Arc::clone(&channel.store)) @@ -818,14 +1049,24 @@ impl App { let should_send = { let mut guard = store.lock().await; - guard.push_event(event.clone()); - guard.active + match event.clone() { + ThreadUiEvent::Bootstrap(bootstrap) => { + guard.set_bootstrap(bootstrap); + guard.active + } + ThreadUiEvent::Notification(notification) => { + guard.push_notification(notification); + guard.active + } + ThreadUiEvent::ServerRequest(_) => true, + } }; if should_send { // Never await a bounded channel send on the main TUI loop: if the receiver falls behind, // `send().await` can block and the UI stops drawing. If the channel is full, wait in a - // spawned task instead. + // spawned task instead. `ServerRequest` events always queue to the thread channel even + // when the thread is inactive so approvals stay scoped to the right thread. match sender.try_send(event) { Ok(()) => {} Err(TrySendError::Full(event)) => { @@ -843,26 +1084,61 @@ impl App { Ok(()) } - async fn enqueue_primary_event(&mut self, event: Event) -> Result<()> { + async fn enqueue_thread_bootstrap(&mut self, bootstrap: ThreadBootstrap) -> Result<()> { + self.enqueue_thread_ui_event(bootstrap.thread_id, ThreadUiEvent::Bootstrap(bootstrap)) + .await + } + + async fn enqueue_thread_notification( + &mut self, + thread_id: ThreadId, + notification: app_proto::ServerNotification, + ) -> Result<()> { + self.enqueue_thread_ui_event(thread_id, ThreadUiEvent::Notification(notification)) + .await + } + + async fn enqueue_thread_server_request( + &mut self, + thread_id: ThreadId, + request: app_proto::ServerRequest, + ) -> Result<()> { + self.enqueue_thread_ui_event(thread_id, ThreadUiEvent::ServerRequest(request)) + .await + } + + async fn enqueue_primary_bootstrap(&mut self, bootstrap: ThreadBootstrap) -> Result<()> { if let Some(thread_id) = self.primary_thread_id { - return self.enqueue_thread_event(thread_id, event).await; + let _ = thread_id; + return self.enqueue_thread_bootstrap(bootstrap).await; } - if let EventMsg::SessionConfigured(session) = &event.msg { - let thread_id = session.session_id; - self.primary_thread_id = Some(thread_id); - self.primary_session_configured = Some(session.clone()); - self.ensure_thread_channel(thread_id); - self.activate_thread_channel(thread_id).await; + let thread_id = bootstrap.thread_id; + self.primary_thread_id = Some(thread_id); + self.ensure_thread_channel(thread_id); + self.activate_thread_channel(thread_id).await; - let pending = std::mem::take(&mut self.pending_primary_events); - for pending_event in pending { - self.enqueue_thread_event(thread_id, pending_event).await?; - } - self.enqueue_thread_event(thread_id, event).await?; - } else { - self.pending_primary_events.push_back(event); + let pending = std::mem::take(&mut self.pending_primary_events); + for pending_event in pending { + self.enqueue_thread_ui_event(thread_id, pending_event) + .await?; } + self.enqueue_thread_bootstrap(bootstrap).await?; + Ok(()) + } + + async fn enqueue_primary_notification( + &mut self, + notification: app_proto::ServerNotification, + ) -> Result<()> { + if let Some(thread_id) = self.primary_thread_id { + return self + .enqueue_thread_notification(thread_id, notification) + .await; + } + + self.pending_primary_events + .push_back(ThreadUiEvent::Notification(notification)); Ok(()) } @@ -939,15 +1215,12 @@ impl App { return Ok(()); } - let thread = match self.server.get_thread(thread_id).await { - Ok(thread) => thread, - Err(err) => { - self.chat_widget.add_error_message(format!( - "Failed to attach to agent thread {thread_id}: {err}" - )); - return Ok(()); - } - }; + if let Err(err) = self.server.get_thread(thread_id).await { + self.chat_widget.add_error_message(format!( + "Failed to attach to agent thread {thread_id}: {err}" + )); + return Ok(()); + } let previous_thread_id = self.active_thread_id; self.store_active_thread_receiver().await; @@ -965,7 +1238,11 @@ impl App { self.active_thread_rx = Some(receiver); let init = self.chatwidget_init_for_forked_or_resumed_thread(tui, self.config.clone()); - let codex_op_tx = crate::chatwidget::spawn_op_forwarder(thread); + let codex_op_tx = crate::chatwidget::spawn_op_forwarder( + self.app_event_tx.clone(), + thread_id, + self.app_server_client.clone(), + ); self.chat_widget = ChatWidget::new_with_op_sender(init, codex_op_tx); self.reset_for_thread_switch(tui)?; @@ -1003,7 +1280,7 @@ impl App { let mut disconnected = false; loop { match rx.try_recv() { - Ok(event) => self.handle_codex_event_now(event), + Ok(event) => self.handle_thread_ui_event_now(event), Err(TryRecvError::Empty) => break, Err(TryRecvError::Disconnected) => { disconnected = true; @@ -1035,8 +1312,14 @@ impl App { /// 1. the event is `ShutdownComplete`; /// 2. the active thread differs from the primary thread; /// 3. the active thread is not the pending shutdown-exit thread. - fn active_non_primary_shutdown_target(&self, msg: &EventMsg) -> Option<(ThreadId, ThreadId)> { - if !matches!(msg, EventMsg::ShutdownComplete) { + fn active_non_primary_shutdown_target( + &self, + event: &ThreadUiEvent, + ) -> Option<(ThreadId, ThreadId)> { + if !matches!( + event, + ThreadUiEvent::Notification(app_proto::ServerNotification::ThreadShutdownCompleted(_)) + ) { return None; } let active_thread_id = self.active_thread_id?; @@ -1048,11 +1331,11 @@ impl App { } fn replay_thread_snapshot(&mut self, snapshot: ThreadEventSnapshot) { - if let Some(event) = snapshot.session_configured { - self.handle_codex_event_replay(event); + if let Some(bootstrap) = snapshot.bootstrap { + self.handle_thread_bootstrap_replay(bootstrap); } - for event in snapshot.events { - self.handle_codex_event_replay(event); + for notification in snapshot.notifications { + self.handle_server_notification_replay(notification); } self.refresh_status_line(); } @@ -1107,6 +1390,13 @@ impl App { SessionSource::Cli, config.model_catalog.clone(), )); + let app_server_client = Arc::new(EmbeddedSessionClient::new(EmbeddedSessionClientArgs { + auth_manager: auth_manager.clone(), + thread_manager: thread_manager.clone(), + config: config.clone(), + cli_overrides: cli_kv_overrides.clone(), + feedback: feedback.clone(), + })); let mut model = thread_manager .get_models_manager() .get_default_model(&config.model, RefreshStrategy::Offline) @@ -1193,16 +1483,19 @@ impl App { status_line_invalid_items_warned: status_line_invalid_items_warned.clone(), otel_manager: otel_manager.clone(), }; - ChatWidget::new(init, thread_manager.clone()) + ChatWidget::new(init, app_server_client.clone()) } SessionSelection::Resume(path) => { - let resumed = thread_manager - .resume_thread_from_rollout(config.clone(), path.clone(), auth_manager.clone()) + let resumed = app_server_client + .thread_resume(thread_resume_params_from_config(path.clone(), &config)) .await - .wrap_err_with(|| { + .map_err(|err| { let path_display = path.display(); - format!("Failed to resume session from {path_display}") + color_eyre::eyre::eyre!( + "Failed to resume session from {path_display}: {err:?}" + ) })?; + let resumed = thread_bootstrap_from_resume_response(resumed)?; let init = crate::chatwidget::ChatWidgetInit { config: config.clone(), frame_requester: tui.frame_requester(), @@ -1223,17 +1516,20 @@ impl App { status_line_invalid_items_warned: status_line_invalid_items_warned.clone(), otel_manager: otel_manager.clone(), }; - ChatWidget::new_from_existing(init, resumed.thread, resumed.session_configured) + ChatWidget::new_from_existing(init, resumed, app_server_client.clone()) } SessionSelection::Fork(path) => { otel_manager.counter("codex.thread.fork", 1, &[("source", "cli_subcommand")]); - let forked = thread_manager - .fork_thread(usize::MAX, config.clone(), path.clone(), false) + let forked = app_server_client + .thread_fork(thread_fork_params_from_config(path.clone(), &config)) .await - .wrap_err_with(|| { + .map_err(|err| { let path_display = path.display(); - format!("Failed to fork session from {path_display}") + color_eyre::eyre::eyre!( + "Failed to fork session from {path_display}: {err:?}" + ) })?; + let forked = thread_bootstrap_from_fork_response(forked)?; let init = crate::chatwidget::ChatWidgetInit { config: config.clone(), frame_requester: tui.frame_requester(), @@ -1254,7 +1550,7 @@ impl App { status_line_invalid_items_warned: status_line_invalid_items_warned.clone(), otel_manager: otel_manager.clone(), }; - ChatWidget::new_from_existing(init, forked.thread, forked.session_configured) + ChatWidget::new_from_existing(init, forked, app_server_client.clone()) } }; @@ -1267,6 +1563,7 @@ impl App { let mut app = Self { server: thread_manager.clone(), + app_server_client: app_server_client.clone(), otel_manager: otel_manager.clone(), app_event_tx, chat_widget, @@ -1297,7 +1594,6 @@ impl App { active_thread_id: None, active_thread_rx: None, primary_thread_id: None, - primary_session_configured: None, pending_primary_events: VecDeque::new(), }; @@ -1353,8 +1649,8 @@ impl App { tui.frame_requester().schedule_frame(); - let mut thread_created_rx = thread_manager.subscribe_thread_created(); - let mut listen_for_threads = true; + let mut app_server_messages_rx = app_server_client.subscribe(); + let mut listen_for_app_server = true; let mut waiting_for_initial_session_configured = wait_for_initial_session_configured; let exit_reason = loop { @@ -1382,17 +1678,17 @@ impl App { Some(event) = tui_events.next() => { app.handle_tui_event(tui, event).await? } - // Listen on new thread creation due to collab tools. - created = thread_created_rx.recv(), if listen_for_threads => { - match created { - Ok(thread_id) => { - app.handle_thread_created(thread_id).await?; + app_server_message = app_server_messages_rx.recv(), if listen_for_app_server => { + match app_server_message { + Ok(EmbeddedSessionMessage::Notification(notification)) => { + app.handle_app_server_notification(notification).await?; } + Ok(EmbeddedSessionMessage::Request(_)) => {} Err(broadcast::error::RecvError::Lagged(_)) => { - tracing::warn!("thread_created receiver lagged; skipping resync"); + tracing::warn!("app-server notification receiver lagged"); } Err(broadcast::error::RecvError::Closed) => { - listen_for_threads = false; + listen_for_app_server = false; } } AppRunControl::Continue @@ -1507,16 +1803,9 @@ impl App { status_line_invalid_items_warned: self.status_line_invalid_items_warned.clone(), otel_manager: self.otel_manager.clone(), }; - self.chat_widget = ChatWidget::new(init, self.server.clone()); + self.chat_widget = ChatWidget::new(init, self.app_server_client.clone()); self.reset_thread_event_state(); - if let Some(summary) = summary { - let mut lines: Vec> = vec![summary.usage_line.clone().into()]; - if let Some(command) = summary.resume_command { - let spans = vec!["To continue this session, run ".into(), command.cyan()]; - lines.push(spans.into()); - } - self.chat_widget.add_plain_history_lines(lines); - } + self.append_session_summary_lines(summary); tui.frame_requester().schedule_frame(); } AppEvent::ClearUi => { @@ -1563,46 +1852,34 @@ impl App { self.chat_widget.thread_name(), ); match self - .server - .resume_thread_from_rollout( - resume_config.clone(), + .app_server_client + .thread_resume(thread_resume_params_from_config( path.clone(), - self.auth_manager.clone(), - ) + &resume_config, + )) .await { - Ok(resumed) => { - self.shutdown_current_thread().await; - self.config = resume_config; - tui.set_notification_method(self.config.tui_notification_method); - self.file_search.update_search_dir(self.config.cwd.clone()); - let init = self.chatwidget_init_for_forked_or_resumed_thread( - tui, - self.config.clone(), - ); - self.chat_widget = ChatWidget::new_from_existing( - init, - resumed.thread, - resumed.session_configured, - ); - self.reset_thread_event_state(); - if let Some(summary) = summary { - let mut lines: Vec> = - vec![summary.usage_line.clone().into()]; - if let Some(command) = summary.resume_command { - let spans = vec![ - "To continue this session, run ".into(), - command.cyan(), - ]; - lines.push(spans.into()); - } - self.chat_widget.add_plain_history_lines(lines); + Ok(resumed) => match thread_bootstrap_from_resume_response(resumed) { + Ok(resumed) => { + self.config = resume_config; + tui.set_notification_method( + self.config.tui_notification_method, + ); + self.file_search.update_search_dir(self.config.cwd.clone()); + self.switch_to_existing_thread(tui, resumed).await; + self.append_session_summary_lines(summary); } - } + Err(err) => { + let path_display = path.display(); + self.chat_widget.add_error_message(format!( + "Failed to resume session from {path_display}: {err:?}" + )); + } + }, Err(err) => { let path_display = path.display(); self.chat_widget.add_error_message(format!( - "Failed to resume session from {path_display}: {err}" + "Failed to resume session from {path_display}: {err:?}" )); } } @@ -1630,39 +1907,26 @@ impl App { // materialized lazily on first user message. if path.exists() { match self - .server - .fork_thread(usize::MAX, self.config.clone(), path.clone(), false) + .app_server_client + .thread_fork(thread_fork_params_from_config(path.clone(), &self.config)) .await { - Ok(forked) => { - self.shutdown_current_thread().await; - let init = self.chatwidget_init_for_forked_or_resumed_thread( - tui, - self.config.clone(), - ); - self.chat_widget = ChatWidget::new_from_existing( - init, - forked.thread, - forked.session_configured, - ); - self.reset_thread_event_state(); - if let Some(summary) = summary { - let mut lines: Vec> = - vec![summary.usage_line.clone().into()]; - if let Some(command) = summary.resume_command { - let spans = vec![ - "To continue this session, run ".into(), - command.cyan(), - ]; - lines.push(spans.into()); - } - self.chat_widget.add_plain_history_lines(lines); + Ok(forked) => match thread_bootstrap_from_fork_response(forked) { + Ok(forked) => { + self.switch_to_existing_thread(tui, forked).await; + self.append_session_summary_lines(summary); } - } + Err(err) => { + let path_display = path.display(); + self.chat_widget.add_error_message(format!( + "Failed to fork current session from {path_display}: {err:?}" + )); + } + }, Err(err) => { let path_display = path.display(); self.chat_widget.add_error_message(format!( - "Failed to fork current session from {path_display}: {err}" + "Failed to fork current session from {path_display}: {err:?}" )); } } @@ -1712,6 +1976,15 @@ impl App { tui.frame_requester().schedule_frame(); } } + AppEvent::ThreadRollbackCompleted { + thread_id, + num_turns, + } => { + self.handle_backtrack_rollback_completed(thread_id, num_turns); + if self.backtrack_render_pending { + tui.frame_requester().schedule_frame(); + } + } AppEvent::StartCommitAnimation => { if self .commit_anim_running @@ -1734,8 +2007,27 @@ impl App { AppEvent::CommitTick => { self.chat_widget.on_commit_tick(); } - AppEvent::CodexEvent(event) => { - self.enqueue_primary_event(event).await?; + AppEvent::ThreadBootstrapped(bootstrap) => { + self.enqueue_primary_bootstrap(bootstrap).await?; + } + AppEvent::AppServerRequest { thread_id, request } => { + self.enqueue_thread_server_request(thread_id, request) + .await?; + } + AppEvent::HistoryEntryReadResponse(response) => { + self.chat_widget.on_get_history_entry_response_v2(response); + } + AppEvent::McpToolsListResponse(response) => { + self.chat_widget.on_list_mcp_tools_v2(response); + } + AppEvent::CustomPromptListResponse(response) => { + self.chat_widget.on_list_custom_prompts_v2(response); + } + AppEvent::SkillsListResponse(response) => { + let cwd = self.chat_widget.config_ref().cwd.clone(); + let errors = errors_for_cwd_v2(&cwd, &response); + emit_skill_load_warnings(&self.app_event_tx, &errors); + self.chat_widget.on_list_skills_v2(response); } AppEvent::Exit(mode) => match mode { ExitMode::ShutdownFirst => { @@ -1743,7 +2035,7 @@ impl App { // its shutdown completion does not trigger agent failover. self.pending_shutdown_exit_thread_id = self.active_thread_id.or(self.chat_widget.thread_id()); - self.chat_widget.submit_op(Op::Shutdown); + self.chat_widget.submit_op(AgentCommand::Shutdown); } ExitMode::Immediate => { self.pending_shutdown_exit_thread_id = None; @@ -1753,7 +2045,7 @@ impl App { AppEvent::FatalExitRequest(message) => { return Ok(AppRunControl::Exit(ExitReason::Fatal(message))); } - AppEvent::CodexOp(op) => { + AppEvent::AgentCommand(op) => { self.chat_widget.submit_op(op); } AppEvent::DiffResult(text) => { @@ -2108,8 +2400,8 @@ impl App { if let Some((sample_paths, extra_count, failed_scan)) = self.chat_widget.world_writable_warning_details() { - self.app_event_tx.send(AppEvent::CodexOp( - Op::OverrideTurnContext { + self.app_event_tx.send(AppEvent::AgentCommand( + AgentCommand::OverrideTurnContext { cwd: None, approval_policy: None, sandbox_policy: None, @@ -2130,8 +2422,8 @@ impl App { }, ); } else { - self.app_event_tx.send(AppEvent::CodexOp( - Op::OverrideTurnContext { + self.app_event_tx.send(AppEvent::AgentCommand( + AgentCommand::OverrideTurnContext { cwd: None, approval_policy: Some(preset.approval), sandbox_policy: Some(preset.sandbox.clone()), @@ -2353,8 +2645,8 @@ impl App { #[cfg(target_os = "windows")] { let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config); - self.app_event_tx - .send(AppEvent::CodexOp(Op::OverrideTurnContext { + self.app_event_tx.send(AppEvent::AgentCommand( + AgentCommand::OverrideTurnContext { cwd: None, approval_policy: None, sandbox_policy: None, @@ -2364,7 +2656,8 @@ impl App { summary: None, collaboration_mode: None, personality: None, - })); + }, + )); } } if let Err(err) = builder.apply().await { @@ -2576,7 +2869,7 @@ impl App { if let Err(err) = self.refresh_in_memory_config_from_disk().await { tracing::warn!(error = %err, "failed to refresh config after app toggle"); } - self.chat_widget.submit_op(Op::ReloadUserConfig); + self.chat_widget.submit_op(AgentCommand::ReloadUserConfig); } Err(err) => { self.chat_widget.add_error_message(format!( @@ -2701,33 +2994,51 @@ impl App { Ok(AppRunControl::Continue) } - fn handle_codex_event_now(&mut self, event: Event) { - let needs_refresh = matches!( - event.msg, - EventMsg::SessionConfigured(_) | EventMsg::TokenCount(_) - ); - // This guard is only for intentional thread-switch shutdowns. - // App-exit shutdowns are tracked by `pending_shutdown_exit_thread_id` - // and resolved in `handle_active_thread_event`. - if self.suppress_shutdown_complete && matches!(event.msg, EventMsg::ShutdownComplete) { - self.suppress_shutdown_complete = false; - return; - } - if let EventMsg::ListSkillsResponse(response) = &event.msg { - let cwd = self.chat_widget.config_ref().cwd.clone(); - let errors = errors_for_cwd(&cwd, response); - emit_skill_load_warnings(&self.app_event_tx, &errors); - } - self.handle_backtrack_event(&event.msg); - self.chat_widget.handle_codex_event(event); - - if needs_refresh { - self.refresh_status_line(); + fn handle_thread_ui_event_now(&mut self, event: ThreadUiEvent) { + match event { + ThreadUiEvent::Bootstrap(bootstrap) => { + self.chat_widget.handle_thread_bootstrap(bootstrap) + } + ThreadUiEvent::Notification(notification) => { + self.handle_server_notification_now(notification) + } + ThreadUiEvent::ServerRequest(request) => { + self.chat_widget.handle_app_server_request(request) + } } } - fn handle_codex_event_replay(&mut self, event: Event) { - self.chat_widget.handle_codex_event_replay(event); + fn handle_thread_bootstrap_replay(&mut self, bootstrap: ThreadBootstrap) { + self.chat_widget.handle_thread_bootstrap_replay(bootstrap); + } + + fn handle_server_notification_replay(&mut self, notification: app_proto::ServerNotification) { + self.chat_widget + .handle_server_notification_replay(notification); + } + + fn handle_server_notification_now(&mut self, notification: app_proto::ServerNotification) { + if self.suppress_shutdown_complete + && matches!( + notification, + app_proto::ServerNotification::ThreadShutdownCompleted(_) + ) + { + self.suppress_shutdown_complete = false; + return; + } + + if let app_proto::ServerNotification::Error(error) = ¬ification + && error + .error + .codex_error_info + .as_ref() + .is_some_and(|info| matches!(info, app_proto::CodexErrorInfo::ThreadRollbackFailed)) + { + self.handle_backtrack_rollback_failed(); + } + + self.chat_widget.handle_server_notification(notification); } /// Handles an event emitted by the currently active thread. @@ -2735,11 +3046,18 @@ impl App { /// This function enforces shutdown intent routing: unexpected non-primary /// thread shutdowns fail over to the primary thread, while user-requested /// app exits consume only the tracked shutdown completion and then proceed. - async fn handle_active_thread_event(&mut self, tui: &mut tui::Tui, event: Event) -> Result<()> { + async fn handle_active_thread_event( + &mut self, + tui: &mut tui::Tui, + event: ThreadUiEvent, + ) -> Result<()> { // Capture this before any potential thread switch: we only want to clear // the exit marker when the currently active thread acknowledges shutdown. - let pending_shutdown_exit_completed = matches!(&event.msg, EventMsg::ShutdownComplete) - && self.pending_shutdown_exit_thread_id == self.active_thread_id; + let pending_shutdown_exit_completed = matches!( + &event, + ThreadUiEvent::Notification(app_proto::ServerNotification::ThreadShutdownCompleted(_)) + ) && self.pending_shutdown_exit_thread_id + == self.active_thread_id; // Processing order matters: // @@ -2750,7 +3068,7 @@ impl App { // This preserves the mental model that user-requested exits do not trigger // failover, while true sub-agent deaths still do. if let Some((closed_thread_id, primary_thread_id)) = - self.active_non_primary_shutdown_target(&event.msg) + self.active_non_primary_shutdown_target(&event) { self.thread_event_channels.remove(&closed_thread_id); self.select_agent_thread(tui, primary_thread_id).await?; @@ -2775,69 +3093,83 @@ impl App { // thread, so unrelated shutdowns cannot consume this marker. self.pending_shutdown_exit_thread_id = None; } - self.handle_codex_event_now(event); + self.handle_thread_ui_event_now(event); if self.backtrack_render_pending { tui.frame_requester().schedule_frame(); } Ok(()) } - async fn handle_thread_created(&mut self, thread_id: ThreadId) -> Result<()> { + async fn handle_thread_started_notification( + &mut self, + notification: app_proto::ThreadStartedNotification, + ) -> Result<()> { + let Some(thread_id) = ThreadId::try_from(notification.thread.id.clone()).ok() else { + tracing::warn!( + "failed to attach listener for invalid thread id {}", + notification.thread.id + ); + return Ok(()); + }; if self.thread_event_channels.contains_key(&thread_id) { return Ok(()); } let thread = match self.server.get_thread(thread_id).await { Ok(thread) => thread, Err(err) => { - tracing::warn!("failed to attach listener for thread {thread_id}: {err}"); + tracing::warn!("failed to attach thread {thread_id}: {err}"); return Ok(()); } }; let config_snapshot = thread.config_snapshot().await; - let event = Event { - id: String::new(), - msg: EventMsg::SessionConfigured(SessionConfiguredEvent { - session_id: thread_id, - forked_from_id: None, - thread_name: None, - model: config_snapshot.model, - model_provider_id: config_snapshot.model_provider_id, - approval_policy: config_snapshot.approval_policy, - sandbox_policy: config_snapshot.sandbox_policy, - cwd: config_snapshot.cwd, - reasoning_effort: config_snapshot.reasoning_effort, - history_log_id: 0, - history_entry_count: 0, - initial_messages: None, - network_proxy: None, - rollout_path: thread.rollout_path(), - }), + let bootstrap = ThreadBootstrap { + thread: notification.thread, + thread_id, + model: config_snapshot.model, + model_provider_id: config_snapshot.model_provider_id, + approval_policy: config_snapshot.approval_policy, + sandbox_policy: config_snapshot.sandbox_policy, + cwd: config_snapshot.cwd, + reasoning_effort: config_snapshot.reasoning_effort, + history_log_id: 0, + history_entry_count: 0, + forked_from_id: None, + network_proxy: None, + rollout_path: thread.rollout_path(), }; let channel = - ThreadEventChannel::new_with_session_configured(THREAD_EVENT_CHANNEL_CAPACITY, event); - let sender = channel.sender.clone(); - let store = Arc::clone(&channel.store); + ThreadEventChannel::new_with_bootstrap(THREAD_EVENT_CHANNEL_CAPACITY, bootstrap); self.thread_event_channels.insert(thread_id, channel); - tokio::spawn(async move { - loop { - let event = match thread.next_event().await { - Ok(event) => event, - Err(err) => { - tracing::debug!("external thread {thread_id} listener stopped: {err}"); - break; - } - }; - let should_send = { - let mut guard = store.lock().await; - guard.push_event(event.clone()); - guard.active - }; - if should_send && let Err(err) = sender.send(event).await { - tracing::debug!("external thread {thread_id} channel closed: {err}"); - break; - } + Ok(()) + } + + async fn handle_app_server_notification( + &mut self, + notification: app_proto::ServerNotification, + ) -> Result<()> { + if let app_proto::ServerNotification::ThreadStarted(thread_started) = notification { + self.handle_thread_started_notification(thread_started) + .await?; + return Ok(()); + } + + if let app_proto::ServerNotification::AccountRateLimitsUpdated(rate_limits) = ¬ification + && let Ok(snapshot) = convert_via_serde(rate_limits.rate_limits.clone()) + { + self.chat_widget.on_rate_limit_snapshot(Some(snapshot)); + } + + if let Some(thread_id) = notification_thread_id(¬ification) { + if self.primary_thread_id.is_some() { + self.enqueue_thread_notification(thread_id, notification) + .await?; + } else { + self.enqueue_primary_notification(notification).await?; } - }); + } else { + self.chat_widget + .handle_global_server_notification(notification); + } Ok(()) } @@ -3234,18 +3566,21 @@ mod tests { .insert(thread_id, ThreadEventChannel::new(1)); app.set_thread_active(thread_id, true).await; - let event = Event { - id: String::new(), - msg: EventMsg::ShutdownComplete, - }; + let event = + ThreadUiEvent::Notification(app_proto::ServerNotification::ThreadShutdownCompleted( + app_proto::ThreadShutdownCompletedNotification { + thread_id: thread_id.to_string(), + }, + )); - app.enqueue_thread_event(thread_id, event.clone()).await?; + app.enqueue_thread_ui_event(thread_id, event.clone()) + .await?; time::timeout( Duration::from_millis(50), - app.enqueue_thread_event(thread_id, event), + app.enqueue_thread_ui_event(thread_id, event), ) .await - .expect("enqueue_thread_event blocked on a full channel")?; + .expect("enqueue_thread_ui_event blocked on a full channel")?; let mut rx = app .thread_event_channels @@ -3323,7 +3658,13 @@ mod tests { app.primary_thread_id = Some(ThreadId::new()); assert_eq!( - app.active_non_primary_shutdown_target(&EventMsg::SkillsUpdateAvailable), + app.active_non_primary_shutdown_target(&ThreadUiEvent::Notification( + app_proto::ServerNotification::SkillsUpdated( + app_proto::SkillsUpdatedNotification { + thread_id: ThreadId::new().to_string(), + } + ), + )), None ); Ok(()) @@ -3338,7 +3679,13 @@ mod tests { app.primary_thread_id = Some(thread_id); assert_eq!( - app.active_non_primary_shutdown_target(&EventMsg::ShutdownComplete), + app.active_non_primary_shutdown_target(&ThreadUiEvent::Notification( + app_proto::ServerNotification::ThreadShutdownCompleted( + app_proto::ThreadShutdownCompletedNotification { + thread_id: thread_id.to_string(), + }, + ), + )), None ); Ok(()) @@ -3354,7 +3701,13 @@ mod tests { app.primary_thread_id = Some(primary_thread_id); assert_eq!( - app.active_non_primary_shutdown_target(&EventMsg::ShutdownComplete), + app.active_non_primary_shutdown_target(&ThreadUiEvent::Notification( + app_proto::ServerNotification::ThreadShutdownCompleted( + app_proto::ThreadShutdownCompletedNotification { + thread_id: active_thread_id.to_string(), + }, + ), + )), Some((active_thread_id, primary_thread_id)) ); Ok(()) @@ -3371,7 +3724,13 @@ mod tests { app.pending_shutdown_exit_thread_id = Some(active_thread_id); assert_eq!( - app.active_non_primary_shutdown_target(&EventMsg::ShutdownComplete), + app.active_non_primary_shutdown_target(&ThreadUiEvent::Notification( + app_proto::ServerNotification::ThreadShutdownCompleted( + app_proto::ThreadShutdownCompletedNotification { + thread_id: active_thread_id.to_string(), + }, + ), + )), None ); Ok(()) @@ -3388,7 +3747,13 @@ mod tests { app.pending_shutdown_exit_thread_id = Some(ThreadId::new()); assert_eq!( - app.active_non_primary_shutdown_target(&EventMsg::ShutdownComplete), + app.active_non_primary_shutdown_target(&ThreadUiEvent::Notification( + app_proto::ServerNotification::ThreadShutdownCompleted( + app_proto::ThreadShutdownCompletedNotification { + thread_id: active_thread_id.to_string(), + }, + ), + )), Some((active_thread_id, primary_thread_id)) ); Ok(()) @@ -3515,12 +3880,20 @@ mod tests { let auth_manager = codex_core::test_support::auth_manager_from_auth( CodexAuth::from_api_key("Test API Key"), ); + let app_server_client = Arc::new(EmbeddedSessionClient::new(EmbeddedSessionClientArgs { + auth_manager: auth_manager.clone(), + thread_manager: server.clone(), + config: config.clone(), + cli_overrides: Vec::new(), + feedback: codex_feedback::CodexFeedback::new(), + })); let file_search = FileSearchManager::new(config.cwd.clone(), app_event_tx.clone()); let model = codex_core::test_support::get_model_offline(config.model.as_deref()); let otel_manager = test_otel_manager(&config, model.as_str()); App { server, + app_server_client, otel_manager, app_event_tx, chat_widget, @@ -3551,7 +3924,6 @@ mod tests { active_thread_id: None, active_thread_rx: None, primary_thread_id: None, - primary_session_configured: None, pending_primary_events: VecDeque::new(), } } @@ -3559,7 +3931,7 @@ mod tests { async fn make_test_app_with_channels() -> ( App, tokio::sync::mpsc::UnboundedReceiver, - tokio::sync::mpsc::UnboundedReceiver, + tokio::sync::mpsc::UnboundedReceiver, ) { let (chat_widget, app_event_tx, rx, op_rx) = make_chatwidget_manual_with_sender().await; let config = chat_widget.config_ref().clone(); @@ -3572,6 +3944,13 @@ mod tests { let auth_manager = codex_core::test_support::auth_manager_from_auth( CodexAuth::from_api_key("Test API Key"), ); + let app_server_client = Arc::new(EmbeddedSessionClient::new(EmbeddedSessionClientArgs { + auth_manager: auth_manager.clone(), + thread_manager: server.clone(), + config: config.clone(), + cli_overrides: Vec::new(), + feedback: codex_feedback::CodexFeedback::new(), + })); let file_search = FileSearchManager::new(config.cwd.clone(), app_event_tx.clone()); let model = codex_core::test_support::get_model_offline(config.model.as_deref()); let otel_manager = test_otel_manager(&config, model.as_str()); @@ -3579,6 +3958,7 @@ mod tests { ( App { server, + app_server_client, otel_manager, app_event_tx, chat_widget, @@ -3609,7 +3989,6 @@ mod tests { active_thread_id: None, active_thread_rx: None, primary_thread_id: None, - primary_session_configured: None, pending_primary_events: VecDeque::new(), }, rx, @@ -4014,7 +4393,7 @@ mod tests { let mut rollback_turns = None; while let Ok(op) = op_rx.try_recv() { - if let Op::ThreadRollback { num_turns } = op { + if let AgentCommand::ThreadRollback { num_turns } = op { rollback_turns = Some(num_turns); } } @@ -4049,7 +4428,7 @@ mod tests { let mut rollback_turns = None; while let Ok(op) = op_rx.try_recv() { - if let Op::ThreadRollback { num_turns } = op { + if let AgentCommand::ThreadRollback { num_turns } = op { rollback_turns = Some(num_turns); } } @@ -4104,8 +4483,8 @@ mod tests { let mut submitted_items: Option> = None; while let Ok(op) = op_rx.try_recv() { match op { - Op::ThreadRollback { .. } => saw_rollback = true, - Op::UserTurn { items, .. } => submitted_items = Some(items), + AgentCommand::ThreadRollback { .. } => saw_rollback = true, + AgentCommand::UserTurn { items, .. } => submitted_items = Some(items), _ => {} } } @@ -4125,7 +4504,7 @@ mod tests { let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await; let session_id = ThreadId::new(); - app.handle_codex_event_replay(Event { + app.chat_widget.handle_codex_event_replay(Event { id: String::new(), msg: EventMsg::SessionConfigured(SessionConfiguredEvent { session_id, @@ -4204,7 +4583,7 @@ mod tests { let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await; let session_id = ThreadId::new(); - app.handle_codex_event_replay(Event { + app.chat_widget.handle_codex_event_replay(Event { id: String::new(), msg: EventMsg::SessionConfigured(SessionConfiguredEvent { session_id, @@ -4237,11 +4616,8 @@ mod tests { }), }); - // Simulate a live rollback arriving before queued replay inserts are drained. - app.handle_codex_event_now(Event { - id: "live-rollback".to_string(), - msg: EventMsg::ThreadRolledBack(ThreadRolledBackEvent { num_turns: 1 }), - }); + // Simulate a live rollback completion arriving before queued replay inserts are drained. + app.handle_backtrack_rollback_completed(session_id, 1); let mut saw_rollback = false; while let Ok(event) = app_event_rx.try_recv() { @@ -4360,8 +4736,8 @@ mod tests { app.shutdown_current_thread().await; match op_rx.try_recv() { - Ok(Op::Shutdown) => {} - Ok(other) => panic!("expected Op::Shutdown, got {other:?}"), + Ok(AgentCommand::Shutdown) => {} + Ok(other) => panic!("expected AgentCommand::Shutdown, got {other:?}"), Err(_) => panic!("expected shutdown op to be sent"), } } diff --git a/codex-rs/tui/src/app_backtrack.rs b/codex-rs/tui/src/app_backtrack.rs index 1bb2c29569..e72c354429 100644 --- a/codex-rs/tui/src/app_backtrack.rs +++ b/codex-rs/tui/src/app_backtrack.rs @@ -27,6 +27,7 @@ use std::any::TypeId; use std::path::PathBuf; use std::sync::Arc; +use crate::agent_command::AgentCommand; use crate::app::App; use crate::app_event::AppEvent; use crate::history_cell::SessionInfoCell; @@ -35,10 +36,6 @@ use crate::pager_overlay::Overlay; use crate::tui; use crate::tui::TuiEvent; use codex_protocol::ThreadId; -use codex_protocol::protocol::CodexErrorInfo; -use codex_protocol::protocol::ErrorEvent; -use codex_protocol::protocol::EventMsg; -use codex_protocol::protocol::Op; use codex_protocol::user_input::TextElement; use color_eyre::eyre::Result; use crossterm::event::KeyCode; @@ -215,7 +212,8 @@ impl App { selection, thread_id: self.chat_widget.thread_id(), }); - self.chat_widget.submit_op(Op::ThreadRollback { num_turns }); + self.chat_widget + .submit_op(AgentCommand::ThreadRollback { num_turns }); self.chat_widget.set_remote_image_urls(remote_image_urls); if !prefill.is_empty() || !text_elements.is_empty() @@ -461,37 +459,38 @@ impl App { tui.frame_requester().schedule_frame(); } - pub(crate) fn handle_backtrack_event(&mut self, event: &EventMsg) { - match event { - EventMsg::ThreadRolledBack(rollback) => { - // `pending_rollback` is set only after this UI sends `Op::ThreadRollback` - // from the backtrack flow. In that case, finish immediately using the - // stored selection (nth user message) so local trim matches the exact - // backtrack target. - // - // When it is `None`, rollback came from replay or another source. We - // queue an AppEvent so rollback trim runs in FIFO order with - // `InsertHistoryCell` events, avoiding races with in-flight transcript - // inserts. - if self.backtrack.pending_rollback.is_some() { - self.finish_pending_backtrack(); - } else { - self.app_event_tx.send(AppEvent::ApplyThreadRollback { - num_turns: rollback.num_turns, - }); - } - } - EventMsg::Error(ErrorEvent { - codex_error_info: Some(CodexErrorInfo::ThreadRollbackFailed), - .. - }) => { - // Core rejected the rollback; clear the guard so the user can retry. - self.backtrack.pending_rollback = None; - } - _ => {} + pub(crate) fn handle_backtrack_rollback_completed( + &mut self, + thread_id: ThreadId, + num_turns: u32, + ) { + // `pending_rollback` is set only after this UI sends `AgentCommand::ThreadRollback` + // from the backtrack flow. In that case, finish immediately using the + // stored selection (nth user message) so local trim matches the exact + // backtrack target. + // + // When it is `None`, rollback came from replay or another source. We + // queue an AppEvent so rollback trim runs in FIFO order with + // `InsertHistoryCell` events, avoiding races with in-flight transcript + // inserts. + if self + .backtrack + .pending_rollback + .as_ref() + .is_some_and(|pending| pending.thread_id == Some(thread_id)) + { + self.finish_pending_backtrack(); + } else { + self.app_event_tx + .send(AppEvent::ApplyThreadRollback { num_turns }); } } + pub(crate) fn handle_backtrack_rollback_failed(&mut self) { + // Core rejected the rollback; clear the guard so the user can retry. + self.backtrack.pending_rollback = None; + } + /// Apply rollback semantics for `ThreadRolledBack` events where this TUI does not have an /// in-flight backtrack request (`pending_rollback` is `None`). /// diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 865ddb29e6..e34907f463 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -10,14 +10,17 @@ use std::path::PathBuf; +use codex_app_server_protocol as app_proto; use codex_chatgpt::connectors::AppInfo; use codex_file_search::FileMatch; use codex_protocol::ThreadId; use codex_protocol::openai_models::ModelPreset; -use codex_protocol::protocol::Event; use codex_protocol::protocol::RateLimitSnapshot; +use codex_protocol::protocol::SessionNetworkProxyRuntime; use codex_utils_approval_presets::ApprovalPreset; +use serde::Serialize; +use crate::agent_command::AgentCommand; use crate::bottom_pane::ApprovalRequest; use crate::bottom_pane::StatusLineItem; use crate::history_cell::HistoryCell; @@ -42,10 +45,39 @@ pub(crate) struct ConnectorsSnapshot { pub(crate) connectors: Vec, } +#[derive(Debug, Clone, Serialize)] +pub(crate) struct ThreadBootstrap { + pub(crate) thread: app_proto::Thread, + pub(crate) thread_id: ThreadId, + pub(crate) model: String, + pub(crate) model_provider_id: String, + pub(crate) approval_policy: AskForApproval, + pub(crate) sandbox_policy: SandboxPolicy, + pub(crate) cwd: PathBuf, + pub(crate) reasoning_effort: Option, + pub(crate) history_log_id: u64, + pub(crate) history_entry_count: usize, + pub(crate) forked_from_id: Option, + pub(crate) network_proxy: Option, + pub(crate) rollout_path: Option, +} + #[allow(clippy::large_enum_variant)] #[derive(Debug)] pub(crate) enum AppEvent { - CodexEvent(Event), + ThreadBootstrapped(ThreadBootstrap), + AppServerRequest { + thread_id: ThreadId, + request: app_proto::ServerRequest, + }, + HistoryEntryReadResponse(app_proto::HistoryEntryReadResponse), + McpToolsListResponse(app_proto::McpToolsListResponse), + CustomPromptListResponse(app_proto::CustomPromptListResponse), + SkillsListResponse(app_proto::SkillsListResponse), + ThreadRollbackCompleted { + thread_id: ThreadId, + num_turns: u32, + }, /// Open the agent picker for switching active threads. OpenAgentPicker, /// Switch the active thread to the selected agent. @@ -74,9 +106,9 @@ pub(crate) enum AppEvent { /// Request to exit the application due to a fatal error. FatalExitRequest(String), - /// Forward an `Op` to the Agent. Using an `AppEvent` for this avoids + /// Forward an agent command to the agent loop. Using an `AppEvent` for this avoids /// bubbling channels through layers of widgets. - CodexOp(codex_protocol::protocol::Op), + AgentCommand(AgentCommand), /// Kick off an asynchronous file search for the given query (text after /// the `@`). Previous searches may be cancelled by the app layer so there @@ -381,7 +413,7 @@ pub(crate) enum ExitMode { ShutdownFirst, /// Exit the UI loop immediately without waiting for shutdown. /// - /// This skips `Op::Shutdown`, so any in-flight work may be dropped and + /// This skips `AgentCommand::Shutdown`, so any in-flight work may be dropped and /// cleanup that normally runs before `ShutdownComplete` can be missed. Immediate, } diff --git a/codex-rs/tui/src/app_event_sender.rs b/codex-rs/tui/src/app_event_sender.rs index c1427b3ff0..0850bbfa74 100644 --- a/codex-rs/tui/src/app_event_sender.rs +++ b/codex-rs/tui/src/app_event_sender.rs @@ -18,7 +18,7 @@ impl AppEventSender { pub(crate) fn send(&self, event: AppEvent) { // Record inbound events for high-fidelity session replay. // Avoid double-logging Ops; those are logged at the point of submission. - if !matches!(event, AppEvent::CodexOp(_)) { + if !matches!(event, AppEvent::AgentCommand(_)) { session_log::log_inbound_app_event(&event); } if let Err(e) = self.app_event_tx.send(event) { diff --git a/codex-rs/tui/src/bottom_pane/approval_overlay.rs b/codex-rs/tui/src/bottom_pane/approval_overlay.rs index bac964b919..8c348e650f 100644 --- a/codex-rs/tui/src/bottom_pane/approval_overlay.rs +++ b/codex-rs/tui/src/bottom_pane/approval_overlay.rs @@ -1,6 +1,8 @@ use std::collections::HashMap; +use std::collections::VecDeque; use std::path::PathBuf; +use crate::agent_command::AgentCommand; use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; use crate::bottom_pane::BottomPaneView; @@ -22,7 +24,6 @@ use codex_protocol::protocol::ElicitationAction; use codex_protocol::protocol::ExecPolicyAmendment; use codex_protocol::protocol::FileChange; use codex_protocol::protocol::NetworkApprovalContext; -use codex_protocol::protocol::Op; use codex_protocol::protocol::ReviewDecision; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; @@ -63,7 +64,7 @@ pub(crate) enum ApprovalRequest { pub(crate) struct ApprovalOverlay { current_request: Option, current_variant: Option, - queue: Vec, + queue: VecDeque, app_event_tx: AppEventSender, list: ListSelectionView, options: Vec, @@ -77,7 +78,7 @@ impl ApprovalOverlay { let mut view = Self { current_request: None, current_variant: None, - queue: Vec::new(), + queue: VecDeque::new(), app_event_tx: app_event_tx.clone(), list: ListSelectionView::new(Default::default(), app_event_tx), options: Vec::new(), @@ -90,7 +91,7 @@ impl ApprovalOverlay { } pub fn enqueue_request(&mut self, req: ApprovalRequest) { - self.queue.push(req); + self.queue.push_back(req); } fn set_current(&mut self, request: ApprovalRequest) { @@ -172,6 +173,27 @@ impl ApprovalOverlay { (options, params) } + fn dispatch_variant_decision(&self, variant: &ApprovalVariant, decision: ApprovalDecision) { + match (variant, decision) { + (ApprovalVariant::Exec { id, command, .. }, ApprovalDecision::Review(decision)) => { + self.handle_exec_decision(id, command, decision); + } + (ApprovalVariant::ApplyPatch { id, .. }, ApprovalDecision::Review(decision)) => { + self.handle_patch_decision(id, decision); + } + ( + ApprovalVariant::McpElicitation { + server_name, + request_id, + }, + ApprovalDecision::McpElicitation(decision), + ) => { + self.handle_elicitation_decision(server_name, request_id, decision); + } + _ => {} + } + } + fn apply_selection(&mut self, actual_idx: usize) { if self.current_complete { return; @@ -180,24 +202,7 @@ impl ApprovalOverlay { return; }; if let Some(variant) = self.current_variant.as_ref() { - match (variant, &option.decision) { - (ApprovalVariant::Exec { id, command, .. }, ApprovalDecision::Review(decision)) => { - self.handle_exec_decision(id, command, decision.clone()); - } - (ApprovalVariant::ApplyPatch { id, .. }, ApprovalDecision::Review(decision)) => { - self.handle_patch_decision(id, decision.clone()); - } - ( - ApprovalVariant::McpElicitation { - server_name, - request_id, - }, - ApprovalDecision::McpElicitation(decision), - ) => { - self.handle_elicitation_decision(server_name, request_id, *decision); - } - _ => {} - } + self.dispatch_variant_decision(variant, option.decision.clone()); } self.current_complete = true; @@ -207,18 +212,20 @@ impl ApprovalOverlay { fn handle_exec_decision(&self, id: &str, command: &[String], decision: ReviewDecision) { let cell = history_cell::new_approval_decision_cell(command.to_vec(), decision.clone()); self.app_event_tx.send(AppEvent::InsertHistoryCell(cell)); - self.app_event_tx.send(AppEvent::CodexOp(Op::ExecApproval { - id: id.to_string(), - turn_id: None, - decision, - })); + self.app_event_tx + .send(AppEvent::AgentCommand(AgentCommand::ExecApproval { + id: id.to_string(), + turn_id: None, + decision, + })); } fn handle_patch_decision(&self, id: &str, decision: ReviewDecision) { - self.app_event_tx.send(AppEvent::CodexOp(Op::PatchApproval { - id: id.to_string(), - decision, - })); + self.app_event_tx + .send(AppEvent::AgentCommand(AgentCommand::PatchApproval { + id: id.to_string(), + decision, + })); } fn handle_elicitation_decision( @@ -228,7 +235,7 @@ impl ApprovalOverlay { decision: ElicitationAction, ) { self.app_event_tx - .send(AppEvent::CodexOp(Op::ResolveElicitation { + .send(AppEvent::AgentCommand(AgentCommand::ResolveElicitation { server_name: server_name.to_string(), request_id: request_id.clone(), decision, @@ -236,7 +243,7 @@ impl ApprovalOverlay { } fn advance_queue(&mut self) { - if let Some(next) = self.queue.pop() { + if let Some(next) = self.queue.pop_front() { self.set_current(next); } else { self.done = true; @@ -293,24 +300,15 @@ impl BottomPaneView for ApprovalOverlay { if !self.current_complete && let Some(variant) = self.current_variant.as_ref() { - match &variant { - ApprovalVariant::Exec { id, command, .. } => { - self.handle_exec_decision(id, command, ReviewDecision::Abort); + let decision = match variant { + ApprovalVariant::Exec { .. } | ApprovalVariant::ApplyPatch { .. } => { + ApprovalDecision::Review(ReviewDecision::Abort) } - ApprovalVariant::ApplyPatch { id, .. } => { - self.handle_patch_decision(id, ReviewDecision::Abort); + ApprovalVariant::McpElicitation { .. } => { + ApprovalDecision::McpElicitation(ElicitationAction::Cancel) } - ApprovalVariant::McpElicitation { - server_name, - request_id, - } => { - self.handle_elicitation_decision( - server_name, - request_id, - ElicitationAction::Cancel, - ); - } - } + }; + self.dispatch_variant_decision(variant, decision); } self.queue.clear(); self.done = true; @@ -611,7 +609,7 @@ mod tests { // We expect at least one CodexOp message in the queue. let mut saw_op = false; while let Ok(ev) = rx.try_recv() { - if matches!(ev, AppEvent::CodexOp(_)) { + if matches!(ev, AppEvent::AgentCommand(_)) { saw_op = true; break; } @@ -639,7 +637,7 @@ mod tests { view.handle_key_event(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::NONE)); let mut saw_op = false; while let Ok(ev) = rx.try_recv() { - if let AppEvent::CodexOp(Op::ExecApproval { decision, .. }) = ev { + if let AppEvent::AgentCommand(AgentCommand::ExecApproval { decision, .. }) = ev { assert_eq!( decision, ReviewDecision::ApprovedExecpolicyAmendment { @@ -796,7 +794,7 @@ mod tests { let mut decision = None; while let Ok(ev) = rx.try_recv() { - if let AppEvent::CodexOp(Op::ExecApproval { decision: d, .. }) = ev { + if let AppEvent::AgentCommand(AgentCommand::ExecApproval { decision: d, .. }) = ev { decision = Some(d); break; } diff --git a/codex-rs/tui/src/bottom_pane/chat_composer_history.rs b/codex-rs/tui/src/bottom_pane/chat_composer_history.rs index f6df497187..765db96fcd 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer_history.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer_history.rs @@ -1,11 +1,11 @@ use std::collections::HashMap; use std::path::PathBuf; +use crate::agent_command::AgentCommand; use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; use crate::bottom_pane::MentionBinding; use crate::mention_codec::decode_history_mentions; -use codex_protocol::protocol::Op; use codex_protocol::user_input::TextElement; /// A composer history entry that can rehydrate draft state. @@ -278,11 +278,11 @@ impl ChatComposerHistory { self.last_history_text = Some(entry.text.clone()); return Some(entry); } else if let Some(log_id) = self.history_log_id { - let op = Op::GetHistoryEntryRequest { + let op = AgentCommand::GetHistoryEntryRequest { offset: global_idx, log_id, }; - app_event_tx.send(AppEvent::CodexOp(op)); + app_event_tx.send(AppEvent::AgentCommand(op)); } None } @@ -291,8 +291,8 @@ impl ChatComposerHistory { #[cfg(test)] mod tests { use super::*; + use crate::agent_command::AgentCommand; use crate::app_event::AppEvent; - use codex_protocol::protocol::Op; use pretty_assertions::assert_eq; use tokio::sync::mpsc::unbounded_channel; @@ -340,11 +340,11 @@ mod tests { // Verify that an AppEvent::CodexOp with the correct GetHistoryEntryRequest was sent. let event = rx.try_recv().expect("expected AppEvent to be sent"); - let AppEvent::CodexOp(history_request1) = event else { + let AppEvent::AgentCommand(history_request1) = event else { panic!("unexpected event variant"); }; assert_eq!( - Op::GetHistoryEntryRequest { + AgentCommand::GetHistoryEntryRequest { log_id: 1, offset: 2 }, @@ -362,11 +362,11 @@ mod tests { // Verify second CodexOp event for offset 1. let event2 = rx.try_recv().expect("expected second event"); - let AppEvent::CodexOp(history_request_2) = event2 else { + let AppEvent::AgentCommand(history_request_2) = event2 else { panic!("unexpected event variant"); }; assert_eq!( - Op::GetHistoryEntryRequest { + AgentCommand::GetHistoryEntryRequest { log_id: 1, offset: 1 }, diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 9c7b940f0e..f4798891cf 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -373,7 +373,7 @@ impl BottomPane { && !self.composer.popup_active() && let Some(status) = &self.status { - // Send Op::Interrupt + // Send AgentCommand::Interrupt status.interrupt(); self.request_redraw(); return InputResult::None; @@ -981,8 +981,8 @@ impl Renderable for BottomPane { #[cfg(test)] mod tests { use super::*; + use crate::agent_command::AgentCommand; use crate::app_event::AppEvent; - use codex_protocol::protocol::Op; use codex_protocol::protocol::SkillScope; use crossterm::event::KeyModifiers; use insta::assert_snapshot; @@ -1432,8 +1432,8 @@ mod tests { while let Ok(ev) = rx.try_recv() { assert!( - !matches!(ev, AppEvent::CodexOp(Op::Interrupt)), - "expected Esc to not send Op::Interrupt when dismissing skill popup" + !matches!(ev, AppEvent::AgentCommand(AgentCommand::Interrupt)), + "expected Esc to not send AgentCommand::Interrupt when dismissing skill popup" ); } assert!( @@ -1470,8 +1470,8 @@ mod tests { while let Ok(ev) = rx.try_recv() { assert!( - !matches!(ev, AppEvent::CodexOp(Op::Interrupt)), - "expected Esc to not send Op::Interrupt while command popup is active" + !matches!(ev, AppEvent::AgentCommand(AgentCommand::Interrupt)), + "expected Esc to not send AgentCommand::Interrupt while command popup is active" ); } assert_eq!(pane.composer_text(), "/"); @@ -1497,8 +1497,11 @@ mod tests { pane.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); assert!( - matches!(rx.try_recv(), Ok(AppEvent::CodexOp(Op::Interrupt))), - "expected Esc to send Op::Interrupt while a task is running" + matches!( + rx.try_recv(), + Ok(AppEvent::AgentCommand(AgentCommand::Interrupt)) + ), + "expected Esc to send AgentCommand::Interrupt while a task is running" ); } diff --git a/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs b/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs index 79b1229800..97f2ab0b02 100644 --- a/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs +++ b/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs @@ -30,7 +30,7 @@ use crate::bottom_pane::selection_popup_common::measure_rows_height; use crate::history_cell; use crate::render::renderable::Renderable; -use codex_protocol::protocol::Op; +use crate::agent_command::AgentCommand; use codex_protocol::request_user_input::RequestUserInputAnswer; use codex_protocol::request_user_input::RequestUserInputEvent; use codex_protocol::request_user_input::RequestUserInputResponse; @@ -312,27 +312,14 @@ impl RequestUserInputOverlay { } pub(super) fn options_required_height(&self, width: u16) -> u16 { - if !self.has_options() { - return 0; - } - - let rows = self.option_rows(); - if rows.is_empty() { - return 1; - } - - let mut state = self - .current_answer() - .map(|answer| answer.options_state) - .unwrap_or_default(); - if state.selected_idx.is_none() { - state.selected_idx = Some(0); - } - - measure_rows_height(&rows, &state, rows.len(), width.max(1)) + self.options_height(width) } pub(super) fn options_preferred_height(&self, width: u16) -> u16 { + self.options_height(width) + } + + fn options_height(&self, width: u16) -> u16 { if !self.has_options() { return 0; } @@ -653,6 +640,24 @@ impl RequestUserInputOverlay { } } + fn move_option_selection(&mut self, next: bool) { + let options_len = self.options_len(); + let moved = if let Some(answer) = self.current_answer_mut() { + if next { + answer.options_state.move_down_wrap(options_len); + } else { + answer.options_state.move_up_wrap(options_len); + } + answer.answer_committed = false; + true + } else { + false + }; + if moved { + self.sync_composer_placeholder(); + } + } + /// Clear the current option selection and hide notes when empty. fn clear_selection(&mut self) { if !self.has_options() { @@ -746,8 +751,8 @@ impl RequestUserInputOverlay { ); } self.app_event_tx - .send(AppEvent::CodexOp(Op::UserInputAnswer { - id: self.request.turn_id.clone(), + .send(AppEvent::AgentCommand(AgentCommand::UserInputAnswer { + id: self.request.call_id.clone(), response: RequestUserInputResponse { answers: answers.clone(), }, @@ -1007,7 +1012,8 @@ impl BottomPaneView for RequestUserInputOverlay { } // TODO: Emit interrupted request_user_input results (including committed answers) // once core supports persisting them reliably without follow-up turn issues. - self.app_event_tx.send(AppEvent::CodexOp(Op::Interrupt)); + self.app_event_tx + .send(AppEvent::AgentCommand(AgentCommand::Interrupt)); self.done = true; return; } @@ -1077,32 +1083,13 @@ impl BottomPaneView for RequestUserInputOverlay { match self.focus { Focus::Options => { - let options_len = self.options_len(); // Keep selection synchronized as the user moves. match key_event.code { KeyCode::Up | KeyCode::Char('k') => { - let moved = if let Some(answer) = self.current_answer_mut() { - answer.options_state.move_up_wrap(options_len); - answer.answer_committed = false; - true - } else { - false - }; - if moved { - self.sync_composer_placeholder(); - } + self.move_option_selection(false); } KeyCode::Down | KeyCode::Char('j') => { - let moved = if let Some(answer) = self.current_answer_mut() { - answer.options_state.move_down_wrap(options_len); - answer.answer_committed = false; - true - } else { - false - }; - if moved { - self.sync_composer_placeholder(); - } + self.move_option_selection(true); } KeyCode::Char(' ') => { self.select_current_option(true); @@ -1165,34 +1152,7 @@ impl BottomPaneView for RequestUserInputOverlay { return; } if self.has_options() && matches!(key_event.code, KeyCode::Up | KeyCode::Down) { - let options_len = self.options_len(); - match key_event.code { - KeyCode::Up => { - let moved = if let Some(answer) = self.current_answer_mut() { - answer.options_state.move_up_wrap(options_len); - answer.answer_committed = false; - true - } else { - false - }; - if moved { - self.sync_composer_placeholder(); - } - } - KeyCode::Down => { - let moved = if let Some(answer) = self.current_answer_mut() { - answer.options_state.move_down_wrap(options_len); - answer.answer_committed = false; - true - } else { - false - }; - if moved { - self.sync_composer_placeholder(); - } - } - _ => {} - } + self.move_option_selection(matches!(key_event.code, KeyCode::Down)); return; } self.ensure_selected_for_notes(); @@ -1223,7 +1183,8 @@ impl BottomPaneView for RequestUserInputOverlay { self.close_unanswered_confirmation(); // TODO: Emit interrupted request_user_input results (including committed answers) // once core supports persisting them reliably without follow-up turn issues. - self.app_event_tx.send(AppEvent::CodexOp(Op::Interrupt)); + self.app_event_tx + .send(AppEvent::AgentCommand(AgentCommand::Interrupt)); self.done = true; return CancellationEvent::Handled; } @@ -1234,7 +1195,8 @@ impl BottomPaneView for RequestUserInputOverlay { // TODO: Emit interrupted request_user_input results (including committed answers) // once core supports persisting them reliably without follow-up turn issues. - self.app_event_tx.send(AppEvent::CodexOp(Op::Interrupt)); + self.app_event_tx + .send(AppEvent::AgentCommand(AgentCommand::Interrupt)); self.done = true; CancellationEvent::Handled } @@ -1300,10 +1262,10 @@ mod tests { fn expect_interrupt_only(rx: &mut tokio::sync::mpsc::UnboundedReceiver) { let event = rx.try_recv().expect("expected interrupt AppEvent"); - let AppEvent::CodexOp(op) = event else { + let AppEvent::AgentCommand(op) = event else { panic!("expected CodexOp"); }; - assert_eq!(op, Op::Interrupt); + assert_eq!(op, AgentCommand::Interrupt); assert!( rx.try_recv().is_err(), "unexpected AppEvents before interrupt completion" @@ -1546,10 +1508,11 @@ mod tests { overlay.submit_answers(); let event = rx.try_recv().expect("expected AppEvent"); - let AppEvent::CodexOp(Op::UserInputAnswer { id, response, .. }) = event else { + let AppEvent::AgentCommand(AgentCommand::UserInputAnswer { id, response, .. }) = event + else { panic!("expected UserInputAnswer"); }; - assert_eq!(id, "turn-1"); + assert_eq!(id, "call-1"); let answer = response.answers.get("q1").expect("answer missing"); assert_eq!(answer.answers, Vec::::new()); } @@ -1568,7 +1531,7 @@ mod tests { overlay.handle_key_event(KeyEvent::from(KeyCode::Enter)); let event = rx.try_recv().expect("expected AppEvent"); - let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else { + let AppEvent::AgentCommand(AgentCommand::UserInputAnswer { response, .. }) = event else { panic!("expected UserInputAnswer"); }; let answer = response.answers.get("q1").expect("answer missing"); @@ -1604,7 +1567,7 @@ mod tests { overlay.handle_key_event(KeyEvent::from(KeyCode::Enter)); let event = rx.try_recv().expect("expected AppEvent"); - let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else { + let AppEvent::AgentCommand(AgentCommand::UserInputAnswer { response, .. }) = event else { panic!("expected UserInputAnswer"); }; let mut expected = HashMap::new(); @@ -1637,7 +1600,7 @@ mod tests { overlay.handle_key_event(KeyEvent::from(KeyCode::Char('2'))); let event = rx.try_recv().expect("expected AppEvent"); - let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else { + let AppEvent::AgentCommand(AgentCommand::UserInputAnswer { response, .. }) = event else { panic!("expected UserInputAnswer"); }; let answer = response.answers.get("q1").expect("answer missing"); @@ -1887,7 +1850,7 @@ mod tests { overlay.handle_key_event(KeyEvent::from(KeyCode::Enter)); let event = rx.try_recv().expect("expected AppEvent"); - let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else { + let AppEvent::AgentCommand(AgentCommand::UserInputAnswer { response, .. }) = event else { panic!("expected UserInputAnswer"); }; let answer = response.answers.get("q1").expect("answer missing"); @@ -2187,7 +2150,7 @@ mod tests { overlay.submit_answers(); let event = rx.try_recv().expect("expected AppEvent"); - let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else { + let AppEvent::AgentCommand(AgentCommand::UserInputAnswer { response, .. }) = event else { panic!("expected UserInputAnswer"); }; let answer = response.answers.get("q1").expect("answer missing"); @@ -2212,7 +2175,7 @@ mod tests { overlay.submit_answers(); let event = rx.try_recv().expect("expected AppEvent"); - let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else { + let AppEvent::AgentCommand(AgentCommand::UserInputAnswer { response, .. }) = event else { panic!("expected UserInputAnswer"); }; let answer = response.answers.get("q1").expect("answer missing"); @@ -2255,7 +2218,7 @@ mod tests { overlay.submit_answers(); let event = rx.try_recv().expect("expected AppEvent"); - let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else { + let AppEvent::AgentCommand(AgentCommand::UserInputAnswer { response, .. }) = event else { panic!("expected UserInputAnswer"); }; let answer = response.answers.get("q1").expect("answer missing"); @@ -2291,7 +2254,7 @@ mod tests { overlay.submit_answers(); let event = rx.try_recv().expect("expected AppEvent"); - let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else { + let AppEvent::AgentCommand(AgentCommand::UserInputAnswer { response, .. }) = event else { panic!("expected UserInputAnswer"); }; let answer = response.answers.get("q1").expect("answer missing"); @@ -2376,7 +2339,7 @@ mod tests { overlay.submit_answers(); let event = rx.try_recv().expect("expected AppEvent"); - let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else { + let AppEvent::AgentCommand(AgentCommand::UserInputAnswer { response, .. }) = event else { panic!("expected UserInputAnswer"); }; let answer = response.answers.get("q1").expect("answer missing"); diff --git a/codex-rs/tui/src/bottom_pane/skills_toggle_view.rs b/codex-rs/tui/src/bottom_pane/skills_toggle_view.rs index 1e0230c904..92ffec9f98 100644 --- a/codex-rs/tui/src/bottom_pane/skills_toggle_view.rs +++ b/codex-rs/tui/src/bottom_pane/skills_toggle_view.rs @@ -12,6 +12,7 @@ use ratatui::text::Line; use ratatui::widgets::Block; use ratatui::widgets::Widget; +use crate::agent_command::AgentCommand; use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; use crate::key_hint; @@ -22,7 +23,6 @@ use crate::render::renderable::Renderable; use crate::skills_helpers::match_skill; use crate::skills_helpers::truncate_skill_name; use crate::style::user_message_style; -use codex_protocol::protocol::Op; use super::CancellationEvent; use super::bottom_pane_view::BottomPaneView; @@ -187,10 +187,11 @@ impl SkillsToggleView { } self.complete = true; self.app_event_tx.send(AppEvent::ManageSkillsClosed); - self.app_event_tx.send(AppEvent::CodexOp(Op::ListSkills { - cwds: Vec::new(), - force_reload: true, - })); + self.app_event_tx + .send(AppEvent::AgentCommand(AgentCommand::ListSkills { + cwds: Vec::new(), + force_reload: true, + })); } fn rows_width(total_width: u16) -> u16 { diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 309ce80ce8..3f96b4ce50 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -37,6 +37,7 @@ use std::sync::atomic::Ordering; use std::time::Duration; use std::time::Instant; +use crate::agent_command::AgentCommand; use crate::bottom_pane::StatusLineItem; use crate::bottom_pane::StatusLineSetupView; use crate::status::RateLimitWindowDisplay; @@ -94,7 +95,6 @@ use codex_protocol::protocol::CodexErrorInfo; use codex_protocol::protocol::CreditsSnapshot; use codex_protocol::protocol::DeprecationNoticeEvent; use codex_protocol::protocol::ErrorEvent; -use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ExecApprovalRequestEvent; use codex_protocol::protocol::ExecCommandBeginEvent; @@ -110,7 +110,6 @@ use codex_protocol::protocol::McpStartupStatus; use codex_protocol::protocol::McpStartupUpdateEvent; use codex_protocol::protocol::McpToolCallBeginEvent; use codex_protocol::protocol::McpToolCallEndEvent; -use codex_protocol::protocol::Op; use codex_protocol::protocol::PatchApplyBeginEvent; use codex_protocol::protocol::RateLimitSnapshot; use codex_protocol::protocol::ReviewRequest; @@ -148,6 +147,8 @@ use ratatui::style::Stylize; use ratatui::text::Line; use ratatui::widgets::Paragraph; use ratatui::widgets::Wrap; +use serde::Serialize; +use serde::de::DeserializeOwned; use tokio::sync::mpsc::UnboundedSender; use tokio::task::JoinHandle; use tracing::debug; @@ -163,6 +164,66 @@ const PLAN_MODE_REASONING_SCOPE_PLAN_ONLY: &str = "Apply to Plan mode override"; const PLAN_MODE_REASONING_SCOPE_ALL_MODES: &str = "Apply to global default and Plan mode override"; const CONNECTORS_SELECTION_VIEW_ID: &str = "connectors-selection"; +fn convert_via_serde(value: T) -> Result +where + T: Serialize, + U: DeserializeOwned, +{ + serde_json::from_value(serde_json::to_value(value)?) +} + +fn convert_web_search_action( + action: app_proto::WebSearchAction, +) -> codex_protocol::models::WebSearchAction { + convert_via_serde(action).unwrap_or(codex_protocol::models::WebSearchAction::Other) +} + +fn command_tokens_from_string(command: &str) -> Vec { + shlex::split(command).unwrap_or_else(|| vec![command.to_string()]) +} + +fn parsed_commands_from_actions(actions: Vec) -> Vec { + actions + .into_iter() + .map(app_proto::CommandAction::into_core) + .collect() +} + +fn file_changes_from_app( + changes: Vec, +) -> HashMap { + changes + .into_iter() + .map(|change| { + let path = PathBuf::from(change.path); + let file_change = match change.kind { + app_proto::PatchChangeKind::Add => codex_protocol::protocol::FileChange::Add { + content: change.diff, + }, + app_proto::PatchChangeKind::Delete => { + codex_protocol::protocol::FileChange::Delete { + content: change.diff, + } + } + app_proto::PatchChangeKind::Update { move_path } => { + codex_protocol::protocol::FileChange::Update { + unified_diff: change.diff, + move_path, + } + } + }; + (path, file_change) + }) + .collect() +} + +fn mcp_request_id_from_app(request_id: app_proto::RequestId) -> codex_protocol::mcp::RequestId { + match request_id { + app_proto::RequestId::String(value) => codex_protocol::mcp::RequestId::String(value), + app_proto::RequestId::Integer(value) => codex_protocol::mcp::RequestId::Integer(value), + } +} + /// Choose the keybinding used to edit the most-recently queued message. /// /// Apple Terminal, Warp, and VSCode integrated terminals intercept or silently @@ -250,6 +311,9 @@ mod agent; use self::agent::spawn_agent; use self::agent::spawn_agent_from_existing; pub(crate) use self::agent::spawn_op_forwarder; +use crate::app_event::ThreadBootstrap; +use codex_app_server::EmbeddedSessionClient; +use codex_app_server_protocol as app_proto; mod session_header; use self::session_header::SessionHeader; mod skills; @@ -267,7 +331,6 @@ use crate::streaming::controller::StreamController; use chrono::Local; use codex_core::AuthManager; use codex_core::CodexAuth; -use codex_core::ThreadManager; use codex_file_search::FileMatch; use codex_protocol::openai_models::InputModality; use codex_protocol::openai_models::ModelPreset; @@ -504,7 +567,7 @@ pub(crate) enum ExternalEditorState { /// /// `ChatWidget` owns the state derived from the protocol event stream (history cells, streaming /// buffers, bottom-pane overlays, and transient status text) and turns key presses into user -/// intent (`Op` submissions and `AppEvent` requests). +/// intent (agent commands and `AppEvent` requests). /// /// It is not responsible for running the agent itself; it reflects progress by updating UI state /// and by sending requests back to codex-core. @@ -514,7 +577,7 @@ pub(crate) enum ExternalEditorState { /// active work, arming the double-press quit shortcut, and requesting shutdown-first exit. pub(crate) struct ChatWidget { app_event_tx: AppEventSender, - codex_op_tx: UnboundedSender, + codex_op_tx: UnboundedSender, bottom_pane: BottomPane, active_cell: Option>, /// Monotonic-ish counter used to invalidate transcript overlay caching. @@ -835,6 +898,7 @@ fn remap_placeholders_for_message(message: UserMessage, next_label: &mut usize) #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum ReplayKind { ResumeInitialMessages, + #[cfg(test)] ThreadSnapshot, } @@ -1066,6 +1130,35 @@ impl ChatWidget { } // --- Small event handlers --- + pub(crate) fn handle_thread_bootstrap(&mut self, bootstrap: ThreadBootstrap) { + self.on_thread_bootstrap(bootstrap); + } + + pub(crate) fn handle_thread_bootstrap_replay(&mut self, bootstrap: ThreadBootstrap) { + self.on_thread_bootstrap(bootstrap); + } + + fn on_thread_bootstrap(&mut self, bootstrap: ThreadBootstrap) { + let session_configured = codex_protocol::protocol::SessionConfiguredEvent { + session_id: bootstrap.thread_id, + forked_from_id: bootstrap.forked_from_id, + thread_name: bootstrap.thread.name.clone(), + model: bootstrap.model, + model_provider_id: bootstrap.model_provider_id, + approval_policy: bootstrap.approval_policy, + sandbox_policy: bootstrap.sandbox_policy, + cwd: bootstrap.cwd, + reasoning_effort: bootstrap.reasoning_effort, + history_log_id: bootstrap.history_log_id, + history_entry_count: bootstrap.history_entry_count, + initial_messages: None, + network_proxy: bootstrap.network_proxy, + rollout_path: bootstrap.rollout_path, + }; + self.on_session_configured(session_configured); + self.replay_thread_turns(bootstrap.thread.turns); + } + fn on_session_configured(&mut self, event: codex_protocol::protocol::SessionConfiguredEvent) { self.bottom_pane .set_history_metadata(event.history_log_id, event.history_entry_count); @@ -1102,8 +1195,8 @@ impl ChatWidget { self.replay_initial_messages(messages); } // Ask codex-core to enumerate custom prompts for this session. - self.submit_op(Op::ListCustomPrompts); - self.submit_op(Op::ListSkills { + self.submit_op(AgentCommand::ListCustomPrompts); + self.submit_op(AgentCommand::ListSkills { cwds: Vec::new(), force_reload: true, }); @@ -1121,6 +1214,710 @@ impl ChatWidget { } } + fn replay_thread_turns(&mut self, turns: Vec) { + for turn in turns { + for item in turn.items { + match item { + app_proto::ThreadItem::UserMessage { content, .. } => { + self.replay_thread_user_message(content); + } + app_proto::ThreadItem::AgentMessage { text, .. } => self.on_agent_message(text), + app_proto::ThreadItem::Plan { text, .. } => self.on_plan_item_completed(text), + app_proto::ThreadItem::Reasoning { + summary, content, .. + } => { + for (index, part) in summary.into_iter().enumerate() { + if index > 0 { + self.on_reasoning_section_break(); + } + self.on_agent_reasoning_delta(part); + } + for part in content { + self.on_agent_reasoning_delta(part); + } + self.on_agent_reasoning_final(); + } + app_proto::ThreadItem::ContextCompaction { .. } => { + self.on_agent_message("Context compacted".to_string()); + } + app_proto::ThreadItem::EnteredReviewMode { review, .. } => { + self.add_to_history(history_cell::new_review_status_line(format!( + ">> Code review started: {review} <<" + ))); + } + app_proto::ThreadItem::ExitedReviewMode { review, .. } => { + self.add_to_history(history_cell::new_review_status_line( + "<< Code review finished >>".to_string(), + )); + if !review.trim().is_empty() { + let mut rendered: Vec> = vec!["".into()]; + append_markdown(&review, None, &mut rendered); + self.add_boxed_history(Box::new(AgentMessageCell::new( + rendered, false, + ))); + } + } + app_proto::ThreadItem::WebSearch { id, query, action } => { + if let Some(action) = action { + self.add_to_history(history_cell::new_web_search_call( + id, + query, + convert_web_search_action(action), + )); + } + } + app_proto::ThreadItem::ImageView { path, .. } => { + self.add_to_history(history_cell::new_view_image_tool_call( + path.into(), + &self.config.cwd, + )); + } + app_proto::ThreadItem::CommandExecution { .. } + | app_proto::ThreadItem::FileChange { .. } + | app_proto::ThreadItem::McpToolCall { .. } + | app_proto::ThreadItem::CollabAgentToolCall { .. } => {} + } + } + if matches!(turn.status, app_proto::TurnStatus::Failed) + && let Some(error) = turn.error + { + self.add_to_history(history_cell::new_error_event(error.message)); + } + } + } + + fn replay_thread_user_message(&mut self, content: Vec) { + let mut text = String::new(); + let mut remote_image_urls = Vec::new(); + let mut local_images = Vec::new(); + for item in content { + match item { + app_proto::UserInput::Text { + text: item_text, .. + } => { + text.push_str(&item_text); + } + app_proto::UserInput::Image { url } => remote_image_urls.push(url), + app_proto::UserInput::LocalImage { path } => local_images.push(path), + app_proto::UserInput::Skill { .. } | app_proto::UserInput::Mention { .. } => {} + } + } + if text.trim().is_empty() && remote_image_urls.is_empty() && local_images.is_empty() { + return; + } + self.add_to_history(history_cell::new_user_prompt( + text, + Vec::new(), + local_images, + remote_image_urls, + )); + self.needs_final_message_separator = false; + } + + pub(crate) fn handle_app_server_request(&mut self, request: app_proto::ServerRequest) { + match request { + app_proto::ServerRequest::CommandExecutionRequestApproval { params, .. } => { + let command = params + .command + .as_deref() + .map(command_tokens_from_string) + .unwrap_or_default(); + let parsed_cmd = params + .command_actions + .map(parsed_commands_from_actions) + .unwrap_or_default(); + let ev = ExecApprovalRequestEvent { + call_id: params.item_id, + approval_id: params.approval_id, + turn_id: params.turn_id, + command, + cwd: params.cwd.unwrap_or_else(|| self.config.cwd.clone()), + reason: params.reason, + network_approval_context: params + .network_approval_context + .and_then(|ctx| convert_via_serde(ctx).ok()), + proposed_execpolicy_amendment: params + .proposed_execpolicy_amendment + .and_then(|amendment| convert_via_serde(amendment).ok()), + parsed_cmd, + }; + self.on_exec_approval_request(String::new(), ev); + } + app_proto::ServerRequest::FileChangeRequestApproval { params, .. } => { + let ev = ApplyPatchApprovalRequestEvent { + call_id: params.item_id, + turn_id: params.turn_id, + changes: HashMap::new(), + reason: params.reason, + grant_root: params.grant_root, + }; + self.on_apply_patch_approval_request(String::new(), ev); + } + app_proto::ServerRequest::McpElicitationRequest { params, .. } => { + let ev = ElicitationRequestEvent { + server_name: params.server_name, + id: mcp_request_id_from_app(params.request_id), + message: params.message, + }; + self.on_elicitation_request(ev); + } + app_proto::ServerRequest::ToolRequestUserInput { params, .. } => { + match convert_via_serde(params.questions) { + Ok(questions) => self.on_request_user_input(RequestUserInputEvent { + call_id: params.item_id, + turn_id: params.turn_id, + questions, + }), + Err(err) => { + tracing::error!("failed to convert request_user_input params: {err}") + } + } + } + app_proto::ServerRequest::DynamicToolCall { .. } + | app_proto::ServerRequest::ChatgptAuthTokensRefresh { .. } + | app_proto::ServerRequest::ApplyPatchApproval { .. } + | app_proto::ServerRequest::ExecCommandApproval { .. } => {} + } + } + + pub(crate) fn handle_global_server_notification( + &mut self, + notification: app_proto::ServerNotification, + ) { + self.handle_server_notification(notification); + } + + pub(crate) fn handle_server_notification( + &mut self, + notification: app_proto::ServerNotification, + ) { + self.dispatch_server_notification(notification, false); + } + + pub(crate) fn handle_server_notification_replay( + &mut self, + notification: app_proto::ServerNotification, + ) { + if matches!( + notification, + app_proto::ServerNotification::ThreadShutdownCompleted(_) + ) { + return; + } + self.dispatch_server_notification(notification, true); + } + + fn dispatch_server_notification( + &mut self, + notification: app_proto::ServerNotification, + from_replay: bool, + ) { + match notification { + app_proto::ServerNotification::ThreadNameUpdated(ev) => { + if let Ok(legacy) = convert_via_serde(ev) { + self.on_thread_name_updated(legacy); + } + } + app_proto::ServerNotification::ThreadTokenUsageUpdated(ev) => { + self.on_thread_token_usage_updated_v2(ev); + } + app_proto::ServerNotification::TurnStarted(_) => { + if !from_replay { + self.on_task_started(); + } + } + app_proto::ServerNotification::TurnCompleted(ev) => { + self.on_turn_completed_v2(ev, from_replay); + } + app_proto::ServerNotification::Warning(ev) => self.on_warning(ev.message), + app_proto::ServerNotification::BackgroundEvent(ev) => { + self.on_background_event(ev.message) + } + app_proto::ServerNotification::DeprecationNotice(ev) => { + self.on_deprecation_notice(DeprecationNoticeEvent { + summary: ev.summary, + details: ev.details, + }); + } + app_proto::ServerNotification::ConfigWarning(ev) => { + let message = ev + .details + .map(|details| format!("{}: {details}", ev.summary)) + .unwrap_or(ev.summary); + self.on_warning(message); + } + app_proto::ServerNotification::Error(ev) => self.on_error_notification_v2(ev), + app_proto::ServerNotification::StreamError(ev) => { + if !from_replay { + self.on_stream_error(ev.error.message, ev.error.additional_details); + } + } + app_proto::ServerNotification::McpServerStartupUpdated(ev) => { + if let Ok(legacy) = convert_via_serde(ev) { + self.on_mcp_startup_update(legacy); + } + } + app_proto::ServerNotification::McpServerStartupCompleted(ev) => { + if let Ok(legacy) = convert_via_serde(ev) { + self.on_mcp_startup_complete(legacy); + } + } + app_proto::ServerNotification::SkillsUpdated(_) => { + self.submit_op(AgentCommand::ListSkills { + cwds: Vec::new(), + force_reload: true, + }); + } + app_proto::ServerNotification::ThreadShutdownCompleted(_) => { + self.on_shutdown_complete() + } + app_proto::ServerNotification::ThreadUndoStarted(ev) => { + self.on_undo_started(UndoStartedEvent { + message: ev.message, + }); + } + app_proto::ServerNotification::ThreadUndoCompleted(ev) => { + self.on_undo_completed(UndoCompletedEvent { + success: ev.success, + message: ev.message, + }); + } + app_proto::ServerNotification::TurnDiffUpdated(ev) => self.on_turn_diff(ev.diff), + app_proto::ServerNotification::TurnPlanUpdated(ev) => { + self.on_plan_update(UpdatePlanArgs { + explanation: ev.explanation, + plan: ev + .plan + .into_iter() + .map(|step| codex_protocol::plan_tool::PlanItemArg { + step: step.step, + status: match step.status { + app_proto::TurnPlanStepStatus::Pending => { + codex_protocol::plan_tool::StepStatus::Pending + } + app_proto::TurnPlanStepStatus::InProgress => { + codex_protocol::plan_tool::StepStatus::InProgress + } + app_proto::TurnPlanStepStatus::Completed => { + codex_protocol::plan_tool::StepStatus::Completed + } + }, + }) + .collect(), + }); + } + app_proto::ServerNotification::AgentMessageDelta(ev) => { + self.on_agent_message_delta(ev.delta); + } + app_proto::ServerNotification::PlanDelta(ev) => self.on_plan_delta(ev.delta), + app_proto::ServerNotification::ReasoningSummaryTextDelta(ev) => { + self.on_agent_reasoning_delta(ev.delta); + } + app_proto::ServerNotification::ReasoningSummaryPartAdded(_) => { + self.on_reasoning_section_break() + } + app_proto::ServerNotification::ReasoningTextDelta(ev) => { + self.on_agent_reasoning_delta(ev.delta) + } + app_proto::ServerNotification::TerminalInteraction(ev) => { + self.on_terminal_interaction(TerminalInteractionEvent { + call_id: ev.item_id, + process_id: ev.process_id, + stdin: ev.stdin, + }); + } + app_proto::ServerNotification::CommandExecutionOutputDelta(ev) => { + self.on_exec_command_output_delta(ExecCommandOutputDeltaEvent { + call_id: ev.item_id, + stream: codex_protocol::protocol::ExecOutputStream::Stdout, + chunk: ev.delta.into_bytes(), + }); + } + app_proto::ServerNotification::FileChangeOutputDelta(_) => {} + app_proto::ServerNotification::McpToolCallProgress(ev) => { + self.on_mcp_tool_call_progress_v2(ev); + } + app_proto::ServerNotification::ItemStarted(ev) => { + self.on_thread_item_started_v2(ev.item, ev.turn_id); + } + app_proto::ServerNotification::ItemCompleted(ev) => { + self.on_thread_item_completed_v2(ev.item, ev.turn_id, from_replay); + } + app_proto::ServerNotification::ContextCompacted(_) => { + self.on_agent_message("Context compacted".to_string()); + } + app_proto::ServerNotification::RawResponseItemCompleted(_) + | app_proto::ServerNotification::ThreadStarted(_) + | app_proto::ServerNotification::ThreadStatusChanged(_) + | app_proto::ServerNotification::ThreadArchived(_) + | app_proto::ServerNotification::ThreadUnarchived(_) + | app_proto::ServerNotification::McpServerOauthLoginCompleted(_) + | app_proto::ServerNotification::AccountUpdated(_) + | app_proto::ServerNotification::AccountRateLimitsUpdated(_) + | app_proto::ServerNotification::AppListUpdated(_) + | app_proto::ServerNotification::FuzzyFileSearchSessionUpdated(_) + | app_proto::ServerNotification::FuzzyFileSearchSessionCompleted(_) + | app_proto::ServerNotification::WindowsWorldWritableWarning(_) + | app_proto::ServerNotification::WindowsSandboxSetupCompleted(_) + | app_proto::ServerNotification::ModelRerouted(_) + | app_proto::ServerNotification::AuthStatusChange(_) + | app_proto::ServerNotification::LoginChatGptComplete(_) + | app_proto::ServerNotification::SessionConfigured(_) + | app_proto::ServerNotification::AccountLoginCompleted(_) => {} + } + } + + fn on_thread_token_usage_updated_v2( + &mut self, + ev: app_proto::ThreadTokenUsageUpdatedNotification, + ) { + let total = TokenUsage { + total_tokens: ev.token_usage.total.total_tokens, + input_tokens: ev.token_usage.total.input_tokens, + cached_input_tokens: ev.token_usage.total.cached_input_tokens, + output_tokens: ev.token_usage.total.output_tokens, + reasoning_output_tokens: ev.token_usage.total.reasoning_output_tokens, + }; + let last = TokenUsage { + total_tokens: ev.token_usage.last.total_tokens, + input_tokens: ev.token_usage.last.input_tokens, + cached_input_tokens: ev.token_usage.last.cached_input_tokens, + output_tokens: ev.token_usage.last.output_tokens, + reasoning_output_tokens: ev.token_usage.last.reasoning_output_tokens, + }; + self.set_token_info(Some(TokenUsageInfo { + total_token_usage: total, + last_token_usage: last, + model_context_window: ev.token_usage.model_context_window, + })); + } + + fn on_turn_completed_v2( + &mut self, + ev: app_proto::TurnCompletedNotification, + from_replay: bool, + ) { + match ev.turn.status { + app_proto::TurnStatus::Completed => self.on_task_complete(None, from_replay), + app_proto::TurnStatus::Interrupted => { + self.on_interrupted_turn(TurnAbortReason::Interrupted) + } + app_proto::TurnStatus::Failed => { + if self.agent_turn_running { + self.finalize_turn(); + self.request_redraw(); + } + } + app_proto::TurnStatus::InProgress => {} + } + } + + fn on_error_notification_v2(&mut self, ev: app_proto::ErrorNotification) { + if ev.will_retry { + return; + } + + let kind = ev + .error + .codex_error_info + .as_ref() + .and_then(|info| match info { + app_proto::CodexErrorInfo::ServerOverloaded => { + Some(RateLimitErrorKind::ServerOverloaded) + } + app_proto::CodexErrorInfo::UsageLimitExceeded => { + Some(RateLimitErrorKind::UsageLimit) + } + app_proto::CodexErrorInfo::ResponseTooManyFailedAttempts { + http_status_code: Some(429), + } => Some(RateLimitErrorKind::Generic), + _ => None, + }); + let message = ev.error.message; + + match kind { + Some(RateLimitErrorKind::ServerOverloaded) => self.on_server_overloaded_error(message), + Some(RateLimitErrorKind::UsageLimit | RateLimitErrorKind::Generic) => { + self.on_error(message) + } + None => self.on_error(message), + } + } + + fn on_mcp_tool_call_progress_v2(&mut self, ev: app_proto::McpToolCallProgressNotification) { + debug!("MCP tool call progress {}: {}", ev.item_id, ev.message); + } + + fn on_thread_item_started_v2(&mut self, item: app_proto::ThreadItem, turn_id: String) { + match item { + app_proto::ThreadItem::CommandExecution { + id, + command, + cwd, + process_id, + command_actions, + .. + } => { + self.on_exec_command_begin(ExecCommandBeginEvent { + call_id: id, + process_id, + turn_id, + command: command_tokens_from_string(&command), + cwd, + parsed_cmd: parsed_commands_from_actions(command_actions), + source: ExecCommandSource::Agent, + interaction_input: None, + }); + } + app_proto::ThreadItem::FileChange { id, changes, .. } => { + self.on_patch_apply_begin(PatchApplyBeginEvent { + call_id: id, + turn_id, + auto_approved: false, + changes: file_changes_from_app(changes), + }); + } + app_proto::ThreadItem::McpToolCall { + id, + server, + tool, + arguments, + .. + } => { + self.on_mcp_tool_call_begin(McpToolCallBeginEvent { + call_id: id, + invocation: codex_protocol::protocol::McpInvocation { + server, + tool, + arguments: (!arguments.is_null()).then_some(arguments), + }, + }); + } + app_proto::ThreadItem::WebSearch { id, .. } => { + self.on_web_search_begin(WebSearchBeginEvent { call_id: id }); + } + app_proto::ThreadItem::UserMessage { .. } + | app_proto::ThreadItem::AgentMessage { .. } + | app_proto::ThreadItem::Plan { .. } + | app_proto::ThreadItem::Reasoning { .. } + | app_proto::ThreadItem::CollabAgentToolCall { .. } + | app_proto::ThreadItem::ImageView { .. } + | app_proto::ThreadItem::EnteredReviewMode { .. } + | app_proto::ThreadItem::ExitedReviewMode { .. } + | app_proto::ThreadItem::ContextCompaction { .. } => {} + } + } + + fn on_thread_item_completed_v2( + &mut self, + item: app_proto::ThreadItem, + turn_id: String, + from_replay: bool, + ) { + match item { + app_proto::ThreadItem::UserMessage { content, .. } => { + if from_replay { + self.replay_thread_user_message(content); + } + } + app_proto::ThreadItem::AgentMessage { id, text, phase } => { + if self.stream_controller.is_none() && !text.is_empty() { + self.on_agent_message(text); + } + self.on_agent_message_item_completed(AgentMessageItem { + id, + content: vec![], + phase, + }); + } + app_proto::ThreadItem::Plan { text, .. } => self.on_plan_item_completed(text), + app_proto::ThreadItem::Reasoning { + summary, content, .. + } => { + if self.reasoning_buffer.is_empty() && self.full_reasoning_buffer.is_empty() { + for (index, part) in summary.into_iter().enumerate() { + if index > 0 { + self.on_reasoning_section_break(); + } + self.on_agent_reasoning_delta(part); + } + for part in content { + self.on_agent_reasoning_delta(part); + } + } + self.on_agent_reasoning_final(); + } + app_proto::ThreadItem::CommandExecution { + id, + command, + cwd, + process_id, + status, + command_actions, + aggregated_output, + exit_code, + duration_ms, + } => { + let status = match status { + app_proto::CommandExecutionStatus::Completed => { + codex_protocol::protocol::ExecCommandStatus::Completed + } + app_proto::CommandExecutionStatus::Failed => { + codex_protocol::protocol::ExecCommandStatus::Failed + } + app_proto::CommandExecutionStatus::Declined => { + codex_protocol::protocol::ExecCommandStatus::Declined + } + app_proto::CommandExecutionStatus::InProgress => return, + }; + let aggregated_output = aggregated_output.unwrap_or_default(); + let default_exit_code = match status { + codex_protocol::protocol::ExecCommandStatus::Completed => 0, + codex_protocol::protocol::ExecCommandStatus::Failed + | codex_protocol::protocol::ExecCommandStatus::Declined => 1, + }; + self.on_exec_command_end(ExecCommandEndEvent { + call_id: id, + process_id, + turn_id, + command: command_tokens_from_string(&command), + cwd, + parsed_cmd: parsed_commands_from_actions(command_actions), + source: ExecCommandSource::Agent, + interaction_input: None, + stdout: String::new(), + stderr: String::new(), + aggregated_output: aggregated_output.clone(), + exit_code: exit_code.unwrap_or(default_exit_code), + duration: Duration::from_millis( + duration_ms + .and_then(|ms| u64::try_from(ms).ok()) + .unwrap_or(0), + ), + formatted_output: aggregated_output, + status, + }); + } + app_proto::ThreadItem::FileChange { + id, + changes, + status, + } => { + let status = match status { + app_proto::PatchApplyStatus::Completed => { + codex_protocol::protocol::PatchApplyStatus::Completed + } + app_proto::PatchApplyStatus::Failed => { + codex_protocol::protocol::PatchApplyStatus::Failed + } + app_proto::PatchApplyStatus::Declined => { + codex_protocol::protocol::PatchApplyStatus::Declined + } + app_proto::PatchApplyStatus::InProgress => return, + }; + let success = matches!( + status, + codex_protocol::protocol::PatchApplyStatus::Completed + ); + let stderr = if success { + String::new() + } else { + "Patch apply failed".to_string() + }; + self.on_patch_apply_end(codex_protocol::protocol::PatchApplyEndEvent { + call_id: id, + turn_id, + stdout: String::new(), + stderr, + success, + changes: file_changes_from_app(changes), + status, + }); + } + app_proto::ThreadItem::McpToolCall { + id, + server, + tool, + status, + arguments, + result, + error, + duration_ms, + } => { + if matches!(status, app_proto::McpToolCallStatus::InProgress) { + return; + } + let result = if let Some(error) = error { + Err(error.message) + } else if let Some(result) = result { + convert_via_serde(result) + .map_err(|err| format!("failed to convert MCP tool result: {err}")) + } else { + Err("MCP tool call failed".to_string()) + }; + self.on_mcp_tool_call_end(McpToolCallEndEvent { + call_id: id, + invocation: codex_protocol::protocol::McpInvocation { + server, + tool, + arguments: (!arguments.is_null()).then_some(arguments), + }, + duration: Duration::from_millis( + duration_ms + .and_then(|ms| u64::try_from(ms).ok()) + .unwrap_or(0), + ), + result, + }); + } + app_proto::ThreadItem::WebSearch { id, query, action } => { + self.on_web_search_end(WebSearchEndEvent { + call_id: id, + query, + action: action + .map(convert_web_search_action) + .unwrap_or(codex_protocol::models::WebSearchAction::Other), + }); + } + app_proto::ThreadItem::ImageView { id, path } => { + self.on_view_image_tool_call(ViewImageToolCallEvent { + call_id: id, + path: path.into(), + }); + } + app_proto::ThreadItem::EnteredReviewMode { review, .. } => { + if self.pre_review_token_info.is_none() { + self.pre_review_token_info = Some(self.token_info.clone()); + } + if !from_replay && !self.bottom_pane.is_task_running() { + self.bottom_pane.set_task_running(true); + } + self.is_review_mode = true; + self.add_to_history(history_cell::new_review_status_line(format!( + ">> Code review started: {review} <<" + ))); + self.request_redraw(); + } + app_proto::ThreadItem::ExitedReviewMode { review, .. } => { + self.is_review_mode = false; + self.restore_pre_review_token_info(); + self.add_to_history(history_cell::new_review_status_line( + "<< Code review finished >>".to_string(), + )); + if !review.trim().is_empty() { + let mut rendered: Vec> = vec!["".into()]; + append_markdown(&review, None, &mut rendered); + self.add_boxed_history(Box::new(AgentMessageCell::new(rendered, false))); + } + self.request_redraw(); + } + app_proto::ThreadItem::ContextCompaction { .. } => { + self.on_agent_message("Context compacted".to_string()); + } + app_proto::ThreadItem::CollabAgentToolCall { .. } => {} + } + } + fn emit_forked_thread_event(&self, forked_from_id: ThreadId) { let app_event_tx = self.app_event_tx.clone(); let codex_home = self.config.codex_home.clone(); @@ -2161,6 +2958,17 @@ impl ChatWidget { .on_history_entry_response(log_id, offset, entry.map(|e| e.text)); } + pub(crate) fn on_get_history_entry_response_v2( + &mut self, + response: app_proto::HistoryEntryReadResponse, + ) { + self.bottom_pane.on_history_entry_response( + response.log_id, + response.offset, + response.entry.map(|entry| entry.text), + ); + } + fn on_shutdown_complete(&mut self) { self.request_immediate_exit(); } @@ -2499,9 +3307,10 @@ impl ChatWidget { let command = shlex::try_join(ev.command.iter().map(String::as_str)) .unwrap_or_else(|_| ev.command.join(" ")); self.notify(Notification::ExecApprovalRequested { command }); + let approval_reply_id = ev.approval_id.clone().unwrap_or_else(|| ev.call_id.clone()); let request = ApprovalRequest::Exec { - id: ev.call_id, + id: approval_reply_id, command: ev.command, reason: ev.reason, network_approval_context: ev.network_approval_context, @@ -2665,7 +3474,35 @@ impl ChatWidget { self.had_work_activity = true; } - pub(crate) fn new(common: ChatWidgetInit, thread_manager: Arc) -> Self { + fn finish_initial_setup(&mut self) { + self.prefetch_rate_limits(); + self.bottom_pane + .set_steer_enabled(self.config.features.enabled(Feature::Steer)); + self.bottom_pane + .set_status_line_enabled(!self.configured_status_line_items().is_empty()); + self.bottom_pane.set_collaboration_modes_enabled( + self.config.features.enabled(Feature::CollaborationModes), + ); + self.sync_personality_command_enabled(); + self.bottom_pane + .set_queued_message_edit_binding(self.queued_message_edit_binding); + #[cfg(target_os = "windows")] + self.bottom_pane.set_windows_degraded_sandbox_active( + codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED + && matches!( + WindowsSandboxLevel::from_config(&self.config), + WindowsSandboxLevel::RestrictedToken + ), + ); + self.update_collaboration_mode_indicator(); + self.bottom_pane + .set_connectors_enabled(self.config.features.enabled(Feature::Apps)); + } + + pub(crate) fn new( + common: ChatWidgetInit, + app_server_client: Arc, + ) -> Self { let ChatWidgetInit { config, frame_requester, @@ -2687,7 +3524,7 @@ impl ChatWidget { let prevent_idle_sleep = config.features.enabled(Feature::PreventIdleSleep); let mut rng = rand::rng(); let placeholder = PLACEHOLDERS[rng.random_range(0..PLACEHOLDERS.len())].to_string(); - let codex_op_tx = spawn_agent(config.clone(), app_event_tx.clone(), thread_manager); + let codex_op_tx = spawn_agent(config.clone(), app_event_tx.clone(), app_server_client); let model_override = model.as_deref(); let model_for_header = model @@ -2802,40 +3639,13 @@ impl ChatWidget { external_editor_state: ExternalEditorState::Closed, }; - widget.prefetch_rate_limits(); - widget - .bottom_pane - .set_steer_enabled(widget.config.features.enabled(Feature::Steer)); - widget - .bottom_pane - .set_status_line_enabled(!widget.configured_status_line_items().is_empty()); - widget.bottom_pane.set_collaboration_modes_enabled( - widget.config.features.enabled(Feature::CollaborationModes), - ); - widget.sync_personality_command_enabled(); - widget - .bottom_pane - .set_queued_message_edit_binding(widget.queued_message_edit_binding); - #[cfg(target_os = "windows")] - widget.bottom_pane.set_windows_degraded_sandbox_active( - codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED - && matches!( - WindowsSandboxLevel::from_config(&widget.config), - WindowsSandboxLevel::RestrictedToken - ), - ); - widget.update_collaboration_mode_indicator(); - - widget - .bottom_pane - .set_connectors_enabled(widget.config.features.enabled(Feature::Apps)); - + widget.finish_initial_setup(); widget } pub(crate) fn new_with_op_sender( common: ChatWidgetInit, - codex_op_tx: UnboundedSender, + codex_op_tx: UnboundedSender, ) -> Self { let ChatWidgetInit { config, @@ -2972,29 +3782,15 @@ impl ChatWidget { external_editor_state: ExternalEditorState::Closed, }; - widget.prefetch_rate_limits(); - widget - .bottom_pane - .set_steer_enabled(widget.config.features.enabled(Feature::Steer)); - widget - .bottom_pane - .set_status_line_enabled(!widget.configured_status_line_items().is_empty()); - widget.bottom_pane.set_collaboration_modes_enabled( - widget.config.features.enabled(Feature::CollaborationModes), - ); - widget.sync_personality_command_enabled(); - widget - .bottom_pane - .set_queued_message_edit_binding(widget.queued_message_edit_binding); - + widget.finish_initial_setup(); widget } /// Create a ChatWidget attached to an existing conversation (e.g., a fork). pub(crate) fn new_from_existing( common: ChatWidgetInit, - conversation: std::sync::Arc, - session_configured: codex_protocol::protocol::SessionConfiguredEvent, + bootstrap: ThreadBootstrap, + app_server_client: Arc, ) -> Self { let ChatWidgetInit { config, @@ -3017,9 +3813,7 @@ impl ChatWidget { let placeholder = PLACEHOLDERS[rng.random_range(0..PLACEHOLDERS.len())].to_string(); let model_override = model.as_deref(); - let header_model = model - .clone() - .unwrap_or_else(|| session_configured.model.clone()); + let header_model = model.clone().unwrap_or_else(|| bootstrap.model.clone()); let active_collaboration_mask = Self::initial_collaboration_mask(&config, models_manager.as_ref(), model_override); let header_model = active_collaboration_mask @@ -3027,9 +3821,9 @@ impl ChatWidget { .and_then(|mask| mask.model.clone()) .unwrap_or(header_model); - let current_cwd = Some(session_configured.cwd.clone()); + let current_cwd = Some(bootstrap.cwd.clone()); let codex_op_tx = - spawn_agent_from_existing(conversation, session_configured, app_event_tx.clone()); + spawn_agent_from_existing(bootstrap.thread_id, app_event_tx.clone(), app_server_client); let fallback_default = Settings { model: header_model.clone(), @@ -3131,30 +3925,8 @@ impl ChatWidget { external_editor_state: ExternalEditorState::Closed, }; - widget.prefetch_rate_limits(); - widget - .bottom_pane - .set_steer_enabled(widget.config.features.enabled(Feature::Steer)); - widget - .bottom_pane - .set_status_line_enabled(!widget.configured_status_line_items().is_empty()); - widget.bottom_pane.set_collaboration_modes_enabled( - widget.config.features.enabled(Feature::CollaborationModes), - ); - widget.sync_personality_command_enabled(); - widget - .bottom_pane - .set_queued_message_edit_binding(widget.queued_message_edit_binding); - #[cfg(target_os = "windows")] - widget.bottom_pane.set_windows_degraded_sandbox_active( - codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED - && matches!( - WindowsSandboxLevel::from_config(&widget.config), - WindowsSandboxLevel::RestrictedToken - ), - ); - widget.update_collaboration_mode_indicator(); - + widget.finish_initial_setup(); + widget.on_thread_bootstrap(bootstrap); widget } @@ -3397,7 +4169,8 @@ impl ChatWidget { } SlashCommand::Compact => { self.clear_token_usage(); - self.app_event_tx.send(AppEvent::CodexOp(Op::Compact)); + self.app_event_tx + .send(AppEvent::AgentCommand(AgentCommand::Compact)); } SlashCommand::Review => { self.open_review_popup(); @@ -3516,7 +4289,7 @@ impl ChatWidget { self.request_quit_without_confirmation(); } // SlashCommand::Undo => { - // self.app_event_tx.send(AppEvent::CodexOp(Op::Undo)); + // self.app_event_tx.send(AppEvent::AgentCommand(AgentCommand::Undo)); // } SlashCommand::Diff => { self.add_diff_in_progress(); @@ -3560,10 +4333,10 @@ impl ChatWidget { self.clean_background_terminals(); } SlashCommand::MemoryDrop => { - self.submit_op(Op::DropMemories); + self.submit_op(AgentCommand::DropMemories); } SlashCommand::MemoryUpdate => { - self.submit_op(Op::UpdateMemories); + self.submit_op(AgentCommand::UpdateMemories); } SlashCommand::Mcp => { self.add_mcp_output(); @@ -3582,42 +4355,10 @@ impl ChatWidget { } } SlashCommand::TestApproval => { - use codex_protocol::protocol::EventMsg; - use std::collections::HashMap; - - use codex_protocol::protocol::ApplyPatchApprovalRequestEvent; - use codex_protocol::protocol::FileChange; - - self.app_event_tx.send(AppEvent::CodexEvent(Event { - id: "1".to_string(), - // msg: EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent { - // call_id: "1".to_string(), - // command: vec!["git".into(), "apply".into()], - // cwd: self.config.cwd.clone(), - // reason: Some("test".to_string()), - // }), - msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent { - call_id: "1".to_string(), - turn_id: "turn-1".to_string(), - changes: HashMap::from([ - ( - PathBuf::from("/tmp/test.txt"), - FileChange::Add { - content: "test".to_string(), - }, - ), - ( - PathBuf::from("/tmp/test2.txt"), - FileChange::Update { - unified_diff: "+test\n-test2".to_string(), - move_path: None, - }, - ), - ]), - reason: None, - grant_root: Some(PathBuf::from("/tmp")), - }), - })); + self.add_info_message( + "Test approval helper is unavailable on the app-server path.".to_string(), + None, + ); } } } @@ -3659,7 +4400,7 @@ impl ChatWidget { self.add_boxed_history(Box::new(cell)); self.request_redraw(); self.app_event_tx - .send(AppEvent::CodexOp(Op::SetThreadName { name })); + .send(AppEvent::AgentCommand(AgentCommand::SetThreadName { name })); self.bottom_pane.drain_pending_submission_state(); } SlashCommand::Plan if !trimmed.is_empty() => { @@ -3698,7 +4439,7 @@ impl ChatWidget { else { return; }; - self.submit_op(Op::Review { + self.submit_op(AgentCommand::Review { review_request: ReviewRequest { target: ReviewTarget::Custom { instructions: prepared_args, @@ -3749,7 +4490,7 @@ impl ChatWidget { }; let cell = Self::rename_confirmation_cell(&name, thread_id); tx.send(AppEvent::InsertHistoryCell(Box::new(cell))); - tx.send(AppEvent::CodexOp(Op::SetThreadName { name })); + tx.send(AppEvent::AgentCommand(AgentCommand::SetThreadName { name })); }), ); @@ -3863,7 +4604,7 @@ impl ChatWidget { ))); return; } - self.submit_op(Op::RunUserShellCommand { + self.submit_op(AgentCommand::RunUserShellCommand { command: cmd.to_string(), }); return; @@ -3980,7 +4721,7 @@ impl ChatWidget { .personality .filter(|_| self.config.features.enabled(Feature::Personality)) .filter(|_| self.current_model_supports_personality()); - let op = Op::UserTurn { + let op = AgentCommand::UserTurn { items, cwd: self.config.cwd.clone(), approval_policy: self.config.permissions.approval_policy.value(), @@ -4008,7 +4749,7 @@ impl ChatWidget { .collect::>(); let history_text = encode_history_mentions(&text, &encoded_mentions); self.codex_op_tx - .send(Op::AddToHistory { text: history_text }) + .send(AgentCommand::AddToHistory { text: history_text }) .unwrap_or_else(|e| { tracing::error!("failed to send AddHistory op: {e}"); }); @@ -4083,13 +4824,15 @@ impl ChatWidget { } } - pub(crate) fn handle_codex_event(&mut self, event: Event) { - let Event { id, msg } = event; + #[cfg(test)] + pub(crate) fn handle_codex_event(&mut self, event: codex_protocol::protocol::Event) { + let codex_protocol::protocol::Event { id, msg } = event; self.dispatch_event_msg(Some(id), msg, None); } - pub(crate) fn handle_codex_event_replay(&mut self, event: Event) { - let Event { msg, .. } = event; + #[cfg(test)] + pub(crate) fn handle_codex_event_replay(&mut self, event: codex_protocol::protocol::Event) { + let codex_protocol::protocol::Event { msg, .. } = event; if matches!(msg, EventMsg::ShutdownComplete) { return; } @@ -4223,7 +4966,7 @@ impl ChatWidget { EventMsg::ListSkillsResponse(ev) => self.on_list_skills(ev), EventMsg::ListRemoteSkillsResponse(_) | EventMsg::RemoteSkillDownloaded(_) => {} EventMsg::SkillsUpdateAvailable => { - self.submit_op(Op::ListSkills { + self.submit_op(AgentCommand::ListSkills { cwds: Vec::new(), force_reload: true, }); @@ -4750,7 +5493,7 @@ impl ChatWidget { } fn clean_background_terminals(&mut self) { - self.submit_op(Op::CleanBackgroundTerminals); + self.submit_op(AgentCommand::CleanBackgroundTerminals); self.add_info_message("Stopping all background terminals.".to_string(), None); } @@ -4920,7 +5663,7 @@ impl ChatWidget { let default_effort: ReasoningEffortConfig = preset.default_reasoning_effort; let switch_actions: Vec = vec![Box::new(move |tx| { - tx.send(AppEvent::CodexOp(Op::OverrideTurnContext { + tx.send(AppEvent::AgentCommand(AgentCommand::OverrideTurnContext { cwd: None, approval_policy: None, sandbox_policy: None, @@ -5040,7 +5783,7 @@ impl ChatWidget { let name = Self::personality_label(personality).to_string(); let description = Some(Self::personality_description(personality).to_string()); let actions: Vec = vec![Box::new(move |tx| { - tx.send(AppEvent::CodexOp(Op::OverrideTurnContext { + tx.send(AppEvent::AgentCommand(AgentCommand::OverrideTurnContext { cwd: None, approval_policy: None, sandbox_policy: None, @@ -5797,7 +6540,7 @@ impl ChatWidget { ) -> Vec { vec![Box::new(move |tx| { let sandbox_clone = sandbox.clone(); - tx.send(AppEvent::CodexOp(Op::OverrideTurnContext { + tx.send(AppEvent::AgentCommand(AgentCommand::OverrideTurnContext { cwd: None, approval_policy: Some(approval), sandbox_policy: Some(sandbox_clone.clone()), @@ -6640,7 +7383,7 @@ impl ChatWidget { /// Update the active collaboration mask. /// /// When collaboration modes are enabled and a preset is selected, - /// the current mode is attached to submissions as `Op::UserTurn { collaboration_mode: Some(...) }`. + /// the current mode is attached to submissions as `AgentCommand::UserTurn { collaboration_mode: Some(...) }`. pub(crate) fn set_collaboration_mask(&mut self, mut mask: CollaborationModeMask) { if !self.collaboration_modes_enabled() { return; @@ -6771,7 +7514,7 @@ impl ChatWidget { if self.config.mcp_servers.is_empty() { self.add_to_history(history_cell::empty_mcp_output()); } else { - self.submit_op(Op::ListMcpTools); + self.submit_op(AgentCommand::ListMcpTools); } } @@ -6957,7 +7700,7 @@ impl ChatWidget { /// Handles a Ctrl+C press at the chat-widget layer. /// /// The first press arms a time-bounded quit shortcut and shows a footer hint via the bottom - /// pane. If cancellable work is active, Ctrl+C also submits `Op::Interrupt` after the shortcut + /// pane. If cancellable work is active, Ctrl+C also submits `AgentCommand::Interrupt` after the shortcut /// is armed. /// /// If the same quit shortcut is pressed again before expiry, this requests a shutdown-first @@ -6980,7 +7723,7 @@ impl ChatWidget { if !DOUBLE_PRESS_QUIT_SHORTCUT_ENABLED { if self.is_cancellable_work_active() { - self.submit_op(Op::Interrupt); + self.submit_op(AgentCommand::Interrupt); } else { self.request_quit_without_confirmation(); } @@ -6997,7 +7740,7 @@ impl ChatWidget { self.arm_quit_shortcut(key); if self.is_cancellable_work_active() { - self.submit_op(Op::Interrupt); + self.submit_op(AgentCommand::Interrupt); } } @@ -7142,11 +7885,11 @@ impl ChatWidget { pub(crate) fn clear_esc_backtrack_hint(&mut self) { self.bottom_pane.clear_esc_backtrack_hint(); } - /// Forward an `Op` directly to codex. - pub(crate) fn submit_op(&mut self, op: Op) { + /// Forward an agent command directly to the session loop. + pub(crate) fn submit_op(&mut self, op: AgentCommand) { // Record outbound operation for session replay fidelity. crate::session_log::log_outbound_op(&op); - if matches!(&op, Op::Review { .. }) && !self.bottom_pane.is_task_running() { + if matches!(&op, AgentCommand::Review { .. }) && !self.bottom_pane.is_task_running() { self.bottom_pane.set_task_running(true); } if let Err(e) = self.codex_op_tx.send(op) { @@ -7164,6 +7907,16 @@ impl ChatWidget { )); } + pub(crate) fn on_list_mcp_tools_v2(&mut self, response: app_proto::McpToolsListResponse) { + self.add_to_history(history_cell::new_mcp_tools_output( + &self.config, + response.tools, + response.resources, + response.resource_templates, + &response.auth_statuses, + )); + } + fn on_list_custom_prompts(&mut self, ev: ListCustomPromptsResponseEvent) { let len = ev.custom_prompts.len(); debug!("received {len} custom prompts"); @@ -7171,10 +7924,24 @@ impl ChatWidget { self.bottom_pane.set_custom_prompts(ev.custom_prompts); } + pub(crate) fn on_list_custom_prompts_v2( + &mut self, + response: app_proto::CustomPromptListResponse, + ) { + match convert_via_serde(response.data) { + Ok(custom_prompts) => self.bottom_pane.set_custom_prompts(custom_prompts), + Err(err) => tracing::error!("failed to convert custom prompts response: {err}"), + } + } + fn on_list_skills(&mut self, ev: ListSkillsResponseEvent) { self.set_skills_from_response(&ev); } + pub(crate) fn on_list_skills_v2(&mut self, response: app_proto::SkillsListResponse) { + self.set_skills_from_app_server_response(&response); + } + pub(crate) fn on_connectors_loaded( &mut self, result: Result, @@ -7275,7 +8042,7 @@ impl ChatWidget { items.push(SelectionItem { name: "Review uncommitted changes".to_string(), actions: vec![Box::new(move |tx: &AppEventSender| { - tx.send(AppEvent::CodexOp(Op::Review { + tx.send(AppEvent::AgentCommand(AgentCommand::Review { review_request: ReviewRequest { target: ReviewTarget::UncommittedChanges, user_facing_hint: None, @@ -7328,7 +8095,7 @@ impl ChatWidget { items.push(SelectionItem { name: format!("{current_branch} -> {branch}"), actions: vec![Box::new(move |tx3: &AppEventSender| { - tx3.send(AppEvent::CodexOp(Op::Review { + tx3.send(AppEvent::AgentCommand(AgentCommand::Review { review_request: ReviewRequest { target: ReviewTarget::BaseBranch { branch: branch.clone(), @@ -7365,7 +8132,7 @@ impl ChatWidget { items.push(SelectionItem { name: subject.clone(), actions: vec![Box::new(move |tx3: &AppEventSender| { - tx3.send(AppEvent::CodexOp(Op::Review { + tx3.send(AppEvent::AgentCommand(AgentCommand::Review { review_request: ReviewRequest { target: ReviewTarget::Commit { sha: sha.clone(), @@ -7402,7 +8169,7 @@ impl ChatWidget { if trimmed.is_empty() { return; } - tx.send(AppEvent::CodexOp(Op::Review { + tx.send(AppEvent::AgentCommand(AgentCommand::Review { review_request: ReviewRequest { target: ReviewTarget::Custom { instructions: trimmed, @@ -7665,7 +8432,7 @@ pub(crate) fn show_review_commit_picker_with_entries( items.push(SelectionItem { name: subject.clone(), actions: vec![Box::new(move |tx3: &AppEventSender| { - tx3.send(AppEvent::CodexOp(Op::Review { + tx3.send(AppEvent::AgentCommand(AgentCommand::Review { review_request: ReviewRequest { target: ReviewTarget::Commit { sha: sha.clone(), diff --git a/codex-rs/tui/src/chatwidget/agent.rs b/codex-rs/tui/src/chatwidget/agent.rs index 63d519a7a5..171e3b3d33 100644 --- a/codex-rs/tui/src/chatwidget/agent.rs +++ b/codex-rs/tui/src/chatwidget/agent.rs @@ -1,134 +1,942 @@ +use std::collections::HashMap; use std::sync::Arc; -use codex_core::CodexThread; -use codex_core::NewThread; -use codex_core::ThreadManager; +use crate::agent_command::AgentCommand; +use crate::app_event::ThreadBootstrap; +use codex_app_server::EmbeddedSessionClient; +use codex_app_server::EmbeddedSessionMessage; +use codex_app_server_protocol as app_proto; +use codex_app_server_protocol::RequestId as JsonRpcRequestId; use codex_core::config::Config; -use codex_protocol::protocol::Event; -use codex_protocol::protocol::EventMsg; -use codex_protocol::protocol::Op; +use codex_protocol::ThreadId; +use serde::Serialize; +use serde::de::DeserializeOwned; +use tokio::sync::Mutex; +use tokio::sync::broadcast::error::RecvError as BroadcastRecvError; use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::unbounded_channel; use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; -/// Spawn the agent bootstrapper and op forwarding loop, returning the -/// `UnboundedSender` used by the UI to submit operations. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +enum PendingServerRequestKey { + ExecApproval(String), + PatchApproval(String), + UserInput(String), + DynamicTool(String), + McpElicitation { + server_name: String, + request_id: codex_protocol::mcp::RequestId, + }, +} + +#[derive(Debug, Default)] +struct PendingServerRequests { + by_key: HashMap, +} + +impl PendingServerRequests { + fn insert(&mut self, request_id: JsonRpcRequestId, request: &app_proto::ServerRequest) { + let Some(key) = pending_key_for_server_request(request) else { + return; + }; + self.by_key.insert(key, request_id); + } + + fn take(&mut self, key: PendingServerRequestKey) -> Option { + self.by_key.remove(&key) + } +} + +async fn take_pending_request_id( + pending_server_requests: &Mutex, + key: PendingServerRequestKey, +) -> Option { + let request_id = pending_server_requests.lock().await.take(key.clone()); + if request_id.is_none() { + tracing::warn!(?key, "pending app-server request not found for response"); + } + request_id +} + +async fn respond_if_pending( + app_server_client: &EmbeddedSessionClient, + pending_server_requests: &Mutex, + key: PendingServerRequestKey, + response: T, +) -> std::result::Result<(), app_proto::JSONRPCErrorError> { + let Some(request_id) = take_pending_request_id(pending_server_requests, key).await else { + return Ok(()); + }; + app_server_client.respond(request_id, response).await +} + +fn pending_key_for_server_request( + request: &app_proto::ServerRequest, +) -> Option { + match request { + app_proto::ServerRequest::CommandExecutionRequestApproval { params, .. } => { + Some(PendingServerRequestKey::ExecApproval( + params + .approval_id + .clone() + .unwrap_or_else(|| params.item_id.clone()), + )) + } + app_proto::ServerRequest::FileChangeRequestApproval { params, .. } => Some( + PendingServerRequestKey::PatchApproval(params.item_id.clone()), + ), + app_proto::ServerRequest::ToolRequestUserInput { params, .. } => { + Some(PendingServerRequestKey::UserInput(params.item_id.clone())) + } + app_proto::ServerRequest::DynamicToolCall { params, .. } => { + Some(PendingServerRequestKey::DynamicTool(params.call_id.clone())) + } + app_proto::ServerRequest::McpElicitationRequest { params, .. } => { + Some(PendingServerRequestKey::McpElicitation { + server_name: params.server_name.clone(), + request_id: match ¶ms.request_id { + app_proto::RequestId::String(value) => { + codex_protocol::mcp::RequestId::String(value.clone()) + } + app_proto::RequestId::Integer(value) => { + codex_protocol::mcp::RequestId::Integer(*value) + } + }, + }) + } + _ => None, + } +} + +fn server_request_thread_id(request: &app_proto::ServerRequest) -> Option<&str> { + match request { + app_proto::ServerRequest::CommandExecutionRequestApproval { params, .. } => { + Some(params.thread_id.as_str()) + } + app_proto::ServerRequest::FileChangeRequestApproval { params, .. } => { + Some(params.thread_id.as_str()) + } + app_proto::ServerRequest::ToolRequestUserInput { params, .. } => { + Some(params.thread_id.as_str()) + } + app_proto::ServerRequest::DynamicToolCall { params, .. } => Some(params.thread_id.as_str()), + app_proto::ServerRequest::McpElicitationRequest { params, .. } => { + Some(params.thread_id.as_str()) + } + app_proto::ServerRequest::ChatgptAuthTokensRefresh { .. } + | app_proto::ServerRequest::ApplyPatchApproval { .. } + | app_proto::ServerRequest::ExecCommandApproval { .. } => None, + } +} + +fn convert_via_serde(value: T) -> Result +where + T: Serialize, + U: DeserializeOwned, +{ + serde_json::from_value(serde_json::to_value(value)?) +} + +fn convert_exec_approval_decision( + decision: codex_protocol::protocol::ReviewDecision, +) -> Option { + match decision { + codex_protocol::protocol::ReviewDecision::Approved => { + Some(app_proto::CommandExecutionApprovalDecision::Accept) + } + codex_protocol::protocol::ReviewDecision::ApprovedForSession => { + Some(app_proto::CommandExecutionApprovalDecision::AcceptForSession) + } + codex_protocol::protocol::ReviewDecision::ApprovedExecpolicyAmendment { + proposed_execpolicy_amendment, + } => match convert_via_serde(proposed_execpolicy_amendment) { + Ok(execpolicy_amendment) => Some( + app_proto::CommandExecutionApprovalDecision::AcceptWithExecpolicyAmendment { + execpolicy_amendment, + }, + ), + Err(err) => { + tracing::error!("failed to convert execpolicy amendment: {err}"); + None + } + }, + codex_protocol::protocol::ReviewDecision::Denied => { + Some(app_proto::CommandExecutionApprovalDecision::Decline) + } + codex_protocol::protocol::ReviewDecision::Abort => { + Some(app_proto::CommandExecutionApprovalDecision::Cancel) + } + } +} + +fn convert_patch_approval_decision( + decision: codex_protocol::protocol::ReviewDecision, +) -> Option { + match decision { + codex_protocol::protocol::ReviewDecision::Approved => { + Some(app_proto::FileChangeApprovalDecision::Accept) + } + codex_protocol::protocol::ReviewDecision::ApprovedForSession => { + Some(app_proto::FileChangeApprovalDecision::AcceptForSession) + } + codex_protocol::protocol::ReviewDecision::Denied => { + Some(app_proto::FileChangeApprovalDecision::Decline) + } + codex_protocol::protocol::ReviewDecision::Abort => { + Some(app_proto::FileChangeApprovalDecision::Cancel) + } + codex_protocol::protocol::ReviewDecision::ApprovedExecpolicyAmendment { .. } => { + tracing::warn!("ignoring execpolicy amendment decision for patch approval"); + Some(app_proto::FileChangeApprovalDecision::Accept) + } + } +} + +fn convert_elicitation_decision( + decision: codex_protocol::approvals::ElicitationAction, +) -> app_proto::McpElicitationDecision { + match decision { + codex_protocol::approvals::ElicitationAction::Accept => { + app_proto::McpElicitationDecision::Accept + } + codex_protocol::approvals::ElicitationAction::Decline => { + app_proto::McpElicitationDecision::Decline + } + codex_protocol::approvals::ElicitationAction::Cancel => { + app_proto::McpElicitationDecision::Cancel + } + } +} + +fn convert_user_input_response( + response: codex_protocol::request_user_input::RequestUserInputResponse, +) -> Result { + convert_via_serde(response) +} + +fn convert_dynamic_tool_response( + response: codex_protocol::dynamic_tools::DynamicToolResponse, +) -> Result { + convert_via_serde(response) +} + +fn convert_review_target( + target: codex_protocol::protocol::ReviewTarget, +) -> Result { + convert_via_serde(target) +} + +fn build_thread_bootstrap( + thread: app_proto::Thread, + model: String, + model_provider_id: String, + cwd: std::path::PathBuf, + approval_policy: app_proto::AskForApproval, + sandbox_policy: app_proto::SandboxPolicy, + reasoning_effort: Option, + history_log_id: u64, + history_entry_count: usize, + forked_from_id: Option, + network_proxy: Option, +) -> Result { + let thread_id = ThreadId::try_from(thread.id.clone()) + .map_err(|err| format!("invalid thread id {}: {err}", thread.id))?; + let forked_from_id = match forked_from_id { + Some(forked_from_id) => Some( + ThreadId::try_from(forked_from_id.clone()) + .map_err(|err| format!("invalid forked thread id {forked_from_id}: {err}"))?, + ), + None => None, + }; + let network_proxy = + network_proxy.map( + |network_proxy| codex_protocol::protocol::SessionNetworkProxyRuntime { + http_addr: network_proxy.http_addr, + socks_addr: network_proxy.socks_addr, + admin_addr: network_proxy.admin_addr, + }, + ); + Ok(ThreadBootstrap { + thread_id, + rollout_path: thread.path.clone(), + thread, + model, + model_provider_id, + approval_policy: approval_policy.to_core(), + sandbox_policy: sandbox_policy.to_core(), + cwd, + reasoning_effort, + history_log_id, + history_entry_count, + forked_from_id, + network_proxy, + }) +} + +fn thread_bootstrap_from_start( + response: app_proto::ThreadStartResponse, +) -> Result { + let app_proto::ThreadStartResponse { + thread, + model, + model_provider, + cwd, + approval_policy, + sandbox, + reasoning_effort, + history_log_id, + history_entry_count, + forked_from_thread_id, + network_proxy, + } = response; + build_thread_bootstrap( + thread, + model, + model_provider, + cwd, + approval_policy, + sandbox, + reasoning_effort, + history_log_id, + history_entry_count, + forked_from_thread_id, + network_proxy, + ) +} + +fn spawn_server_request_tracker( + app_server_client: Arc, + app_event_tx: AppEventSender, + thread_id: ThreadId, + pending_server_requests: Arc>, +) { + let thread_id_str = thread_id.to_string(); + tokio::spawn(async move { + let mut rx = app_server_client.subscribe(); + loop { + match rx.recv().await { + Ok(EmbeddedSessionMessage::Request(request)) => { + if server_request_thread_id(&request.request) != Some(thread_id_str.as_str()) { + continue; + } + pending_server_requests + .lock() + .await + .insert(request.request_id, &request.request); + app_event_tx.send(AppEvent::AppServerRequest { + thread_id, + request: request.request, + }); + } + Ok(EmbeddedSessionMessage::Notification( + app_proto::ServerNotification::ThreadShutdownCompleted(notification), + )) => { + if notification.thread_id == thread_id_str { + break; + } + } + Ok(EmbeddedSessionMessage::Notification(_)) => {} + Err(BroadcastRecvError::Lagged(skipped)) => { + tracing::warn!( + %thread_id, + skipped, + "server-request tracker lagged on app-server broadcast" + ); + } + Err(BroadcastRecvError::Closed) => break, + } + } + }); +} + +fn spawn_active_turn_tracker( + app_server_client: Arc, + thread_id: ThreadId, + active_turn_id: Arc>>, +) { + let thread_id = thread_id.to_string(); + tokio::spawn(async move { + let mut rx = app_server_client.subscribe(); + loop { + match rx.recv().await { + Ok(EmbeddedSessionMessage::Notification( + app_proto::ServerNotification::TurnStarted(notification), + )) => { + if notification.thread_id != thread_id { + continue; + } + *active_turn_id.lock().await = Some(notification.turn.id); + } + Ok(EmbeddedSessionMessage::Notification( + app_proto::ServerNotification::TurnCompleted(notification), + )) => { + if notification.thread_id != thread_id { + continue; + } + let mut guard = active_turn_id.lock().await; + if guard.as_deref() == Some(notification.turn.id.as_str()) { + *guard = None; + } + } + Ok(EmbeddedSessionMessage::Notification( + app_proto::ServerNotification::ThreadShutdownCompleted(notification), + )) => { + if notification.thread_id != thread_id { + continue; + } + *active_turn_id.lock().await = None; + break; + } + Ok(EmbeddedSessionMessage::Notification(_)) + | Ok(EmbeddedSessionMessage::Request(_)) => {} + Err(BroadcastRecvError::Lagged(skipped)) => { + *active_turn_id.lock().await = None; + tracing::warn!( + thread_id = %thread_id, + skipped, + "active-turn tracker lagged on app-server broadcast; cleared active turn" + ); + } + Err(BroadcastRecvError::Closed) => break, + } + } + }); +} + +async fn run_command_loop( + app_server_client: Arc, + app_event_tx: AppEventSender, + thread_id: ThreadId, + pending_server_requests: Arc>, + active_turn_id: Arc>>, + mut codex_op_rx: tokio::sync::mpsc::UnboundedReceiver, +) { + let thread_id_str = thread_id.to_string(); + while let Some(command) = codex_op_rx.recv().await { + let result = match command { + AgentCommand::Interrupt => { + let turn_id = active_turn_id.lock().await.clone(); + if let Some(turn_id) = turn_id { + app_server_client + .request::(|request_id| { + app_proto::ClientRequest::TurnInterrupt { + request_id, + params: app_proto::TurnInterruptParams { + thread_id: thread_id_str.clone(), + turn_id, + }, + } + }) + .await + .map(|_| ()) + } else { + tracing::warn!("interrupt requested with no active turn id"); + Ok(()) + } + } + AgentCommand::CleanBackgroundTerminals => app_server_client + .request::(|request_id| { + app_proto::ClientRequest::ThreadBackgroundTerminalsClean { + request_id, + params: app_proto::ThreadBackgroundTerminalsCleanParams { + thread_id: thread_id_str.clone(), + }, + } + }) + .await + .map(|_| ()), + AgentCommand::UserTurn { + items, + cwd, + approval_policy, + sandbox_policy, + model, + effort, + summary, + final_output_json_schema, + collaboration_mode, + personality, + } => app_server_client + .request::(|request_id| { + app_proto::ClientRequest::TurnStart { + request_id, + params: app_proto::TurnStartParams { + thread_id: thread_id_str.clone(), + input: items.into_iter().map(Into::into).collect(), + cwd: Some(cwd), + approval_policy: Some(approval_policy.into()), + sandbox_policy: Some(sandbox_policy.into()), + model: Some(model), + effort, + summary: Some(summary), + personality, + output_schema: final_output_json_schema, + collaboration_mode, + }, + } + }) + .await + .map(|_| ()), + AgentCommand::OverrideTurnContext { + cwd, + approval_policy, + sandbox_policy, + windows_sandbox_level, + model, + effort, + summary, + collaboration_mode, + personality, + } => app_server_client + .request::(|request_id| { + app_proto::ClientRequest::ThreadContextUpdate { + request_id, + params: app_proto::ThreadContextUpdateParams { + thread_id: thread_id_str.clone(), + cwd, + approval_policy: approval_policy.map(Into::into), + sandbox_policy: sandbox_policy.map(Into::into), + windows_sandbox_level, + model, + effort, + summary, + personality, + collaboration_mode, + }, + } + }) + .await + .map(|_| ()), + AgentCommand::ExecApproval { + id, + turn_id: _, + decision, + } => { + let Some(decision) = convert_exec_approval_decision(decision) else { + continue; + }; + respond_if_pending( + &app_server_client, + &pending_server_requests, + PendingServerRequestKey::ExecApproval(id), + app_proto::CommandExecutionRequestApprovalResponse { decision }, + ) + .await + } + AgentCommand::PatchApproval { id, decision } => { + let Some(decision) = convert_patch_approval_decision(decision) else { + continue; + }; + respond_if_pending( + &app_server_client, + &pending_server_requests, + PendingServerRequestKey::PatchApproval(id), + app_proto::FileChangeRequestApprovalResponse { decision }, + ) + .await + } + AgentCommand::ResolveElicitation { + server_name, + request_id, + decision, + } => { + respond_if_pending( + &app_server_client, + &pending_server_requests, + PendingServerRequestKey::McpElicitation { + server_name, + request_id, + }, + app_proto::McpElicitationRequestResponse { + decision: convert_elicitation_decision(decision), + }, + ) + .await + } + AgentCommand::UserInputAnswer { id, response } => { + let Some(request_id) = take_pending_request_id( + &pending_server_requests, + PendingServerRequestKey::UserInput(id), + ) + .await + else { + continue; + }; + match convert_user_input_response(response) { + Ok(response) => app_server_client.respond(request_id, response).await, + Err(err) => Err(app_proto::JSONRPCErrorError { + code: -32603, + message: format!("failed to convert request_user_input response: {err}"), + data: None, + }), + } + } + AgentCommand::DynamicToolResponse { id, response } => { + let Some(request_id) = take_pending_request_id( + &pending_server_requests, + PendingServerRequestKey::DynamicTool(id), + ) + .await + else { + continue; + }; + match convert_dynamic_tool_response(response) { + Ok(response) => app_server_client.respond(request_id, response).await, + Err(err) => Err(app_proto::JSONRPCErrorError { + code: -32603, + message: format!("failed to convert dynamic tool response: {err}"), + data: None, + }), + } + } + AgentCommand::AddToHistory { text } => app_server_client + .request::(|request_id| { + app_proto::ClientRequest::HistoryAppend { + request_id, + params: app_proto::HistoryAppendParams { + thread_id: thread_id_str.clone(), + text, + }, + } + }) + .await + .map(|_| ()), + AgentCommand::GetHistoryEntryRequest { offset, log_id } => { + let response = app_server_client + .request::(|request_id| { + app_proto::ClientRequest::HistoryEntryRead { + request_id, + params: app_proto::HistoryEntryReadParams { log_id, offset }, + } + }) + .await; + response.map(|response| { + app_event_tx.send(AppEvent::HistoryEntryReadResponse(response)); + }) + } + AgentCommand::ListMcpTools => { + let response = app_server_client + .request::(|request_id| { + app_proto::ClientRequest::McpToolsList { + request_id, + params: app_proto::McpToolsListParams {}, + } + }) + .await; + response.map(|response| { + app_event_tx.send(AppEvent::McpToolsListResponse(response)); + }) + } + AgentCommand::ReloadUserConfig => app_server_client + .request::(|request_id| { + app_proto::ClientRequest::ThreadConfigReload { + request_id, + params: app_proto::ThreadConfigReloadParams { + thread_id: thread_id_str.clone(), + }, + } + }) + .await + .map(|_| ()), + AgentCommand::ListCustomPrompts => { + let response = app_server_client + .request::(|request_id| { + app_proto::ClientRequest::CustomPromptList { + request_id, + params: app_proto::CustomPromptListParams {}, + } + }) + .await; + response + .map(|response| app_event_tx.send(AppEvent::CustomPromptListResponse(response))) + } + AgentCommand::ListSkills { cwds, force_reload } => { + let response = app_server_client + .request::(|request_id| { + app_proto::ClientRequest::SkillsList { + request_id, + params: app_proto::SkillsListParams { + cwds, + force_reload, + per_cwd_extra_user_roots: None, + }, + } + }) + .await; + response.map(|response| app_event_tx.send(AppEvent::SkillsListResponse(response))) + } + AgentCommand::Compact => app_server_client + .request::(|request_id| { + app_proto::ClientRequest::ThreadCompactStart { + request_id, + params: app_proto::ThreadCompactStartParams { + thread_id: thread_id_str.clone(), + }, + } + }) + .await + .map(|_| ()), + AgentCommand::DropMemories => app_server_client + .request::(|request_id| { + app_proto::ClientRequest::ThreadMemoriesDrop { + request_id, + params: app_proto::ThreadMemoriesDropParams { + thread_id: thread_id_str.clone(), + }, + } + }) + .await + .map(|_| ()), + AgentCommand::UpdateMemories => app_server_client + .request::(|request_id| { + app_proto::ClientRequest::ThreadMemoriesUpdate { + request_id, + params: app_proto::ThreadMemoriesUpdateParams { + thread_id: thread_id_str.clone(), + }, + } + }) + .await + .map(|_| ()), + AgentCommand::SetThreadName { name } => app_server_client + .request::(|request_id| { + app_proto::ClientRequest::ThreadSetName { + request_id, + params: app_proto::ThreadSetNameParams { + thread_id: thread_id_str.clone(), + name, + }, + } + }) + .await + .map(|_| ()), + AgentCommand::Undo => app_server_client + .request::(|request_id| { + app_proto::ClientRequest::ThreadUndo { + request_id, + params: app_proto::ThreadUndoParams { + thread_id: thread_id_str.clone(), + }, + } + }) + .await + .map(|_| ()), + AgentCommand::ThreadRollback { num_turns } => app_server_client + .request::(|request_id| { + app_proto::ClientRequest::ThreadRollback { + request_id, + params: app_proto::ThreadRollbackParams { + thread_id: thread_id_str.clone(), + num_turns, + }, + } + }) + .await + .map(|_| { + app_event_tx.send(AppEvent::ThreadRollbackCompleted { + thread_id, + num_turns, + }); + }), + AgentCommand::Review { review_request } => { + let target = match convert_review_target(review_request.target) { + Ok(target) => target, + Err(err) => { + tracing::error!("failed to convert review target: {err}"); + continue; + } + }; + app_server_client + .request::(|request_id| { + app_proto::ClientRequest::ReviewStart { + request_id, + params: app_proto::ReviewStartParams { + thread_id: thread_id_str.clone(), + target, + delivery: None, + }, + } + }) + .await + .map(|_| ()) + } + AgentCommand::Shutdown => app_server_client + .request::(|request_id| { + app_proto::ClientRequest::ThreadShutdown { + request_id, + params: app_proto::ThreadShutdownParams { + thread_id: thread_id_str.clone(), + }, + } + }) + .await + .map(|_| ()), + AgentCommand::RunUserShellCommand { command } => app_server_client + .request::(|request_id| { + app_proto::ClientRequest::ThreadUserShellCommandRun { + request_id, + params: app_proto::ThreadUserShellCommandRunParams { + thread_id: thread_id_str.clone(), + command, + }, + } + }) + .await + .map(|_| ()), + }; + + if let Err(err) = result { + tracing::error!("failed to handle agent command via app-server: {err:?}"); + } + } +} + +fn sandbox_mode_from_policy( + policy: &codex_protocol::protocol::SandboxPolicy, +) -> Option { + match policy { + codex_protocol::protocol::SandboxPolicy::ReadOnly { .. } => { + Some(app_proto::SandboxMode::ReadOnly) + } + codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { .. } => { + Some(app_proto::SandboxMode::WorkspaceWrite) + } + codex_protocol::protocol::SandboxPolicy::DangerFullAccess => { + Some(app_proto::SandboxMode::DangerFullAccess) + } + codex_protocol::protocol::SandboxPolicy::ExternalSandbox { .. } => None, + } +} + +fn thread_start_params_from_config(config: &Config) -> app_proto::ThreadStartParams { + app_proto::ThreadStartParams { + model: config.model.clone(), + model_provider: Some(config.model_provider_id.clone()), + cwd: Some(config.cwd.to_string_lossy().to_string()), + approval_policy: Some(app_proto::AskForApproval::from( + *config.permissions.approval_policy.get(), + )), + sandbox: sandbox_mode_from_policy(config.permissions.sandbox_policy.get()), + personality: config.personality, + ..Default::default() + } +} + +/// Spawn the agent bootstrapper and command forwarding loop. pub(crate) fn spawn_agent( config: Config, app_event_tx: AppEventSender, - server: Arc, -) -> UnboundedSender { - let (codex_op_tx, mut codex_op_rx) = unbounded_channel::(); + app_server_client: Arc, +) -> UnboundedSender { + let (codex_op_tx, codex_op_rx) = unbounded_channel::(); let app_event_tx_clone = app_event_tx; tokio::spawn(async move { - let NewThread { - thread, - session_configured, - .. - } = match server.start_thread(config).await { - Ok(v) => v, + let response = match app_server_client + .thread_start(thread_start_params_from_config(&config)) + .await + { + Ok(response) => response, Err(err) => { - let message = format!("Failed to initialize codex: {err}"); + let message = format!("Failed to initialize codex: {err:?}"); tracing::error!("{message}"); - app_event_tx_clone.send(AppEvent::CodexEvent(Event { - id: "".to_string(), - msg: EventMsg::Error(err.to_error_event(None)), - })); app_event_tx_clone.send(AppEvent::FatalExitRequest(message)); - tracing::error!("failed to initialize codex: {err}"); return; } }; - // Forward the captured `SessionConfigured` event so it can be rendered in the UI. - let ev = codex_protocol::protocol::Event { - // The `id` does not matter for rendering, so we can use a fake value. - id: "".to_string(), - msg: codex_protocol::protocol::EventMsg::SessionConfigured(session_configured), + let bootstrap = match thread_bootstrap_from_start(response) { + Ok(bootstrap) => bootstrap, + Err(err) => { + let message = format!("Failed to initialize codex: {err}"); + tracing::error!("{message}"); + app_event_tx_clone.send(AppEvent::FatalExitRequest(message)); + return; + } }; - app_event_tx_clone.send(AppEvent::CodexEvent(ev)); - - let thread_clone = thread.clone(); - tokio::spawn(async move { - while let Some(op) = codex_op_rx.recv().await { - let id = thread_clone.submit(op).await; - if let Err(e) = id { - tracing::error!("failed to submit op: {e}"); - } - } - }); - - while let Ok(event) = thread.next_event().await { - let is_shutdown_complete = matches!(event.msg, EventMsg::ShutdownComplete); - app_event_tx_clone.send(AppEvent::CodexEvent(event)); - if is_shutdown_complete { - // ShutdownComplete is terminal for a thread; drop this receiver task so - // the Arc can be released and thread resources can clean up. - break; - } - } + let thread_id = bootstrap.thread_id; + let pending_server_requests = Arc::new(Mutex::new(PendingServerRequests::default())); + let active_turn_id = Arc::new(Mutex::new(None)); + spawn_server_request_tracker( + app_server_client.clone(), + app_event_tx_clone.clone(), + thread_id, + pending_server_requests.clone(), + ); + spawn_active_turn_tracker(app_server_client.clone(), thread_id, active_turn_id.clone()); + tokio::spawn(run_command_loop( + app_server_client.clone(), + app_event_tx_clone.clone(), + thread_id, + pending_server_requests, + active_turn_id, + codex_op_rx, + )); + app_event_tx_clone.send(AppEvent::ThreadBootstrapped(bootstrap)); }); codex_op_tx } /// Spawn agent loops for an existing thread (e.g., a forked thread). -/// Sends the provided `SessionConfiguredEvent` immediately, then forwards subsequent -/// events and accepts Ops for submission. pub(crate) fn spawn_agent_from_existing( - thread: std::sync::Arc, - session_configured: codex_protocol::protocol::SessionConfiguredEvent, + thread_id: ThreadId, app_event_tx: AppEventSender, -) -> UnboundedSender { - let (codex_op_tx, mut codex_op_rx) = unbounded_channel::(); + app_server_client: Arc, +) -> UnboundedSender { + let (codex_op_tx, codex_op_rx) = unbounded_channel::(); let app_event_tx_clone = app_event_tx; tokio::spawn(async move { - // Forward the captured `SessionConfigured` event so it can be rendered in the UI. - let ev = codex_protocol::protocol::Event { - id: "".to_string(), - msg: codex_protocol::protocol::EventMsg::SessionConfigured(session_configured), - }; - app_event_tx_clone.send(AppEvent::CodexEvent(ev)); - - let thread_clone = thread.clone(); - tokio::spawn(async move { - while let Some(op) = codex_op_rx.recv().await { - let id = thread_clone.submit(op).await; - if let Err(e) = id { - tracing::error!("failed to submit op: {e}"); - } - } - }); - - while let Ok(event) = thread.next_event().await { - let is_shutdown_complete = matches!(event.msg, EventMsg::ShutdownComplete); - app_event_tx_clone.send(AppEvent::CodexEvent(event)); - if is_shutdown_complete { - // ShutdownComplete is terminal for a thread; drop this receiver task so - // the Arc can be released and thread resources can clean up. - break; - } - } + let pending_server_requests = Arc::new(Mutex::new(PendingServerRequests::default())); + let active_turn_id = Arc::new(Mutex::new(None)); + spawn_server_request_tracker( + app_server_client.clone(), + app_event_tx_clone.clone(), + thread_id, + pending_server_requests.clone(), + ); + spawn_active_turn_tracker(app_server_client.clone(), thread_id, active_turn_id.clone()); + tokio::spawn(run_command_loop( + app_server_client, + app_event_tx_clone.clone(), + thread_id, + pending_server_requests, + active_turn_id, + codex_op_rx, + )); }); codex_op_tx } -/// Spawn an op-forwarding loop for an existing thread without subscribing to events. -pub(crate) fn spawn_op_forwarder(thread: std::sync::Arc) -> UnboundedSender { - let (codex_op_tx, mut codex_op_rx) = unbounded_channel::(); +/// Spawn a command-forwarding loop for an existing thread without subscribing to events. +pub(crate) fn spawn_op_forwarder( + app_event_tx: AppEventSender, + thread_id: ThreadId, + app_server_client: Arc, +) -> UnboundedSender { + let (codex_op_tx, codex_op_rx) = unbounded_channel::(); + let pending_server_requests = Arc::new(Mutex::new(PendingServerRequests::default())); + let active_turn_id = Arc::new(Mutex::new(None)); + spawn_server_request_tracker( + app_server_client.clone(), + app_event_tx.clone(), + thread_id, + pending_server_requests.clone(), + ); + spawn_active_turn_tracker(app_server_client.clone(), thread_id, active_turn_id.clone()); - tokio::spawn(async move { - while let Some(op) = codex_op_rx.recv().await { - if let Err(e) = thread.submit(op).await { - tracing::error!("failed to submit op: {e}"); - } - } - }); + tokio::spawn(run_command_loop( + app_server_client, + app_event_tx, + thread_id, + pending_server_requests, + active_turn_id, + codex_op_rx, + )); codex_op_tx } diff --git a/codex-rs/tui/src/chatwidget/skills.rs b/codex-rs/tui/src/chatwidget/skills.rs index 1d71cc61ec..bac904c5b1 100644 --- a/codex-rs/tui/src/chatwidget/skills.rs +++ b/codex-rs/tui/src/chatwidget/skills.rs @@ -12,6 +12,7 @@ use crate::bottom_pane::SkillsToggleView; use crate::bottom_pane::popup_consts::standard_popup_hint_line; use crate::skills_helpers::skill_description; use crate::skills_helpers::skill_display_name; +use codex_app_server_protocol as app_proto; use codex_chatgpt::connectors::AppInfo; use codex_core::connectors::connector_mention_slug; use codex_core::skills::model::SkillDependencies; @@ -141,6 +142,22 @@ impl ChatWidget { self.skills_all = skills; self.set_skills(Some(enabled_skills_for_mentions(&self.skills_all))); } + + pub(crate) fn set_skills_from_app_server_response( + &mut self, + response: &app_proto::SkillsListResponse, + ) { + match super::convert_via_serde::<_, Vec>(response.data.clone()) { + Ok(entries) => { + let skills = skills_for_cwd(&self.config.cwd, &entries); + self.skills_all = skills; + self.set_skills(Some(enabled_skills_for_mentions(&self.skills_all))); + } + Err(err) => { + tracing::error!("failed to convert skills response: {err}"); + } + } + } } fn skills_for_cwd(cwd: &Path, skills_entries: &[SkillsListEntry]) -> Vec { diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index d7424f720f..8be1c4743f 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -5,6 +5,7 @@ //! changes show up as stable, reviewable diffs. use super::*; +use crate::agent_command::AgentCommand; use crate::app_event::AppEvent; use crate::app_event::ExitMode; use crate::app_event_sender::AppEventSender; @@ -68,7 +69,6 @@ use codex_protocol::protocol::ItemCompletedEvent; use codex_protocol::protocol::McpStartupCompleteEvent; use codex_protocol::protocol::McpStartupStatus; use codex_protocol::protocol::McpStartupUpdateEvent; -use codex_protocol::protocol::Op; use codex_protocol::protocol::PatchApplyBeginEvent; use codex_protocol::protocol::PatchApplyEndEvent; use codex_protocol::protocol::PatchApplyStatus as CorePatchApplyStatus; @@ -529,8 +529,8 @@ async fn submission_preserves_text_elements_and_local_images() { chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); let items = match next_submit_op(&mut op_rx) { - Op::UserTurn { items, .. } => items, - other => panic!("expected Op::UserTurn, got {other:?}"), + AgentCommand::UserTurn { items, .. } => items, + other => panic!("expected AgentCommand::UserTurn, got {other:?}"), }; assert_eq!(items.len(), 2); assert_eq!( @@ -615,8 +615,8 @@ async fn submission_with_remote_and_local_images_keeps_local_placeholder_numberi chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); let items = match next_submit_op(&mut op_rx) { - Op::UserTurn { items, .. } => items, - other => panic!("expected Op::UserTurn, got {other:?}"), + AgentCommand::UserTurn { items, .. } => items, + other => panic!("expected AgentCommand::UserTurn, got {other:?}"), }; assert_eq!(items.len(), 3); assert_eq!( @@ -698,8 +698,8 @@ async fn enter_with_only_remote_images_submits_user_turn() { chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); let items = match next_submit_op(&mut op_rx) { - Op::UserTurn { items, .. } => items, - other => panic!("expected Op::UserTurn, got {other:?}"), + AgentCommand::UserTurn { items, .. } => items, + other => panic!("expected AgentCommand::UserTurn, got {other:?}"), }; assert_eq!( items, @@ -907,8 +907,8 @@ async fn submission_prefers_selected_duplicate_skill_path() { chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); let items = match next_submit_op(&mut op_rx) { - Op::UserTurn { items, .. } => items, - other => panic!("expected Op::UserTurn, got {other:?}"), + AgentCommand::UserTurn { items, .. } => items, + other => panic!("expected AgentCommand::UserTurn, got {other:?}"), }; let selected_skill_paths = items .iter() @@ -1197,13 +1197,13 @@ async fn interrupted_turn_restore_keeps_active_mode_for_resubmission() { chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); match next_submit_op(&mut op_rx) { - Op::UserTurn { + AgentCommand::UserTurn { collaboration_mode: Some(CollaborationMode { mode, .. }), personality: None, .. } => assert_eq!(mode, expected_mode), other => { - panic!("expected Op::UserTurn with active mode, got {other:?}") + panic!("expected AgentCommand::UserTurn with active mode, got {other:?}") } } assert_eq!(chat.active_collaboration_mode_kind(), expected_mode); @@ -1525,7 +1525,18 @@ async fn helpers_are_available_and_do_not_panic() { status_line_invalid_items_warned: Arc::new(AtomicBool::new(false)), otel_manager, }; - let mut w = ChatWidget::new(init, thread_manager); + let app_server_client = Arc::new(codex_app_server::EmbeddedSessionClient::new( + codex_app_server::EmbeddedSessionClientArgs { + auth_manager: codex_core::test_support::auth_manager_from_auth( + CodexAuth::from_api_key("test"), + ), + thread_manager, + config: init.config.clone(), + cli_overrides: Vec::new(), + feedback: codex_feedback::CodexFeedback::new(), + }, + )); + let mut w = ChatWidget::new(init, app_server_client); // Basic construction sanity. let _ = &mut w; } @@ -1552,11 +1563,11 @@ async fn make_chatwidget_manual( ) -> ( ChatWidget, tokio::sync::mpsc::UnboundedReceiver, - tokio::sync::mpsc::UnboundedReceiver, + tokio::sync::mpsc::UnboundedReceiver, ) { let (tx_raw, rx) = unbounded_channel::(); let app_event_tx = AppEventSender::new(tx_raw); - let (op_tx, op_rx) = unbounded_channel::(); + let (op_tx, op_rx) = unbounded_channel::(); let mut cfg = test_config().await; let resolved_model = model_override .map(str::to_owned) @@ -1673,12 +1684,12 @@ async fn make_chatwidget_manual( (widget, rx, op_rx) } -// ChatWidget may emit other `Op`s (e.g. history/logging updates) on the same channel; this helper +// ChatWidget may emit other `AgentCommand`s (e.g. history/logging updates) on the same channel; this helper // filters until we see a submission op. -fn next_submit_op(op_rx: &mut tokio::sync::mpsc::UnboundedReceiver) -> Op { +fn next_submit_op(op_rx: &mut tokio::sync::mpsc::UnboundedReceiver) -> AgentCommand { loop { match op_rx.try_recv() { - Ok(op @ Op::UserTurn { .. }) => return op, + Ok(op @ AgentCommand::UserTurn { .. }) => return op, Ok(_) => continue, Err(TryRecvError::Empty) => panic!("expected a submit op but queue was empty"), Err(TryRecvError::Disconnected) => panic!("expected submit op but channel closed"), @@ -1686,10 +1697,10 @@ fn next_submit_op(op_rx: &mut tokio::sync::mpsc::UnboundedReceiver) -> Op { } } -fn assert_no_submit_op(op_rx: &mut tokio::sync::mpsc::UnboundedReceiver) { +fn assert_no_submit_op(op_rx: &mut tokio::sync::mpsc::UnboundedReceiver) { while let Ok(op) = op_rx.try_recv() { assert!( - !matches!(op, Op::UserTurn { .. }), + !matches!(op, AgentCommand::UserTurn { .. }), "unexpected submit op: {op:?}" ); } @@ -1736,7 +1747,7 @@ pub(crate) async fn make_chatwidget_manual_with_sender() -> ( ChatWidget, AppEventSender, tokio::sync::mpsc::UnboundedReceiver, - tokio::sync::mpsc::UnboundedReceiver, + tokio::sync::mpsc::UnboundedReceiver, ) { let (widget, rx, op_rx) = make_chatwidget_manual(None).await; let app_event_tx = widget.app_event_tx.clone(); @@ -2162,7 +2173,7 @@ async fn submit_user_message_with_mode_sets_coding_collaboration_mode() { chat.submit_user_message_with_mode("Implement the plan.".to_string(), default_mode); match next_submit_op(&mut op_rx) { - Op::UserTurn { + AgentCommand::UserTurn { collaboration_mode: Some(CollaborationMode { mode: ModeKind::Default, @@ -2172,7 +2183,7 @@ async fn submit_user_message_with_mode_sets_coding_collaboration_mode() { .. } => {} other => { - panic!("expected Op::UserTurn with default collab mode, got {other:?}") + panic!("expected AgentCommand::UserTurn with default collab mode, got {other:?}") } } } @@ -2459,7 +2470,7 @@ async fn submit_user_message_with_mode_allows_same_mode_during_running_turn() { assert_eq!(chat.active_collaboration_mode_kind(), ModeKind::Plan); assert!(chat.queued_user_messages.is_empty()); match next_submit_op(&mut op_rx) { - Op::UserTurn { + AgentCommand::UserTurn { collaboration_mode: Some(CollaborationMode { mode: ModeKind::Plan, @@ -2469,7 +2480,7 @@ async fn submit_user_message_with_mode_allows_same_mode_during_running_turn() { .. } => {} other => { - panic!("expected Op::UserTurn with plan collab mode, got {other:?}") + panic!("expected AgentCommand::UserTurn with plan collab mode, got {other:?}") } } } @@ -2494,13 +2505,13 @@ async fn submit_user_message_with_mode_submits_when_plan_stream_is_not_active() assert_eq!(chat.active_collaboration_mode_kind(), expected_mode); assert!(chat.queued_user_messages.is_empty()); match next_submit_op(&mut op_rx) { - Op::UserTurn { + AgentCommand::UserTurn { collaboration_mode: Some(CollaborationMode { mode, .. }), personality: None, .. } => assert_eq!(mode, expected_mode), other => { - panic!("expected Op::UserTurn with default collab mode, got {other:?}") + panic!("expected AgentCommand::UserTurn with default collab mode, got {other:?}") } } } @@ -3179,8 +3190,8 @@ async fn streaming_final_answer_keeps_task_running_state() { chat.handle_key_event(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)); match op_rx.try_recv() { - Ok(Op::Interrupt) => {} - other => panic!("expected Op::Interrupt, got {other:?}"), + Ok(AgentCommand::Interrupt) => {} + other => panic!("expected AgentCommand::Interrupt, got {other:?}"), } assert!(!chat.bottom_pane.quit_shortcut_hint_visible()); } @@ -3367,11 +3378,11 @@ async fn steer_enter_submits_when_plan_stream_is_not_active() { assert!(chat.queued_user_messages.is_empty()); match next_submit_op(&mut op_rx) { - Op::UserTurn { + AgentCommand::UserTurn { personality: Some(Personality::Pragmatic), .. } => {} - other => panic!("expected Op::UserTurn, got {other:?}"), + other => panic!("expected AgentCommand::UserTurn, got {other:?}"), } } @@ -4113,7 +4124,7 @@ async fn collab_slash_command_opens_picker_and_updates_mode() { .set_composer_text("hello".to_string(), Vec::new(), Vec::new()); chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); match next_submit_op(&mut op_rx) { - Op::UserTurn { + AgentCommand::UserTurn { collaboration_mode: Some(CollaborationMode { mode: ModeKind::Default, @@ -4123,7 +4134,7 @@ async fn collab_slash_command_opens_picker_and_updates_mode() { .. } => {} other => { - panic!("expected Op::UserTurn with code collab mode, got {other:?}") + panic!("expected AgentCommand::UserTurn with code collab mode, got {other:?}") } } @@ -4131,7 +4142,7 @@ async fn collab_slash_command_opens_picker_and_updates_mode() { .set_composer_text("follow up".to_string(), Vec::new(), Vec::new()); chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); match next_submit_op(&mut op_rx) { - Op::UserTurn { + AgentCommand::UserTurn { collaboration_mode: Some(CollaborationMode { mode: ModeKind::Default, @@ -4141,7 +4152,7 @@ async fn collab_slash_command_opens_picker_and_updates_mode() { .. } => {} other => { - panic!("expected Op::UserTurn with code collab mode, got {other:?}") + panic!("expected AgentCommand::UserTurn with code collab mode, got {other:?}") } } } @@ -4195,8 +4206,8 @@ async fn plan_slash_command_with_args_submits_prompt_in_plan_mode() { chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); let items = match next_submit_op(&mut op_rx) { - Op::UserTurn { items, .. } => items, - other => panic!("expected Op::UserTurn, got {other:?}"), + AgentCommand::UserTurn { items, .. } => items, + other => panic!("expected AgentCommand::UserTurn, got {other:?}"), }; assert_eq!(items.len(), 1); assert_eq!( @@ -4247,7 +4258,18 @@ async fn collaboration_modes_defaults_to_code_on_startup() { otel_manager, }; - let chat = ChatWidget::new(init, thread_manager); + let app_server_client = Arc::new(codex_app_server::EmbeddedSessionClient::new( + codex_app_server::EmbeddedSessionClientArgs { + auth_manager: codex_core::test_support::auth_manager_from_auth( + CodexAuth::from_api_key("test"), + ), + thread_manager, + config: init.config.clone(), + cli_overrides: Vec::new(), + feedback: codex_feedback::CodexFeedback::new(), + }, + )); + let chat = ChatWidget::new(init, app_server_client); assert_eq!(chat.active_collaboration_mode_kind(), ModeKind::Default); assert_eq!(chat.current_model(), resolved_model); } @@ -4296,7 +4318,18 @@ async fn experimental_mode_plan_is_ignored_on_startup() { otel_manager, }; - let chat = ChatWidget::new(init, thread_manager); + let app_server_client = Arc::new(codex_app_server::EmbeddedSessionClient::new( + codex_app_server::EmbeddedSessionClientArgs { + auth_manager: codex_core::test_support::auth_manager_from_auth( + CodexAuth::from_api_key("test"), + ), + thread_manager, + config: init.config.clone(), + cli_overrides: Vec::new(), + feedback: codex_feedback::CodexFeedback::new(), + }, + )); + let chat = ChatWidget::new(init, app_server_client); assert_eq!(chat.active_collaboration_mode_kind(), ModeKind::Default); assert_eq!(chat.current_model(), resolved_model); } @@ -4363,7 +4396,7 @@ async fn collab_mode_is_sent_after_enabling() { .set_composer_text("hello".to_string(), Vec::new(), Vec::new()); chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); match next_submit_op(&mut op_rx) { - Op::UserTurn { + AgentCommand::UserTurn { collaboration_mode: Some(CollaborationMode { mode: ModeKind::Default, @@ -4373,7 +4406,7 @@ async fn collab_mode_is_sent_after_enabling() { .. } => {} other => { - panic!("expected Op::UserTurn, got {other:?}") + panic!("expected AgentCommand::UserTurn, got {other:?}") } } } @@ -4387,12 +4420,14 @@ async fn collab_mode_toggle_on_applies_default_preset() { .set_composer_text("before toggle".to_string(), Vec::new(), Vec::new()); chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); match next_submit_op(&mut op_rx) { - Op::UserTurn { + AgentCommand::UserTurn { collaboration_mode: None, personality: Some(Personality::Pragmatic), .. } => {} - other => panic!("expected Op::UserTurn without collaboration_mode, got {other:?}"), + other => { + panic!("expected AgentCommand::UserTurn without collaboration_mode, got {other:?}") + } } chat.set_feature_enabled(Feature::CollaborationModes, true); @@ -4401,7 +4436,7 @@ async fn collab_mode_toggle_on_applies_default_preset() { .set_composer_text("after toggle".to_string(), Vec::new(), Vec::new()); chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); match next_submit_op(&mut op_rx) { - Op::UserTurn { + AgentCommand::UserTurn { collaboration_mode: Some(CollaborationMode { mode: ModeKind::Default, @@ -4411,7 +4446,7 @@ async fn collab_mode_toggle_on_applies_default_preset() { .. } => {} other => { - panic!("expected Op::UserTurn with default collaboration_mode, got {other:?}") + panic!("expected AgentCommand::UserTurn with default collaboration_mode, got {other:?}") } } @@ -4431,11 +4466,11 @@ async fn user_turn_includes_personality_from_config() { .set_composer_text("hello".to_string(), Vec::new(), Vec::new()); chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); match next_submit_op(&mut op_rx) { - Op::UserTurn { + AgentCommand::UserTurn { personality: Some(Personality::Friendly), .. } => {} - other => panic!("expected Op::UserTurn with friendly personality, got {other:?}"), + other => panic!("expected AgentCommand::UserTurn with friendly personality, got {other:?}"), } } @@ -4463,7 +4498,7 @@ async fn slash_clean_submits_background_terminal_cleanup() { chat.dispatch_command(SlashCommand::Clean); - assert_matches!(op_rx.try_recv(), Ok(Op::CleanBackgroundTerminals)); + assert_matches!(op_rx.try_recv(), Ok(AgentCommand::CleanBackgroundTerminals)); let cells = drain_insert_history(&mut rx); assert_eq!(cells.len(), 1, "expected cleanup confirmation message"); let rendered = lines_to_single_string(&cells[0]); @@ -4509,7 +4544,7 @@ async fn slash_memory_drop_submits_drop_memories_op() { chat.dispatch_command(SlashCommand::MemoryDrop); - assert_matches!(op_rx.try_recv(), Ok(Op::DropMemories)); + assert_matches!(op_rx.try_recv(), Ok(AgentCommand::DropMemories)); } #[tokio::test] @@ -4518,7 +4553,7 @@ async fn slash_memory_update_submits_update_memories_op() { chat.dispatch_command(SlashCommand::MemoryUpdate); - assert_matches!(op_rx.try_recv(), Ok(Op::UpdateMemories)); + assert_matches!(op_rx.try_recv(), Ok(AgentCommand::UpdateMemories)); } #[tokio::test] @@ -4727,7 +4762,7 @@ async fn review_commit_picker_shows_subjects_without_timestamps() { ); } -/// Submitting the custom prompt view sends Op::Review with the typed prompt +/// Submitting the custom prompt view sends AgentCommand::Review with the typed prompt /// and uses the same text for the user-facing hint. #[tokio::test] async fn custom_prompt_submit_sends_review_op() { @@ -4738,10 +4773,10 @@ async fn custom_prompt_submit_sends_review_op() { chat.handle_paste(" please audit dependencies ".to_string()); chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); - // Expect AppEvent::CodexOp(Op::Review { .. }) with trimmed prompt + // Expect AppEvent::AgentCommand(AgentCommand::Review { .. }) with trimmed prompt let evt = rx.try_recv().expect("expected one app event"); match evt { - AppEvent::CodexOp(Op::Review { review_request }) => { + AppEvent::AgentCommand(AgentCommand::Review { review_request }) => { assert_eq!( review_request, ReviewRequest { @@ -5615,7 +5650,7 @@ async fn server_overloaded_error_does_not_switch_models() { } while let Ok(event) = op_rx.try_recv() { - if let Op::OverrideTurnContext { model, .. } = event { + if let AgentCommand::OverrideTurnContext { model, .. } = event { assert!( model.is_none(), "did not expect OverrideTurnContext model update on server-overloaded error" @@ -6061,7 +6096,7 @@ async fn approvals_popup_navigation_skips_disabled() { assert!( app_events.iter().any(|ev| matches!( ev, - AppEvent::CodexOp(Op::OverrideTurnContext { + AppEvent::AgentCommand(AgentCommand::OverrideTurnContext { approval_policy: Some(AskForApproval::OnRequest), personality: None, .. @@ -6072,7 +6107,7 @@ async fn approvals_popup_navigation_skips_disabled() { assert!( !app_events.iter().any(|ev| matches!( ev, - AppEvent::CodexOp(Op::OverrideTurnContext { + AppEvent::AgentCommand(AgentCommand::OverrideTurnContext { approval_policy: Some(AskForApproval::Never), personality: None, .. @@ -7115,7 +7150,7 @@ async fn apply_patch_approval_sends_op_with_call_id() { // Expect a CodexOp with PatchApproval carrying the call id. let mut found = false; while let Ok(app_ev) = rx.try_recv() { - if let AppEvent::CodexOp(Op::PatchApproval { id, decision }) = app_ev { + if let AppEvent::AgentCommand(AgentCommand::PatchApproval { id, decision }) = app_ev { assert_eq!(id, "call-999"); assert_matches!(decision, codex_protocol::protocol::ReviewDecision::Approved); found = true; @@ -7148,9 +7183,9 @@ async fn apply_patch_full_flow_integration_like() { // 2) User approves via 'y' and App receives a CodexOp chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE)); - let mut maybe_op: Option = None; + let mut maybe_op: Option = None; while let Ok(app_ev) = rx.try_recv() { - if let AppEvent::CodexOp(op) = app_ev { + if let AppEvent::AgentCommand(op) = app_ev { maybe_op = Some(op); break; } @@ -7163,7 +7198,7 @@ async fn apply_patch_full_flow_integration_like() { .try_recv() .expect("expected op forwarded to codex channel"); match forwarded { - Op::PatchApproval { id, decision } => { + AgentCommand::PatchApproval { id, decision } => { assert_eq!(id, "call-1"); assert_matches!(decision, codex_protocol::protocol::ReviewDecision::Approved); } diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 87d7089591..98740c0448 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -56,6 +56,7 @@ use tracing_subscriber::prelude::*; use uuid::Uuid; mod additional_dirs; +mod agent_command; mod app; mod app_backtrack; mod app_event; diff --git a/codex-rs/tui/src/session_log.rs b/codex-rs/tui/src/session_log.rs index e512c2f3cd..cfd17d937f 100644 --- a/codex-rs/tui/src/session_log.rs +++ b/codex-rs/tui/src/session_log.rs @@ -7,10 +7,10 @@ use std::sync::Mutex; use std::sync::OnceLock; use codex_core::config::Config; -use codex_protocol::protocol::Op; use serde::Serialize; use serde_json::json; +use crate::agent_command::AgentCommand; use crate::app_event::AppEvent; static LOGGER: LazyLock = LazyLock::new(SessionLogger::new); @@ -125,8 +125,27 @@ pub(crate) fn log_inbound_app_event(event: &AppEvent) { } match event { - AppEvent::CodexEvent(ev) => { - write_record("to_tui", "codex_event", ev); + AppEvent::ThreadBootstrapped(ev) => { + write_record("to_tui", "thread_bootstrapped", ev); + } + AppEvent::AppServerRequest { thread_id, request } => { + let payload = json!({ + "thread_id": thread_id, + "request": request, + }); + write_record("to_tui", "app_server_request", &payload); + } + AppEvent::HistoryEntryReadResponse(resp) => { + write_record("to_tui", "history_entry_read_response", resp); + } + AppEvent::McpToolsListResponse(resp) => { + write_record("to_tui", "mcp_tools_list_response", resp); + } + AppEvent::CustomPromptListResponse(resp) => { + write_record("to_tui", "custom_prompt_list_response", resp); + } + AppEvent::SkillsListResponse(resp) => { + write_record("to_tui", "skills_list_response", resp); } AppEvent::NewSession => { let value = json!({ @@ -185,7 +204,7 @@ pub(crate) fn log_inbound_app_event(event: &AppEvent) { } } -pub(crate) fn log_outbound_op(op: &Op) { +pub(crate) fn log_outbound_op(op: &AgentCommand) { if !LOGGER.is_enabled() { return; } diff --git a/codex-rs/tui/src/status_indicator_widget.rs b/codex-rs/tui/src/status_indicator_widget.rs index fa266f9760..983be70517 100644 --- a/codex-rs/tui/src/status_indicator_widget.rs +++ b/codex-rs/tui/src/status_indicator_widget.rs @@ -7,7 +7,7 @@ use std::time::Duration; use std::time::Instant; -use codex_protocol::protocol::Op; +use crate::agent_command::AgentCommand; use crossterm::event::KeyCode; use ratatui::buffer::Buffer; use ratatui::layout::Rect; @@ -89,7 +89,8 @@ impl StatusIndicatorWidget { } pub(crate) fn interrupt(&self) { - self.app_event_tx.send(AppEvent::CodexOp(Op::Interrupt)); + self.app_event_tx + .send(AppEvent::AgentCommand(AgentCommand::Interrupt)); } /// Update the animated header label (left of the brackets).