Files
codex/codex-rs/tui/src/app/agent_message_consolidation.rs
T
Felipe Coury f896ab9f25 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
2026-07-17 23:19:12 +00:00

100 lines
3.7 KiB
Rust

//! Transcript consolidation for finalized streaming agent messages.
//!
//! During streaming, the chat widget emits transient `AgentMessageCell`s so it
//! can animate stable lines into scrollback while keeping the active mutable
//! tail in the bottom pane. Once the answer finishes, the app replaces that
//! trailing run with a single source-backed `AgentMarkdownCell`. This makes the
//! transcript the canonical owner of the raw markdown source used for future
//! resize re-renders.
use std::path::PathBuf;
use std::sync::Arc;
use color_eyre::eyre::Result;
use super::App;
use super::resize_reflow::trailing_run_start;
use crate::app_event::ConsolidationScrollbackReflow;
use crate::history_cell;
use crate::history_cell::HistoryCell;
use crate::inline_visualization::InlineVisualizationContext;
use crate::pager_overlay::Overlay;
use crate::tui;
impl App {
pub(super) fn handle_consolidate_agent_message(
&mut self,
tui: &mut tui::Tui,
source: String,
cwd: PathBuf,
inline_visualization_context: Option<InlineVisualizationContext>,
scrollback_reflow: ConsolidationScrollbackReflow,
deferred_history_cell: Option<Box<dyn HistoryCell>>,
) -> Result<()> {
// Some finalize paths must preserve a last provisional stream cell long
// enough for queue ordering, then fold it into the canonical
// source-backed cell during consolidation.
if let Some(cell) = deferred_history_cell {
let cell: Arc<dyn HistoryCell> = cell.into();
if let Some(Overlay::Transcript(t)) = &mut self.overlay {
t.insert_cell(cell.clone());
}
self.transcript_cells.push(cell);
}
// Walk backward to find the contiguous run of streaming AgentMessageCells that
// belong to the just-finalized stream.
let end = self.transcript_cells.len();
tracing::debug!(
"ConsolidateAgentMessage: transcript_cells.len()={end}, source_len={}",
source.len()
);
let start = trailing_run_start::<history_cell::AgentMessageCell>(&self.transcript_cells);
if start < end {
tracing::debug!(
"ConsolidateAgentMessage: replacing cells [{start}..{end}] with AgentMarkdownCell"
);
let consolidated: Arc<dyn HistoryCell> = Arc::new(
history_cell::AgentMarkdownCell::new_with_inline_visualizations(
source,
&cwd,
inline_visualization_context,
),
);
self.transcript_cells
.splice(start..end, std::iter::once(consolidated.clone()));
if let Some(Overlay::Transcript(t)) = &mut self.overlay {
t.consolidate_cells(start..end, consolidated.clone());
tui.frame_requester().schedule_frame();
}
self.finish_agent_message_consolidation(tui, scrollback_reflow)?;
} else {
tracing::debug!(
"ConsolidateAgentMessage: no cells to consolidate(start={start}, end={end})",
);
self.maybe_finish_stream_reflow(tui)?;
}
Ok(())
}
fn finish_agent_message_consolidation(
&mut self,
tui: &mut tui::Tui,
scrollback_reflow: ConsolidationScrollbackReflow,
) -> Result<()> {
match scrollback_reflow {
ConsolidationScrollbackReflow::IfResizeReflowRan => {
self.maybe_finish_stream_reflow(tui)?;
}
ConsolidationScrollbackReflow::Required => {
self.finish_required_stream_reflow(tui)?;
}
}
Ok(())
}
}