Files
codex/prs/bolinfest/study/PR-1672-study.md
2025-09-02 15:17:45 -07:00

7.6 KiB
Raw Blame History

PR #1672 Review Takeaways — Easily Selectable History (bolinfest)

DOs

  • Keep the protocol boundary clean: move UI-only types out of protocol.rs.

    • Example: put FinalOutput in codex-common (or similar), not core/protocol.
    // codex-common/src/final_output.rs
    use std::fmt;
    use codex_core::protocol::TokenUsage;
    use serde::{Serialize, Deserialize};
    
    #[derive(Debug, Clone, Deserialize, Serialize)]
    pub struct FinalOutput {
        pub token_usage: TokenUsage,
        pub final_message: Option<String>,
    }
    
    impl From<TokenUsage> for FinalOutput {
        fn from(token_usage: TokenUsage) -> Self {
            Self { token_usage, final_message: None }
        }
    }
    
    impl fmt::Display for FinalOutput {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            let u = &self.token_usage;
            write!(
                f,
                "Token usage: total={} input={}{} output={}{}",
                u.total_tokens,
                u.input_tokens,
                u.cached_input_tokens
                    .map(|c| format!(" (cached {c})"))
                    .unwrap_or_default(),
                u.output_tokens,
                u.reasoning_output_tokens
                    .map(|r| format!(" (reasoning {r})"))
                    .unwrap_or_default()
            )
        }
    }
    
    // codex-rs/cli/src/main.rs
    let usage = codex_tui::run_main(tui_cli, codex_linux_sandbox_exe)?;
    println!("{}", codex_common::FinalOutput::from(usage));
    
  • Use doc comments and attach them to items.

    /// Buffers assistant response deltas until a final message arrives.
    /// This avoids partial-line rendering in scrollback.
    struct ChatWidget<'a> {
        /// Accumulates streamed assistant text.
        answer_buffer: String,
        // ...
    }
    
  • Keep comments current; remove stale references.

    // Before: mentions a removed design doc
    /// … hybrid scrollback model described in `fix-history-plan.md`.
    
    // After: describe the behavior without referencing non-existent docs
    /// Appends immutable history above the inline viewport.
    
  • DRY repetitive “append last history” code with a helper.

    impl ChatWidget<'_> {
        fn emit_last_history_entry(&mut self) {
            if let Some(lines) = self.conversation_history.last_entry_plain_lines() {
                self.app_event_tx.send(AppEvent::InsertHistory(lines));
            }
        }
    
        fn submit_user_message(&mut self, msg: String) {
            self.conversation_history.add_user_message(msg);
            self.emit_last_history_entry();
        }
    }
    
  • Encapsulate wrapping/normalization logic in a small struct (not closures).

    // tui/src/insert_history.rs (outline)
    struct LineBuilder { term_width: usize, spans: Vec<Span<'static>>, width: usize }
    
    impl LineBuilder {
        fn new(term_width: usize) -> Self { Self { term_width, spans: vec![], width: 0 } }
        fn push_word(&mut self, word: &mut String, style: Style, out: &mut Vec<Line<'static>>) { /* … */ }
        fn consume_whitespace(&mut self, ws: &mut String, style: Style, out: &mut Vec<Line<'static>>) { /* … */ }
        fn flush_line(&mut self, out: &mut Vec<Line<'static>>) { /* … */ }
    }
    
  • Normalize and wrap text before Terminal::insert_before.

    pub(crate) fn insert_history_lines(terminal: &mut tui::Tui, lines: Vec<Line<'static>>) {
        let term_width = terminal.size().map(|a| a.width).unwrap_or(80) as usize;
        let mut physical: Vec<Line<'static>> = Vec::new();
    
        for logical in lines {
            let mut b = LineBuilder::new(term_width);
            let mut ws = String::new();
    
            for span in logical.spans {
                let style = span.style;
                let mut word = String::new();
                for ch in span.content.chars() {
                    if ch == '\n' { b.push_word(&mut word, style, &mut physical); ws.clear(); b.flush_line(&mut physical); continue; }
                    if ch.is_whitespace() { b.push_word(&mut word, style, &mut physical); ws.push(ch); }
                    else { b.consume_whitespace(&mut ws, style, &mut physical); word.push(ch); }
                }
                b.push_word(&mut word, style, &mut physical);
            }
            if !b.spans.is_empty() { physical.push(Line::from(std::mem::take(&mut b.spans))); }
            else { physical.push(Line::from(Vec::<Span<'static>>::new())); }
        }
    
        let total = physical.len() as u16;
        terminal.insert_before(total, |buf| {
            for (i, line) in physical.into_iter().enumerate() {
                Paragraph::new(line).render(Rect { x: 0, y: i as u16, width: buf.area.width, height: 1 }, buf);
            }
        }).ok();
    }
    
  • Preserve or clearly indicate unsupported outputs (e.g., images).

    match cell {
        HistoryCell::CompletedMcpToolCallWithImageOutput { .. } => vec![
            Line::from("tool result (image output omitted)"),
            Line::from(""),
        ],
        _ => view.lines.clone(),
    }
    
  • Keep docs and config in sync when removing features.

    // core/src/config_types.rs
    #[derive(Deserialize, Debug, Clone, PartialEq, Default)]
    pub struct Tui {} // remove disable_mouse_capture here
    
    // config.md
    [tui]
    # (mouse capture option removed to match code)
    
  • Remove noisy/unnecessary comments.

    // Before
    use crate::tui; // for the Tui type alias
    
    // After
    use crate::tui;
    

DONTs

  • Dont put UI/CLI glue types in core/src/protocol.rs.

    // Avoid
    // core/src/protocol.rs
    #[derive(Debug)] pub struct FinalOutput { /* UI concern */ }
    
  • Dont leave obsolete references in comments.

    // Avoid: referring to deleted docs or plans
    /// See `fix-history-plan.md` for details.
    
  • Dont use // where /// doc comments are appropriate.

    // Avoid
    // Buffers assistant response deltas.
    
    // Prefer
    /// Buffers assistant response deltas.
    
  • Dont stream partial fragments that cause truncation/flicker; flush on full message or at newline boundaries.

    // Avoid: re-rendering on every tiny delta
    self.answer_buffer.push_str(&delta);
    self.conversation_history.replace_prev_agent_message(&self.config, self.answer_buffer.clone());
    
    // Prefer: flush only complete lines (or final)
    self.answer_buffer.push_str(&delta);
    if let Some(idx) = self.answer_buffer.rfind('\n') {
        let complete = self.answer_buffer[..=idx].to_string();
        self.conversation_history.add_agent_message(&self.config, complete);
        self.emit_last_history_entry();
        self.answer_buffer = self.answer_buffer[idx+1..].to_string();
    }
    
  • Dont assume Paragraph::wrap will apply during insert_before; perform explicit wrapping.

    // Avoid: relying on implicit wrap with insert_before
    Paragraph::new(line).wrap(Wrap { trim: true });
    
    // Prefer: pre-wrap into single-row Lines, then render each on its own row
    
  • Dont duplicate formatting/printing logic across binaries; centralize via Display.

    // Avoid
    println!("{}", format!("Token usage: total={}", usage.total_tokens));
    
    // Prefer
    println!("{}", codex_common::FinalOutput::from(usage));
    
  • Dont leave half-removed features in code or docs.

    // Avoid: keeping `disable_mouse_capture` in docs while code ignores it
    
  • Dont use large, stateful inline closures for formatting/flow; use small functions or structs.

    // Avoid
    let flush_word = |word: &mut String, spans: &mut Vec<Span<'static>>, width: &mut usize, /* … */| { /* … */ };
    
    // Prefer
    struct LineBuilder { /* … */ }
    impl LineBuilder { fn push_word(&mut self, /* … */) { /* … */ } }