Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6,135 changes: 0 additions & 6,135 deletions codex-rs/tui/src/history_cell.rs

This file was deleted.

360 changes: 360 additions & 0 deletions codex-rs/tui/src/history_cell/approvals.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,360 @@
//! Approval, denial, and review-status transcript cells.

use super::*;

fn truncate_exec_snippet(full_cmd: &str) -> String {
let mut snippet = match full_cmd.split_once('\n') {
Some((first, _)) => format!("{first} ..."),
None => full_cmd.to_string(),
};
snippet = truncate_text(&snippet, /*max_graphemes*/ 80);
snippet
}

fn exec_snippet(command: &[String]) -> String {
let full_cmd = strip_bash_lc_and_escape(command);
truncate_exec_snippet(&full_cmd)
}

fn non_empty_exec_snippet(command: &[String]) -> Option<String> {
let snippet = exec_snippet(command);
(!snippet.is_empty()).then_some(snippet)
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ReviewDecision {
Approved,
ApprovedExecpolicyAmendment {
proposed_execpolicy_amendment: ExecPolicyAmendment,
},
ApprovedForSession,
NetworkPolicyAmendment {
network_policy_amendment: NetworkPolicyAmendment,
},
Denied,
TimedOut,
Abort,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ApprovalDecisionSubject {
Command(Vec<String>),
NetworkAccess { target: String },
}

pub fn new_approval_decision_cell(
subject: ApprovalDecisionSubject,
decision: ReviewDecision,
actor: ApprovalDecisionActor,
) -> Box<dyn HistoryCell> {
use ReviewDecision::*;
use codex_protocol::approvals::NetworkPolicyRuleAction;

let (symbol, summary): (Span<'static>, Vec<Span<'static>>) = match decision {
Approved => match subject {
ApprovalDecisionSubject::Command(command) => {
let summary = if let Some(snippet) = non_empty_exec_snippet(&command) {
vec![
actor.subject().into(),
"approved".bold(),
" codex to run ".into(),
Span::from(snippet).dim(),
" this time".bold(),
]
} else {
vec![
actor.subject().into(),
"approved".bold(),
" this request".into(),
" this time".bold(),
]
};
("✔ ".green(), summary)
}
ApprovalDecisionSubject::NetworkAccess { target } => (
"✔ ".green(),
vec![
actor.subject().into(),
"approved".bold(),
" codex network access to ".into(),
Span::from(target).dim(),
" this time".bold(),
],
),
},
ApprovedExecpolicyAmendment {
proposed_execpolicy_amendment,
} => {
let snippet = Span::from(exec_snippet(&proposed_execpolicy_amendment.command)).dim();
(
"✔ ".green(),
vec![
actor.subject().into(),
"approved".bold(),
" codex to always run commands that start with ".into(),
snippet,
],
)
}
ApprovedForSession => match subject {
ApprovalDecisionSubject::Command(command) => {
let summary = if let Some(snippet) = non_empty_exec_snippet(&command) {
vec![
actor.subject().into(),
"approved".bold(),
" codex to run ".into(),
Span::from(snippet).dim(),
" every time this session".bold(),
]
} else {
vec![
actor.subject().into(),
"approved".bold(),
" this request".into(),
" every time this session".bold(),
]
};
("✔ ".green(), summary)
}
ApprovalDecisionSubject::NetworkAccess { target } => (
"✔ ".green(),
vec![
actor.subject().into(),
"approved".bold(),
" codex network access to ".into(),
Span::from(target).dim(),
" every time this session".bold(),
],
),
},
NetworkPolicyAmendment {
network_policy_amendment,
} => {
let target = match subject {
ApprovalDecisionSubject::NetworkAccess { target } => target,
ApprovalDecisionSubject::Command(_) => network_policy_amendment.host,
};
match network_policy_amendment.action {
NetworkPolicyRuleAction::Allow => (
"✔ ".green(),
vec![
actor.subject().into(),
"persisted".bold(),
" Codex network access to ".into(),
Span::from(target).dim(),
],
),
NetworkPolicyRuleAction::Deny => (
"✗ ".red(),
vec![
actor.subject().into(),
"denied".bold(),
" codex network access to ".into(),
Span::from(target).dim(),
" and saved that rule".into(),
],
),
}
}
Denied => match subject {
ApprovalDecisionSubject::Command(command) => {
let summary = if let Some(snippet) = non_empty_exec_snippet(&command) {
let snippet = Span::from(snippet).dim();
match actor {
ApprovalDecisionActor::User => vec![
actor.subject().into(),
"did not approve".bold(),
" codex to run ".into(),
snippet,
],
ApprovalDecisionActor::Guardian => vec![
"Request ".into(),
"denied".bold(),
" for codex to run ".into(),
snippet,
],
}
} else {
match actor {
ApprovalDecisionActor::User => vec![
actor.subject().into(),
"did not approve".bold(),
" this request".into(),
],
ApprovalDecisionActor::Guardian => {
vec!["Request ".into(), "denied".bold()]
}
}
};
("✗ ".red(), summary)
}
ApprovalDecisionSubject::NetworkAccess { target } => (
"✗ ".red(),
vec![
actor.subject().into(),
"did not approve".bold(),
" codex network access to ".into(),
Span::from(target).dim(),
],
),
},
TimedOut => match subject {
ApprovalDecisionSubject::Command(command) => {
let summary = if let Some(snippet) = non_empty_exec_snippet(&command) {
vec![
"Review ".into(),
"timed out".bold(),
" before codex could run ".into(),
Span::from(snippet).dim(),
]
} else {
vec![
"Review ".into(),
"timed out".bold(),
" before this request could be approved".into(),
]
};
("✗ ".red(), summary)
}
ApprovalDecisionSubject::NetworkAccess { target } => (
"✗ ".red(),
vec![
"Review ".into(),
"timed out".bold(),
" before codex could access ".into(),
Span::from(target).dim(),
],
),
},
Abort => match subject {
ApprovalDecisionSubject::Command(command) => {
let summary = if let Some(snippet) = non_empty_exec_snippet(&command) {
vec![
actor.subject().into(),
"canceled".bold(),
" the request to run ".into(),
Span::from(snippet).dim(),
]
} else {
vec![
actor.subject().into(),
"canceled".bold(),
" this request".into(),
]
};
("✗ ".red(), summary)
}
ApprovalDecisionSubject::NetworkAccess { target } => (
"✗ ".red(),
vec![
actor.subject().into(),
"canceled".bold(),
" the request for codex network access to ".into(),
Span::from(target).dim(),
],
),
},
};

Box::new(PrefixedWrappedHistoryCell::new(
Line::from(summary),
symbol,
" ",
))
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApprovalDecisionActor {
User,
Guardian,
}

impl ApprovalDecisionActor {
fn subject(self) -> &'static str {
match self {
Self::User => "You ",
Self::Guardian => "Auto-reviewer ",
}
}
}

pub fn new_guardian_denied_patch_request(files: Vec<String>) -> Box<dyn HistoryCell> {
let mut summary = vec![
"Request ".into(),
"denied".bold(),
" for codex to apply ".into(),
];
if files.len() == 1 {
summary.push("a patch touching ".into());
summary.push(Span::from(files[0].clone()).dim());
} else {
summary.push("a patch touching ".into());
summary.push(Span::from(files.len().to_string()).dim());
summary.push(" files".into());
}

Box::new(PrefixedWrappedHistoryCell::new(
Line::from(summary),
"✗ ".red(),
" ",
))
}

pub fn new_guardian_denied_action_request(summary: String) -> Box<dyn HistoryCell> {
let line = Line::from(vec![
"Request ".into(),
"denied".bold(),
" for ".into(),
Span::from(summary).dim(),
]);
Box::new(PrefixedWrappedHistoryCell::new(line, "✗ ".red(), " "))
}

pub fn new_guardian_approved_action_request(summary: String) -> Box<dyn HistoryCell> {
let line = Line::from(vec![
"Request ".into(),
"approved".bold(),
" for ".into(),
Span::from(summary).dim(),
]);
Box::new(PrefixedWrappedHistoryCell::new(line, "✔ ".green(), " "))
}

pub fn new_guardian_timed_out_patch_request(files: Vec<String>) -> Box<dyn HistoryCell> {
let mut summary = vec![
"Review ".into(),
"timed out".bold(),
" before codex could apply ".into(),
];
if files.len() == 1 {
summary.push("a patch touching ".into());
summary.push(Span::from(files[0].clone()).dim());
} else {
summary.push("a patch touching ".into());
summary.push(Span::from(files.len().to_string()).dim());
summary.push(" files".into());
}

Box::new(PrefixedWrappedHistoryCell::new(
Line::from(summary),
"✗ ".red(),
" ",
))
}

pub fn new_guardian_timed_out_action_request(summary: String) -> Box<dyn HistoryCell> {
let line = Line::from(vec![
"Review ".into(),
"timed out".bold(),
" before ".into(),
Span::from(summary).dim(),
]);
Box::new(PrefixedWrappedHistoryCell::new(line, "✗ ".red(), " "))
}

/// Cyan history cell line showing the current review status.
pub(crate) fn new_review_status_line(message: String) -> PlainHistoryCell {
PlainHistoryCell {
lines: vec![Line::from(message.cyan())],
}
}
Loading
Loading