tui: fix approval dialog for large commands (#3087)
#### Summary - Emit a “Proposed Command” history cell when an ExecApprovalRequest arrives (parity with proposed patches). - Simplify the approval dialog: show only the reason/instructions; move the command preview into history. - Make approval/abort decision history concise: - Single line snippet; if multiline, show first line + " ...". - Truncate to 80 graphemes with ellipsis for very long commands. #### Details - History - Add `new_proposed_command` to render a header and indented command preview. - Use shared `prefix_lines` helper for first/subsequent line prefixes. - Approval UI - `UserApprovalWidget` no longer renders the command in the modal; shows optional `reason` text only. - Decision history renders an inline, dimmed snippet per rules above. - Tests (snapshot-based) - Proposed/decision flow for short command. - Proposed multi-line + aborted decision snippet with “ ...”. - Very long one-line command -> truncated snippet with “…”. - Updated existing exec approval snapshots and test reasons. <img width="1053" height="704" alt="Screenshot 2025-09-03 at 11 57 35 AM" src="https://github.com/user-attachments/assets/9ed4c316-9daf-4ac1-80ff-7ae1f481dd10" /> after approving: <img width="1053" height="704" alt="Screenshot 2025-09-03 at 11 58 18 AM" src="https://github.com/user-attachments/assets/a44e243f-eb9d-42ea-87f4-171b3fb481e7" /> rejection: <img width="1053" height="207" alt="Screenshot 2025-09-03 at 11 58 45 AM" src="https://github.com/user-attachments/assets/a022664b-ae0e-4b70-a388-509208707934" /> big command: https://github.com/user-attachments/assets/2dd976e5-799f-4af7-9682-a046e66cc494
This commit is contained in:
@@ -265,6 +265,104 @@ fn lines_to_single_string(lines: &[ratatui::text::Line<'static>]) -> String {
|
||||
s
|
||||
}
|
||||
|
||||
// (removed experimental resize snapshot test)
|
||||
|
||||
#[test]
|
||||
fn exec_approval_emits_proposed_command_and_decision_history() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
|
||||
|
||||
// Trigger an exec approval request with a short, single-line command
|
||||
let ev = ExecApprovalRequestEvent {
|
||||
call_id: "call-short".into(),
|
||||
command: vec!["bash".into(), "-lc".into(), "echo hello world".into()],
|
||||
cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
|
||||
reason: Some(
|
||||
"this is a test reason such as one that would be produced by the model".into(),
|
||||
),
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-short".into(),
|
||||
msg: EventMsg::ExecApprovalRequest(ev),
|
||||
});
|
||||
|
||||
// Snapshot the Proposed Command cell emitted into history
|
||||
let proposed = drain_insert_history(&mut rx)
|
||||
.pop()
|
||||
.expect("expected proposed command cell");
|
||||
assert_snapshot!(
|
||||
"exec_approval_history_proposed_short",
|
||||
lines_to_single_string(&proposed)
|
||||
);
|
||||
|
||||
// Approve via keyboard and verify a concise decision history line is added
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
|
||||
let decision = drain_insert_history(&mut rx)
|
||||
.pop()
|
||||
.expect("expected decision cell in history");
|
||||
assert_snapshot!(
|
||||
"exec_approval_history_decision_approved_short",
|
||||
lines_to_single_string(&decision)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_approval_decision_truncates_multiline_and_long_commands() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
|
||||
|
||||
// Multiline command: should render proposed command fully in history with prefixes
|
||||
let ev_multi = ExecApprovalRequestEvent {
|
||||
call_id: "call-multi".into(),
|
||||
command: vec!["bash".into(), "-lc".into(), "echo line1\necho line2".into()],
|
||||
cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
|
||||
reason: Some(
|
||||
"this is a test reason such as one that would be produced by the model".into(),
|
||||
),
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-multi".into(),
|
||||
msg: EventMsg::ExecApprovalRequest(ev_multi),
|
||||
});
|
||||
let proposed_multi = drain_insert_history(&mut rx)
|
||||
.pop()
|
||||
.expect("expected proposed multiline command cell");
|
||||
assert_snapshot!(
|
||||
"exec_approval_history_proposed_multiline",
|
||||
lines_to_single_string(&proposed_multi)
|
||||
);
|
||||
|
||||
// Deny via keyboard; decision snippet should be single-line and elided with " ..."
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE));
|
||||
let aborted_multi = drain_insert_history(&mut rx)
|
||||
.pop()
|
||||
.expect("expected aborted decision cell (multiline)");
|
||||
assert_snapshot!(
|
||||
"exec_approval_history_decision_aborted_multiline",
|
||||
lines_to_single_string(&aborted_multi)
|
||||
);
|
||||
|
||||
// Very long single-line command: decision snippet should be truncated <= 80 chars with trailing ...
|
||||
let long = format!("echo {}", "a".repeat(200));
|
||||
let ev_long = ExecApprovalRequestEvent {
|
||||
call_id: "call-long".into(),
|
||||
command: vec!["bash".into(), "-lc".into(), long.clone()],
|
||||
cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
|
||||
reason: None,
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-long".into(),
|
||||
msg: EventMsg::ExecApprovalRequest(ev_long),
|
||||
});
|
||||
drain_insert_history(&mut rx); // proposed cell not needed for this assertion
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE));
|
||||
let aborted_long = drain_insert_history(&mut rx)
|
||||
.pop()
|
||||
.expect("expected aborted decision cell (long)");
|
||||
assert_snapshot!(
|
||||
"exec_approval_history_decision_aborted_long",
|
||||
lines_to_single_string(&aborted_long)
|
||||
);
|
||||
}
|
||||
|
||||
// --- Small helpers to tersely drive exec begin/end and snapshot active cell ---
|
||||
fn begin_exec(chat: &mut ChatWidget, call_id: &str, raw_cmd: &str) {
|
||||
// Build the full command vec and parse it using core's parser,
|
||||
@@ -714,7 +812,9 @@ fn approval_modal_exec_snapshot() {
|
||||
call_id: "call-approve-cmd".into(),
|
||||
command: vec!["bash".into(), "-lc".into(), "echo hello world".into()],
|
||||
cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
|
||||
reason: Some("Model wants to run a command".into()),
|
||||
reason: Some(
|
||||
"this is a test reason such as one that would be produced by the model".into(),
|
||||
),
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-approve".into(),
|
||||
@@ -907,7 +1007,9 @@ fn status_widget_and_approval_modal_snapshot() {
|
||||
call_id: "call-approve-exec".into(),
|
||||
command: vec!["echo".into(), "hello world".into()],
|
||||
cwd: std::path::PathBuf::from("/tmp"),
|
||||
reason: Some("Codex wants to run a command".into()),
|
||||
reason: Some(
|
||||
"this is a test reason such as one that would be produced by the model".into(),
|
||||
),
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
id: "sub-approve-exec".into(),
|
||||
|
||||
Reference in New Issue
Block a user