mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:23:33 +00:00
feat(tui/order): enforce strict global ordering and remove turn-window logic\n\n- Add strict and global ordered inserts\n- Require for LLM/tool events; panic if missing\n- Replace unordered pushes with ordered using synthetic internal keys (out=-1)\n- Remove turn-window fields/logic; delete legacy helpers\n- Seed per-stream keys from ; enforce for Answer/Reasoning/MCP\n- Carry with pending ExecEnd and flush with correct order\n- Simplify interrupts queue; drop window fields and pass through \n- Clean up dead code and fix warnings (0 warnings)\n\nValidated with ./build-fast.sh (no warnings).
This commit is contained in:
+87
-18
@@ -102,6 +102,7 @@ pub(crate) struct App<'a> {
|
||||
|
||||
/// If true, enable lightweight timing collection and report on exit.
|
||||
timing_enabled: bool,
|
||||
timing: TimingStats,
|
||||
}
|
||||
|
||||
/// Aggregate parameters needed to create a `ChatWidget`, as creation may be
|
||||
@@ -276,31 +277,32 @@ impl App<'_> {
|
||||
clear_on_first_frame: true,
|
||||
last_esc_time: None,
|
||||
timing_enabled: enable_perf,
|
||||
timing: TimingStats::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Schedule a redraw if one is not already pending.
|
||||
/// Schedule a redraw immediately and open a short debounce window to coalesce
|
||||
/// subsequent requests. Crucially, even if a timer is already armed (e.g., an
|
||||
/// animation scheduled a future frame), we still trigger an immediate redraw
|
||||
/// to keep keypress echo latency low.
|
||||
#[allow(clippy::unwrap_used)]
|
||||
fn schedule_redraw(&self) {
|
||||
// Attempt to set the flag to `true`. If it was already `true`, another
|
||||
// redraw is already pending so we can return early.
|
||||
// Always issue a leading-edge redraw for responsiveness.
|
||||
self.app_event_tx.send(AppEvent::Redraw);
|
||||
|
||||
// Arm debounce window if not already armed.
|
||||
if self
|
||||
.pending_redraw
|
||||
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
|
||||
.is_err()
|
||||
.is_ok()
|
||||
{
|
||||
return;
|
||||
let pending_redraw = self.pending_redraw.clone();
|
||||
thread::spawn(move || {
|
||||
thread::sleep(REDRAW_DEBOUNCE);
|
||||
pending_redraw.store(false, Ordering::Release);
|
||||
});
|
||||
}
|
||||
// Leading-edge draw now for lower input latency; coalesce further
|
||||
// requests until the debounce window elapses.
|
||||
self.app_event_tx.send(AppEvent::Redraw);
|
||||
|
||||
let pending_redraw = self.pending_redraw.clone();
|
||||
thread::spawn(move || {
|
||||
thread::sleep(REDRAW_DEBOUNCE);
|
||||
pending_redraw.store(false, Ordering::Release);
|
||||
});
|
||||
}
|
||||
|
||||
/// Schedule a redraw after the specified duration
|
||||
@@ -361,8 +363,17 @@ impl App<'_> {
|
||||
AppEvent::RequestRedraw => {
|
||||
self.schedule_redraw();
|
||||
}
|
||||
AppEvent::FlushPendingExecEnds => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.flush_pending_exec_ends();
|
||||
}
|
||||
self.schedule_redraw();
|
||||
}
|
||||
AppEvent::Redraw => {
|
||||
if self.timing_enabled { self.timing.on_redraw_begin(); }
|
||||
let t0 = Instant::now();
|
||||
std::io::stdout().sync_update(|_| self.draw_next_frame(terminal))??;
|
||||
if self.timing_enabled { self.timing.on_redraw_end(t0); }
|
||||
}
|
||||
AppEvent::StartCommitAnimation => {
|
||||
if self
|
||||
@@ -398,6 +409,7 @@ impl App<'_> {
|
||||
}
|
||||
}
|
||||
AppEvent::KeyEvent(mut key_event) => {
|
||||
if self.timing_enabled { self.timing.on_key(); }
|
||||
// On terminals that do not support keyboard enhancement flags
|
||||
// (notably some Windows Git Bash/mintty setups), crossterm may
|
||||
// report only Release events. Normalize such events to Press so
|
||||
@@ -1093,11 +1105,16 @@ impl App<'_> {
|
||||
|
||||
/// Return a human-readable performance summary if timing was enabled.
|
||||
pub(crate) fn perf_summary(&self) -> Option<String> {
|
||||
if !self.timing_enabled { return None; }
|
||||
match &self.app_state {
|
||||
AppState::Chat { widget } => Some(widget.perf_summary()),
|
||||
AppState::Onboarding { .. } => None,
|
||||
if !self.timing_enabled {
|
||||
return None;
|
||||
}
|
||||
let mut out = String::new();
|
||||
if let AppState::Chat { widget } = &self.app_state {
|
||||
out.push_str(&widget.perf_summary());
|
||||
out.push_str("\n\n");
|
||||
}
|
||||
out.push_str(&self.timing.summarize());
|
||||
Some(out)
|
||||
}
|
||||
|
||||
fn draw_next_frame(&mut self, terminal: &mut tui::Tui) -> Result<()> {
|
||||
@@ -1182,3 +1199,55 @@ fn should_show_login_screen(login_status: crate::LoginStatus, _config: &Config)
|
||||
}
|
||||
|
||||
// (legacy tests removed)
|
||||
#[derive(Default, Clone, Debug)]
|
||||
struct TimingStats {
|
||||
frames_drawn: u64,
|
||||
redraw_events: u64,
|
||||
key_events: u64,
|
||||
draw_ns: Vec<u64>,
|
||||
key_to_frame_ns: Vec<u64>,
|
||||
last_key_event: Option<Instant>,
|
||||
key_waiting_for_frame: bool,
|
||||
}
|
||||
|
||||
impl TimingStats {
|
||||
fn on_key(&mut self) {
|
||||
self.key_events = self.key_events.saturating_add(1);
|
||||
self.last_key_event = Some(Instant::now());
|
||||
self.key_waiting_for_frame = true;
|
||||
}
|
||||
fn on_redraw_begin(&mut self) { self.redraw_events = self.redraw_events.saturating_add(1); }
|
||||
fn on_redraw_end(&mut self, started: Instant) {
|
||||
self.frames_drawn = self.frames_drawn.saturating_add(1);
|
||||
let dt = started.elapsed().as_nanos() as u64;
|
||||
self.draw_ns.push(dt);
|
||||
if self.key_waiting_for_frame {
|
||||
if let Some(t0) = self.last_key_event.take() {
|
||||
let d = t0.elapsed().as_nanos() as u64;
|
||||
self.key_to_frame_ns.push(d);
|
||||
}
|
||||
self.key_waiting_for_frame = false;
|
||||
}
|
||||
}
|
||||
fn pct(ns: &[u64], p: f64) -> f64 {
|
||||
if ns.is_empty() { return 0.0; }
|
||||
let mut v = ns.to_vec();
|
||||
v.sort_unstable();
|
||||
let idx = ((v.len() as f64 - 1.0) * p).round() as usize;
|
||||
(v[idx] as f64) / 1_000_000.0
|
||||
}
|
||||
fn summarize(&self) -> String {
|
||||
let draw_p50 = Self::pct(&self.draw_ns, 0.50);
|
||||
let draw_p95 = Self::pct(&self.draw_ns, 0.95);
|
||||
let kf_p50 = Self::pct(&self.key_to_frame_ns, 0.50);
|
||||
let kf_p95 = Self::pct(&self.key_to_frame_ns, 0.95);
|
||||
format!(
|
||||
"app-timing: frames={}\n redraw_events={} key_events={}\n draw_ms: p50={:.2} p95={:.2}\n key->frame_ms: p50={:.2} p95={:.2}",
|
||||
self.frames_drawn,
|
||||
self.redraw_events,
|
||||
self.key_events,
|
||||
draw_p50, draw_p95,
|
||||
kf_p50, kf_p95,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,11 @@ pub(crate) enum AppEvent {
|
||||
/// Multiple requests are coalesced by the central frame scheduler.
|
||||
ScheduleFrameIn(Duration),
|
||||
|
||||
/// Internal: flush any pending out-of-order ExecEnd events that did not
|
||||
/// receive a matching ExecBegin within a short pairing window. This lets
|
||||
/// the TUI render a fallback "Ran call_<id>" cell so output is not lost.
|
||||
FlushPendingExecEnds,
|
||||
|
||||
KeyEvent(KeyEvent),
|
||||
|
||||
MouseEvent(MouseEvent),
|
||||
|
||||
+403
-701
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ use crate::app_event::AppEvent;
|
||||
use crate::height_manager::HeightEvent;
|
||||
use crate::history_cell::{self, HistoryCell};
|
||||
use crate::history_cell::CommandOutput;
|
||||
use codex_core::protocol::{ExecCommandBeginEvent, ExecCommandEndEvent};
|
||||
use codex_core::protocol::{ExecCommandBeginEvent, ExecCommandEndEvent, OrderMeta};
|
||||
|
||||
pub(super) fn finalize_exec_cell_at(
|
||||
chat: &mut ChatWidget<'_>,
|
||||
@@ -297,7 +297,7 @@ pub(super) fn try_merge_completed_exec_at(chat: &mut ChatWidget<'_>, idx: usize)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn handle_exec_begin_now(chat: &mut ChatWidget<'_>, ev: ExecCommandBeginEvent) {
|
||||
pub(super) fn handle_exec_begin_now(chat: &mut ChatWidget<'_>, ev: ExecCommandBeginEvent, order: &OrderMeta) {
|
||||
if chat.ended_call_ids.contains(&super::ExecCallId(ev.call_id.clone())) { return; }
|
||||
for cell in &chat.history_cells { cell.trigger_fade(); }
|
||||
let parsed_command = ev.parsed_cmd.clone();
|
||||
@@ -320,7 +320,8 @@ pub(super) fn handle_exec_begin_now(chat: &mut ChatWidget<'_>, ev: ExecCommandBe
|
||||
let idx = if let Some(i) = agg_index { i } else {
|
||||
// Reserve an ordered slot for the read aggregation header if the provider
|
||||
// supplied OrderMeta; otherwise it will fall back to unordered.
|
||||
let i = chat.push_cell_maybe_ordered(history_cell::ReadAggregationCell::new());
|
||||
let key = ChatWidget::order_key_from_order_meta(Some(order));
|
||||
let i = chat.history_insert_with_key_global(Box::new(history_cell::ReadAggregationCell::new()), key);
|
||||
// If the immediately-previous cell is also a ReadAggregationCell (from an
|
||||
// earlier attempt), merge into it so consecutive Read blocks collapse into
|
||||
// a single "Read" section as per UX rules.
|
||||
@@ -379,7 +380,8 @@ pub(super) fn handle_exec_begin_now(chat: &mut ChatWidget<'_>, ev: ExecCommandBe
|
||||
}
|
||||
|
||||
let cell = history_cell::new_active_exec_command(ev.command.clone(), parsed_command.clone());
|
||||
let idx = chat.push_cell_maybe_ordered(cell);
|
||||
let key = ChatWidget::order_key_from_order_meta(Some(order));
|
||||
let idx = chat.history_insert_with_key_global(Box::new(cell), key);
|
||||
chat.exec.running_commands.insert(
|
||||
super::ExecCallId(ev.call_id.clone()),
|
||||
super::RunningCommand { command: ev.command.clone(), parsed: parsed_command, history_index: Some(idx) },
|
||||
@@ -405,7 +407,7 @@ pub(super) fn handle_exec_begin_now(chat: &mut ChatWidget<'_>, ev: ExecCommandBe
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn handle_exec_end_now(chat: &mut ChatWidget<'_>, ev: ExecCommandEndEvent) {
|
||||
pub(super) fn handle_exec_end_now(chat: &mut ChatWidget<'_>, ev: ExecCommandEndEvent, order: &OrderMeta) {
|
||||
chat.ended_call_ids.insert(super::ExecCallId(ev.call_id.clone()));
|
||||
let ExecCommandEndEvent { call_id, exit_code, duration: _, stdout, stderr } = ev;
|
||||
let cmd = chat.exec.running_commands.remove(&super::ExecCallId(call_id.clone()));
|
||||
@@ -484,8 +486,8 @@ pub(super) fn handle_exec_end_now(chat: &mut ChatWidget<'_>, ev: ExecCommandEndE
|
||||
|
||||
if !replaced {
|
||||
if let Some(c) = completed_opt.take() {
|
||||
// Respect any pending ordered key captured from OrderMeta for this event.
|
||||
let idx = chat.push_cell_maybe_ordered(c);
|
||||
let key = ChatWidget::order_key_from_order_meta(Some(order));
|
||||
let idx = chat.history_insert_with_key_global(Box::new(c), key);
|
||||
// Attempt standard merge with previous Exec if applicable.
|
||||
crate::chatwidget::exec_tools::try_merge_completed_exec_at(chat, idx);
|
||||
}
|
||||
|
||||
@@ -15,13 +15,13 @@ use super::tools;
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum QueuedInterrupt {
|
||||
ExecApproval { seq: u64, id: String, ev: ExecApprovalRequestEvent },
|
||||
ApplyPatchApproval { seq: u64, id: String, ev: ApplyPatchApprovalRequestEvent, order: Option<codex_core::protocol::OrderMeta>, turn_win: Option<usize> },
|
||||
ExecBegin { seq: u64, ev: ExecCommandBeginEvent, order: Option<codex_core::protocol::OrderMeta>, turn_win: Option<usize>, turn_id: Option<String> },
|
||||
ExecEnd { seq: u64, ev: ExecCommandEndEvent, turn_win: Option<usize> },
|
||||
McpBegin { seq: u64, ev: McpToolCallBeginEvent, order: Option<codex_core::protocol::OrderMeta>, turn_win: Option<usize>, turn_id: Option<String> },
|
||||
McpEnd { seq: u64, ev: McpToolCallEndEvent, turn_win: Option<usize> },
|
||||
PatchEnd { seq: u64, ev: PatchApplyEndEvent, order: Option<codex_core::protocol::OrderMeta>, turn_win: Option<usize> },
|
||||
PlanUpdate { seq: u64, ev: UpdatePlanArgs, order: Option<codex_core::protocol::OrderMeta>, turn_win: Option<usize> },
|
||||
ApplyPatchApproval { seq: u64, id: String, ev: ApplyPatchApprovalRequestEvent },
|
||||
ExecBegin { seq: u64, ev: ExecCommandBeginEvent, order: Option<codex_core::protocol::OrderMeta> },
|
||||
ExecEnd { seq: u64, ev: ExecCommandEndEvent, order: Option<codex_core::protocol::OrderMeta> },
|
||||
McpBegin { seq: u64, ev: McpToolCallBeginEvent, order: Option<codex_core::protocol::OrderMeta> },
|
||||
McpEnd { seq: u64, ev: McpToolCallEndEvent, order: Option<codex_core::protocol::OrderMeta> },
|
||||
PatchEnd { seq: u64, ev: PatchApplyEndEvent },
|
||||
PlanUpdate { seq: u64, ev: UpdatePlanArgs, order: Option<codex_core::protocol::OrderMeta> },
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -46,34 +46,32 @@ impl InterruptManager {
|
||||
seq: u64,
|
||||
id: String,
|
||||
ev: ApplyPatchApprovalRequestEvent,
|
||||
order: Option<codex_core::protocol::OrderMeta>,
|
||||
turn_win: Option<usize>,
|
||||
) {
|
||||
self.queue.push(QueuedInterrupt::ApplyPatchApproval { seq, id, ev, order, turn_win });
|
||||
self.queue.push(QueuedInterrupt::ApplyPatchApproval { seq, id, ev });
|
||||
}
|
||||
|
||||
pub(crate) fn push_exec_begin(&mut self, seq: u64, ev: ExecCommandBeginEvent, order: Option<codex_core::protocol::OrderMeta>, turn_win: Option<usize>, turn_id: Option<String>) {
|
||||
self.queue.push(QueuedInterrupt::ExecBegin { seq, ev, order, turn_win, turn_id });
|
||||
pub(crate) fn push_exec_begin(&mut self, seq: u64, ev: ExecCommandBeginEvent, order: Option<codex_core::protocol::OrderMeta>) {
|
||||
self.queue.push(QueuedInterrupt::ExecBegin { seq, ev, order });
|
||||
}
|
||||
|
||||
pub(crate) fn push_exec_end(&mut self, seq: u64, ev: ExecCommandEndEvent, turn_win: Option<usize>) {
|
||||
self.queue.push(QueuedInterrupt::ExecEnd { seq, ev, turn_win });
|
||||
pub(crate) fn push_exec_end(&mut self, seq: u64, ev: ExecCommandEndEvent, order: Option<codex_core::protocol::OrderMeta>) {
|
||||
self.queue.push(QueuedInterrupt::ExecEnd { seq, ev, order });
|
||||
}
|
||||
|
||||
pub(crate) fn push_mcp_begin(&mut self, seq: u64, ev: McpToolCallBeginEvent, order: Option<codex_core::protocol::OrderMeta>, turn_win: Option<usize>, turn_id: Option<String>) {
|
||||
self.queue.push(QueuedInterrupt::McpBegin { seq, ev, order, turn_win, turn_id });
|
||||
pub(crate) fn push_mcp_begin(&mut self, seq: u64, ev: McpToolCallBeginEvent, order: Option<codex_core::protocol::OrderMeta>) {
|
||||
self.queue.push(QueuedInterrupt::McpBegin { seq, ev, order });
|
||||
}
|
||||
|
||||
pub(crate) fn push_mcp_end(&mut self, seq: u64, ev: McpToolCallEndEvent, turn_win: Option<usize>) {
|
||||
self.queue.push(QueuedInterrupt::McpEnd { seq, ev, turn_win });
|
||||
pub(crate) fn push_mcp_end(&mut self, seq: u64, ev: McpToolCallEndEvent, order: Option<codex_core::protocol::OrderMeta>) {
|
||||
self.queue.push(QueuedInterrupt::McpEnd { seq, ev, order });
|
||||
}
|
||||
|
||||
pub(crate) fn push_patch_end(&mut self, seq: u64, ev: PatchApplyEndEvent, order: Option<codex_core::protocol::OrderMeta>, turn_win: Option<usize>) {
|
||||
self.queue.push(QueuedInterrupt::PatchEnd { seq, ev, order, turn_win });
|
||||
pub(crate) fn push_patch_end(&mut self, seq: u64, ev: PatchApplyEndEvent) {
|
||||
self.queue.push(QueuedInterrupt::PatchEnd { seq, ev });
|
||||
}
|
||||
|
||||
pub(crate) fn push_plan_update(&mut self, seq: u64, ev: UpdatePlanArgs, order: Option<codex_core::protocol::OrderMeta>, turn_win: Option<usize>) {
|
||||
self.queue.push(QueuedInterrupt::PlanUpdate { seq, ev, order, turn_win });
|
||||
pub(crate) fn push_plan_update(&mut self, seq: u64, ev: UpdatePlanArgs, order: Option<codex_core::protocol::OrderMeta>) {
|
||||
self.queue.push(QueuedInterrupt::PlanUpdate { seq, ev, order });
|
||||
}
|
||||
|
||||
pub(crate) fn flush_all(&mut self, chat: &mut ChatWidget<'_>) {
|
||||
@@ -82,48 +80,25 @@ impl InterruptManager {
|
||||
for q in self.queue.drain(..) {
|
||||
match q {
|
||||
QueuedInterrupt::ExecApproval { id, ev, .. } => chat.handle_exec_approval_now(id, ev),
|
||||
QueuedInterrupt::ApplyPatchApproval { seq, id, ev, order, turn_win } => {
|
||||
let ok = super::ChatWidget::order_key_from_order_meta(order.as_ref())
|
||||
.unwrap_or_else(|| chat.next_unordered_seq());
|
||||
tracing::info!("[order/flush] ApplyPatchApproval: queue_seq={} chosen_key={}", seq, ok.0);
|
||||
chat.set_pending_insert_seq(ok.0);
|
||||
if let Some(wi) = turn_win { chat.pending_insert_turn = Some(wi); }
|
||||
QueuedInterrupt::ApplyPatchApproval { seq: _, id, ev } => {
|
||||
chat.handle_apply_patch_approval_now(id, ev);
|
||||
}
|
||||
QueuedInterrupt::ExecBegin { seq, ev, order, turn_win, turn_id } => {
|
||||
let ok = super::ChatWidget::order_key_from_order_meta(order.as_ref())
|
||||
.unwrap_or_else(|| chat.next_unordered_seq());
|
||||
tracing::info!("[order/flush] ExecBegin: queue_seq={} chosen_key={}", seq, ok.0);
|
||||
chat.set_pending_insert_seq(ok.0);
|
||||
if let Some(wi) = turn_win { chat.pending_insert_turn = Some(wi); }
|
||||
if let Some(tid) = turn_id { chat.call_id_to_turn.insert(ev.call_id.clone(), tid); }
|
||||
chat.handle_exec_begin_now(ev);
|
||||
QueuedInterrupt::ExecBegin { seq: _, ev, order, .. } => {
|
||||
let ord = order.as_ref().expect("missing OrderMeta in queued ExecBegin");
|
||||
chat.handle_exec_begin_now(ev, ord);
|
||||
}
|
||||
QueuedInterrupt::ExecEnd { ev, turn_win, .. } => { if let Some(wi) = turn_win { chat.pending_insert_turn = Some(wi); } chat.handle_exec_end_now(ev) },
|
||||
QueuedInterrupt::McpBegin { seq, ev, order, turn_win, turn_id } => {
|
||||
let ok = super::ChatWidget::order_key_from_order_meta(order.as_ref())
|
||||
.unwrap_or_else(|| chat.next_unordered_seq());
|
||||
tracing::info!("[order/flush] McpBegin: queue_seq={} chosen_key={}", seq, ok.0);
|
||||
chat.set_pending_insert_seq(ok.0);
|
||||
if let Some(wi) = turn_win { chat.pending_insert_turn = Some(wi); }
|
||||
if let Some(tid) = turn_id { chat.call_id_to_turn.insert(ev.call_id.clone(), tid); }
|
||||
tools::mcp_begin(chat, ev);
|
||||
QueuedInterrupt::ExecEnd { ev, order, .. } => { let ord = order.as_ref().expect("missing OrderMeta in queued ExecEnd"); chat.handle_exec_end_now(ev, ord) },
|
||||
QueuedInterrupt::McpBegin { seq: _, ev, order, .. } => {
|
||||
let ok = super::ChatWidget::order_key_from_order_meta(order.as_ref());
|
||||
tools::mcp_begin(chat, ev, ok);
|
||||
}
|
||||
QueuedInterrupt::McpEnd { ev, turn_win, .. } => { if let Some(wi) = turn_win { chat.pending_insert_turn = Some(wi); } tools::mcp_end(chat, ev) },
|
||||
QueuedInterrupt::PatchEnd { seq, ev, order, turn_win } => {
|
||||
let ok = super::ChatWidget::order_key_from_order_meta(order.as_ref())
|
||||
.unwrap_or_else(|| chat.next_unordered_seq());
|
||||
tracing::info!("[order/flush] PatchEnd: queue_seq={} chosen_key={}", seq, ok.0);
|
||||
chat.set_pending_insert_seq(ok.0);
|
||||
if let Some(wi) = turn_win { chat.pending_insert_turn = Some(wi); }
|
||||
QueuedInterrupt::McpEnd { ev, order, .. } => { let ok = super::ChatWidget::order_key_from_order_meta(order.as_ref()); tools::mcp_end(chat, ev, ok) },
|
||||
QueuedInterrupt::PatchEnd { seq: _, ev } => {
|
||||
chat.handle_patch_apply_end_now(ev);
|
||||
}
|
||||
QueuedInterrupt::PlanUpdate { seq, ev, order, turn_win } => {
|
||||
let ok = super::ChatWidget::order_key_from_order_meta(order.as_ref())
|
||||
.unwrap_or_else(|| chat.next_unordered_seq());
|
||||
tracing::info!("[order/flush] PlanUpdate: queue_seq={} chosen_key={}", seq, ok.0);
|
||||
if let Some(wi) = turn_win { chat.pending_insert_turn = Some(wi); }
|
||||
chat.history_insert_with_seq(crate::history_cell::new_plan_update(ev), ok.0);
|
||||
QueuedInterrupt::PlanUpdate { ev, order, .. } => {
|
||||
let ok = super::ChatWidget::order_key_from_order_meta(order.as_ref());
|
||||
let _ = chat.history_insert_with_key_global(Box::new(crate::history_cell::new_plan_update(ev)), ok);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
//! Tool (Web Search, MCP) lifecycle handlers extracted from ChatWidget.
|
||||
|
||||
use super::ChatWidget;
|
||||
use super::{ChatWidget, OrderKey};
|
||||
use crate::history_cell;
|
||||
use codex_core::protocol::{McpToolCallBeginEvent, McpToolCallEndEvent};
|
||||
|
||||
pub(super) fn web_search_begin(chat: &mut ChatWidget<'_>, call_id: String, query: Option<String>) {
|
||||
pub(super) fn web_search_begin(chat: &mut ChatWidget<'_>, call_id: String, query: Option<String>, key: OrderKey) {
|
||||
for cell in &chat.history_cells { cell.trigger_fade(); }
|
||||
chat.finalize_active_stream();
|
||||
chat.flush_interrupt_queue();
|
||||
|
||||
let cell = history_cell::new_running_web_search(query.clone());
|
||||
let idx = chat.push_cell_maybe_ordered(cell);
|
||||
let idx = chat.history_insert_with_key_global(Box::new(cell), key);
|
||||
tracing::info!("[order] WebSearchBegin call_id={} idx={}", call_id, idx);
|
||||
chat.tools_state
|
||||
.running_web_search
|
||||
@@ -59,17 +59,17 @@ pub(super) fn web_search_complete(chat: &mut ChatWidget<'_>, call_id: String, qu
|
||||
chat.maybe_hide_spinner();
|
||||
}
|
||||
|
||||
pub(super) fn mcp_begin(chat: &mut ChatWidget<'_>, ev: McpToolCallBeginEvent) {
|
||||
pub(super) fn mcp_begin(chat: &mut ChatWidget<'_>, ev: McpToolCallBeginEvent, key: OrderKey) {
|
||||
for cell in &chat.history_cells { cell.trigger_fade(); }
|
||||
let McpToolCallBeginEvent { call_id, invocation } = ev;
|
||||
let cell = history_cell::new_running_mcp_tool_call(invocation);
|
||||
let idx = chat.push_cell_maybe_ordered(cell);
|
||||
let idx = chat.history_insert_with_key_global(Box::new(cell), key);
|
||||
chat.tools_state
|
||||
.running_custom_tools
|
||||
.insert(super::ToolCallId(call_id), idx);
|
||||
}
|
||||
|
||||
pub(super) fn mcp_end(chat: &mut ChatWidget<'_>, ev: McpToolCallEndEvent) {
|
||||
pub(super) fn mcp_end(chat: &mut ChatWidget<'_>, ev: McpToolCallEndEvent, key: OrderKey) {
|
||||
let McpToolCallEndEvent { call_id, duration, invocation, result } = ev;
|
||||
let success = !result.as_ref().map(|r| r.is_error.unwrap_or(false)).unwrap_or(false);
|
||||
let completed = history_cell::new_completed_mcp_tool_call(80, invocation, duration, success, result);
|
||||
@@ -79,5 +79,5 @@ pub(super) fn mcp_end(chat: &mut ChatWidget<'_>, ev: McpToolCallEndEvent) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
chat.history_push(completed);
|
||||
let _ = chat.history_insert_with_key_global(Box::new(completed), key);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ pub(crate) enum HeightEvent {
|
||||
Resize,
|
||||
HudToggle(bool),
|
||||
ComposerModeChange,
|
||||
HistoryAppend,
|
||||
HistoryFinalize,
|
||||
RunBegin,
|
||||
RunEnd,
|
||||
@@ -88,7 +87,6 @@ impl HeightManager {
|
||||
HeightEvent::Resize
|
||||
| HeightEvent::HudToggle(_)
|
||||
| HeightEvent::ComposerModeChange
|
||||
| HeightEvent::HistoryAppend
|
||||
| HeightEvent::HistoryFinalize
|
||||
| HeightEvent::RunBegin
|
||||
| HeightEvent::RunEnd => {
|
||||
|
||||
@@ -4308,6 +4308,30 @@ pub(crate) fn new_user_prompt(message: String) -> PlainHistoryCell {
|
||||
}
|
||||
}
|
||||
|
||||
/// Render a queued user message that will be sent in the next turn.
|
||||
/// Visually identical to a normal user cell, but the header shows a
|
||||
/// small "(queued)" suffix so it’s clear it hasn’t been executed yet.
|
||||
pub(crate) fn new_queued_user_prompt(message: String) -> PlainHistoryCell {
|
||||
use ratatui::style::Style;
|
||||
use ratatui::text::Span;
|
||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||||
// Header: "user (queued)"
|
||||
lines.push(Line::from(vec![
|
||||
Span::from("user "),
|
||||
Span::from("(queued)").style(Style::default().fg(crate::colors::text_dim())),
|
||||
]));
|
||||
// Normalize and render body like normal user messages
|
||||
let normalized = normalize_overwrite_sequences(&message);
|
||||
let expanded = expand_tabs_to_spaces(&normalized, 4);
|
||||
let content: Vec<Line<'static>> = expanded
|
||||
.lines()
|
||||
.map(|l| ansi_escape_line(l))
|
||||
.collect();
|
||||
let content = trim_empty_lines(content);
|
||||
lines.extend(content);
|
||||
PlainHistoryCell { lines, kind: HistoryCellType::User }
|
||||
}
|
||||
|
||||
/// Expand horizontal tabs to spaces using a fixed tab stop.
|
||||
/// This prevents terminals from applying their own tab expansion after
|
||||
/// ratatui has computed layout, which can otherwise cause glyphs to appear
|
||||
|
||||
Reference in New Issue
Block a user