From be22c4072c9bd894def158a2e0ef0ce02b193d7a Mon Sep 17 00:00:00 2001 From: Francis Chalissery Date: Thu, 9 Apr 2026 17:30:16 -0700 Subject: [PATCH] Add command summary metadata for exec command lifecycle --- .../schema/json/ServerNotification.json | 28 ++++++++++++ .../codex_app_server_protocol.schemas.json | 28 ++++++++++++ .../codex_app_server_protocol.v2.schemas.json | 28 ++++++++++++ .../json/v2/ItemCompletedNotification.json | 28 ++++++++++++ .../json/v2/ItemStartedNotification.json | 28 ++++++++++++ .../schema/json/v2/ReviewStartResponse.json | 28 ++++++++++++ .../schema/json/v2/ThreadForkResponse.json | 28 ++++++++++++ .../schema/json/v2/ThreadListResponse.json | 28 ++++++++++++ .../json/v2/ThreadMetadataUpdateResponse.json | 28 ++++++++++++ .../schema/json/v2/ThreadReadResponse.json | 28 ++++++++++++ .../schema/json/v2/ThreadResumeResponse.json | 28 ++++++++++++ .../json/v2/ThreadRollbackResponse.json | 28 ++++++++++++ .../schema/json/v2/ThreadStartResponse.json | 28 ++++++++++++ .../json/v2/ThreadStartedNotification.json | 28 ++++++++++++ .../json/v2/ThreadUnarchiveResponse.json | 28 ++++++++++++ .../json/v2/TurnCompletedNotification.json | 28 ++++++++++++ .../schema/json/v2/TurnStartResponse.json | 28 ++++++++++++ .../json/v2/TurnStartedNotification.json | 28 ++++++++++++ .../schema/typescript/v2/CommandSummary.ts | 13 ++++++ .../schema/typescript/v2/ThreadItem.ts | 11 +++-- .../schema/typescript/v2/index.ts | 1 + .../src/protocol/item_builders.rs | 5 +++ .../src/protocol/thread_history.rs | 9 ++++ .../app-server-protocol/src/protocol/v2.rs | 21 +++++++++ codex-rs/app-server/README.md | 4 +- .../app-server/src/bespoke_event_handling.rs | 4 ++ codex-rs/core/src/tasks/user_shell.rs | 4 ++ codex-rs/core/src/tools/events.rs | 45 +++++++++---------- .../core/src/tools/handlers/unified_exec.rs | 5 +++ .../core/src/unified_exec/async_watcher.rs | 8 ++++ codex-rs/core/src/unified_exec/mod.rs | 2 + .../core/src/unified_exec/process_manager.rs | 6 +++ codex-rs/protocol/src/protocol.rs | 16 +++++++ codex-rs/tools/src/local_tool.rs | 27 +++++++++++ codex-rs/tui/src/app/app_server_adapter.rs | 18 ++++++++ codex-rs/tui/src/chatwidget.rs | 14 ++++++ .../tui/src/chatwidget/tests/app_server.rs | 2 + .../tui/src/chatwidget/tests/exec_flow.rs | 1 + codex-rs/tui/src/chatwidget/tests/helpers.rs | 4 ++ .../src/chatwidget/tests/status_and_layout.rs | 2 + 40 files changed, 696 insertions(+), 30 deletions(-) create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/CommandSummary.ts diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index d598b2cd50..109100ed5e 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -792,6 +792,23 @@ ], "type": "string" }, + "CommandSummary": { + "properties": { + "past": { + "description": "Past-tense prose summary, for example \"Pulled most recent code\".", + "type": "string" + }, + "present": { + "description": "Present-tense prose summary, for example \"Pulling most recent code\".", + "type": "string" + } + }, + "required": [ + "past", + "present" + ], + "type": "object" + }, "ConfigWarningNotification": { "properties": { "details": { @@ -2739,6 +2756,17 @@ }, "type": "array" }, + "commandSummary": { + "anyOf": [ + { + "$ref": "#/definitions/CommandSummary" + }, + { + "type": "null" + } + ], + "description": "Optional short prose summaries for non-technical surfaces." + }, "cwd": { "description": "The command's working directory.", "type": "string" 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 d3447132ab..9c8fd6189b 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 @@ -6273,6 +6273,23 @@ ], "type": "string" }, + "CommandSummary": { + "properties": { + "past": { + "description": "Past-tense prose summary, for example \"Pulled most recent code\".", + "type": "string" + }, + "present": { + "description": "Present-tense prose summary, for example \"Pulling most recent code\".", + "type": "string" + } + }, + "required": [ + "past", + "present" + ], + "type": "object" + }, "Config": { "additionalProperties": true, "properties": { @@ -12921,6 +12938,17 @@ }, "type": "array" }, + "commandSummary": { + "anyOf": [ + { + "$ref": "#/definitions/v2/CommandSummary" + }, + { + "type": "null" + } + ], + "description": "Optional short prose summaries for non-technical surfaces." + }, "cwd": { "description": "The command's working directory.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 6f2efa4e12..3eab326687 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -2921,6 +2921,23 @@ ], "type": "string" }, + "CommandSummary": { + "properties": { + "past": { + "description": "Past-tense prose summary, for example \"Pulled most recent code\".", + "type": "string" + }, + "present": { + "description": "Present-tense prose summary, for example \"Pulling most recent code\".", + "type": "string" + } + }, + "required": [ + "past", + "present" + ], + "type": "object" + }, "Config": { "additionalProperties": true, "properties": { @@ -10776,6 +10793,17 @@ }, "type": "array" }, + "commandSummary": { + "anyOf": [ + { + "$ref": "#/definitions/CommandSummary" + }, + { + "type": "null" + } + ], + "description": "Optional short prose summaries for non-technical surfaces." + }, "cwd": { "description": "The command's working directory.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json index 2883670c88..07f54c635c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json @@ -195,6 +195,23 @@ ], "type": "string" }, + "CommandSummary": { + "properties": { + "past": { + "description": "Past-tense prose summary, for example \"Pulled most recent code\".", + "type": "string" + }, + "present": { + "description": "Present-tense prose summary, for example \"Pulling most recent code\".", + "type": "string" + } + }, + "required": [ + "past", + "present" + ], + "type": "object" + }, "DynamicToolCallOutputContentItem": { "oneOf": [ { @@ -664,6 +681,17 @@ }, "type": "array" }, + "commandSummary": { + "anyOf": [ + { + "$ref": "#/definitions/CommandSummary" + }, + { + "type": "null" + } + ], + "description": "Optional short prose summaries for non-technical surfaces." + }, "cwd": { "description": "The command's working directory.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json index c2e71ccba9..7f71590d14 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json @@ -195,6 +195,23 @@ ], "type": "string" }, + "CommandSummary": { + "properties": { + "past": { + "description": "Past-tense prose summary, for example \"Pulled most recent code\".", + "type": "string" + }, + "present": { + "description": "Present-tense prose summary, for example \"Pulling most recent code\".", + "type": "string" + } + }, + "required": [ + "past", + "present" + ], + "type": "object" + }, "DynamicToolCallOutputContentItem": { "oneOf": [ { @@ -664,6 +681,17 @@ }, "type": "array" }, + "commandSummary": { + "anyOf": [ + { + "$ref": "#/definitions/CommandSummary" + }, + { + "type": "null" + } + ], + "description": "Optional short prose summaries for non-technical surfaces." + }, "cwd": { "description": "The command's working directory.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json index a7fe2e8d60..e3ac64f993 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json @@ -331,6 +331,23 @@ ], "type": "string" }, + "CommandSummary": { + "properties": { + "past": { + "description": "Past-tense prose summary, for example \"Pulled most recent code\".", + "type": "string" + }, + "present": { + "description": "Present-tense prose summary, for example \"Pulling most recent code\".", + "type": "string" + } + }, + "required": [ + "past", + "present" + ], + "type": "object" + }, "DynamicToolCallOutputContentItem": { "oneOf": [ { @@ -807,6 +824,17 @@ }, "type": "array" }, + "commandSummary": { + "anyOf": [ + { + "$ref": "#/definitions/CommandSummary" + }, + { + "type": "null" + } + ], + "description": "Optional short prose summaries for non-technical surfaces." + }, "cwd": { "description": "The command's working directory.", "type": "string" 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 9b1870198a..19411c00a5 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -396,6 +396,23 @@ ], "type": "string" }, + "CommandSummary": { + "properties": { + "past": { + "description": "Past-tense prose summary, for example \"Pulled most recent code\".", + "type": "string" + }, + "present": { + "description": "Present-tense prose summary, for example \"Pulling most recent code\".", + "type": "string" + } + }, + "required": [ + "past", + "present" + ], + "type": "object" + }, "DynamicToolCallOutputContentItem": { "oneOf": [ { @@ -1321,6 +1338,17 @@ }, "type": "array" }, + "commandSummary": { + "anyOf": [ + { + "$ref": "#/definitions/CommandSummary" + }, + { + "type": "null" + } + ], + "description": "Optional short prose summaries for non-technical surfaces." + }, "cwd": { "description": "The command's working directory.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json index 426f34ce35..d2e4de547e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json @@ -334,6 +334,23 @@ ], "type": "string" }, + "CommandSummary": { + "properties": { + "past": { + "description": "Past-tense prose summary, for example \"Pulled most recent code\".", + "type": "string" + }, + "present": { + "description": "Present-tense prose summary, for example \"Pulling most recent code\".", + "type": "string" + } + }, + "required": [ + "past", + "present" + ], + "type": "object" + }, "DynamicToolCallOutputContentItem": { "oneOf": [ { @@ -1079,6 +1096,17 @@ }, "type": "array" }, + "commandSummary": { + "anyOf": [ + { + "$ref": "#/definitions/CommandSummary" + }, + { + "type": "null" + } + ], + "description": "Optional short prose summaries for non-technical surfaces." + }, "cwd": { "description": "The command's working directory.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json index c869a79749..caf712f127 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json @@ -334,6 +334,23 @@ ], "type": "string" }, + "CommandSummary": { + "properties": { + "past": { + "description": "Past-tense prose summary, for example \"Pulled most recent code\".", + "type": "string" + }, + "present": { + "description": "Present-tense prose summary, for example \"Pulling most recent code\".", + "type": "string" + } + }, + "required": [ + "past", + "present" + ], + "type": "object" + }, "DynamicToolCallOutputContentItem": { "oneOf": [ { @@ -1079,6 +1096,17 @@ }, "type": "array" }, + "commandSummary": { + "anyOf": [ + { + "$ref": "#/definitions/CommandSummary" + }, + { + "type": "null" + } + ], + "description": "Optional short prose summaries for non-technical surfaces." + }, "cwd": { "description": "The command's working directory.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json index 9569860c38..9f98745e70 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json @@ -334,6 +334,23 @@ ], "type": "string" }, + "CommandSummary": { + "properties": { + "past": { + "description": "Past-tense prose summary, for example \"Pulled most recent code\".", + "type": "string" + }, + "present": { + "description": "Present-tense prose summary, for example \"Pulling most recent code\".", + "type": "string" + } + }, + "required": [ + "past", + "present" + ], + "type": "object" + }, "DynamicToolCallOutputContentItem": { "oneOf": [ { @@ -1079,6 +1096,17 @@ }, "type": "array" }, + "commandSummary": { + "anyOf": [ + { + "$ref": "#/definitions/CommandSummary" + }, + { + "type": "null" + } + ], + "description": "Optional short prose summaries for non-technical surfaces." + }, "cwd": { "description": "The command's working directory.", "type": "string" 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 5143545ec1..1d4988925f 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -396,6 +396,23 @@ ], "type": "string" }, + "CommandSummary": { + "properties": { + "past": { + "description": "Past-tense prose summary, for example \"Pulled most recent code\".", + "type": "string" + }, + "present": { + "description": "Present-tense prose summary, for example \"Pulling most recent code\".", + "type": "string" + } + }, + "required": [ + "past", + "present" + ], + "type": "object" + }, "DynamicToolCallOutputContentItem": { "oneOf": [ { @@ -1321,6 +1338,17 @@ }, "type": "array" }, + "commandSummary": { + "anyOf": [ + { + "$ref": "#/definitions/CommandSummary" + }, + { + "type": "null" + } + ], + "description": "Optional short prose summaries for non-technical surfaces." + }, "cwd": { "description": "The command's working directory.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json index 502dd3961f..36952bffb9 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json @@ -334,6 +334,23 @@ ], "type": "string" }, + "CommandSummary": { + "properties": { + "past": { + "description": "Past-tense prose summary, for example \"Pulled most recent code\".", + "type": "string" + }, + "present": { + "description": "Present-tense prose summary, for example \"Pulling most recent code\".", + "type": "string" + } + }, + "required": [ + "past", + "present" + ], + "type": "object" + }, "DynamicToolCallOutputContentItem": { "oneOf": [ { @@ -1079,6 +1096,17 @@ }, "type": "array" }, + "commandSummary": { + "anyOf": [ + { + "$ref": "#/definitions/CommandSummary" + }, + { + "type": "null" + } + ], + "description": "Optional short prose summaries for non-technical surfaces." + }, "cwd": { "description": "The command's working directory.", "type": "string" 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 c07b4d8258..79adb9e2d2 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -396,6 +396,23 @@ ], "type": "string" }, + "CommandSummary": { + "properties": { + "past": { + "description": "Past-tense prose summary, for example \"Pulled most recent code\".", + "type": "string" + }, + "present": { + "description": "Present-tense prose summary, for example \"Pulling most recent code\".", + "type": "string" + } + }, + "required": [ + "past", + "present" + ], + "type": "object" + }, "DynamicToolCallOutputContentItem": { "oneOf": [ { @@ -1321,6 +1338,17 @@ }, "type": "array" }, + "commandSummary": { + "anyOf": [ + { + "$ref": "#/definitions/CommandSummary" + }, + { + "type": "null" + } + ], + "description": "Optional short prose summaries for non-technical surfaces." + }, "cwd": { "description": "The command's working directory.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json index ff87af2069..da84e23c63 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json @@ -334,6 +334,23 @@ ], "type": "string" }, + "CommandSummary": { + "properties": { + "past": { + "description": "Past-tense prose summary, for example \"Pulled most recent code\".", + "type": "string" + }, + "present": { + "description": "Present-tense prose summary, for example \"Pulling most recent code\".", + "type": "string" + } + }, + "required": [ + "past", + "present" + ], + "type": "object" + }, "DynamicToolCallOutputContentItem": { "oneOf": [ { @@ -1079,6 +1096,17 @@ }, "type": "array" }, + "commandSummary": { + "anyOf": [ + { + "$ref": "#/definitions/CommandSummary" + }, + { + "type": "null" + } + ], + "description": "Optional short prose summaries for non-technical surfaces." + }, "cwd": { "description": "The command's working directory.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json index daf821c374..bdf6623898 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json @@ -334,6 +334,23 @@ ], "type": "string" }, + "CommandSummary": { + "properties": { + "past": { + "description": "Past-tense prose summary, for example \"Pulled most recent code\".", + "type": "string" + }, + "present": { + "description": "Present-tense prose summary, for example \"Pulling most recent code\".", + "type": "string" + } + }, + "required": [ + "past", + "present" + ], + "type": "object" + }, "DynamicToolCallOutputContentItem": { "oneOf": [ { @@ -1079,6 +1096,17 @@ }, "type": "array" }, + "commandSummary": { + "anyOf": [ + { + "$ref": "#/definitions/CommandSummary" + }, + { + "type": "null" + } + ], + "description": "Optional short prose summaries for non-technical surfaces." + }, "cwd": { "description": "The command's working directory.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json index 82c2b3c76c..17f46e5f0f 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json @@ -331,6 +331,23 @@ ], "type": "string" }, + "CommandSummary": { + "properties": { + "past": { + "description": "Past-tense prose summary, for example \"Pulled most recent code\".", + "type": "string" + }, + "present": { + "description": "Present-tense prose summary, for example \"Pulling most recent code\".", + "type": "string" + } + }, + "required": [ + "past", + "present" + ], + "type": "object" + }, "DynamicToolCallOutputContentItem": { "oneOf": [ { @@ -807,6 +824,17 @@ }, "type": "array" }, + "commandSummary": { + "anyOf": [ + { + "$ref": "#/definitions/CommandSummary" + }, + { + "type": "null" + } + ], + "description": "Optional short prose summaries for non-technical surfaces." + }, "cwd": { "description": "The command's working directory.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json index ebb2065cb8..4bc853c1b8 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json @@ -331,6 +331,23 @@ ], "type": "string" }, + "CommandSummary": { + "properties": { + "past": { + "description": "Past-tense prose summary, for example \"Pulled most recent code\".", + "type": "string" + }, + "present": { + "description": "Present-tense prose summary, for example \"Pulling most recent code\".", + "type": "string" + } + }, + "required": [ + "past", + "present" + ], + "type": "object" + }, "DynamicToolCallOutputContentItem": { "oneOf": [ { @@ -807,6 +824,17 @@ }, "type": "array" }, + "commandSummary": { + "anyOf": [ + { + "$ref": "#/definitions/CommandSummary" + }, + { + "type": "null" + } + ], + "description": "Optional short prose summaries for non-technical surfaces." + }, "cwd": { "description": "The command's working directory.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json index 8b7c2bc410..018873f54b 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json @@ -331,6 +331,23 @@ ], "type": "string" }, + "CommandSummary": { + "properties": { + "past": { + "description": "Past-tense prose summary, for example \"Pulled most recent code\".", + "type": "string" + }, + "present": { + "description": "Present-tense prose summary, for example \"Pulling most recent code\".", + "type": "string" + } + }, + "required": [ + "past", + "present" + ], + "type": "object" + }, "DynamicToolCallOutputContentItem": { "oneOf": [ { @@ -807,6 +824,17 @@ }, "type": "array" }, + "commandSummary": { + "anyOf": [ + { + "$ref": "#/definitions/CommandSummary" + }, + { + "type": "null" + } + ], + "description": "Optional short prose summaries for non-technical surfaces." + }, "cwd": { "description": "The command's working directory.", "type": "string" diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CommandSummary.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CommandSummary.ts new file mode 100644 index 0000000000..3e5a8c51be --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CommandSummary.ts @@ -0,0 +1,13 @@ +// 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 CommandSummary = { +/** + * Present-tense prose summary, for example "Pulling most recent code". + */ +present: string, +/** + * Past-tense prose summary, for example "Pulled most recent code". + */ +past: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts index 9202f3728f..1bf1395f61 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts @@ -10,6 +10,7 @@ import type { CollabAgentToolCallStatus } from "./CollabAgentToolCallStatus"; import type { CommandAction } from "./CommandAction"; import type { CommandExecutionSource } from "./CommandExecutionSource"; import type { CommandExecutionStatus } from "./CommandExecutionStatus"; +import type { CommandSummary } from "./CommandSummary"; import type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem"; import type { DynamicToolCallStatus } from "./DynamicToolCallStatus"; import type { FileUpdateChange } from "./FileUpdateChange"; @@ -44,15 +45,19 @@ commandActions: Array, /** * The command's output, aggregated from stdout and stderr. */ -aggregatedOutput: string | null, +aggregatedOutput: string | null, /** * The command's exit code. */ -exitCode: number | null, +exitCode: number | null, /** * The duration of the command execution in milliseconds. */ -durationMs: number | null, } | { "type": "fileChange", id: string, changes: Array, status: PatchApplyStatus, } | { "type": "mcpToolCall", id: string, server: string, tool: string, status: McpToolCallStatus, arguments: JsonValue, result: McpToolCallResult | null, error: McpToolCallError | null, +durationMs: number | null, +/** + * Optional short prose summaries for non-technical surfaces. + */ +commandSummary: CommandSummary | null, } | { "type": "fileChange", id: string, changes: Array, status: PatchApplyStatus, } | { "type": "mcpToolCall", id: string, server: string, tool: string, status: McpToolCallStatus, arguments: JsonValue, result: McpToolCallResult | null, error: McpToolCallError | null, /** * The duration of the MCP tool call in milliseconds. */ 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 7f95634e9d..a7dc009108 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -55,6 +55,7 @@ export type { CommandExecutionRequestApprovalParams } from "./CommandExecutionRe export type { CommandExecutionRequestApprovalResponse } from "./CommandExecutionRequestApprovalResponse"; export type { CommandExecutionSource } from "./CommandExecutionSource"; export type { CommandExecutionStatus } from "./CommandExecutionStatus"; +export type { CommandSummary } from "./CommandSummary"; export type { Config } from "./Config"; export type { ConfigBatchWriteParams } from "./ConfigBatchWriteParams"; export type { ConfigEdit } from "./ConfigEdit"; diff --git a/codex-rs/app-server-protocol/src/protocol/item_builders.rs b/codex-rs/app-server-protocol/src/protocol/item_builders.rs index 9853f69f40..a3fc0acad1 100644 --- a/codex-rs/app-server-protocol/src/protocol/item_builders.rs +++ b/codex-rs/app-server-protocol/src/protocol/item_builders.rs @@ -82,6 +82,7 @@ pub fn build_command_execution_approval_request_item( aggregated_output: None, exit_code: None, duration_ms: None, + command_summary: None, } } @@ -102,6 +103,7 @@ pub fn build_command_execution_begin_item(payload: &ExecCommandBeginEvent) -> Th aggregated_output: None, exit_code: None, duration_ms: None, + command_summary: payload.command_summary.clone().map(Into::into), } } @@ -129,6 +131,7 @@ pub fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> Thread aggregated_output, exit_code: Some(payload.exit_code), duration_ms: Some(duration_ms), + command_summary: payload.command_summary.clone().map(Into::into), } } @@ -157,6 +160,7 @@ pub fn build_item_from_guardian_event( aggregated_output: None, exit_code: None, duration_ms: None, + command_summary: None, }) } GuardianAssessmentAction::Execve { @@ -189,6 +193,7 @@ pub fn build_item_from_guardian_event( aggregated_output: None, exit_code: None, duration_ms: None, + command_summary: None, }) } GuardianAssessmentAction::ApplyPatch { .. } diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history.rs b/codex-rs/app-server-protocol/src/protocol/thread_history.rs index b005657968..7c8a5dd645 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -1791,6 +1791,7 @@ mod tests { }], source: ExecCommandSource::Agent, interaction_input: None, + command_summary: None, stdout: String::new(), stderr: String::new(), aggregated_output: "hello world\n".into(), @@ -1844,6 +1845,7 @@ mod tests { aggregated_output: Some("hello world\n".into()), exit_code: Some(0), duration_ms: Some(12), + command_summary: None, } ); assert_eq!( @@ -2008,6 +2010,7 @@ mod tests { parsed_cmd: vec![ParsedCommand::Unknown { cmd: "ls".into() }], source: ExecCommandSource::Agent, interaction_input: None, + command_summary: None, stdout: String::new(), stderr: "exec command rejected by user".into(), aggregated_output: "exec command rejected by user".into(), @@ -2056,6 +2059,7 @@ mod tests { aggregated_output: Some("exec command rejected by user".into()), exit_code: Some(-1), duration_ms: Some(0), + command_summary: None, } ); assert_eq!( @@ -2141,6 +2145,7 @@ mod tests { aggregated_output: None, exit_code: None, duration_ms: None, + command_summary: None, } ); } @@ -2200,6 +2205,7 @@ mod tests { aggregated_output: None, exit_code: None, duration_ms: None, + command_summary: None, } ); } @@ -2248,6 +2254,7 @@ mod tests { }], source: ExecCommandSource::Agent, interaction_input: None, + command_summary: None, stdout: "done\n".into(), stderr: String::new(), aggregated_output: "done\n".into(), @@ -2289,6 +2296,7 @@ mod tests { aggregated_output: Some("done\n".into()), exit_code: Some(0), duration_ms: Some(5), + command_summary: None, } ); } @@ -2337,6 +2345,7 @@ mod tests { }], source: ExecCommandSource::Agent, interaction_input: None, + command_summary: None, stdout: "done\n".into(), stderr: String::new(), aggregated_output: "done\n".into(), diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index de96ef2bf2..f217f048de 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -4392,6 +4392,8 @@ pub enum ThreadItem { /// The duration of the command execution in milliseconds. #[ts(type = "number | null")] duration_ms: Option, + /// Optional short prose summaries for non-technical surfaces. + command_summary: Option, }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] @@ -4920,6 +4922,25 @@ pub enum CommandExecutionStatus { Declined, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct CommandSummary { + /// Present-tense prose summary, for example "Pulling most recent code". + pub present: String, + /// Past-tense prose summary, for example "Pulled most recent code". + pub past: String, +} + +impl From for CommandSummary { + fn from(value: codex_protocol::protocol::CommandSummary) -> Self { + Self { + present: value.present, + past: value.past, + } + } +} + impl From for CommandExecutionStatus { fn from(value: CoreExecCommandStatus) -> Self { Self::from(&value) diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 609a792f3d..5b3d09e087 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -948,7 +948,7 @@ Today both notifications carry an empty `items` array even when item events were - `agentMessage` — `{id, text}` containing the accumulated agent reply. - `plan` — `{id, text}` emitted for plan-mode turns; plan text can stream via `item/plan/delta` (experimental). - `reasoning` — `{id, summary, content}` where `summary` holds streamed reasoning summaries (applicable for most OpenAI models) and `content` holds raw reasoning blocks (applicable for e.g. open source models). -- `commandExecution` — `{id, command, cwd, status, commandActions, aggregatedOutput?, exitCode?, durationMs?}` for sandboxed commands; `status` is `inProgress`, `completed`, `failed`, or `declined`. +- `commandExecution` — `{id, command, cwd, status, commandActions, commandSummary?, aggregatedOutput?, exitCode?, durationMs?}` for sandboxed commands; `status` is `inProgress`, `completed`, `failed`, or `declined`. `commandSummary`, when present, contains short `present` and `past` prose labels for non-technical surfaces. - `fileChange` — `{id, changes, status}` describing proposed edits; `changes` list `{path, kind, diff}` and `status` is `inProgress`, `completed`, `failed`, or `declined`. - `mcpToolCall` — `{id, server, tool, status, arguments, result?, error?}` describing MCP calls; `status` is `inProgress`, `completed`, or `failed`. - `collabToolCall` — `{id, tool, status, senderThreadId, receiverThreadId?, newThreadId?, prompt?, agentStatus?}` describing collab tool calls (`spawn_agent`, `send_input`, `resume_agent`, `wait`, `close_agent`); `status` is `inProgress`, `completed`, or `failed`. @@ -987,7 +987,7 @@ There are additional item-specific events: #### commandExecution - `item/commandExecution/outputDelta` — streams stdout/stderr for the command; append deltas in order to render live output alongside `aggregatedOutput` in the final item. - Final `commandExecution` items include parsed `commandActions`, `status`, `exitCode`, and `durationMs` so the UI can summarize what ran and whether it succeeded. + Final `commandExecution` items include parsed `commandActions`, optional `commandSummary`, `status`, `exitCode`, and `durationMs` so the UI can summarize what ran and whether it succeeded. #### fileChange diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index a6a5951be8..fdd2aa08d2 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -1612,6 +1612,7 @@ pub(crate) async fn apply_bespoke_event_handling( aggregated_output: None, exit_code: None, duration_ms: None, + command_summary: exec_command_begin_event.command_summary.map(Into::into), }; let notification = ItemStartedNotification { thread_id: conversation_id.to_string(), @@ -1983,6 +1984,7 @@ async fn start_command_execution_item( aggregated_output: None, exit_code: None, duration_ms: None, + command_summary: None, }, }; outgoing @@ -2027,6 +2029,7 @@ async fn complete_command_execution_item( aggregated_output: None, exit_code: None, duration_ms: None, + command_summary: None, }; let notification = ItemCompletedNotification { thread_id: conversation_id.to_string(), @@ -3209,6 +3212,7 @@ mod tests { aggregated_output: None, exit_code: None, duration_ms: None, + command_summary: None, } ); } diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index d872116a87..d4d761d980 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -153,6 +153,7 @@ pub(crate) async fn execute_user_shell_command( parsed_cmd: parsed_cmd.clone(), source: ExecCommandSource::UserShell, interaction_input: None, + command_summary: None, }), ) .await; @@ -221,6 +222,7 @@ pub(crate) async fn execute_user_shell_command( parsed_cmd: parsed_cmd.clone(), source: ExecCommandSource::UserShell, interaction_input: None, + command_summary: None, stdout: String::new(), stderr: aborted_message.clone(), aggregated_output: aborted_message.clone(), @@ -245,6 +247,7 @@ pub(crate) async fn execute_user_shell_command( parsed_cmd: parsed_cmd.clone(), source: ExecCommandSource::UserShell, interaction_input: None, + command_summary: None, stdout: output.stdout.text.clone(), stderr: output.stderr.text.clone(), aggregated_output: output.aggregated_output.text.clone(), @@ -289,6 +292,7 @@ pub(crate) async fn execute_user_shell_command( parsed_cmd, source: ExecCommandSource::UserShell, interaction_input: None, + command_summary: None, stdout: exec_output.stdout.text.clone(), stderr: exec_output.stderr.text.clone(), aggregated_output: exec_output.aggregated_output.text.clone(), diff --git a/codex-rs/core/src/tools/events.rs b/codex-rs/core/src/tools/events.rs index 58438cf563..03e7525da9 100644 --- a/codex-rs/core/src/tools/events.rs +++ b/codex-rs/core/src/tools/events.rs @@ -7,6 +7,7 @@ use codex_protocol::error::CodexErr; use codex_protocol::error::SandboxErr; use codex_protocol::exec_output::ExecToolCallOutput; use codex_protocol::parse_command::ParsedCommand; +use codex_protocol::protocol::CommandSummary; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ExecCommandBeginEvent; use codex_protocol::protocol::ExecCommandEndEvent; @@ -61,27 +62,20 @@ pub(crate) enum ToolEventFailure { Rejected(String), } -pub(crate) async fn emit_exec_command_begin( - ctx: ToolEventCtx<'_>, - command: &[String], - cwd: &Path, - parsed_cmd: &[ParsedCommand], - source: ExecCommandSource, - interaction_input: Option, - process_id: Option<&str>, -) { +async fn emit_exec_command_begin(ctx: ToolEventCtx<'_>, exec_input: ExecCommandInput<'_>) { ctx.session .send_event( ctx.turn, EventMsg::ExecCommandBegin(ExecCommandBeginEvent { call_id: ctx.call_id.to_string(), - process_id: process_id.map(str::to_owned), + process_id: exec_input.process_id.map(str::to_owned), turn_id: ctx.turn.sub_id.clone(), - command: command.to_vec(), - cwd: cwd.to_path_buf(), - parsed_cmd: parsed_cmd.to_vec(), - source, - interaction_input, + command: exec_input.command.to_vec(), + cwd: exec_input.cwd.to_path_buf(), + parsed_cmd: exec_input.parsed_cmd.to_vec(), + source: exec_input.source, + interaction_input: exec_input.interaction_input.map(str::to_owned), + command_summary: exec_input.command_summary.cloned(), }), ) .await; @@ -105,6 +99,7 @@ pub(crate) enum ToolEmitter { source: ExecCommandSource, parsed_cmd: Vec, process_id: Option, + command_summary: Option, }, } @@ -137,6 +132,7 @@ impl ToolEmitter { cwd: PathBuf, source: ExecCommandSource, process_id: Option, + command_summary: Option, ) -> Self { let parsed_cmd = parse_command(command); Self::UnifiedExec { @@ -145,6 +141,7 @@ impl ToolEmitter { source, parsed_cmd, process_id, + command_summary, } } @@ -169,6 +166,7 @@ impl ToolEmitter { *source, /*interaction_input*/ None, /*process_id*/ None, + /*command_summary*/ None, ), stage, ) @@ -266,6 +264,7 @@ impl ToolEmitter { source, parsed_cmd, process_id, + command_summary, }, stage, ) => { @@ -278,6 +277,7 @@ impl ToolEmitter { *source, /*interaction_input*/ None, process_id.as_deref(), + command_summary.as_ref(), ), stage, ) @@ -370,6 +370,7 @@ struct ExecCommandInput<'a> { source: ExecCommandSource, interaction_input: Option<&'a str>, process_id: Option<&'a str>, + command_summary: Option<&'a CommandSummary>, } impl<'a> ExecCommandInput<'a> { @@ -380,6 +381,7 @@ impl<'a> ExecCommandInput<'a> { source: ExecCommandSource, interaction_input: Option<&'a str>, process_id: Option<&'a str>, + command_summary: Option<&'a CommandSummary>, ) -> Self { Self { command, @@ -388,6 +390,7 @@ impl<'a> ExecCommandInput<'a> { source, interaction_input, process_id, + command_summary, } } } @@ -409,16 +412,7 @@ async fn emit_exec_stage( ) { match stage { ToolEventStage::Begin => { - emit_exec_command_begin( - ctx, - exec_input.command, - exec_input.cwd, - exec_input.parsed_cmd, - exec_input.source, - exec_input.interaction_input.map(str::to_owned), - exec_input.process_id, - ) - .await; + emit_exec_command_begin(ctx, exec_input).await; } ToolEventStage::Success(output) | ToolEventStage::Failure(ToolEventFailure::Output(output)) => { @@ -483,6 +477,7 @@ async fn emit_exec_end( parsed_cmd: exec_input.parsed_cmd.to_vec(), source: exec_input.source, interaction_input: exec_input.interaction_input.map(str::to_owned), + command_summary: exec_input.command_summary.cloned(), stdout: exec_result.stdout, stderr: exec_result.stderr, aggregated_output: exec_result.aggregated_output, diff --git a/codex-rs/core/src/tools/handlers/unified_exec.rs b/codex-rs/core/src/tools/handlers/unified_exec.rs index fc1ad94b4d..eaa302f21a 100644 --- a/codex-rs/core/src/tools/handlers/unified_exec.rs +++ b/codex-rs/core/src/tools/handlers/unified_exec.rs @@ -26,6 +26,7 @@ use codex_features::Feature; use codex_otel::SessionTelemetry; use codex_otel::TOOL_CALL_UNIFIED_EXEC_METRIC; use codex_protocol::models::PermissionProfile; +use codex_protocol::protocol::CommandSummary; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::TerminalInteractionEvent; use codex_shell_command::is_safe_command::is_known_safe_command; @@ -59,6 +60,8 @@ pub(crate) struct ExecCommandArgs { justification: Option, #[serde(default)] prefix_rule: Option>, + #[serde(default, rename = "commandSummary")] + command_summary: Option, } #[derive(Debug, Deserialize)] @@ -217,6 +220,7 @@ impl ToolHandler for UnifiedExecHandler { additional_permissions, justification, prefix_rule, + command_summary, .. } = args; @@ -325,6 +329,7 @@ impl ToolHandler for UnifiedExecHandler { .permissions_preapproved, justification, prefix_rule, + command_summary, }, &context, ) diff --git a/codex-rs/core/src/unified_exec/async_watcher.rs b/codex-rs/core/src/unified_exec/async_watcher.rs index a5da040976..0ac01bbcf2 100644 --- a/codex-rs/core/src/unified_exec/async_watcher.rs +++ b/codex-rs/core/src/unified_exec/async_watcher.rs @@ -19,6 +19,7 @@ use crate::tools::events::ToolEventStage; use crate::unified_exec::head_tail_buffer::HeadTailBuffer; use codex_protocol::exec_output::ExecToolCallOutput; use codex_protocol::exec_output::StreamOutput; +use codex_protocol::protocol::CommandSummary; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ExecCommandOutputDeltaEvent; use codex_protocol::protocol::ExecCommandSource; @@ -114,6 +115,7 @@ pub(crate) fn spawn_exit_watcher( process_id: i32, transcript: Arc>, started_at: Instant, + command_summary: Option, ) { let exit_token = process.cancellation_token(); let output_drained = process.output_drained_notify(); @@ -134,6 +136,7 @@ pub(crate) fn spawn_exit_watcher( transcript, message, duration, + command_summary, ) .await; } else { @@ -149,6 +152,7 @@ pub(crate) fn spawn_exit_watcher( String::new(), exit_code, duration, + command_summary, ) .await; } @@ -202,6 +206,7 @@ pub(crate) async fn emit_exec_end_for_unified_exec( fallback_output: String, exit_code: i32, duration: Duration, + command_summary: Option, ) { let aggregated_output = resolve_aggregated_output(&transcript, fallback_output).await; let output = ExecToolCallOutput { @@ -223,6 +228,7 @@ pub(crate) async fn emit_exec_end_for_unified_exec( cwd, ExecCommandSource::UnifiedExecStartup, process_id, + command_summary, ); emitter .emit(event_ctx, ToolEventStage::Success(output)) @@ -240,6 +246,7 @@ pub(crate) async fn emit_failed_exec_end_for_unified_exec( transcript: Arc>, message: String, duration: Duration, + command_summary: Option, ) { let stdout = resolve_aggregated_output(&transcript, String::new()).await; let aggregated_output = if stdout.is_empty() { @@ -266,6 +273,7 @@ pub(crate) async fn emit_failed_exec_end_for_unified_exec( cwd, ExecCommandSource::UnifiedExecStartup, process_id, + command_summary, ); emitter .emit( diff --git a/codex-rs/core/src/unified_exec/mod.rs b/codex-rs/core/src/unified_exec/mod.rs index 3f3b018df4..ca66734730 100644 --- a/codex-rs/core/src/unified_exec/mod.rs +++ b/codex-rs/core/src/unified_exec/mod.rs @@ -29,6 +29,7 @@ use std::sync::Weak; use codex_network_proxy::NetworkProxy; use codex_protocol::models::PermissionProfile; +use codex_protocol::protocol::CommandSummary; use codex_utils_absolute_path::AbsolutePathBuf; use rand::Rng; use rand::rng; @@ -99,6 +100,7 @@ pub(crate) struct ExecCommandRequest { pub additional_permissions_preapproved: bool, pub justification: Option, pub prefix_rule: Option>, + pub command_summary: Option, } #[derive(Debug)] diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index 9d77ac4e97..1b26ea9b4e 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -192,6 +192,7 @@ impl UnifiedExecProcessManager { cwd.to_path_buf(), ExecCommandSource::UnifiedExecStartup, Some(request.process_id.to_string()), + request.command_summary.clone(), ); emitter.emit(event_ctx, ToolEventStage::Begin).await; @@ -214,6 +215,7 @@ impl UnifiedExecProcessManager { request.tty, network_approval_id, Arc::clone(&transcript), + request.command_summary.clone(), ) .await; } @@ -260,6 +262,7 @@ impl UnifiedExecProcessManager { Arc::clone(&transcript), message.clone(), wall_time, + request.command_summary.clone(), ) .await; } @@ -304,6 +307,7 @@ impl UnifiedExecProcessManager { text.clone(), exit, wall_time, + request.command_summary.clone(), ) .await; @@ -532,6 +536,7 @@ impl UnifiedExecProcessManager { tty: bool, network_approval_id: Option, transcript: Arc>, + command_summary: Option, ) { let entry = ProcessEntry { process: Arc::clone(&process), @@ -576,6 +581,7 @@ impl UnifiedExecProcessManager { process_id, transcript, started_at, + command_summary, ); } diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index f1bbc0a7ff..f218b96085 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -2972,6 +2972,14 @@ pub enum ExecCommandStatus { Declined, } +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] +pub struct CommandSummary { + /// Present-tense prose summary, for example "Pulling most recent code". + pub present: String, + /// Past-tense prose summary, for example "Pulled most recent code". + pub past: String, +} + #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] pub struct ExecCommandBeginEvent { /// Identifier so this can be paired with the ExecCommandEnd event. @@ -2994,6 +3002,10 @@ pub struct ExecCommandBeginEvent { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub interaction_input: Option, + /// Optional short prose summary of the command for non-technical surfaces. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub command_summary: Option, } #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] @@ -3018,6 +3030,10 @@ pub struct ExecCommandEndEvent { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub interaction_input: Option, + /// Optional short prose summary of the command for non-technical surfaces. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub command_summary: Option, /// Captured stdout pub stdout: String, diff --git a/codex-rs/tools/src/local_tool.rs b/codex-rs/tools/src/local_tool.rs index 3e369ab1e3..5083ad82d9 100644 --- a/codex-rs/tools/src/local_tool.rs +++ b/codex-rs/tools/src/local_tool.rs @@ -54,6 +54,33 @@ pub fn create_exec_command_tool(options: CommandToolOptions) -> ToolSpec { "Maximum number of tokens to return. Excess output will be truncated.".to_string(), )), ), + ( + "commandSummary".to_string(), + JsonSchema { + description: Some( + "Optional short prose summaries for non-technical UI: present tense while running and past tense after completion, each 3-10 words.".to_string(), + ), + ..JsonSchema::object( + BTreeMap::from([ + ( + "present".to_string(), + JsonSchema::string(Some( + "Present-tense summary, e.g. \"Pulling most recent code\"." + .to_string(), + )), + ), + ( + "past".to_string(), + JsonSchema::string(Some( + "Past-tense summary, e.g. \"Pulled most recent code\".".to_string(), + )), + ), + ]), + Some(vec!["present".to_string(), "past".to_string()]), + Some(false.into()), + ) + }, + ), ]); if options.allow_login_shell { properties.insert( diff --git a/codex-rs/tui/src/app/app_server_adapter.rs b/codex-rs/tui/src/app/app_server_adapter.rs index 0bd78f353a..d34d71468f 100644 --- a/codex-rs/tui/src/app/app_server_adapter.rs +++ b/codex-rs/tui/src/app/app_server_adapter.rs @@ -897,6 +897,7 @@ fn command_execution_started_event(turn_id: &str, item: &ThreadItem) -> Option Option Option aggregated_output, exit_code, duration_ms, + command_summary, } = item else { return None; @@ -978,6 +986,12 @@ fn command_execution_completed_event(turn_id: &str, item: &ThreadItem) -> Option .collect(), source: source.to_core(), interaction_input: None, + command_summary: command_summary.clone().map(|summary| { + codex_protocol::protocol::CommandSummary { + present: summary.present, + past: summary.past, + } + }), stdout: String::new(), stderr: String::new(), aggregated_output: aggregated_output.clone(), @@ -1167,6 +1181,7 @@ mod tests { aggregated_output: None, exit_code: None, duration_ms: None, + command_summary: None, }; let (_, started_events) = server_notification_thread_events( @@ -1223,6 +1238,7 @@ mod tests { aggregated_output: Some("hello world\n".to_string()), exit_code: Some(0), duration_ms: Some(5), + command_summary: None, }; let (_, completed_events) = server_notification_thread_events( ServerNotification::ItemCompleted(ItemCompletedNotification { @@ -1258,6 +1274,7 @@ mod tests { aggregated_output: None, exit_code: None, duration_ms: None, + command_summary: None, }; let events = @@ -1308,6 +1325,7 @@ mod tests { aggregated_output: Some("hello world\n".to_string()), exit_code: Some(0), duration_ms: Some(5), + command_summary: None, }], status: TurnStatus::Completed, error: None, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 3575799b21..cc7c9e8508 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -6121,6 +6121,7 @@ impl ChatWidget { aggregated_output, exit_code, duration_ms, + command_summary, } => { if matches!( status, @@ -6138,6 +6139,12 @@ impl ChatWidget { .collect(), source: source.to_core(), interaction_input: None, + command_summary: command_summary.map(|summary| { + codex_protocol::protocol::CommandSummary { + present: summary.present, + past: summary.past, + } + }), }); } else { let aggregated_output = aggregated_output.unwrap_or_default(); @@ -6153,6 +6160,12 @@ impl ChatWidget { .collect(), source: source.to_core(), interaction_input: None, + command_summary: command_summary.map(|summary| { + codex_protocol::protocol::CommandSummary { + present: summary.present, + past: summary.past, + } + }), stdout: String::new(), stderr: String::new(), aggregated_output: aggregated_output.clone(), @@ -6712,6 +6725,7 @@ impl ChatWidget { .collect(), source: source.to_core(), interaction_input: None, + command_summary: None, }); } ThreadItem::FileChange { id, changes, .. } => { diff --git a/codex-rs/tui/src/chatwidget/tests/app_server.rs b/codex-rs/tui/src/chatwidget/tests/app_server.rs index 09bc9a7013..cf4c8b623a 100644 --- a/codex-rs/tui/src/chatwidget/tests/app_server.rs +++ b/codex-rs/tui/src/chatwidget/tests/app_server.rs @@ -201,6 +201,7 @@ async fn live_app_server_command_execution_strips_shell_wrapper() { aggregated_output: None, exit_code: None, duration_ms: None, + command_summary: None, }, }), /*replay_kind*/ None, @@ -222,6 +223,7 @@ async fn live_app_server_command_execution_strips_shell_wrapper() { aggregated_output: Some("Hello, world!\n".to_string()), exit_code: Some(0), duration_ms: Some(5), + command_summary: None, }, }), /*replay_kind*/ None, diff --git a/codex-rs/tui/src/chatwidget/tests/exec_flow.rs b/codex-rs/tui/src/chatwidget/tests/exec_flow.rs index d271fc0076..432a091cc9 100644 --- a/codex-rs/tui/src/chatwidget/tests/exec_flow.rs +++ b/codex-rs/tui/src/chatwidget/tests/exec_flow.rs @@ -367,6 +367,7 @@ async fn exec_end_without_begin_uses_event_command() { parsed_cmd, source: ExecCommandSource::Agent, interaction_input: None, + command_summary: None, stdout: "done".to_string(), stderr: String::new(), aggregated_output: "done".to_string(), diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs index 0a7deee589..d108cdc454 100644 --- a/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -489,6 +489,7 @@ pub(super) fn begin_exec_with_source( parsed_cmd, source, interaction_input, + command_summary: None, }; chat.handle_codex_event(Event { id: call_id.to_string(), @@ -514,6 +515,7 @@ pub(super) fn begin_unified_exec_startup( parsed_cmd: Vec::new(), source: ExecCommandSource::UnifiedExecStartup, interaction_input: None, + command_summary: None, }; chat.handle_codex_event(Event { id: call_id.to_string(), @@ -629,6 +631,7 @@ pub(super) fn end_exec( source, interaction_input, process_id, + command_summary, } = begin_event; chat.handle_codex_event(Event { id: call_id.clone(), @@ -641,6 +644,7 @@ pub(super) fn end_exec( parsed_cmd, source, interaction_input, + command_summary, stdout: stdout.to_string(), stderr: stderr.to_string(), aggregated_output: aggregated.clone(), diff --git a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs index b04ce74a14..c673381867 100644 --- a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs +++ b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs @@ -1462,6 +1462,7 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() { parsed_cmd: parsed_cmd.clone(), source: ExecCommandSource::Agent, interaction_input: None, + command_summary: None, }), }); chat.handle_codex_event(Event { @@ -1475,6 +1476,7 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() { parsed_cmd, source: ExecCommandSource::Agent, interaction_input: None, + command_summary: None, stdout: String::new(), stderr: String::new(), aggregated_output: String::new(),