mirror of
https://github.com/openai/codex.git
synced 2026-04-30 19:32:04 +03:00
fix(tui): conditionally restore status indicator using message phase (#10947)
TLDR: use new message phase field emitted by preamble-supported models to determine whether an AgentMessage is mid-turn commentary. if so, restore the status indicator afterwards to indicate the turn has not completed. ### Problem `commit_tick` hides the status indicator while streaming assistant text. For preamble-capable models, that text can be commentary mid-turn, so hiding was correct during streaming but restore timing mattered: - restoring too aggressively caused jitter/flashing - not restoring caused indicator to stay hidden before subsequent work (tool calls, web search, etc.) ### Fix - Add optional `phase` to `AgentMessageItem` and propagate it from `ResponseItem::Message` - Keep indicator hidden during streamed commit ticks, restore only when: - assistant item completes as `phase=commentary`, and - stream queues are idle + task is still running. - Treat `phase=None` as final-answer behavior (no restore) to keep existing behavior for non-preamble models ### Tests Add/update tests for: - no idle-tick restore without commentary completion - commentary completion restoring status before tool begin - snapshot coverage for preamble/status behavior --------- Co-authored-by: Josh McKinney <joshka@openai.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
use crate::models::MessagePhase;
|
||||
use crate::models::WebSearchAction;
|
||||
use crate::protocol::AgentMessageEvent;
|
||||
use crate::protocol::AgentReasoningEvent;
|
||||
@@ -40,9 +41,21 @@ pub enum AgentMessageContent {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
|
||||
/// Assistant-authored message payload used in turn-item streams.
|
||||
///
|
||||
/// `phase` is optional because not all providers/models emit it. Consumers
|
||||
/// should use it when present, but retain legacy completion semantics when it
|
||||
/// is `None`.
|
||||
pub struct AgentMessageItem {
|
||||
pub id: String,
|
||||
pub content: Vec<AgentMessageContent>,
|
||||
/// Optional phase metadata carried through from `ResponseItem::Message`.
|
||||
///
|
||||
/// This is currently used by TUI rendering to distinguish mid-turn
|
||||
/// commentary from a final answer and avoid status-indicator jitter.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub phase: Option<MessagePhase>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
|
||||
@@ -172,10 +185,13 @@ impl AgentMessageItem {
|
||||
Self {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
content: content.to_vec(),
|
||||
phase: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_legacy_events(&self) -> Vec<EventMsg> {
|
||||
// Legacy events only preserve visible assistant text; `phase` has no
|
||||
// representation in the v1 event stream.
|
||||
self.content
|
||||
.iter()
|
||||
.map(|c| match c {
|
||||
|
||||
@@ -74,8 +74,17 @@ pub enum ContentItem {
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
/// Classifies an assistant message as interim commentary or final answer text.
|
||||
///
|
||||
/// Providers do not emit this consistently, so callers must treat `None` as
|
||||
/// "phase unknown" and keep compatibility behavior for legacy models.
|
||||
pub enum MessagePhase {
|
||||
/// Mid-turn assistant text (for example preamble/progress narration).
|
||||
///
|
||||
/// Additional tool calls or assistant output may follow before turn
|
||||
/// completion.
|
||||
Commentary,
|
||||
/// The assistant's terminal answer text for the current turn.
|
||||
FinalAnswer,
|
||||
}
|
||||
|
||||
@@ -93,7 +102,8 @@ pub enum ResponseItem {
|
||||
#[ts(optional)]
|
||||
end_turn: Option<bool>,
|
||||
// Optional output-message phase (for example: "commentary", "final_answer").
|
||||
// Do not use directly; availability can vary by provider and model.
|
||||
// Availability varies by provider/model, so downstream consumers must
|
||||
// preserve fallback behavior when this is absent.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
phase: Option<MessagePhase>,
|
||||
|
||||
Reference in New Issue
Block a user