mirror of
https://github.com/openai/codex.git
synced 2026-11-14 21:40:39 +00:00
## What changed - Group threads needing approval first in `/subagents`, mark them with `(needs approval)`, and initially select the first waiting thread. - Refresh the open picker and approval notice as requests and agent metadata change, preserving the selected thread when rows move. - Use consistent agent labels across the picker and approval notice, preferring agent paths and using UUID suffixes to distinguish unnamed agents. - Track approval requests that arrive before thread metadata and refresh pending approvals after reconnecting. ## Testing Extend the picker test and snapshots to cover approvals arriving while the picker is open, agent paths arriving later, and selection staying on the same thread after reordering. GitOrigin-RevId: b7dd943d151ce98d1978f7c6b9f5e16903b9075b
150 lines
4.1 KiB
Rust
150 lines
4.1 KiB
Rust
use ratatui::buffer::Buffer;
|
|
use ratatui::layout::Rect;
|
|
use ratatui::style::Stylize;
|
|
use ratatui::text::Line;
|
|
use ratatui::widgets::Paragraph;
|
|
|
|
use crate::render::renderable::Renderable;
|
|
use crate::wrapping::RtOptions;
|
|
use crate::wrapping::adaptive_wrap_lines;
|
|
|
|
/// Widget that lists inactive threads with outstanding approval requests.
|
|
pub(crate) struct PendingThreadApprovals {
|
|
threads: Vec<String>,
|
|
}
|
|
|
|
impl PendingThreadApprovals {
|
|
pub(crate) fn new() -> Self {
|
|
Self {
|
|
threads: Vec::new(),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn set_threads(&mut self, threads: Vec<String>) -> bool {
|
|
if self.threads == threads {
|
|
return false;
|
|
}
|
|
self.threads = threads;
|
|
true
|
|
}
|
|
|
|
pub(crate) fn is_empty(&self) -> bool {
|
|
self.threads.is_empty()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn threads(&self) -> &[String] {
|
|
&self.threads
|
|
}
|
|
|
|
fn as_renderable(&self, width: u16) -> Box<dyn Renderable> {
|
|
if self.threads.is_empty() || width < 4 {
|
|
return Box::new(());
|
|
}
|
|
|
|
let mut lines = Vec::new();
|
|
for thread in self.threads.iter().take(3) {
|
|
let wrapped = adaptive_wrap_lines(
|
|
std::iter::once(Line::from(format!("Approval needed in {thread}"))),
|
|
RtOptions::new(width as usize)
|
|
.initial_indent(Line::from(vec![" ".into(), "!".red().bold(), " ".into()]))
|
|
.subsequent_indent(Line::from(" ")),
|
|
);
|
|
lines.extend(wrapped);
|
|
}
|
|
|
|
if self.threads.len() > 3 {
|
|
lines.push(Line::from(" ...".dim().italic()));
|
|
}
|
|
|
|
lines.push(
|
|
Line::from(vec![
|
|
" ".into(),
|
|
"/subagents".cyan().bold(),
|
|
" to review requests".dim(),
|
|
])
|
|
.dim(),
|
|
);
|
|
|
|
Paragraph::new(lines).into()
|
|
}
|
|
}
|
|
|
|
impl Renderable for PendingThreadApprovals {
|
|
fn render(&self, area: Rect, buf: &mut Buffer) {
|
|
if area.is_empty() {
|
|
return;
|
|
}
|
|
|
|
self.as_renderable(area.width).render(area, buf);
|
|
}
|
|
|
|
fn desired_height(&self, width: u16) -> u16 {
|
|
self.as_renderable(width).desired_height(width)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use insta::assert_snapshot;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
fn snapshot_rows(widget: &PendingThreadApprovals, width: u16) -> String {
|
|
let height = widget.desired_height(width);
|
|
let mut buf = Buffer::empty(Rect::new(0, 0, width, height));
|
|
widget.render(Rect::new(0, 0, width, height), &mut buf);
|
|
|
|
(0..height)
|
|
.map(|y| {
|
|
(0..width)
|
|
.map(|x| buf[(x, y)].symbol().chars().next().unwrap_or(' '))
|
|
.collect::<String>()
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\n")
|
|
}
|
|
|
|
#[test]
|
|
fn desired_height_empty() {
|
|
let widget = PendingThreadApprovals::new();
|
|
assert_eq!(widget.desired_height(/*width*/ 40), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn render_single_thread_snapshot() {
|
|
let mut widget = PendingThreadApprovals::new();
|
|
widget.set_threads(vec!["Robie [explorer]".to_string()]);
|
|
|
|
assert_snapshot!(
|
|
snapshot_rows(&widget, /*width*/ 40).replace(' ', "."),
|
|
@r"
|
|
..!.Approval.needed.in.Robie.[explorer].
|
|
..../subagents.to.review.requests.......
|
|
"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn render_multiple_threads_snapshot() {
|
|
let mut widget = PendingThreadApprovals::new();
|
|
widget.set_threads(vec![
|
|
"Main [default]".to_string(),
|
|
"Robie [explorer]".to_string(),
|
|
"Inspector".to_string(),
|
|
"Extra agent".to_string(),
|
|
]);
|
|
|
|
assert_snapshot!(
|
|
snapshot_rows(&widget, /*width*/ 44).replace(' ', "."),
|
|
@r"
|
|
..!.Approval.needed.in.Main.[default].......
|
|
..!.Approval.needed.in.Robie.[explorer].....
|
|
..!.Approval.needed.in.Inspector............
|
|
............................................
|
|
..../subagents.to.review.requests...........
|
|
"
|
|
);
|
|
}
|
|
}
|