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 authored and copyberry committed 2026-07-17 23:19:12 +00:00
1 parent 97dbd01a3d
commit f896ab9f25
23 files changed
+2123 -106

No files matched your search

+15 -1
View File
@@ -45,10 +45,17 @@ impl ChatWidget {
if let Some(source) = source {
let source =
parse_assistant_markdown(&source, self.config.cwd.as_path()).visible_markdown;
let inline_visualization_context = self.thread_id.and_then(|thread_id| {
crate::inline_visualization::InlineVisualizationContext::from_config(
&self.config,
thread_id,
)
});
self.note_stream_consolidation_queued();
self.app_event_tx.send(AppEvent::ConsolidateAgentMessage {
source,
cwd: self.config.cwd.to_path_buf(),
inline_visualization_context,
scrollback_reflow,
deferred_history_cell,
});
@@ -408,10 +415,17 @@ impl ChatWidget {
// Reset the flag even if we don't show separator (no work was done)
self.transcript.needs_final_message_separator = false;
}
self.stream_controller = Some(StreamController::new(
let inline_visualization_context = self.thread_id.and_then(|thread_id| {
crate::inline_visualization::InlineVisualizationContext::from_config(
&self.config,
thread_id,
)
});
self.stream_controller = Some(StreamController::new_with_inline_visualizations(
self.current_stream_width(/*reserved_cols*/ 2),
&self.config.cwd,
self.history_render_mode(),
inline_visualization_context,
));
}
if let Some(controller) = self.stream_controller.as_mut()