Files
codex/codex-rs/tui/src/app_server_approval_conversions.rs
T
Eric Traut 4abcb8d1da Restore rich tool details in persisted TUI transcripts (#46710)
## Why

Loaded transcripts reduce several tool calls and file changes to basic status summaries, losing details available in live history cells.

## What changed

- Reuse rich history cells for completed commands, MCP calls, patches, agent activity, image generation, and notices.
- Add dynamic tool cells with bounded output previews while retaining arguments and full output in detailed and raw views.
- Preserve last-known statuses for incomplete calls, unavailable results, and declined or failed operations. Keep legacy command text and terminal interaction output when rich reconstruction is unavailable.
- Remove redundant move annotations from patch diffs and retain patch activity IDs.
- Filter untrusted control characters before emitting terminal scrollback, while preserving semantic hyperlinks.

## Testing

Add unit and snapshot coverage for compact and detailed tool rendering, status fallbacks, retained output, patch restoration, and control-character filtering with and without hyperlinks.

GitOrigin-RevId: 73d754262768cf6d91f3ea4040116bf93cf87784
2026-09-19 20:24:41 +00:00

186 lines
7.1 KiB
Rust

//! Narrow conversion helpers for approval-related app-server payloads.
//!
//! The TUI mostly keeps app-server approval types intact. These helpers cover
//! the remaining cases where the UI consumes a private file-change display
//! model or needs to translate a granted permission response for outbound
//! submission.
use crate::diff_model::FileChange;
use codex_app_server_protocol::AdditionalNetworkPermissions;
use codex_app_server_protocol::FileUpdateChange;
use codex_app_server_protocol::GrantedPermissionProfile;
use codex_app_server_protocol::PatchChangeKind;
use codex_protocol::request_permissions::RequestPermissionProfile as CoreRequestPermissionProfile;
use std::collections::HashMap;
use std::path::PathBuf;
pub(crate) fn granted_permission_profile_from_request(
value: CoreRequestPermissionProfile,
) -> GrantedPermissionProfile {
GrantedPermissionProfile {
network: value.network.map(|network| AdditionalNetworkPermissions {
enabled: network.enabled,
}),
file_system: value.file_system.map(Into::into),
}
}
pub(crate) fn file_update_changes_to_display(
changes: Vec<FileUpdateChange>,
) -> HashMap<PathBuf, FileChange> {
changes
.into_iter()
.map(|mut change| {
let path = PathBuf::from(change.path);
let file_change = match change.kind {
PatchChangeKind::Add => FileChange::Add {
content: change.diff,
},
PatchChangeKind::Delete => FileChange::Delete {
content: change.diff,
},
PatchChangeKind::Update { move_path } => {
if let Some(path) = &move_path
&& let Some(diff) = change
.diff
.strip_suffix(&format!("\n\nMoved to: {}", path.display()))
{
change.diff.truncate(diff.len());
}
FileChange::Update {
unified_diff: change.diff,
move_path,
}
}
};
(path, file_change)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::file_update_changes_to_display;
use super::granted_permission_profile_from_request;
use crate::diff_model::FileChange;
use codex_app_server_protocol::AdditionalFileSystemPermissions;
use codex_app_server_protocol::AdditionalNetworkPermissions;
use codex_app_server_protocol::FileSystemAccessMode;
use codex_app_server_protocol::FileSystemPath;
use codex_app_server_protocol::FileSystemSandboxEntry;
use codex_app_server_protocol::FileSystemSpecialPath;
use codex_app_server_protocol::FileUpdateChange;
use codex_app_server_protocol::GrantedPermissionProfile;
use codex_app_server_protocol::PatchChangeKind;
use codex_app_server_protocol::RequestPermissionProfile;
use codex_protocol::request_permissions::RequestPermissionProfile as CoreRequestPermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use std::collections::HashMap;
use std::path::PathBuf;
fn absolute_path(path: &str) -> AbsolutePathBuf {
AbsolutePathBuf::try_from(PathBuf::from(path)).expect("path must be absolute")
}
#[test]
fn converts_file_update_changes_to_display() {
assert_eq!(
file_update_changes_to_display(vec![FileUpdateChange {
path: "foo.txt".to_string(),
kind: PatchChangeKind::Add,
diff: "hello\n".to_string(),
}]),
HashMap::from([(
PathBuf::from("foo.txt"),
FileChange::Add {
content: "hello\n".to_string(),
},
)])
);
}
#[test]
fn converts_request_permissions_into_granted_permissions() {
let request = RequestPermissionProfile {
network: Some(AdditionalNetworkPermissions {
enabled: Some(true),
}),
file_system: Some(AdditionalFileSystemPermissions {
read: Some(vec![absolute_path("/tmp/read-only").into()]),
write: Some(vec![absolute_path("/tmp/write").into()]),
glob_scan_max_depth: None,
entries: None,
}),
};
let request = CoreRequestPermissionProfile::try_from(request)
.expect("API paths should convert to native paths");
assert_eq!(
granted_permission_profile_from_request(request),
GrantedPermissionProfile {
network: Some(AdditionalNetworkPermissions {
enabled: Some(true),
}),
file_system: Some(AdditionalFileSystemPermissions {
read: Some(vec![absolute_path("/tmp/read-only").into()]),
write: Some(vec![absolute_path("/tmp/write").into()]),
glob_scan_max_depth: None,
entries: Some(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: absolute_path("/tmp/read-only").into(),
},
access: FileSystemAccessMode::Read,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: absolute_path("/tmp/write").into(),
},
access: FileSystemAccessMode::Write,
},
]),
}),
}
);
}
#[test]
fn converts_request_permissions_into_canonical_granted_permissions() {
let request = RequestPermissionProfile {
network: None,
file_system: Some(AdditionalFileSystemPermissions {
read: None,
write: None,
glob_scan_max_depth: None,
entries: Some(vec![FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Write,
}]),
}),
};
let request = CoreRequestPermissionProfile::try_from(request)
.expect("API paths should convert to native paths");
assert_eq!(
granted_permission_profile_from_request(request),
GrantedPermissionProfile {
network: None,
file_system: Some(AdditionalFileSystemPermissions {
read: None,
write: None,
glob_scan_max_depth: None,
entries: Some(vec![FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Write,
}]),
}),
}
);
}
}