Files
codex/codex-rs/tui/src/chatwidget/transcript_export.rs
T
Felipe Coury 7a8092a447 Preserve Markdown formatting when copying TUI responses (#42847)
## What changed

- Add rendered HTML alongside the original Markdown when copying a whole response through the native clipboard, so rich-text destinations preserve headings, lists, tables, code, and other formatting.
- Keep code-block, blockquote, transcript, terminal, and WSL clipboard copies as plain text.
- Escape raw HTML, omit remote image sources, and render unsupported link destinations as visible text when producing clipboard HTML.

## Testing

- Add coverage for rich formatting, local and unsafe links, inert images, fenced tables, copy selection formats, and clipboard fallbacks.

GitOrigin-RevId: c6f5edd88b65f9e9b1ddc5618442868cf47ee560
2026-09-04 20:38:08 +00:00

78 lines
3.0 KiB
Rust

//! Destination and filename prompts for on-demand transcript exports.
use super::*;
use crate::app_event::TranscriptExportDestination;
impl ChatWidget {
pub(crate) fn copy_transcript_to_clipboard(&mut self, markdown: &str) {
match crate::clipboard_copy::copy_to_clipboard(
markdown,
crate::clipboard_copy::CopyFormat::PlainText,
) {
Ok(lease) => {
self.clipboard_lease = lease;
self.add_info_message(
"Copied conversation to clipboard".to_string(),
/*hint*/ None,
);
}
Err(error) => self.add_error_message(format!("Copy failed: {error}")),
}
}
pub(super) fn show_transcript_export_popup(&mut self) {
self.show_selection_view(SelectionViewParams {
title: Some("Export conversation".to_string()),
subtitle: Some("Save the complete conversation as Markdown".to_string()),
footer_hint: Some(standard_popup_hint_line()),
items: vec![
SelectionItem {
name: "Copy to clipboard".to_string(),
description: Some("Copy the complete Markdown transcript".to_string()),
is_disabled: cfg!(target_os = "android"),
actions: vec![Box::new(|tx| {
tx.send(AppEvent::ExportTranscript {
destination: TranscriptExportDestination::Clipboard,
});
})],
dismiss_on_select: true,
..Default::default()
},
SelectionItem {
name: "Save to file".to_string(),
description: Some("Choose a Markdown filename".to_string()),
actions: vec![Box::new(|tx| {
tx.send(AppEvent::OpenTranscriptExportFilePrompt);
})],
dismiss_on_select: true,
..Default::default()
},
],
..Default::default()
});
self.defer_input_until_settings_applied();
self.request_redraw();
}
pub(crate) fn show_transcript_export_file_prompt(&mut self) {
let tx = self.app_event_tx.clone();
let filename = self.thread_id().map_or_else(
|| "codex-session.md".to_string(),
|thread_id| format!("codex-session-{thread_id}.md"),
);
let view = CustomPromptView::new(
"Save conversation".to_string(),
"Type a filename and press Enter".to_string(),
filename,
/*context_label*/ None,
Box::new(move |filename| {
tx.send(AppEvent::ExportTranscript {
destination: TranscriptExportDestination::File(PathBuf::from(filename)),
});
}),
);
self.bottom_pane.show_text_prompt(view);
self.request_redraw();
}
}