mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
Show file destinations in TUI change approvals (#39285)
## What changed - Show a description and the destination paths affected by each file-change approval. - Include both source and target paths for moves, format cross-platform paths for display, and show `unavailable` when change details are missing. - Recover file changes from buffered events and thread snapshots so live and replayed approvals retain their destination details. ## Testing - Add coverage for active and replayed approvals, moved and cross-platform paths, and the missing-destination fallback. GitOrigin-RevId: 24f90b313fd8643f90cbead1504c788b140d7fed
This commit is contained in:
@@ -217,6 +217,7 @@ mod background_requests;
|
||||
mod config_persistence;
|
||||
mod connector_mentions;
|
||||
mod event_dispatch;
|
||||
mod file_change_approvals;
|
||||
mod history_pagination;
|
||||
mod history_ui;
|
||||
mod input;
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
//! Recover file-change details for live and replayed approval requests.
|
||||
|
||||
use super::*;
|
||||
|
||||
impl App {
|
||||
pub(super) async fn thread_file_change_changes(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
turn_id: &str,
|
||||
item_id: &str,
|
||||
) -> Option<Vec<codex_app_server_protocol::FileUpdateChange>> {
|
||||
let channel = self.thread_event_channels.get(&thread_id)?;
|
||||
let store = channel.store.lock().await;
|
||||
store.file_change_changes(turn_id, item_id)
|
||||
}
|
||||
|
||||
pub(super) async fn handle_thread_event_now_recovering_file_changes(
|
||||
&mut self,
|
||||
event: ThreadBufferedEvent,
|
||||
) {
|
||||
let changes = match (&event, self.active_thread_id) {
|
||||
(ThreadBufferedEvent::Request(request), Some(thread_id)) => match request.as_ref() {
|
||||
ServerRequest::FileChangeRequestApproval { params, .. } => {
|
||||
self.thread_file_change_changes(thread_id, ¶ms.turn_id, ¶ms.item_id)
|
||||
.await
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
match (changes, event) {
|
||||
(Some(changes), ThreadBufferedEvent::Request(request)) => {
|
||||
if self
|
||||
.pending_app_server_requests
|
||||
.contains_server_request(request.as_ref())
|
||||
{
|
||||
self.handle_file_change_request(*request, changes);
|
||||
}
|
||||
}
|
||||
(_, event) => self.handle_thread_event_now(event),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn handle_file_change_request(
|
||||
&mut self,
|
||||
request: ServerRequest,
|
||||
changes: Vec<codex_app_server_protocol::FileUpdateChange>,
|
||||
) {
|
||||
let ServerRequest::FileChangeRequestApproval { params, .. } = request else {
|
||||
return;
|
||||
};
|
||||
self.chat_widget.on_apply_patch_approval_request(
|
||||
params.item_id.clone(),
|
||||
crate::approval_events::ApplyPatchApprovalRequestEvent {
|
||||
call_id: params.item_id,
|
||||
turn_id: params.turn_id,
|
||||
changes: crate::app_server_approval_conversions::file_update_changes_to_display(
|
||||
changes,
|
||||
),
|
||||
reason: params.reason,
|
||||
grant_root: params.grant_root,
|
||||
},
|
||||
);
|
||||
if self.startup_protected_input_boundary && !self.chat_widget.has_active_view() {
|
||||
self.startup_pending_protected_request = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3435,6 +3435,125 @@ async fn inactive_thread_file_change_approval_recovers_buffered_changes() {
|
||||
assert!(rendered.contains("1 +hello"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn active_thread_file_change_approval_recovers_buffered_changes() {
|
||||
let (mut app, _app_event_rx, _op_rx) = make_test_app_with_channels().await;
|
||||
let thread_id = ThreadId::new();
|
||||
app.active_thread_id = Some(thread_id);
|
||||
app.startup_protected_input_boundary = true;
|
||||
app.enqueue_thread_notification(
|
||||
thread_id,
|
||||
ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
thread_id: thread_id.to_string(),
|
||||
turn_id: "turn-active-approval".to_string(),
|
||||
started_at_ms: 0,
|
||||
item: ThreadItem::FileChange {
|
||||
id: "patch-active-approval".to_string(),
|
||||
changes: vec![FileUpdateChange {
|
||||
path: "visible-target.md".to_string(),
|
||||
kind: PatchChangeKind::Add,
|
||||
diff: "hello\n".to_string(),
|
||||
}],
|
||||
status: codex_app_server_protocol::PatchApplyStatus::InProgress,
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("enqueue file change item");
|
||||
|
||||
let request = ServerRequest::FileChangeRequestApproval {
|
||||
request_id: AppServerRequestId::Integer(10),
|
||||
params: FileChangeRequestApprovalParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
turn_id: "turn-active-approval".to_string(),
|
||||
item_id: "patch-active-approval".to_string(),
|
||||
started_at_ms: 0,
|
||||
reason: None,
|
||||
grant_root: None,
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
app.pending_app_server_requests
|
||||
.note_server_request(&request),
|
||||
None
|
||||
);
|
||||
app.chat_widget.handle_server_notification(
|
||||
agent_message_delta_notification(thread_id, "turn-active-approval", "agent-1", "streaming"),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
let event = ThreadBufferedEvent::Request(Box::new(request));
|
||||
app.handle_thread_event_now_recovering_file_changes(event)
|
||||
.await;
|
||||
|
||||
assert!(!app.chat_widget.has_active_view());
|
||||
assert!(app.startup_pending_protected_request);
|
||||
app.chat_widget.handle_server_notification(
|
||||
ServerNotification::ItemCompleted(codex_app_server_protocol::ItemCompletedNotification {
|
||||
thread_id: thread_id.to_string(),
|
||||
turn_id: "turn-active-approval".to_string(),
|
||||
completed_at_ms: 0,
|
||||
item: ThreadItem::AgentMessage {
|
||||
id: "agent-1".to_string(),
|
||||
text: "streaming".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
},
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
|
||||
let rendered = render_bottom_popup(&app.chat_widget, /*width*/ 100);
|
||||
let destination = app.chat_widget.config_ref().cwd.join("visible-target.md");
|
||||
assert!(rendered.contains("Description: Apply proposed file edits"));
|
||||
assert!(rendered.contains(&format!("Destination: {}", destination.display())));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn replayed_file_change_approval_recovers_snapshot_changes() {
|
||||
let (mut app, _app_event_rx, _op_rx) = make_test_app_with_channels().await;
|
||||
let thread_id = ThreadId::new();
|
||||
let cwd = test_path_buf("/tmp/project").abs();
|
||||
app.replay_thread_snapshot(
|
||||
ThreadEventSnapshot {
|
||||
session: Some(test_thread_session(thread_id, cwd.clone().into_path_buf())),
|
||||
turns: vec![test_turn(
|
||||
"turn-replayed-approval",
|
||||
TurnStatus::InProgress,
|
||||
vec![ThreadItem::FileChange {
|
||||
id: "patch-replayed-approval".to_string(),
|
||||
changes: vec![FileUpdateChange {
|
||||
path: "visible-target.md".to_string(),
|
||||
kind: PatchChangeKind::Add,
|
||||
diff: "hello\n".to_string(),
|
||||
}],
|
||||
status: codex_app_server_protocol::PatchApplyStatus::InProgress,
|
||||
}],
|
||||
)],
|
||||
events: vec![ThreadBufferedEvent::Request(Box::new(
|
||||
ServerRequest::FileChangeRequestApproval {
|
||||
request_id: AppServerRequestId::Integer(11),
|
||||
params: FileChangeRequestApprovalParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
turn_id: "turn-replayed-approval".to_string(),
|
||||
item_id: "patch-replayed-approval".to_string(),
|
||||
started_at_ms: 0,
|
||||
reason: None,
|
||||
grant_root: None,
|
||||
},
|
||||
},
|
||||
))],
|
||||
input_state: None,
|
||||
},
|
||||
/*resume_restored_queue*/ false,
|
||||
);
|
||||
|
||||
let rendered = render_bottom_popup(&app.chat_widget, /*width*/ 100).replace(
|
||||
&cwd.join("visible-target.md").display().to_string(),
|
||||
"/tmp/project/visible-target.md",
|
||||
);
|
||||
assert_app_snapshot!("replayed_file_change_approval", rendered);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn inactive_thread_permissions_approval_preserves_file_system_permissions() {
|
||||
let app = make_test_app().await;
|
||||
|
||||
@@ -196,35 +196,7 @@ impl ThreadEventStore {
|
||||
turn_id: &str,
|
||||
item_id: &str,
|
||||
) -> Option<Vec<codex_app_server_protocol::FileUpdateChange>> {
|
||||
self.buffer
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|event| match event {
|
||||
ThreadBufferedEvent::Notification(notification) => match notification.as_ref() {
|
||||
ServerNotification::ItemStarted(notification)
|
||||
if turn_id_matches(turn_id, ¬ification.turn_id) =>
|
||||
{
|
||||
file_change_item_changes(¬ification.item, item_id)
|
||||
}
|
||||
ServerNotification::ItemCompleted(notification)
|
||||
if turn_id_matches(turn_id, ¬ification.turn_id) =>
|
||||
{
|
||||
file_change_item_changes(¬ification.item, item_id)
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
ThreadBufferedEvent::Request(_)
|
||||
| ThreadBufferedEvent::HistoryEntryResponse(_)
|
||||
| ThreadBufferedEvent::FeedbackSubmission(_) => None,
|
||||
})
|
||||
.or_else(|| {
|
||||
self.turns
|
||||
.iter()
|
||||
.rev()
|
||||
.filter(|turn| turn_id_matches(turn_id, &turn.id))
|
||||
.flat_map(|turn| turn.items.iter().rev())
|
||||
.find_map(|item| file_change_item_changes(item, item_id))
|
||||
})
|
||||
file_change_changes(self.buffer.iter(), &self.turns, turn_id, item_id)
|
||||
}
|
||||
|
||||
pub(super) fn snapshot(&self) -> ThreadEventSnapshot {
|
||||
@@ -298,6 +270,38 @@ fn turn_id_matches(request_turn_id: &str, candidate_turn_id: &str) -> bool {
|
||||
request_turn_id.is_empty() || request_turn_id == candidate_turn_id
|
||||
}
|
||||
|
||||
pub(super) fn file_change_changes<'a>(
|
||||
events: impl DoubleEndedIterator<Item = &'a ThreadBufferedEvent>,
|
||||
turns: &'a [Turn],
|
||||
turn_id: &str,
|
||||
item_id: &str,
|
||||
) -> Option<Vec<codex_app_server_protocol::FileUpdateChange>> {
|
||||
let event_items = events.rev().filter_map(|event| {
|
||||
let ThreadBufferedEvent::Notification(notification) = event else {
|
||||
return None;
|
||||
};
|
||||
let (candidate_turn_id, item) = match notification.as_ref() {
|
||||
ServerNotification::ItemStarted(notification) => {
|
||||
(¬ification.turn_id, ¬ification.item)
|
||||
}
|
||||
ServerNotification::ItemCompleted(notification) => {
|
||||
(¬ification.turn_id, ¬ification.item)
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
turn_id_matches(turn_id, candidate_turn_id).then_some(item)
|
||||
});
|
||||
event_items
|
||||
.chain(
|
||||
turns
|
||||
.iter()
|
||||
.rev()
|
||||
.filter(|turn| turn_id_matches(turn_id, &turn.id))
|
||||
.flat_map(|turn| turn.items.iter().rev()),
|
||||
)
|
||||
.find_map(|item| file_change_item_changes(item, item_id))
|
||||
}
|
||||
|
||||
fn file_change_item_changes(
|
||||
item: &ThreadItem,
|
||||
item_id: &str,
|
||||
|
||||
@@ -202,17 +202,6 @@ impl App {
|
||||
store.session.as_ref().map(|session| session.cwd.clone())
|
||||
}
|
||||
|
||||
async fn thread_file_change_changes(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
turn_id: &str,
|
||||
item_id: &str,
|
||||
) -> Option<Vec<codex_app_server_protocol::FileUpdateChange>> {
|
||||
let channel = self.thread_event_channels.get(&thread_id)?;
|
||||
let store = channel.store.lock().await;
|
||||
store.file_change_changes(turn_id, item_id)
|
||||
}
|
||||
|
||||
pub(super) async fn interactive_request_for_thread_request(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
@@ -1451,7 +1440,10 @@ impl App {
|
||||
let mut disconnected = false;
|
||||
loop {
|
||||
match rx.try_recv() {
|
||||
Ok(event) => self.handle_thread_event_now(event),
|
||||
Ok(event) => {
|
||||
self.handle_thread_event_now_recovering_file_changes(event)
|
||||
.await
|
||||
}
|
||||
Err(TryRecvError::Empty) => break,
|
||||
Err(TryRecvError::Disconnected) => {
|
||||
disconnected = true;
|
||||
@@ -1503,6 +1495,25 @@ impl App {
|
||||
snapshot: ThreadEventSnapshot,
|
||||
resume_restored_queue: bool,
|
||||
) {
|
||||
let request_changes = snapshot
|
||||
.events
|
||||
.iter()
|
||||
.map(|event| {
|
||||
let ThreadBufferedEvent::Request(request) = event else {
|
||||
return None;
|
||||
};
|
||||
let ServerRequest::FileChangeRequestApproval { params, .. } = request.as_ref()
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
file_change_changes(
|
||||
snapshot.events.iter(),
|
||||
&snapshot.turns,
|
||||
¶ms.turn_id,
|
||||
¶ms.item_id,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
self.refresh_mcp_startup_expected_servers_from_config();
|
||||
let should_buffer_replay = !snapshot.turns.is_empty() || !snapshot.events.is_empty();
|
||||
if should_buffer_replay {
|
||||
@@ -1536,11 +1547,16 @@ impl App {
|
||||
self.chat_widget
|
||||
.replay_thread_turns(snapshot.turns, ReplayKind::ThreadSnapshot);
|
||||
}
|
||||
for event in snapshot.events {
|
||||
for (event, changes) in snapshot.events.into_iter().zip(request_changes) {
|
||||
if suppress_replay_notices && replay_filter::event_is_notice(&event) {
|
||||
continue;
|
||||
}
|
||||
self.handle_thread_event_replay(event);
|
||||
match (event, changes) {
|
||||
(ThreadBufferedEvent::Request(request), Some(changes)) => {
|
||||
self.handle_file_change_request(*request, changes)
|
||||
}
|
||||
(event, _) => self.handle_thread_event_replay(event),
|
||||
}
|
||||
}
|
||||
if should_buffer_replay {
|
||||
self.app_event_tx
|
||||
@@ -1753,7 +1769,8 @@ impl App {
|
||||
self.pending_shutdown_exit_thread_id = None;
|
||||
}
|
||||
let had_active_view = self.chat_widget.has_active_view();
|
||||
self.handle_thread_event_now(event);
|
||||
self.handle_thread_event_now_recovering_file_changes(event)
|
||||
.await;
|
||||
if !had_active_view
|
||||
&& self.chat_widget.has_active_view()
|
||||
&& self.startup_protected_input_boundary
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
//! Render file-change approval descriptions and cross-platform destinations.
|
||||
|
||||
use super::approval_overlay::ApplyPatchApprovalRequest;
|
||||
use crate::diff_model::FileChange;
|
||||
use crate::render::renderable::Renderable;
|
||||
use codex_utils_path_uri::LegacyAppPathString;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::widgets::Wrap;
|
||||
|
||||
pub(super) fn build_header(request: &ApplyPatchApprovalRequest) -> Box<dyn Renderable> {
|
||||
let mut header = Vec::new();
|
||||
if let Some(thread_label) = &request.thread_label {
|
||||
header.push(Line::from(vec![
|
||||
"Thread: ".into(),
|
||||
thread_label.clone().bold(),
|
||||
]));
|
||||
}
|
||||
|
||||
if !header.is_empty() {
|
||||
header.push(Line::from(""));
|
||||
}
|
||||
|
||||
let description = request
|
||||
.reason
|
||||
.as_deref()
|
||||
.filter(|reason| !reason.is_empty())
|
||||
.unwrap_or("Apply proposed file edits");
|
||||
header.push(Line::from_iter([
|
||||
"Description: ".into(),
|
||||
description.to_string().italic(),
|
||||
]));
|
||||
|
||||
let mut destinations: Vec<_> = request
|
||||
.changes
|
||||
.iter()
|
||||
.flat_map(|(path, change)| {
|
||||
let move_destination = match change {
|
||||
FileChange::Update {
|
||||
move_path: Some(move_path),
|
||||
..
|
||||
} => Some(move_path),
|
||||
FileChange::Add { .. }
|
||||
| FileChange::Delete { .. }
|
||||
| FileChange::Update {
|
||||
move_path: None, ..
|
||||
} => None,
|
||||
};
|
||||
std::iter::once(path).chain(move_destination)
|
||||
})
|
||||
.map(|path| {
|
||||
LegacyAppPathString::from_path(path)
|
||||
.to_inferred_path_uri()
|
||||
.map(|path| path.inferred_native_path_string())
|
||||
.unwrap_or_else(|| request.cwd.join(path).display().to_string())
|
||||
})
|
||||
.collect();
|
||||
destinations.sort();
|
||||
destinations.dedup();
|
||||
if destinations.is_empty() {
|
||||
header.push(Line::from(vec![
|
||||
"Destination: ".into(),
|
||||
"unavailable".bold(),
|
||||
]));
|
||||
}
|
||||
for destination in destinations {
|
||||
header.push(Line::from_iter([
|
||||
"Destination: ".into(),
|
||||
destination.bold(),
|
||||
]));
|
||||
}
|
||||
Box::new(Paragraph::new(header).wrap(Wrap { trim: false }))
|
||||
}
|
||||
@@ -742,30 +742,7 @@ fn build_header(request: &ApprovalRequest) -> Box<dyn Renderable> {
|
||||
}
|
||||
Box::new(Paragraph::new(header).wrap(Wrap { trim: false }))
|
||||
}
|
||||
ApprovalRequest::ApplyPatch(request) => {
|
||||
let mut header: Vec<Box<dyn Renderable>> = Vec::new();
|
||||
if let Some(thread_label) = &request.thread_label {
|
||||
header.push(Box::new(Line::from(vec![
|
||||
"Thread: ".into(),
|
||||
thread_label.clone().bold(),
|
||||
])));
|
||||
}
|
||||
if let Some(reason) = &request.reason
|
||||
&& !reason.is_empty()
|
||||
{
|
||||
if !header.is_empty() {
|
||||
header.push(Box::new(Line::from("")));
|
||||
}
|
||||
header.push(Box::new(
|
||||
Paragraph::new(Line::from_iter([
|
||||
"Reason: ".into(),
|
||||
reason.clone().italic(),
|
||||
]))
|
||||
.wrap(Wrap { trim: false }),
|
||||
));
|
||||
}
|
||||
Box::new(ColumnRenderable::with(header))
|
||||
}
|
||||
ApprovalRequest::ApplyPatch(request) => super::apply_patch_header::build_header(request),
|
||||
ApprovalRequest::McpElicitation(request) => {
|
||||
let mut lines = Vec::new();
|
||||
if let Some(thread_label) = &request.thread_label {
|
||||
@@ -1124,6 +1101,8 @@ mod tests {
|
||||
use codex_protocol::models::FileSystemPermissions;
|
||||
use codex_protocol::models::NetworkPermissions;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_absolute_path::test_support::PathExt;
|
||||
use codex_utils_absolute_path::test_support::test_path_buf;
|
||||
use crossterm::event::KeyModifiers;
|
||||
use insta::assert_snapshot;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -2128,12 +2107,30 @@ mod tests {
|
||||
content: "one\ntwo\nthree\n".to_string(),
|
||||
},
|
||||
);
|
||||
changes.insert(
|
||||
PathBuf::from("old.txt"),
|
||||
FileChange::Update {
|
||||
unified_diff: String::new(),
|
||||
move_path: Some(PathBuf::from("new.txt")),
|
||||
},
|
||||
);
|
||||
#[cfg(windows)]
|
||||
let (foreign_path, foreign_move_path) = ("/remote/old.txt", "/remote/new.txt");
|
||||
#[cfg(not(windows))]
|
||||
let (foreign_path, foreign_move_path) = (r"C:\workspace\old.txt", r"C:\workspace\new.txt");
|
||||
changes.insert(
|
||||
PathBuf::from(foreign_path),
|
||||
FileChange::Update {
|
||||
unified_diff: String::new(),
|
||||
move_path: Some(PathBuf::from(foreign_move_path)),
|
||||
},
|
||||
);
|
||||
let request = ApprovalRequest::ApplyPatch(ApplyPatchApprovalRequest {
|
||||
thread_id: ThreadId::new(),
|
||||
thread_label: Some("Banach [worker]".to_string()),
|
||||
id: "test".to_string(),
|
||||
reason: None,
|
||||
cwd: absolute_path("/tmp"),
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
changes,
|
||||
});
|
||||
let keymap = crate::keymap::RuntimeKeymap::defaults();
|
||||
@@ -2146,10 +2143,36 @@ mod tests {
|
||||
);
|
||||
let rendered = render_overlay_lines(&view, /*width*/ 120);
|
||||
assert!(rendered.contains("Thread: Banach [worker]"));
|
||||
assert!(rendered.contains("Description: Apply proposed file edits"));
|
||||
for path in ["/tmp/bug1.txt", "/tmp/new.txt", "/tmp/old.txt"] {
|
||||
assert!(rendered.contains(&format!("Destination: {}", test_path_buf(path).display())));
|
||||
}
|
||||
for path in [foreign_path, foreign_move_path] {
|
||||
assert!(rendered.contains(&format!("Destination: {path}")));
|
||||
}
|
||||
assert!(rendered.contains("o to open thread"));
|
||||
assert!(!rendered.contains("$ apply_patch"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_patch_prompt_without_changes_shows_unavailable_destination() {
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx);
|
||||
let request = ApprovalRequest::ApplyPatch(ApplyPatchApprovalRequest {
|
||||
thread_id: ThreadId::new(),
|
||||
thread_label: None,
|
||||
id: "test".to_string(),
|
||||
reason: None,
|
||||
cwd: absolute_path("/tmp"),
|
||||
changes: HashMap::new(),
|
||||
});
|
||||
let view = make_overlay(request, tx, Features::with_defaults());
|
||||
assert_snapshot!(
|
||||
"approval_overlay_patch_destination_unavailable",
|
||||
normalize_snapshot_paths(render_overlay_lines(&view, /*width*/ 120))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_exec_prompt_title_includes_host() {
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
|
||||
@@ -56,6 +56,7 @@ use std::time::Instant;
|
||||
|
||||
mod action_required_title;
|
||||
mod app_link_view;
|
||||
mod apply_patch_header;
|
||||
mod approval_overlay;
|
||||
mod mcp_server_elicitation;
|
||||
mod multi_select_picker;
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/approval_overlay.rs
|
||||
expression: "normalize_snapshot_paths(render_overlay_lines(&view, 120))"
|
||||
---
|
||||
Would you like to make the following edits?
|
||||
|
||||
Description: Apply proposed file edits
|
||||
Destination: unavailable
|
||||
|
||||
› 1. Yes, proceed (y)
|
||||
2. Yes, and don't ask again for these files (a)
|
||||
3. No, and tell Codex what to do differently (esc)
|
||||
|
||||
Press enter to confirm or esc to cancel
|
||||
+2
-1
@@ -4,7 +4,8 @@ expression: contents
|
||||
---
|
||||
Would you like to make the following edits?
|
||||
|
||||
Reason: The model wants to apply changes
|
||||
Description: The model wants to apply changes
|
||||
Destination: /tmp/project/README.md
|
||||
|
||||
› 1. Yes, proceed (y)
|
||||
2. Yes, and don't ask again for these files (a)
|
||||
|
||||
@@ -1691,6 +1691,7 @@ async fn approval_modal_exec_multiline_prefix_hides_execpolicy_option_snapshot()
|
||||
#[tokio::test]
|
||||
async fn approval_modal_patch_snapshot() -> anyhow::Result<()> {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.config.cwd = test_path_buf("/tmp/project").abs();
|
||||
chat.config
|
||||
.permissions
|
||||
.approval_policy
|
||||
@@ -1709,7 +1710,7 @@ async fn approval_modal_patch_snapshot() -> anyhow::Result<()> {
|
||||
turn_id: "turn-approve-patch".into(),
|
||||
changes,
|
||||
reason: Some("The model wants to apply changes".into()),
|
||||
grant_root: Some(PathBuf::from("/tmp")),
|
||||
grant_root: Some(test_path_buf("/tmp/project")),
|
||||
};
|
||||
handle_apply_patch_approval_request(&mut chat, "sub-approve-patch", ev);
|
||||
|
||||
@@ -1722,7 +1723,7 @@ async fn approval_modal_patch_snapshot() -> anyhow::Result<()> {
|
||||
.expect("draw patch approval modal");
|
||||
let contents = terminal.backend().vt100().screen().contents();
|
||||
assert!(!contents.contains("$ apply_patch"));
|
||||
assert_chatwidget_snapshot!("approval_modal_patch", contents);
|
||||
assert_chatwidget_snapshot!("approval_modal_patch", normalize_snapshot_paths(contents));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ pub(super) fn normalize_snapshot_paths(text: impl Into<String>) -> String {
|
||||
text = text.replace(&platform_path, unix_path);
|
||||
}
|
||||
}
|
||||
text = text.replace("/tmp/project\\", "/tmp/project/");
|
||||
|
||||
let platform_test_cwd = test_path_display("/tmp/project");
|
||||
if platform_test_cwd == "/tmp/project" {
|
||||
|
||||
@@ -14,7 +14,7 @@ impl ChatWidget {
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn on_apply_patch_approval_request(
|
||||
pub(crate) fn on_apply_patch_approval_request(
|
||||
&mut self,
|
||||
_id: String,
|
||||
ev: ApplyPatchApprovalRequestEvent,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
source: tui/src/app/tests.rs
|
||||
expression: rendered
|
||||
---
|
||||
Would you like to make the following edits?
|
||||
|
||||
Description: Apply proposed file edits
|
||||
Destination: /tmp/project/visible-target.md
|
||||
|
||||
› 1. Yes, proceed (y)
|
||||
2. Yes, and don't ask again for these files (a)
|
||||
3. No, and tell Codex what to do differently (esc)
|
||||
|
||||
Press enter to confirm or esc to cancel
|
||||
Reference in New Issue
Block a user