mirror of
https://github.com/openai/codex.git
synced 2026-09-06 15:29:32 +00:00
## Why `codex-rs/tui/src/history_cell.rs` had become the dumping ground for transcript rendering: the shared trait, common helpers, and the concrete cells for messages, plans, MCP/search, notices, patches, approvals, session chrome, and separators all lived together. That made small transcript changes require reopening a very large file and made ownership less obvious. ## What changed - Replaced the monolithic `history_cell.rs` with a `history_cell/` module tree organized by concern. - Kept the existing `crate::history_cell::*` surface stable through re-exports in `history_cell/mod.rs`. - Moved the existing render coverage into `history_cell/tests.rs`. ## Reviewer notes - This PR is intentionally mechanical in mature — existing code and tests moving into files that match their concern. - The snapshot files under `codex-rs/tui/src/history_cell/snapshots/` moved with the extracted test module. `insta` resolves these unnamed snapshots relative to the source file that declares them, so this is path churn only; snapshot contents were not updated. - The small non-mechanical seam edits are limited to split fallout: sibling-module visibility for shared cell containers, moving approval-specific exec-snippet helpers beside approvals, fixing the separator module path, and keeping a couple of existing test helpers reachable after extraction.
361 lines
12 KiB
Rust
361 lines
12 KiB
Rust
//! 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())],
|
|
}
|
|
}
|