Render inline visualization links in the TUI (#33925)

## Why

Assistant-authored inline visualizations need a terminal fallback that lets users open generated artifacts in a browser.

## What changed

- Recognize `::codex-inline-vis{file="..."}` directives in assistant Markdown and replace them with browser links during streaming, finalized history rendering, and local transcript previews.
- Materialize visualization fragments in sandboxed viewer documents with a content security policy, while restricting artifacts to HTML files within the thread's visualization directory and limiting their size.
- Show an explicit unavailable message for invalid or missing artifacts, hide incomplete streaming directives, and preserve directive text in code blocks and user Markdown.
- Add a trusted file-link path for generated viewers without enabling arbitrary `file:` links in Markdown.

## Testing

Add coverage for directive rewriting, streaming and replay, artifact validation, sandboxed viewer generation, trusted terminal hyperlinks, and literal preservation.

GitOrigin-RevId: a666b4ebc82bb5798ad45bd137fd815d538e1de2
This commit is contained in:
Felipe Coury
2026-07-17 23:19:12 +00:00
committed by copyberry
parent 97dbd01a3d
commit f896ab9f25
23 changed files with 2123 additions and 106 deletions
+11 -1
View File
@@ -10,6 +10,7 @@ use crate::history_cell::PlainHistoryCell;
use crate::history_cell::ReasoningSummaryCell;
use crate::history_cell::UserHistoryCell;
use crate::history_cell::split_reasoning_summary_parts;
use crate::inline_visualization::InlineVisualizationContext;
use crate::multi_agents::sub_agent_activity_summary;
use codex_app_server_protocol::Thread;
use codex_app_server_protocol::ThreadItem;
@@ -31,6 +32,7 @@ pub(crate) async fn load_session_transcript(
app_server: &mut AppServerSession,
thread_id: ThreadId,
raw_reasoning_visibility: RawReasoningVisibility,
codex_home: Option<&std::path::Path>,
) -> std::io::Result<TranscriptCells> {
let thread = app_server
.thread_read(thread_id, /*include_turns*/ true)
@@ -39,14 +41,21 @@ pub(crate) async fn load_session_transcript(
Ok(thread_to_transcript_cells(
&thread,
raw_reasoning_visibility,
codex_home,
))
}
pub(crate) fn thread_to_transcript_cells(
thread: &Thread,
raw_reasoning_visibility: RawReasoningVisibility,
codex_home: Option<&std::path::Path>,
) -> TranscriptCells {
let cwd = thread.cwd.as_path();
let inline_visualization_context = codex_home.and_then(|codex_home| {
ThreadId::from_string(&thread.id)
.ok()
.and_then(|thread_id| InlineVisualizationContext::new(codex_home, thread_id))
});
let mut cells: TranscriptCells = Vec::new();
for item in thread.turns.iter().flat_map(|turn| turn.items.iter()) {
match item {
@@ -85,9 +94,10 @@ pub(crate) fn thread_to_transcript_cells(
ThreadItem::AgentMessage { text, .. } => {
let parsed = parse_assistant_markdown(text, cwd);
if !parsed.visible_markdown.trim().is_empty() {
cells.push(Arc::new(AgentMarkdownCell::new(
cells.push(Arc::new(AgentMarkdownCell::new_with_inline_visualizations(
parsed.visible_markdown,
cwd,
inline_visualization_context.clone(),
)));
}
}