fix(tui/history): demote earlier Answer outputs mid-turn

Add `mid_turn` to assistant history records and cells, and retroactively mark previous Answer streams as mid‑turn when a new Answer id arrives so only the final output gets gutter/bold styling.

Also upgrade legacy `gpt-5-codex` slugs to `gpt-5.1-codex`, bump GPT‑5 profile/review defaults to `gpt-5.2`, and refresh related TUI snapshots.
This commit is contained in:
James Peter
2025-12-12 12:29:43 +10:00
parent d9f886a069
commit fe40d37adc
11 changed files with 131 additions and 20 deletions
+12 -8
View File
@@ -2333,6 +2333,14 @@ fn upgrade_legacy_model_slug(slug: &str) -> Option<String> {
return None;
}
if let Some(rest) = slug.strip_prefix("test-gpt-5-codex") {
return Some(format!("test-gpt-5.1-codex{rest}"));
}
if let Some(rest) = slug.strip_prefix("gpt-5-codex") {
return Some(format!("gpt-5.1-codex{rest}"));
}
// Upgrade Anthropic Opus 4.1 to 4.5
if slug.eq_ignore_ascii_case("claude-opus-4.1") {
return Some("claude-opus-4.5".to_string());
@@ -2344,11 +2352,7 @@ fn upgrade_legacy_model_slug(slug: &str) -> Option<String> {
}
// Keep codex variants on their existing 5.1 line; there is no 5.2 codex.
if slug.starts_with("gpt-5.1-codex")
|| slug.starts_with("test-gpt-5.1-codex")
|| slug.starts_with("gpt-5-codex")
|| slug.starts_with("test-gpt-5-codex")
{
if slug.starts_with("gpt-5.1-codex") || slug.starts_with("test-gpt-5.1-codex") {
return None;
}
@@ -3934,14 +3938,14 @@ model_verbosity = "high"
gpt5_profile_overrides,
fixture.code_home(),
)?;
assert_eq!("gpt-5.1", gpt5_profile_config.model);
assert_eq!("gpt-5.2", gpt5_profile_config.model);
assert_eq!(OPENAI_DEFAULT_REVIEW_MODEL, gpt5_profile_config.review_model);
assert_eq!(
ReasoningEffort::High,
gpt5_profile_config.review_model_reasoning_effort
);
assert_eq!(
find_family_for_model("gpt-5.1").expect("known model slug"),
find_family_for_model("gpt-5.2").expect("known model slug"),
gpt5_profile_config.model_family
);
assert!(gpt5_profile_config.model_context_window.is_some());
@@ -4135,7 +4139,7 @@ model_verbosity = "high"
upgrade_legacy_model_slugs(&mut cfg);
assert_eq!(cfg.model.as_deref(), Some("gpt-5.1-codex"));
assert_eq!(cfg.review_model.as_deref(), Some("gpt-5.1"));
assert_eq!(cfg.review_model.as_deref(), Some("gpt-5.2"));
}
#[test]
+9
View File
@@ -849,6 +849,11 @@ pub struct AssistantMessageState {
pub citations: Vec<String>,
pub metadata: Option<MessageMetadata>,
pub token_usage: Option<TokenUsage>,
/// True when this assistant output was emitted before the final Answer
/// within the same turn (e.g., mid‑turn progress update). Used by the TUI
/// to suppress gutter/bold styling.
#[serde(default)]
pub mid_turn: bool,
pub created_at: SystemTime,
}
@@ -1494,6 +1499,7 @@ impl HistoryState {
citations,
metadata,
token_usage,
mid_turn: false,
created_at: SystemTime::now(),
};
let id = self.next_history_id();
@@ -2536,6 +2542,7 @@ mod tests {
citations: Vec::new(),
metadata: None,
token_usage: None,
mid_turn: false,
created_at: SystemTime::UNIX_EPOCH,
});
@@ -2584,6 +2591,7 @@ mod tests {
citations: Vec::new(),
metadata: None,
token_usage: None,
mid_turn: false,
created_at: SystemTime::UNIX_EPOCH,
});
@@ -2995,6 +3003,7 @@ mod tests {
citations: vec!["cite".into()],
metadata: Some(metadata.clone()),
token_usage: metadata.token_usage.clone(),
mid_turn: false,
created_at: now,
}));
+2
View File
@@ -1,3 +1,5 @@
#![allow(dead_code)]
use std::path::Path;
use std::sync::{Arc, Mutex};
+74
View File
@@ -1605,6 +1605,11 @@ pub(crate) struct ChatWidget<'a> {
// at once into scrollback so the history contains a single message.
// Cache of the last finalized assistant message to suppress immediate duplicates
last_assistant_message: Option<String>,
// Track the most recent finalized Answer output item within the current turn.
// When a new Answer stream id arrives, we retroactively mark the previous
// assistant message as a mid-turn update for styling.
last_answer_stream_id_in_turn: Option<String>,
last_answer_history_id_in_turn: Option<HistoryId>,
// Cache of the last user text we submitted (for context passing to review/resolve agents)
last_user_message: Option<String>,
// Cache of the last developer/system note we injected (hidden messages)
@@ -3791,6 +3796,7 @@ impl ChatWidget<'_> {
citations: Vec::new(),
metadata: None,
token_usage: None,
mid_turn: false,
created_at: SystemTime::now(),
};
let greeting_cell =
@@ -5839,6 +5845,8 @@ impl ChatWidget<'_> {
rate_limit_secondary_next_reset_at: None,
content_buffer: String::new(),
last_assistant_message: None,
last_answer_stream_id_in_turn: None,
last_answer_history_id_in_turn: None,
last_user_message: None,
last_developer_message: None,
pending_turn_origin: None,
@@ -6173,6 +6181,8 @@ impl ChatWidget<'_> {
rate_limit_secondary_next_reset_at: None,
content_buffer: String::new(),
last_assistant_message: None,
last_answer_stream_id_in_turn: None,
last_answer_history_id_in_turn: None,
last_user_message: None,
last_developer_message: None,
pending_turn_origin: None,
@@ -12184,6 +12194,9 @@ impl ChatWidget<'_> {
self.ensure_lingering_execs_cleared();
self.stream_state.seq_answer_final = Some(event.event_seq);
if !id.trim().is_empty() {
self.maybe_mark_previous_answer_mid_turn(&id);
}
// Strict order for the stream id
let ok = match event.order.as_ref() {
Some(om) => self.provider_order_key_from_order_meta(om),
@@ -12299,6 +12312,7 @@ impl ChatWidget<'_> {
tracing::debug!("Ignoring Answer delta for closed id={}", id);
return;
}
self.maybe_mark_previous_answer_mid_turn(&id);
// Seed/refresh order key for this Answer stream id (must have OrderMeta)
let ok = match event.order.as_ref() {
Some(om) => self.provider_order_key_from_order_meta(om),
@@ -12449,6 +12463,8 @@ impl ChatWidget<'_> {
// Reset stream headers for new turn
self.stream.reset_headers_for_new_turn();
self.stream_state.current_kind = None;
self.last_answer_stream_id_in_turn = None;
self.last_answer_history_id_in_turn = None;
// New turn: clear closed id guards
self.stream_state.closed_answer_ids.clear();
self.stream_state.closed_reasoning_ids.clear();
@@ -14078,6 +14094,7 @@ impl ChatWidget<'_> {
citations: Vec::new(),
metadata: None,
token_usage: None,
mid_turn: false,
created_at: SystemTime::now(),
};
let assistant_cell =
@@ -23815,6 +23832,48 @@ Have we met every part of this goal and is there no further work to do?"#
}
}
fn maybe_mark_previous_answer_mid_turn(&mut self, new_stream_id: &str) {
let Some(prev_stream_id) = self.last_answer_stream_id_in_turn.as_deref() else {
return;
};
if prev_stream_id == new_stream_id {
return;
}
let Some(prev_history_id) = self.last_answer_history_id_in_turn else {
return;
};
let mut changed = false;
if let Some(idx) = self
.history_cell_ids
.iter()
.rposition(|hid| *hid == Some(prev_history_id))
{
if let Some(cell) = self.history_cells[idx]
.as_any_mut()
.downcast_mut::<history_cell::AssistantMarkdownCell>()
{
if !cell.state().mid_turn {
cell.set_mid_turn(true);
changed = true;
}
}
}
if let Some(record) = self.history_state.record_mut(prev_history_id) {
if let HistoryRecord::AssistantMessage(state) = record {
if !state.mid_turn {
state.mid_turn = true;
changed = true;
}
}
}
if changed {
self.mark_history_dirty();
self.request_redraw();
}
}
fn finalize_answer_stream_state(
&mut self,
stream_id: Option<&str>,
@@ -24005,6 +24064,8 @@ Have we met every part of this goal and is there no further work to do?"#
let cell = history_cell::AssistantMarkdownCell::from_state(state, &self.config);
self.history_insert_existing_record(Box::new(cell), key, "answer-review", history_id);
self.last_answer_stream_id_in_turn = id.clone();
self.last_answer_history_id_in_turn = Some(history_id);
// Advance Auto Drive after the assistant message has been finalized.
self.auto_on_assistant_final();
self.maybe_hide_spinner();
@@ -24129,6 +24190,7 @@ Have we met every part of this goal and is there no further work to do?"#
idx
);
let state = self.finalize_answer_stream_state(id.as_deref(), &final_source);
let history_id = state.id;
let cell = history_cell::AssistantMarkdownCell::from_state(state, &self.config);
self.history_replace_at(idx, Box::new(cell));
if let Some(ref want) = id {
@@ -24137,6 +24199,8 @@ Have we met every part of this goal and is there no further work to do?"#
.insert(StreamId(want.clone()));
}
self.autoscroll_if_near_bottom();
self.last_answer_stream_id_in_turn = id.clone();
self.last_answer_history_id_in_turn = Some(history_id);
// Final cell committed via replacement; now advance Auto Drive.
self.auto_on_assistant_final();
self.maybe_hide_spinner();
@@ -24163,12 +24227,15 @@ Have we met every part of this goal and is there no further work to do?"#
);
let state =
self.finalize_answer_stream_state(id.as_deref(), &final_source);
let history_id = state.id;
let cell = history_cell::AssistantMarkdownCell::from_state(state, &self.config);
self.history_replace_at(idx, Box::new(cell));
self.stream_state
.closed_answer_ids
.insert(StreamId(want.clone()));
self.autoscroll_if_near_bottom();
self.last_answer_stream_id_in_turn = id.clone();
self.last_answer_history_id_in_turn = Some(history_id);
// Final cell replaced in-place; advance Auto Drive now.
self.auto_on_assistant_final();
self.maybe_hide_spinner();
@@ -24210,9 +24277,12 @@ Have we met every part of this goal and is there no further work to do?"#
);
let state =
self.finalize_answer_stream_state(id.as_deref(), &final_source);
let history_id = state.id;
let cell = history_cell::AssistantMarkdownCell::from_state(state, &self.config);
self.history_replace_at(idx, Box::new(cell));
self.autoscroll_if_near_bottom();
self.last_answer_stream_id_in_turn = id.clone();
self.last_answer_history_id_in_turn = Some(history_id);
// Final assistant content revised; advance Auto Drive now.
self.auto_on_assistant_final();
self.maybe_hide_spinner();
@@ -24258,6 +24328,7 @@ Have we met every part of this goal and is there no further work to do?"#
Self::debug_fmt_order_key(key)
);
let state = self.finalize_answer_stream_state(id.as_deref(), &final_source);
let history_id = state.id;
let cell = history_cell::AssistantMarkdownCell::from_state(state, &self.config);
let _ = self.history_insert_with_key_global(Box::new(cell), key);
if let Some(ref want) = id {
@@ -24265,6 +24336,8 @@ Have we met every part of this goal and is there no further work to do?"#
.closed_answer_ids
.insert(StreamId(want.clone()));
}
self.last_answer_stream_id_in_turn = id.clone();
self.last_answer_history_id_in_turn = Some(history_id);
// Ordered insert completed; advance Auto Drive now that the assistant
// message is present in history.
self.auto_on_assistant_final();
@@ -32887,6 +32960,7 @@ impl ChatWidget<'_> {
citations: Vec::new(),
metadata: None,
token_usage: None,
mid_turn: false,
created_at: SystemTime::now(),
};
history_cell::AssistantMarkdownCell::from_state(state, &self.config)
+17 -1
View File
@@ -41,6 +41,14 @@ impl AssistantMarkdownCell {
self.layout_cache.borrow_mut().clear();
}
pub(crate) fn set_mid_turn(&mut self, mid_turn: bool) {
if self.state.mid_turn == mid_turn {
return;
}
self.state.mid_turn = mid_turn;
self.layout_cache.borrow_mut().clear();
}
pub(crate) fn stream_id(&self) -> Option<&str> {
self.state.stream_id.as_deref()
}
@@ -254,6 +262,14 @@ impl HistoryCell for AssistantMarkdownCell {
HistoryCellType::Assistant
}
fn gutter_symbol(&self) -> Option<&'static str> {
if self.state.mid_turn {
None
} else {
super::gutter_symbol_for_kind(self.kind())
}
}
fn display_lines(&self) -> Vec<Line<'static>> {
assistant_markdown_lines_with_context(&self.state, self.file_opener, &self.cwd)
}
@@ -305,7 +321,7 @@ pub(crate) fn assistant_markdown_lines_with_context(
&mut out,
file_opener,
cwd,
true,
!state.mid_turn,
);
let bright = crate::colors::text_bright();
for line in out.iter_mut().skip(1) {
+1
View File
@@ -116,6 +116,7 @@ pub(crate) fn stream_lines_from_state_with_context(
.metadata
.as_ref()
.and_then(|meta| meta.token_usage.clone()),
mid_turn: false,
created_at: state.last_updated_at,
};
@@ -1,5 +1,6 @@
---
source: tui/tests/vt100_chatwidget_snapshot.rs
assertion_line: 2250
expression: open
---
+------------------------------------------------------------------------------------------------+
@@ -7,7 +8,7 @@ expression: open
+------------------------------------------------------------------------------------------------+
+Settings ▸ Overview-----------------------------------------------------------------------------+
| |
| › Model Model: GPT-5.1-Codex (Medium) |
| › Model Model: GPT-5.1-Codex-Max (Medium) |
| + Choose the language model used for new completions. |
| +----------------------------------------+ |
| Theme Theme|Settings Overview |mond |
@@ -1,5 +1,6 @@
---
source: tui/tests/vt100_chatwidget_snapshot.rs
assertion_line: 2267
expression: section_open
---
+------------------------------------------------------------------------------------------------+
@@ -13,12 +14,12 @@ expression: section_open
| Agents | Reason|Model Shortcuts | | |
| Prompts | | | | |
| Auto Drive | GPT-5.|• Esc Return to overview | | |
| Review | Optimi|• Tab Cycle sections | | |
| Planning | Hig|• Shift+Tab Cycle backwards |r complex or ambiguous proble | |
| Validation | Med|• ? Toggle this help |sts reasoning based on the ta | |
| Chrome | Low| |d reasoning | |
| MCP | |Press Esc to close | | |
| Notifications | GPT-5.+------------------------------+ | |
| Review | Latest|• Tab Cycle sections |s across knowledge, reasoning | |
| Planning | XHi|• Shift+Tab Cycle backwards |for complex problems | |
| Validation | Hig|• ? Toggle this help |r complex or ambiguous proble | |
| Chrome | Med| |f reasoning depth and latency | |
| MCP | Low|Press Esc to close |oning; useful for straightfor | |
| Notifications | +------------------------------+ | |
| Limits | | |
| +----------------------------------------------------------------------+ |
| ↑ ↓ Move Enter Open Esc Close ? Help |
@@ -1,5 +1,6 @@
---
source: tui/tests/vt100_chatwidget_snapshot.rs
assertion_line: 2179
expression: output
---
+------------------------------------------------------------------------------------------------+
@@ -7,7 +8,7 @@ expression: output
+------------------------------------------------------------------------------------------------+
+Settings ▸ Overview-----------------------------------------------------------------------------+
| |
| › Model Model: GPT-5.1-Codex (Medium) |
| › Model Model: GPT-5.1-Codex-Max (Medium) |
| + Choose the language model used for new completions. |
| |
| Theme Theme: Light - Photon (16-color) · Spinner: Diamond |
@@ -18,7 +19,7 @@ expression: output
| |
| |
| Agents Enabled: 9/10 |
| Agents Enabled: 10/11 |
| + Configure linked agents and default task permissions. |
| |
| ↑ ↓ Move Enter Open Esc Close ? Help |
@@ -1,5 +1,6 @@
---
source: tui/tests/vt100_chatwidget_snapshot.rs
assertion_line: 2191
expression: output
---
+--------------------------------------------------------+
@@ -7,7 +8,7 @@ expression: output
+--------------------------------------------------------+
+Settings ▸ Overview-------------------------------------+
| |
| › Model Model: GPT-5.1-Codex (Medium) |
| › Model Model: GPT-5.1-Codex-Max (Medium) |
| + Choose the language model used for new comp... |
| |
| Theme Theme: Light - Photon (16-colo... |
@@ -1,5 +1,6 @@
---
source: tui/tests/vt100_chatwidget_snapshot.rs
assertion_line: 2217
expression: output
---
+------------------------------------------------------------------------------------------------+
@@ -7,7 +8,7 @@ expression: output
+------------------------------------------------------------------------------------------------+
+Settings ▸ Overview-----------------------------------------------------------------------------+
| |
| › Model Model: GPT-5.1-Codex (Medium) |
| › Model Model: GPT-5.1-Codex-Max (Medium) |
| + Choose the language model used for new completions. |
| |
| Theme Theme: Light - Photon (16-color) · Spinner: Diamond |