diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index e2865aa318..9774408594 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -570,6 +570,18 @@ client_request_definitions! { serialization: None, response: v2::ThreadTurnsListResponse, }, + ThreadItemsList => "thread/items/list" { + params: v2::ThreadItemsListParams, + // Explicitly concurrent: this primarily reads append-only rollout storage. + serialization: None, + response: v2::ThreadItemsListResponse, + }, + ThreadItemContentRead => "thread/item/content/read" { + params: v2::ThreadItemContentReadParams, + // Explicitly concurrent: this reads deferred item content without mutating the thread. + serialization: None, + response: v2::ThreadItemContentReadResponse, + }, /// Append raw Responses API items to the thread history without starting a user turn. ThreadInjectItems => "thread/inject_items" { params: v2::ThreadInjectItemsParams, 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 019c9fa83e..4ac7d083ac 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -10,6 +10,7 @@ use crate::protocol::v2::CollabAgentToolCallStatus; use crate::protocol::v2::CommandExecutionStatus; use crate::protocol::v2::DynamicToolCallOutputContentItem; use crate::protocol::v2::DynamicToolCallStatus; +use crate::protocol::v2::ImageGenerationContent; use crate::protocol::v2::McpToolCallError; use crate::protocol::v2::McpToolCallResult; use crate::protocol::v2::McpToolCallStatus; @@ -17,6 +18,7 @@ use crate::protocol::v2::ThreadItem; use crate::protocol::v2::Turn; use crate::protocol::v2::TurnError as V2TurnError; use crate::protocol::v2::TurnError; +use crate::protocol::v2::TurnItemsView; use crate::protocol::v2::TurnStatus; use crate::protocol::v2::UserInput; use crate::protocol::v2::WebSearchAction; @@ -580,6 +582,9 @@ impl ThreadHistoryBuilder { id: payload.call_id.clone(), status: String::new(), revised_prompt: None, + content: ImageGenerationContent::Unavailable { + reason: "image generation has not completed".to_string(), + }, result: String::new(), saved_path: None, }; @@ -591,6 +596,11 @@ impl ThreadHistoryBuilder { id: payload.call_id.clone(), status: payload.status.clone(), revised_prompt: payload.revised_prompt.clone(), + content: ImageGenerationContent::Inline { + data_base64: payload.result.clone(), + mime_type: None, + byte_length: None, + }, result: payload.result.clone(), saved_path: payload.saved_path.clone(), }; @@ -1160,6 +1170,7 @@ impl From for Turn { fn from(value: PendingTurn) -> Self { Self { id: value.id, + items_view: TurnItemsView::Full, items: value.items, error: value.error, status: value.status, @@ -1174,6 +1185,7 @@ impl From<&PendingTurn> for Turn { fn from(value: &PendingTurn) -> Self { Self { id: value.id.clone(), + items_view: TurnItemsView::Full, items: value.items.clone(), error: value.error.clone(), status: value.status.clone(), @@ -1442,6 +1454,7 @@ mod tests { turns[0], Turn { id: "turn-image".into(), + items_view: TurnItemsView::Full, status: TurnStatus::Completed, error: None, started_at: None, @@ -1459,6 +1472,11 @@ mod tests { id: "ig_123".into(), status: "completed".into(), revised_prompt: Some("final prompt".into()), + content: ImageGenerationContent::Inline { + data_base64: "Zm9v".into(), + mime_type: None, + byte_length: None, + }, result: "Zm9v".into(), saved_path: Some(test_path_buf("/tmp/ig_123.png").abs()), }, @@ -2706,6 +2724,7 @@ mod tests { turns, vec![Turn { id: "turn-compact".into(), + items_view: TurnItemsView::Full, status: TurnStatus::Completed, error: None, started_at: None, @@ -2961,6 +2980,7 @@ mod tests { turns[0], Turn { id: "turn-a".into(), + items_view: TurnItemsView::Full, status: TurnStatus::Completed, error: None, started_at: None, diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 068a9c5f5e..e9924405e1 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -3735,9 +3735,19 @@ pub struct ThreadResumeParams { pub developer_instructions: Option, #[ts(optional = nullable)] pub personality: Option, - /// When true, return only thread metadata and live-resume state without - /// populating `thread.turns`. This is useful when the client plans to call - /// `thread/turns/list` immediately after resuming. + /// Controls whether large item payloads, such as generated image bytes, are embedded in + /// returned ThreadItems or replaced with deferred content references. + /// + /// Defaults to `inline` for compatibility. New clients that may load large histories should + /// prefer `deferred` and fetch bytes on demand with `thread/item/content/read`. + #[ts(optional = nullable)] + pub large_content: Option, + /// When false, `thread/resume` returns the traditional fully hydrated thread history: + /// `thread.turns` is populated and each returned Turn has `itemsView: "full"`. + /// + /// When true, return only thread metadata and live-resume state without populating + /// `thread.turns`. Clients that choose this scalable mode should page history with + /// `thread/turns/list`, then hydrate individual summary turns with `thread/items/list`. #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub exclude_turns: bool, /// If true, persist additional rollout EventMsg variants required to @@ -3837,6 +3847,13 @@ pub struct ThreadForkParams { pub base_instructions: Option, #[ts(optional = nullable)] pub developer_instructions: Option, + /// Controls whether large item payloads, such as generated image bytes, are embedded in + /// returned ThreadItems or replaced with deferred content references. + /// + /// Defaults to `inline` for compatibility. New clients that may load large histories should + /// prefer `deferred` and fetch bytes on demand with `thread/item/content/read`. + #[ts(optional = nullable)] + pub large_content: Option, #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub ephemeral: bool, /// When true, return only thread metadata and live fork state without @@ -4408,7 +4425,18 @@ pub enum ThreadActiveFlag { #[ts(export_to = "v2/")] pub struct ThreadReadParams { pub thread_id: String, - /// When true, include turns and their items from rollout history. + /// Controls whether large item payloads, such as generated image bytes, are embedded in + /// returned ThreadItems or replaced with deferred content references. + /// + /// Defaults to `inline` for compatibility. New clients that may load large histories should + /// prefer `deferred` and fetch bytes on demand with `thread/item/content/read`. + #[ts(optional = nullable)] + pub large_content: Option, + /// When true, include the traditional fully hydrated thread history: + /// `thread.turns` is populated and each returned Turn has `itemsView: "full"`. + /// + /// When false, `thread/read` returns metadata only. Clients can page history separately + /// with `thread/turns/list`, then hydrate individual summary turns with `thread/items/list`. #[serde(default)] pub include_turns: bool, } @@ -4425,6 +4453,12 @@ pub struct ThreadReadResponse { #[ts(export_to = "v2/")] pub struct ThreadTurnsListParams { pub thread_id: String, + /// Controls whether large item payloads in summary Turn items are embedded inline or returned + /// as deferred content references. + /// + /// Defaults to `inline` for compatibility. New clients should prefer `deferred`. + #[ts(optional = nullable)] + pub large_content: Option, /// Opaque cursor to pass to the next call to continue after the last turn. #[ts(optional = nullable)] pub cursor: Option, @@ -4440,6 +4474,11 @@ pub struct ThreadTurnsListParams { #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct ThreadTurnsListResponse { + /// Summary turns for paged history UIs. + /// + /// Each Turn returned by `thread/turns/list` has `itemsView: "summary"` and includes only + /// the first user message and final assistant message in `items` when those items are + /// available. Use `thread/items/list` to hydrate the complete item list for a turn. pub data: Vec, /// Opaque cursor to pass to the next call to continue after the last turn. /// if None, there are no more turns to return. @@ -4451,6 +4490,82 @@ pub struct ThreadTurnsListResponse { pub backwards_cursor: Option, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadItemsListParams { + pub thread_id: String, + /// Turn to hydrate after receiving a summary Turn from `thread/turns/list`. + pub turn_id: String, + /// Controls whether large item payloads, such as generated image bytes, are embedded in + /// returned ThreadItems or replaced with deferred content references. + /// + /// Defaults to `inline` for compatibility. New clients should prefer `deferred`. + #[ts(optional = nullable)] + pub large_content: Option, + /// Opaque cursor to pass to the next call to continue after the last item. + #[ts(optional = nullable)] + pub cursor: Option, + /// Optional item page size. + #[ts(optional = nullable)] + pub limit: Option, + /// Optional item pagination direction; defaults to ascending. + #[ts(optional = nullable)] + pub sort_direction: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadItemsListResponse { + /// Full ThreadItems for the requested turn page. Accumulate pages until `nextCursor` is null + /// to hydrate the client-side Turn representation. + pub data: Vec, + /// Opaque cursor to pass to the next call to continue after the last item. + /// if None, there are no more items to return. + pub next_cursor: Option, + /// Opaque cursor to pass as `cursor` when reversing `sortDirection`. + /// This is only populated when the page contains at least one item. + /// Use it with the opposite `sortDirection` to include the anchor item again + /// and catch updates to that item. + pub backwards_cursor: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadItemContentReadParams { + pub thread_id: String, + pub turn_id: String, + pub item_id: String, + /// Opaque content identifier returned from a deferred item content placeholder. + pub content_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadItemContentReadResponse { + /// Media type of the returned content when known. + pub mime_type: Option, + /// Base64-encoded content bytes. + pub data_base64: String, + /// Decoded byte length when known. + #[ts(type = "number | null")] + pub byte_length: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase", export_to = "v2/")] +pub enum LargeContentMode { + /// Preserve legacy behavior and embed large payloads directly in returned ThreadItems. + Inline, + /// Return metadata placeholders for large payloads and fetch bytes through + /// `thread/item/content/read`. + Deferred, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -5146,10 +5261,10 @@ impl From for TokenUsageBreakdown { #[ts(export_to = "v2/")] pub struct Turn { pub id: String, - /// Only populated on a `thread/resume` or `thread/fork` response. - /// For all other responses and notifications returning a Turn, - /// the items field will be an empty list. + /// Turn items at the level of detail described by `itemsView`. pub items: Vec, + /// Indicates how much of this Turn's item history is present in `items`. + pub items_view: TurnItemsView, pub status: TurnStatus, /// Only populated when the Turn's status is failed. pub error: Option, @@ -5164,6 +5279,22 @@ pub struct Turn { pub duration_ms: Option, } +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase", export_to = "v2/")] +pub enum TurnItemsView { + /// `items` was not loaded for this Turn. The field is intentionally empty. + NotLoaded, + /// `items` contains only a display summary for this Turn. + /// + /// For `thread/turns/list`, this currently means the first user message and the final + /// assistant message when those items are available. + Summary, + /// `items` contains every ThreadItem available from persisted app-server history for this + /// Turn. + Full, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -5948,6 +6079,14 @@ pub enum ThreadItem { id: String, status: String, revised_prompt: Option, + /// Structured image content metadata. New clients should render from this field instead + /// of reading the legacy `result` field directly. + content: ImageGenerationContent, + /// Legacy base64-encoded image result. + /// + /// This is populated in `largeContent: "inline"` mode for compatibility. In + /// `largeContent: "deferred"` mode, clients should expect this to be empty and use + /// `content` plus `thread/item/content/read` instead. result: String, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] @@ -5964,6 +6103,36 @@ pub enum ThreadItem { ContextCompaction { id: String }, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(tag = "type", export_to = "v2/")] +pub enum ImageGenerationContent { + /// Image bytes are embedded directly in the item. + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + Inline { + data_base64: String, + mime_type: Option, + #[ts(type = "number | null")] + byte_length: Option, + }, + /// Image bytes are available through `thread/item/content/read`. + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + Deferred { + content_id: String, + mime_type: Option, + #[ts(type = "number | null")] + byte_length: Option, + width: Option, + height: Option, + }, + /// Image content is not available yet, or is no longer available. + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + Unavailable { reason: String }, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase", export_to = "v2/")] @@ -6421,6 +6590,11 @@ impl From for ThreadItem { id: image.id, status: image.status, revised_prompt: image.revised_prompt, + content: ImageGenerationContent::Inline { + data_base64: image.result.clone(), + mime_type: None, + byte_length: None, + }, result: image.result, saved_path: image.saved_path, }, diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index b986a8545c..0c8ec299d5 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -149,7 +149,9 @@ Example with notification opt-out: - `thread/list` — page through stored rollouts; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `cwd`, and `searchTerm` filters. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. - `thread/loaded/list` — list the thread ids currently loaded in memory. - `thread/read` — read a stored thread by id without resuming it; optionally include turns via `includeTurns`. The returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. -- `thread/turns/list` — page through a stored thread’s turn history without resuming it; supports cursor-based pagination with `sortDirection`, `nextCursor`, and `backwardsCursor`. +- `thread/turns/list` — page through a stored thread’s turn history without resuming it; supports cursor-based pagination with `sortDirection`, `nextCursor`, and `backwardsCursor`. Returned turns use `itemsView: "summary"` and include only the first user message and final assistant message in `items` when available. +- `thread/items/list` — page through the full item history for a single turn without resuming the thread; supports cursor-based pagination with `sortDirection`, `nextCursor`, and `backwardsCursor`. +- `thread/item/content/read` — fetch large deferred item content, such as generated image bytes, after a history-loading API returns a placeholder. - `thread/metadata/update` — patch stored thread metadata in sqlite; currently supports updating persisted `gitInfo` fields and returns the refreshed `thread`. - `thread/memoryMode/set` — experimental; set a thread’s persisted memory eligibility to `"enabled"` or `"disabled"` for either a loaded thread or a stored rollout; returns `{}` on success. - `memory/reset` — experimental; clear the current `CODEX_HOME/memories` directory and reset persisted memory stage data in sqlite while preserving existing thread memory modes; returns `{}` on success. @@ -276,7 +278,14 @@ Valid `personality` values are `"friendly"`, `"pragmatic"`, and `"none"`. When ` To continue a stored session, call `thread/resume` with the `thread.id` you previously recorded. The response shape matches `thread/start`. When the stored session includes persisted token usage, the server emits `thread/tokenUsage/updated` immediately after the response so clients can render restored usage before the next turn starts. You can also pass the same configuration overrides supported by `thread/start`, including `approvalsReviewer`. -By default, `thread/resume` includes the reconstructed turn history in `thread.turns`. Pass `excludeTurns: true` to return only thread metadata and live resume state, then call `thread/turns/list` separately if you want to page the turn history over the network. In that mode the server also skips replaying restored `thread/tokenUsage/updated`, which avoids rebuilding turns just to attribute historical usage. +App-server supports two history loading modes: + +- Fully hydrated history: the default for `thread/resume`, and the behavior for `thread/read` when `includeTurns: true`. The response includes `thread.turns`; each returned turn has `itemsView: "full"` and `items` contains every `ThreadItem` available from persisted app-server history. +- Paged history: pass `excludeTurns: true` to `thread/resume`, or omit `includeTurns` on `thread/read`, to receive only thread metadata. Then call `thread/turns/list` for summary turns and `thread/items/list` to hydrate the full item list for an individual turn. + +Both modes can defer large item payloads. Pass `largeContent: "deferred"` when loading history to receive metadata placeholders for large content, such as generated images, and fetch bytes later with `thread/item/content/read`. Omit `largeContent`, or pass `"inline"`, to preserve the legacy behavior of embedding large payloads directly in returned `ThreadItem`s. + +In paged history mode, `thread/resume` skips replaying restored `thread/tokenUsage/updated`, which avoids rebuilding turns just to attribute historical usage. By default, resume uses the latest persisted `model` and `reasoningEffort` values associated with the thread. Supplying any of `model`, `modelProvider`, `config.model`, or `config.model_reasoning_effort` disables that persisted fallback and uses the explicit overrides plus normal config resolution instead. @@ -402,7 +411,7 @@ Later, after the idle unload timeout: ### Example: Read a thread -Use `thread/read` to fetch a stored thread by id without resuming it. Pass `includeTurns` when you want the full rollout history loaded into `thread.turns`. The returned thread includes `agentNickname` and `agentRole` for AgentControl-spawned thread sub-agents when available. +Use `thread/read` to fetch a stored thread by id without resuming it. By default, this returns metadata only so clients can page history with `thread/turns/list` and `thread/items/list`. Pass `includeTurns` when you want the fully hydrated rollout history loaded into `thread.turns`; returned turns use `itemsView: "full"`. The returned thread includes `agentNickname` and `agentRole` for AgentControl-spawned thread sub-agents when available. ```json { "method": "thread/read", "id": 22, "params": { "threadId": "thr_123" } } @@ -420,7 +429,7 @@ Use `thread/read` to fetch a stored thread by id without resuming it. Pass `incl ### Example: List thread turns -Use `thread/turns/list` to page a stored thread’s turn history without resuming it. By default, results are sorted descending so clients can start at the present and fetch older turns with `nextCursor`. The response also includes `backwardsCursor`; pass it as `cursor` on a later request with `sortDirection: "asc"` to fetch turns newer than the first item from the earlier page. +Use `thread/turns/list` to page a stored thread’s turn history without resuming it. This endpoint is the scalable alternative to loading fully hydrated `thread.turns` in `thread/resume` or `thread/read`. By default, results are sorted descending so clients can start at the present and fetch older turns with `nextCursor`. The response also includes `backwardsCursor`; pass it as `cursor` on a later request with `sortDirection: "asc"` to fetch turns newer than the first item from the earlier page. Returned turns use `itemsView: "summary"`; their `items` field contains only the first user message and final assistant message when those items are available. ```json { "method": "thread/turns/list", "id": 24, "params": { @@ -435,6 +444,42 @@ Use `thread/turns/list` to page a stored thread’s turn history without resumin } } ``` +### Example: List turn items + +Use `thread/items/list` to hydrate a summarized turn returned from `thread/turns/list`. By default, results are sorted ascending so clients can append items in natural turn order. Accumulate pages until `nextCursor` is null, then replace the client-side turn’s `items` with the collected item list and treat that local turn representation as fully hydrated. Pass `largeContent: "deferred"` to keep large item payloads as placeholders even while hydrating the item list. The response also includes `backwardsCursor`; pass it as `cursor` on a later request with `sortDirection: "desc"` to fetch items before the first item from the earlier page. + +```json +{ "method": "thread/items/list", "id": 25, "params": { + "threadId": "thr_123", + "turnId": "turn_456", + "limit": 50, + "sortDirection": "asc" +} } +{ "id": 25, "result": { + "data": [ ... ], + "nextCursor": "newer-items-cursor-or-null", + "backwardsCursor": "older-items-cursor-or-null" +} } +``` + +### Example: Read deferred item content + +Some `ThreadItem`s can contain large payloads. For example, `imageGeneration` items expose structured `content`; when `content.type` is `"deferred"`, render a placeholder from the available metadata and call `thread/item/content/read` when the bytes are needed: + +```json +{ "method": "thread/item/content/read", "id": 26, "params": { + "threadId": "thr_123", + "turnId": "turn_456", + "itemId": "ig_789", + "contentId": "img_abc" +} } +{ "id": 26, "result": { + "mimeType": "image/png", + "dataBase64": "...", + "byteLength": 1234567 +} } +``` + ### Example: Update stored thread metadata Use `thread/metadata/update` to patch sqlite-backed metadata for a thread without resuming it. Today this supports persisted `gitInfo`; omitted fields are left unchanged, while explicit `null` clears a stored value. diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index e8a6cb9cc0..4d54839242 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -77,6 +77,7 @@ use codex_app_server_protocol::TurnCompletedNotification; use codex_app_server_protocol::TurnDiffUpdatedNotification; use codex_app_server_protocol::TurnError; use codex_app_server_protocol::TurnInterruptResponse; +use codex_app_server_protocol::TurnItemsView; use codex_app_server_protocol::TurnPlanStep; use codex_app_server_protocol::TurnPlanUpdatedNotification; use codex_app_server_protocol::TurnStartedNotification; @@ -167,6 +168,7 @@ pub(crate) async fn apply_bespoke_event_handling( let state = thread_state.lock().await; state.active_turn_snapshot().unwrap_or_else(|| Turn { id: payload.turn_id.clone(), + items_view: TurnItemsView::NotLoaded, items: Vec::new(), error: None, status: TurnStatus::InProgress, @@ -1408,6 +1410,7 @@ async fn emit_turn_completed_with_status( thread_id: conversation_id.to_string(), turn: Turn { id: event_turn_id, + items_view: TurnItemsView::NotLoaded, items: vec![], error: turn_completion_metadata.error, status: turn_completion_metadata.status, diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 62d250dc0e..ac15495c99 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -225,6 +225,7 @@ use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnError; use codex_app_server_protocol::TurnInterruptParams; use codex_app_server_protocol::TurnInterruptResponse; +use codex_app_server_protocol::TurnItemsView; use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::TurnStatus; @@ -6777,6 +6778,7 @@ impl CodexMessageProcessor { .await; let turn = Turn { id: turn_id, + items_view: TurnItemsView::NotLoaded, items: vec![], error: None, status: TurnStatus::InProgress, @@ -7154,6 +7156,7 @@ impl CodexMessageProcessor { Turn { id: turn_id, + items_view: TurnItemsView::Summary, items, error: None, status: TurnStatus::InProgress, @@ -9901,6 +9904,24 @@ fn reconstruct_thread_turns_for_turns_list( if let Some(active_turn) = active_turn { merge_turn_history_with_active_turn(&mut turns, active_turn); } + for turn in &mut turns { + let first_user_item_index = turn + .items + .iter() + .position(|item| matches!(item, ThreadItem::UserMessage { .. })); + let final_agent_item_index = turn + .items + .iter() + .rposition(|item| matches!(item, ThreadItem::AgentMessage { .. })); + let mut item_index = 0; + turn.items.retain(|_| { + let keep = Some(item_index) == first_user_item_index + || Some(item_index) == final_agent_item_index; + item_index += 1; + keep + }); + turn.items_view = TurnItemsView::Summary; + } turns } @@ -10074,6 +10095,7 @@ mod tests { ))]; let active_turn = Turn { id: "live-turn".to_string(), + items_view: TurnItemsView::Full, items: vec![ThreadItem::UserMessage { id: "live-user-message".to_string(), content: vec![V2UserInput::Text { @@ -10095,7 +10117,9 @@ mod tests { Some(active_turn.clone()), ); - assert_eq!(turns.last(), Some(&active_turn)); + let mut expected_turn = active_turn; + expected_turn.items_view = TurnItemsView::Summary; + assert_eq!(turns.last(), Some(&expected_turn)); } #[test] diff --git a/codex-rs/app-server/src/in_process.rs b/codex-rs/app-server/src/in_process.rs index 0f7a31d6cb..57c6e1d917 100644 --- a/codex-rs/app-server/src/in_process.rs +++ b/codex-rs/app-server/src/in_process.rs @@ -735,6 +735,7 @@ mod tests { use codex_app_server_protocol::ThreadStartResponse; use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnCompletedNotification; + use codex_app_server_protocol::TurnItemsView; use codex_app_server_protocol::TurnStatus; use codex_core::config::ConfigBuilder; use pretty_assertions::assert_eq; @@ -946,6 +947,7 @@ mod tests { thread_id: "thread-1".to_string(), turn: Turn { id: "turn-1".to_string(), + items_view: TurnItemsView::NotLoaded, items: Vec::new(), status: TurnStatus::Completed, error: None,