Files
codex/codex-rs/tui/src/skill_error_prompt.rs
T
Thibault Sottiaux a8d5ad37b8 feat: experimental support for skills.md (#7412)
This change prototypes support for Skills with the CLI. This is an
**experimental** feature for internal testing.

---------

Co-authored-by: Gav Verma <gverma@openai.com>
2025-12-01 20:22:35 -08:00

165 lines
4.4 KiB
Rust

use crate::tui::FrameRequester;
use crate::tui::Tui;
use crate::tui::TuiEvent;
use codex_core::skills::SkillError;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyEventKind;
use crossterm::event::KeyModifiers;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::prelude::Stylize as _;
use ratatui::text::Line;
use ratatui::widgets::Block;
use ratatui::widgets::Borders;
use ratatui::widgets::Clear;
use ratatui::widgets::Paragraph;
use ratatui::widgets::Widget;
use ratatui::widgets::WidgetRef;
use ratatui::widgets::Wrap;
use tokio_stream::StreamExt;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum SkillErrorPromptOutcome {
Continue,
Exit,
}
pub(crate) async fn run_skill_error_prompt(
tui: &mut Tui,
errors: &[SkillError],
) -> SkillErrorPromptOutcome {
struct AltScreenGuard<'a> {
tui: &'a mut Tui,
}
impl<'a> AltScreenGuard<'a> {
fn enter(tui: &'a mut Tui) -> Self {
let _ = tui.enter_alt_screen();
Self { tui }
}
}
impl Drop for AltScreenGuard<'_> {
fn drop(&mut self) {
let _ = self.tui.leave_alt_screen();
}
}
let alt = AltScreenGuard::enter(tui);
let mut screen = SkillErrorScreen::new(alt.tui.frame_requester(), errors);
let _ = alt.tui.draw(u16::MAX, |frame| {
frame.render_widget_ref(&screen, frame.area());
});
let events = alt.tui.event_stream();
tokio::pin!(events);
while !screen.is_done() {
if let Some(event) = events.next().await {
match event {
TuiEvent::Key(key_event) => screen.handle_key(key_event),
TuiEvent::Paste(_) => {}
TuiEvent::Draw => {
let _ = alt.tui.draw(u16::MAX, |frame| {
frame.render_widget_ref(&screen, frame.area());
});
}
}
} else {
screen.confirm_continue();
break;
}
}
screen.outcome()
}
struct SkillErrorScreen {
request_frame: FrameRequester,
lines: Vec<Line<'static>>,
done: bool,
exit: bool,
}
impl SkillErrorScreen {
fn new(request_frame: FrameRequester, errors: &[SkillError]) -> Self {
let mut lines: Vec<Line<'static>> = Vec::new();
lines.push(Line::from("Skill validation errors detected".bold()));
lines.push(Line::from(
"Fix these SKILL.md files and restart. Invalid skills are ignored until resolved. Press enter or esc to continue, Ctrl+C or Ctrl+D to exit.",
));
lines.push(Line::from(""));
for error in errors {
let message = format!("- {}: {}", error.path.display(), error.message);
lines.push(Line::from(message));
}
Self {
request_frame,
lines,
done: false,
exit: false,
}
}
fn is_done(&self) -> bool {
self.done
}
fn confirm_continue(&mut self) {
self.done = true;
self.exit = false;
self.request_frame.schedule_frame();
}
fn confirm_exit(&mut self) {
self.done = true;
self.exit = true;
self.request_frame.schedule_frame();
}
fn outcome(&self) -> SkillErrorPromptOutcome {
if self.exit {
SkillErrorPromptOutcome::Exit
} else {
SkillErrorPromptOutcome::Continue
}
}
fn handle_key(&mut self, key_event: KeyEvent) {
if key_event.kind == KeyEventKind::Release {
return;
}
if key_event
.modifiers
.intersects(KeyModifiers::CONTROL | KeyModifiers::META)
&& matches!(key_event.code, KeyCode::Char('c') | KeyCode::Char('d'))
{
self.confirm_exit();
return;
}
match key_event.code {
KeyCode::Enter | KeyCode::Esc | KeyCode::Char(' ') | KeyCode::Char('q') => {
self.confirm_continue();
}
_ => {}
}
}
}
impl WidgetRef for &SkillErrorScreen {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
Clear.render(area, buf);
let block = Block::default()
.title("Skill errors".bold())
.borders(Borders::ALL);
Paragraph::new(self.lines.clone())
.block(block)
.wrap(Wrap { trim: true })
.render(area, buf);
}
}