mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
## Summary `ChatWidget` has been carrying several independent domains in one large state bag: transcript bookkeeping, turn lifecycle, queued input, status surfaces, connectors, review mode, and protocol dispatch. That makes otherwise-local changes hard to reason about because unrelated fields and side effects live beside each other in `chatwidget.rs`. This is the first cleanup PR in a larger decomposition effort. It does not try to make `chatwidget.rs` small in one sweep; instead, it establishes focused state boundaries that later handler, popup, rendering, and effect-synchronization extractions can build on. This PR keeps `ChatWidget` as the composition layer while moving focused state into smaller `codex-tui` modules. The widget still owns effects that touch the bottom pane, app events, command submission, redraw scheduling, and terminal-title updates. ## Changes - Add focused state modules under `codex-rs/tui/src/chatwidget/` for input queues, turn lifecycle, transcript bookkeeping, status state, connectors, review mode, and app-server protocol dispatch. - Update `ChatWidget` to hold grouped state structs and route input/lifecycle/status operations through those focused helpers. - Move app-server notification dispatch into `chatwidget/protocol.rs` while leaving feature handlers and side effects on `ChatWidget`. - Replace the large manual `ChatWidget` test literal with the normal constructor plus narrow test overrides, so future state moves do not require every field to be restated in test setup. - Update existing tests to access the new grouped state or narrower helpers without changing snapshot behavior. ## Longer-term direction Follow-up PRs can continue shrinking `chatwidget.rs` by moving behavior, not just state, into focused modules: - Extract input/submission flow, turn/stream handling, and tool-cell lifecycles into domain modules that call the new state reducers. - Move popup/settings builders and rendering helpers out of the main widget file so `ChatWidget` stays focused on composition. - Reduce direct `BottomPane` mutation by applying domain-specific sync outputs at clearer boundaries.
152 lines
5.8 KiB
Rust
152 lines
5.8 KiB
Rust
//! Transcript and active-cell bookkeeping for `ChatWidget`.
|
|
|
|
use super::HistoryCell;
|
|
use super::MAX_AGENT_COPY_HISTORY;
|
|
|
|
#[derive(Debug)]
|
|
pub(super) struct AgentTurnMarkdown {
|
|
pub(super) user_turn_count: usize,
|
|
pub(super) markdown: String,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub(super) struct TranscriptState {
|
|
pub(super) active_cell: Option<Box<dyn HistoryCell>>,
|
|
/// Monotonic-ish counter used to invalidate transcript overlay caching.
|
|
pub(super) active_cell_revision: u64,
|
|
/// Raw markdown of the most recently completed agent response that
|
|
/// survived any local thread rollback.
|
|
pub(super) last_agent_markdown: Option<String>,
|
|
/// Copyable agent responses keyed by the number of visible user turns at
|
|
/// the time the response completed.
|
|
pub(super) agent_turn_markdowns: Vec<AgentTurnMarkdown>,
|
|
/// Number of user turns currently reflected in the visible transcript.
|
|
pub(super) visible_user_turn_count: usize,
|
|
/// True when rollback discarded the requested copy source because it was
|
|
/// older than the retained copy history.
|
|
pub(super) copy_history_evicted_by_rollback: bool,
|
|
/// Raw markdown of the most recently completed proposed plan.
|
|
pub(super) latest_proposed_plan_markdown: Option<String>,
|
|
/// Whether this turn already produced a copyable response.
|
|
pub(super) saw_copy_source_this_turn: bool,
|
|
/// Whether the next streamed assistant content should be preceded by a final message separator.
|
|
pub(super) needs_final_message_separator: bool,
|
|
/// Whether the current turn performed "work" (exec commands, MCP tool calls, patch applications).
|
|
pub(super) had_work_activity: bool,
|
|
/// Whether the current turn emitted a plan update.
|
|
pub(super) saw_plan_update_this_turn: bool,
|
|
/// Whether the current turn emitted a proposed plan item that has not been superseded by a
|
|
/// later steer.
|
|
pub(super) saw_plan_item_this_turn: bool,
|
|
/// Latest `update_plan` checklist task counts for terminal-title rendering.
|
|
pub(super) last_plan_progress: Option<(usize, usize)>,
|
|
/// Incremental buffer for streamed plan content.
|
|
pub(super) plan_delta_buffer: String,
|
|
/// True while a plan item is streaming.
|
|
pub(super) plan_item_active: bool,
|
|
}
|
|
|
|
impl TranscriptState {
|
|
pub(super) fn new(active_cell: Option<Box<dyn HistoryCell>>) -> Self {
|
|
Self {
|
|
active_cell,
|
|
..Self::default()
|
|
}
|
|
}
|
|
|
|
pub(super) fn bump_active_cell_revision(&mut self) {
|
|
// Wrapping avoids overflow; wraparound would require 2^64 bumps and at
|
|
// worst causes a one-time cache-key collision.
|
|
self.active_cell_revision = self.active_cell_revision.wrapping_add(1);
|
|
}
|
|
|
|
pub(super) fn record_agent_markdown(&mut self, markdown: String) {
|
|
match self.agent_turn_markdowns.last_mut() {
|
|
Some(entry) if entry.user_turn_count == self.visible_user_turn_count => {
|
|
entry.markdown = markdown.clone();
|
|
}
|
|
_ => {
|
|
self.agent_turn_markdowns.push(AgentTurnMarkdown {
|
|
user_turn_count: self.visible_user_turn_count,
|
|
markdown: markdown.clone(),
|
|
});
|
|
if self.agent_turn_markdowns.len() > MAX_AGENT_COPY_HISTORY {
|
|
self.agent_turn_markdowns.remove(0);
|
|
}
|
|
}
|
|
}
|
|
self.last_agent_markdown = Some(markdown);
|
|
self.copy_history_evicted_by_rollback = false;
|
|
self.saw_copy_source_this_turn = true;
|
|
}
|
|
|
|
pub(super) fn record_visible_user_turn(&mut self) {
|
|
self.visible_user_turn_count = self.visible_user_turn_count.saturating_add(1);
|
|
}
|
|
|
|
pub(super) fn reset_copy_history(&mut self) {
|
|
self.last_agent_markdown = None;
|
|
self.agent_turn_markdowns.clear();
|
|
self.visible_user_turn_count = 0;
|
|
self.copy_history_evicted_by_rollback = false;
|
|
self.saw_copy_source_this_turn = false;
|
|
}
|
|
|
|
pub(super) fn truncate_copy_history_to_user_turn_count(&mut self, user_turn_count: usize) {
|
|
self.visible_user_turn_count = user_turn_count;
|
|
let had_copy_history = !self.agent_turn_markdowns.is_empty();
|
|
self.agent_turn_markdowns
|
|
.retain(|entry| entry.user_turn_count <= user_turn_count);
|
|
self.last_agent_markdown = self
|
|
.agent_turn_markdowns
|
|
.last()
|
|
.map(|entry| entry.markdown.clone());
|
|
self.copy_history_evicted_by_rollback =
|
|
had_copy_history && self.last_agent_markdown.is_none();
|
|
self.saw_copy_source_this_turn = false;
|
|
}
|
|
|
|
pub(super) fn reset_turn_flags(&mut self) {
|
|
self.saw_copy_source_this_turn = false;
|
|
self.saw_plan_update_this_turn = false;
|
|
self.saw_plan_item_this_turn = false;
|
|
self.had_work_activity = false;
|
|
self.latest_proposed_plan_markdown = None;
|
|
self.plan_delta_buffer.clear();
|
|
self.plan_item_active = false;
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use pretty_assertions::assert_eq;
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn active_cell_revision_wraps() {
|
|
let mut state = TranscriptState {
|
|
active_cell_revision: u64::MAX,
|
|
..TranscriptState::default()
|
|
};
|
|
|
|
state.bump_active_cell_revision();
|
|
|
|
assert_eq!(state.active_cell_revision, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn copy_history_tracks_latest_visible_turn() {
|
|
let mut state = TranscriptState::default();
|
|
state.record_visible_user_turn();
|
|
state.record_agent_markdown("first".to_string());
|
|
state.record_visible_user_turn();
|
|
state.record_agent_markdown("second".to_string());
|
|
|
|
state.truncate_copy_history_to_user_turn_count(/*user_turn_count*/ 1);
|
|
|
|
assert_eq!(state.last_agent_markdown.as_deref(), Some("first"));
|
|
assert!(!state.copy_history_evicted_by_rollback);
|
|
}
|
|
}
|