Protect inline visualization viewers from sandbox writes (#38306)

## Why

Inline visualization viewer documents must remain outside locations that a
sandboxed session can modify before they are opened in a browser.

## What changed

- Materialize viewer documents in a dedicated cache under `CODEX_HOME`, keyed
  by the source and artifact thread IDs, instead of alongside visualization
  artifacts.
- Create visualization links only when the active filesystem policy cannot
  write to the viewer cache. This also disables links for full-disk-write
  sessions and applies the policy consistently when loading transcript history.
- Reject viewer cache paths containing symbolic links and track materialized
  documents in memory so unchanged viewers are reused without trusting existing
  file contents.

## Testing

- Verify that rendering an unchanged visualization reuses the existing viewer
  file, while an updated fragment refreshes it.

GitOrigin-RevId: 12b7fbe522a68076e5d683a987b7801940d131c2
This commit is contained in:
viyatb-oai authored and copyberry committed 2026-08-13 04:08:23 +00:00
1 parent 363427b5e3
commit 902bd9e06b
10 files changed
+116 -52

No files matched your search

+2 -2
View File
@@ -126,7 +126,7 @@ impl App {
&cwd, &cwd,
user_items.iter().map(|(_, item)| item.clone()), user_items.iter().map(|(_, item)| item.clone()),
visibility, visibility,
Some(self.config.codex_home.as_path()), Some(&self.config),
); );
let mut persisted_user_cells = user_items let mut persisted_user_cells = user_items
.into_iter() .into_iter()
@@ -204,7 +204,7 @@ impl App {
&cwd, &cwd,
items, items,
visibility, visibility,
Some(self.config.codex_home.as_path()), Some(&self.config),
); );
if self.backtrack.overlay_preview_active { if self.backtrack.overlay_preview_active {
self.backtrack.nth_user_message = self.backtrack.nth_user_message.saturating_add( self.backtrack.nth_user_message = self.backtrack.nth_user_message.saturating_add(
@@ -406,7 +406,7 @@ async fn older_pagination_reconciles_review_prompts_across_page_boundaries() ->
&app.config.cwd, &app.config.cwd,
started.turns.iter().flat_map(|turn| turn.items.clone()), started.turns.iter().flat_map(|turn| turn.items.clone()),
crate::thread_transcript::RawReasoningVisibility::Hidden, crate::thread_transcript::RawReasoningVisibility::Hidden,
Some(app.config.codex_home.as_path()), Some(&app.config),
); );
app.enqueue_primary_thread_session(started.session, started.turns) app.enqueue_primary_thread_session(started.session, started.turns)
.await?; .await?;
@@ -575,7 +575,7 @@ async fn transcript_home_loads_every_older_history_page() -> Result<()> {
&app.config.cwd, &app.config.cwd,
started.turns.iter().flat_map(|turn| turn.items.clone()), started.turns.iter().flat_map(|turn| turn.items.clone()),
crate::thread_transcript::RawReasoningVisibility::Hidden, crate::thread_transcript::RawReasoningVisibility::Hidden,
Some(app.config.codex_home.as_path()), Some(&app.config),
); );
app.enqueue_primary_thread_session(started.session, started.turns) app.enqueue_primary_thread_session(started.session, started.turns)
.await?; .await?;
@@ -718,7 +718,7 @@ async fn remote_legacy_history_start_negotiates_once_for_resume_and_fork() -> Re
&mut app_server, &mut app_server,
paginated_thread_id, paginated_thread_id,
crate::thread_transcript::RawReasoningVisibility::Hidden, crate::thread_transcript::RawReasoningVisibility::Hidden,
Some(app.config.codex_home.as_path()), Some(&app.config),
vec![Arc::new(PlainHistoryCell::new(vec!["visible".into()]))], vec![Arc::new(PlainHistoryCell::new(vec!["visible".into()]))],
) )
.await .await
@@ -940,7 +940,7 @@ async fn underfilled_scrollback_fetches_older_pages_without_opening_the_transcri
&app.config.cwd, &app.config.cwd,
started.turns.iter().flat_map(|turn| turn.items.clone()), started.turns.iter().flat_map(|turn| turn.items.clone()),
crate::thread_transcript::RawReasoningVisibility::Hidden, crate::thread_transcript::RawReasoningVisibility::Hidden,
Some(app.config.codex_home.as_path()), Some(&app.config),
); );
initial_cells.insert( initial_cells.insert(
/*index*/ 0, /*index*/ 0,
@@ -1076,7 +1076,7 @@ async fn paginated_workflows_never_request_full_thread_history() -> Result<()> {
&mut app_server, &mut app_server,
paginated_thread_id, paginated_thread_id,
crate::thread_transcript::RawReasoningVisibility::Hidden, crate::thread_transcript::RawReasoningVisibility::Hidden,
Some(app.config.codex_home.as_path()), Some(&app.config),
) )
.await?; .await?;
assert!(!cells.is_empty()); assert!(!cells.is_empty());
@@ -1105,7 +1105,7 @@ async fn paginated_workflows_never_request_full_thread_history() -> Result<()> {
&mut app_server, &mut app_server,
legacy_thread_id, legacy_thread_id,
crate::thread_transcript::RawReasoningVisibility::Hidden, crate::thread_transcript::RawReasoningVisibility::Hidden,
Some(app.config.codex_home.as_path()), Some(&app.config),
) )
.await?; .await?;
let legacy_reads = recorded_params(&requests, "thread/read"); let legacy_reads = recorded_params(&requests, "thread/read");
@@ -1287,7 +1287,7 @@ async fn cold_paginated_subagent_transcript_excludes_inherited_parent_history()
&mut app_server, &mut app_server,
child_thread_id, child_thread_id,
crate::thread_transcript::RawReasoningVisibility::Hidden, crate::thread_transcript::RawReasoningVisibility::Hidden,
Some(app.config.codex_home.as_path()), Some(&app.config),
) )
.await?; .await?;
let visible_history = cells let visible_history = cells
+4 -4
View File
@@ -24,6 +24,7 @@ use crate::history_cell::ReasoningSummaryCell;
use crate::history_cell::SessionInfoCell; use crate::history_cell::SessionInfoCell;
use crate::history_cell::UserHistoryCell; use crate::history_cell::UserHistoryCell;
use crate::history_cell::raw_lines_from_source; use crate::history_cell::raw_lines_from_source;
use crate::legacy_core::config::Config;
use crate::thread_transcript::RawReasoningVisibility; use crate::thread_transcript::RawReasoningVisibility;
use crate::thread_transcript::thread_items_to_transcript_cells; use crate::thread_transcript::thread_items_to_transcript_cells;
@@ -42,12 +43,11 @@ impl App {
} else { } else {
RawReasoningVisibility::Hidden RawReasoningVisibility::Hidden
}; };
let codex_home = self.config.codex_home.to_path_buf();
let cells = load_export_transcript( let cells = load_export_transcript(
app_server, app_server,
thread_id, thread_id,
visibility, visibility,
Some(codex_home.as_path()), Some(&self.config),
self.transcript_cells.clone(), self.transcript_cells.clone(),
) )
.await?; .await?;
@@ -77,7 +77,7 @@ pub(super) async fn load_export_transcript(
app_server: &mut AppServerSession, app_server: &mut AppServerSession,
thread_id: ThreadId, thread_id: ThreadId,
visibility: RawReasoningVisibility, visibility: RawReasoningVisibility,
codex_home: Option<&Path>, config: Option<&Config>,
visible_transcript: Vec<Arc<dyn HistoryCell>>, visible_transcript: Vec<Arc<dyn HistoryCell>>,
) -> Result<Vec<Arc<dyn HistoryCell>>, String> { ) -> Result<Vec<Arc<dyn HistoryCell>>, String> {
let mut thread = app_server let mut thread = app_server
@@ -123,7 +123,7 @@ pub(super) async fn load_export_transcript(
&thread.cwd, &thread.cwd,
[item], [item],
visibility, visibility,
codex_home, config,
)); ));
} }
} }
@@ -308,7 +308,7 @@ fn rendered_history_rows(
&thread.cwd, &thread.cwd,
items, items,
visibility, visibility,
Some(config.codex_home.as_path()), Some(config),
) )
.into_iter() .into_iter()
.fold(rendered_rows, |rows, cell| { .fold(rendered_rows, |rows, cell| {
+40 -8
View File
@@ -19,6 +19,8 @@ use std::ops::Range;
use std::path::Component; use std::path::Component;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex;
use url::Url; use url::Url;
use uuid::Uuid; use uuid::Uuid;
@@ -33,9 +35,12 @@ const MAX_FRAGMENT_BYTES: u64 = 2 * 1024 * 1024;
pub(crate) struct InlineVisualizationContext { pub(crate) struct InlineVisualizationContext {
visualizations_dir: PathBuf, visualizations_dir: PathBuf,
thread_dir: PathBuf, thread_dir: PathBuf,
viewer_dir: PathBuf,
materialized_viewers: Arc<Mutex<HashMap<PathBuf, String>>>,
} }
impl InlineVisualizationContext { impl InlineVisualizationContext {
#[cfg(test)]
pub(crate) fn new(codex_home: &Path, thread_id: ThreadId) -> Option<Self> { pub(crate) fn new(codex_home: &Path, thread_id: ThreadId) -> Option<Self> {
Self::new_with_writable_roots(codex_home, thread_id, std::iter::empty()) Self::new_with_writable_roots(codex_home, thread_id, std::iter::empty())
} }
@@ -44,15 +49,33 @@ impl InlineVisualizationContext {
config: &crate::legacy_core::config::Config, config: &crate::legacy_core::config::Config,
thread_id: ThreadId, thread_id: ThreadId,
) -> Option<Self> { ) -> Option<Self> {
let writable_roots = config let file_system_policy = config.permissions.file_system_sandbox_policy();
.permissions if file_system_policy.has_full_disk_write_access() {
.file_system_sandbox_policy() return None;
.get_writable_roots_with_cwd(config.cwd.as_path()); }
Self::new_with_writable_roots( let writable_roots = file_system_policy.get_writable_roots_with_cwd(config.cwd.as_path());
let context = Self::new_with_writable_roots(
config.codex_home.as_path(), config.codex_home.as_path(),
thread_id, thread_id,
writable_roots.iter().map(|root| root.root.as_path()), writable_roots.iter().map(|root| root.root.as_path()),
) )?;
let viewer_caches = [
config.codex_home.as_path().join("visualization-viewers"),
context.viewer_dir.parent()?.parent()?.to_path_buf(),
];
for viewer_cache in viewer_caches {
if file_system_policy.can_write_path_with_cwd(&viewer_cache, config.cwd.as_path())
|| file_system_policy
.can_write_path_with_cwd(viewer_cache.parent()?, config.cwd.as_path())
|| writable_roots.iter().any(|root| {
root.is_path_writable(&viewer_cache)
|| root.root.as_path().starts_with(&viewer_cache)
})
{
return None;
}
}
Some(context)
} }
fn new_with_writable_roots<'a>( fn new_with_writable_roots<'a>(
@@ -60,6 +83,7 @@ impl InlineVisualizationContext {
thread_id: ThreadId, thread_id: ThreadId,
writable_roots: impl IntoIterator<Item = &'a Path>, writable_roots: impl IntoIterator<Item = &'a Path>,
) -> Option<Self> { ) -> Option<Self> {
let codex_home = fs::canonicalize(codex_home).ok()?;
let thread_id = thread_id.to_string(); let thread_id = thread_id.to_string();
let uuid = Uuid::parse_str(&thread_id).ok()?; let uuid = Uuid::parse_str(&thread_id).ok()?;
let timestamp = uuid.get_timestamp()?; let timestamp = uuid.get_timestamp()?;
@@ -74,11 +98,17 @@ impl InlineVisualizationContext {
[thread_dir] => (*thread_dir).to_path_buf(), [thread_dir] => (*thread_dir).to_path_buf(),
_ => visualizations_dir _ => visualizations_dir
.join(created_at.format("%Y/%m/%d").to_string()) .join(created_at.format("%Y/%m/%d").to_string())
.join(thread_id), .join(&thread_id),
}; };
let artifact_thread_id = thread_dir.file_name()?.to_owned();
Some(Self { Some(Self {
visualizations_dir, visualizations_dir,
thread_dir, thread_dir,
viewer_dir: codex_home
.join("visualization-viewers")
.join(thread_id)
.join(artifact_thread_id),
materialized_viewers: Arc::default(),
}) })
} }
@@ -110,7 +140,9 @@ impl InlineVisualizationContext {
if !fragment_path.starts_with(&thread_dir) { if !fragment_path.starts_with(&thread_dir) {
return None; return None;
} }
let viewer_path = materialize_document(&fragment_path, &thread_dir).ok()?; let viewer_path =
materialize_document(&fragment_path, &self.viewer_dir, &self.materialized_viewers)
.ok()?;
Url::from_file_path(viewer_path).ok() Url::from_file_path(viewer_path).ok()
} }
} }
@@ -1,8 +1,10 @@
use super::MAX_FRAGMENT_BYTES; use super::MAX_FRAGMENT_BYTES;
use std::collections::HashMap;
use std::fs; use std::fs;
use std::io::Write; use std::io::Write;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Mutex;
const FRAGMENT_PLACEHOLDER: &str = "<!--__INLINE_VISUALIZATION_FRAGMENT__-->"; const FRAGMENT_PLACEHOLDER: &str = "<!--__INLINE_VISUALIZATION_FRAGMENT__-->";
const VIEWER_DIRECTORY_NAME: &str = ".codex-viewers"; const VIEWER_DIRECTORY_NAME: &str = ".codex-viewers";
@@ -14,7 +16,11 @@ const VIEWER_RUNTIME: &str = include_str!("assets/visualize.html");
const FRAME_CSP: &str = "default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' blob: data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://fonts.bunny.net https://fonts.googleapis.com https://fonts.gstatic.com https://unpkg.com; style-src 'unsafe-inline' blob: data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://fonts.bunny.net https://fonts.googleapis.com https://fonts.gstatic.com https://unpkg.com; img-src blob: data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://fonts.bunny.net https://fonts.googleapis.com https://fonts.gstatic.com https://unpkg.com; font-src blob: data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://fonts.bunny.net https://fonts.googleapis.com https://fonts.gstatic.com https://unpkg.com; media-src blob: data:; worker-src blob:; connect-src blob: data:; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'"; const FRAME_CSP: &str = "default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' blob: data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://fonts.bunny.net https://fonts.googleapis.com https://fonts.gstatic.com https://unpkg.com; style-src 'unsafe-inline' blob: data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://fonts.bunny.net https://fonts.googleapis.com https://fonts.gstatic.com https://unpkg.com; img-src blob: data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://fonts.bunny.net https://fonts.googleapis.com https://fonts.gstatic.com https://unpkg.com; font-src blob: data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://fonts.bunny.net https://fonts.googleapis.com https://fonts.gstatic.com https://unpkg.com; media-src blob: data:; worker-src blob:; connect-src blob: data:; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'";
const SHELL_STYLE: &str = ":root{color-scheme:light dark;background:light-dark(rgb(255 255 255), rgb(24 24 24))}html,body{margin:0}body{box-sizing:border-box;padding:1rem;background:inherit}iframe{display:block;width:100%;max-width:736px;height:calc(100vh - 2rem);margin:0 auto;border:0}"; const SHELL_STYLE: &str = ":root{color-scheme:light dark;background:light-dark(rgb(255 255 255), rgb(24 24 24))}html,body{margin:0}body{box-sizing:border-box;padding:1rem;background:inherit}iframe{display:block;width:100%;max-width:736px;height:calc(100vh - 2rem);margin:0 auto;border:0}";
pub(super) fn materialize_document(path: &Path, thread_dir: &Path) -> std::io::Result<PathBuf> { pub(super) fn materialize_document(
path: &Path,
viewer_dir: &Path,
materialized_viewers: &Mutex<HashMap<PathBuf, String>>,
) -> std::io::Result<PathBuf> {
let metadata = path.metadata()?; let metadata = path.metadata()?;
if !metadata.is_file() || metadata.len() > MAX_FRAGMENT_BYTES { if !metadata.is_file() || metadata.len() > MAX_FRAGMENT_BYTES {
return Err(std::io::Error::other("invalid visualization fragment")); return Err(std::io::Error::other("invalid visualization fragment"));
@@ -28,29 +34,30 @@ pub(super) fn materialize_document(path: &Path, thread_dir: &Path) -> std::io::R
.replace('-', " "); .replace('-', " ");
let document = render_fragment(&fragment, &title); let document = render_fragment(&fragment, &title);
let thread_dir = fs::canonicalize(thread_dir)?; let viewer_dir = viewer_dir.join(VIEWER_DIRECTORY_NAME);
let viewer_dir = thread_dir.join(VIEWER_DIRECTORY_NAME);
fs::create_dir_all(&viewer_dir)?; fs::create_dir_all(&viewer_dir)?;
let viewer_dir = fs::canonicalize(viewer_dir)?; if fs::canonicalize(&viewer_dir)? != viewer_dir {
if !viewer_dir.starts_with(&thread_dir) {
return Err(std::io::Error::other( return Err(std::io::Error::other(
"visualization viewer directory escapes thread directory", "visualization viewer directory must not contain symbolic links",
)); ));
} }
let file_name = path let file_name = path
.file_name() .file_name()
.ok_or_else(|| std::io::Error::other("visualization fragment has no file name"))?; .ok_or_else(|| std::io::Error::other("visualization fragment has no file name"))?;
let viewer_path = viewer_dir.join(file_name); let viewer_path = viewer_dir.join(file_name);
if fs::read_to_string(&viewer_path).is_ok_and(|existing| existing == document) { let mut materialized_viewers = materialized_viewers
.lock()
.map_err(|_| std::io::Error::other("visualization viewer cache is unavailable"))?;
if materialized_viewers.get(&viewer_path) == Some(&document) {
return Ok(viewer_path); return Ok(viewer_path);
} }
let mut temporary = tempfile::NamedTempFile::new_in(&viewer_dir)?; let mut temporary = tempfile::NamedTempFile::new_in(&viewer_dir)?;
temporary.write_all(document.as_bytes())?; temporary.write_all(document.as_bytes())?;
temporary.flush()?; temporary.flush()?;
temporary temporary
.persist(&viewer_path) .persist(&viewer_path)
.map_err(|error| error.error)?; .map_err(|error| error.error)?;
materialized_viewers.insert(viewer_path.clone(), document);
Ok(viewer_path) Ok(viewer_path)
} }
@@ -244,12 +244,37 @@ fn viewer_reuses_path_and_refreshes_static_document() {
let (_codex_home, context) = context_with_fragment("<div>first</div>"); let (_codex_home, context) = context_with_fragment("<div>first</div>");
let first_url = context.link_for("chart.html").expect("first viewer link"); let first_url = context.link_for("chart.html").expect("first viewer link");
let viewer_path = first_url.to_file_path().expect("viewer file path"); let viewer_path = first_url.to_file_path().expect("viewer file path");
let original_viewer_metadata = fs::metadata(&viewer_path).expect("read viewer metadata");
assert!( assert!(
fs::read_to_string(&viewer_path) fs::read_to_string(&viewer_path)
.expect("read first viewer") .expect("read first viewer")
.contains("first") .contains("first")
); );
let reused_url = context.link_for("chart.html").expect("reused viewer link");
assert_eq!(reused_url, first_url);
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
assert_eq!(
fs::metadata(&viewer_path)
.expect("read reused viewer metadata")
.ino(),
original_viewer_metadata.ino()
);
}
#[cfg(windows)]
{
use std::os::windows::fs::MetadataExt;
assert_eq!(
fs::metadata(&viewer_path)
.expect("read reused viewer metadata")
.creation_time(),
original_viewer_metadata.creation_time()
);
}
fs::write(context.thread_dir.join("chart.html"), "<div>second</div>").expect("update fragment"); fs::write(context.thread_dir.join("chart.html"), "<div>second</div>").expect("update fragment");
let second_url = context.link_for("chart.html").expect("second viewer link"); let second_url = context.link_for("chart.html").expect("second viewer link");
+2 -2
View File
@@ -3164,7 +3164,7 @@ mod tests {
let preview = crate::resume_picker::load_transcript_preview( let preview = crate::resume_picker::load_transcript_preview(
&mut app_server, &mut app_server,
thread_id, thread_id,
/*codex_home*/ None, /*config*/ None,
) )
.await?; .await?;
assert!(!preview.is_empty()); assert!(!preview.is_empty());
@@ -3173,7 +3173,7 @@ mod tests {
&mut app_server, &mut app_server,
thread_id, thread_id,
crate::thread_transcript::RawReasoningVisibility::Hidden, crate::thread_transcript::RawReasoningVisibility::Hidden,
/*codex_home*/ None, /*config*/ None,
) )
.await?; .await?;
assert!(cells.len() > 100); assert!(cells.len() > 100);
+12 -13
View File
@@ -445,7 +445,7 @@ async fn run_resume_picker_with_launch_context(
archive_request_handle, archive_request_handle,
include_non_interactive, include_non_interactive,
raw_reasoning_visibility(config), raw_reasoning_visibility(config),
(!uses_remote_workspace).then(|| config.codex_home.to_path_buf()), (!uses_remote_workspace).then(|| config.clone()),
bg_tx, bg_tx,
), ),
bg_rx, bg_rx,
@@ -499,7 +499,7 @@ pub async fn run_fork_picker_with_app_server(
archive_request_handle, archive_request_handle,
/*include_non_interactive*/ false, /*include_non_interactive*/ false,
raw_reasoning_visibility(config), raw_reasoning_visibility(config),
(!uses_remote_workspace).then(|| config.codex_home.to_path_buf()), (!uses_remote_workspace).then(|| config.clone()),
bg_tx, bg_tx,
), ),
bg_rx, bg_rx,
@@ -642,7 +642,7 @@ fn spawn_app_server_page_loader(
archive_request_handle: AppServerRequestHandle, archive_request_handle: AppServerRequestHandle,
include_non_interactive: bool, include_non_interactive: bool,
raw_reasoning_visibility: RawReasoningVisibility, raw_reasoning_visibility: RawReasoningVisibility,
codex_home: Option<PathBuf>, config: Option<Config>,
bg_tx: mpsc::UnboundedSender<BackgroundEvent>, bg_tx: mpsc::UnboundedSender<BackgroundEvent>,
) -> PickerLoader { ) -> PickerLoader {
let (request_tx, mut request_rx) = mpsc::unbounded_channel::<PickerLoadRequest>(); let (request_tx, mut request_rx) = mpsc::unbounded_channel::<PickerLoadRequest>();
@@ -671,8 +671,7 @@ fn spawn_app_server_page_loader(
} }
PickerLoadRequest::Preview { thread_id } => { PickerLoadRequest::Preview { thread_id } => {
let preview = let preview =
load_transcript_preview(&mut app_server, thread_id, codex_home.as_deref()) load_transcript_preview(&mut app_server, thread_id, config.as_ref()).await;
.await;
let _ = bg_tx.send(BackgroundEvent::Preview { thread_id, preview }); let _ = bg_tx.send(BackgroundEvent::Preview { thread_id, preview });
} }
PickerLoadRequest::Transcript { PickerLoadRequest::Transcript {
@@ -684,7 +683,7 @@ fn spawn_app_server_page_loader(
&mut app_server, &mut app_server,
thread_id, thread_id,
raw_reasoning_visibility, raw_reasoning_visibility,
codex_home.as_deref(), config.as_ref(),
) => { ) => {
let _ = bg_tx.send(BackgroundEvent::Transcript { let _ = bg_tx.send(BackgroundEvent::Transcript {
thread_id, thread_id,
@@ -875,7 +874,7 @@ async fn load_app_server_page(
pub(crate) async fn load_transcript_preview( pub(crate) async fn load_transcript_preview(
app_server: &mut AppServerSession, app_server: &mut AppServerSession,
thread_id: ThreadId, thread_id: ThreadId,
codex_home: Option<&Path>, config: Option<&Config>,
) -> std::io::Result<Vec<TranscriptPreviewLine>> { ) -> std::io::Result<Vec<TranscriptPreviewLine>> {
const MAX_PREVIEW_LINES: usize = 6; const MAX_PREVIEW_LINES: usize = 6;
@@ -896,10 +895,10 @@ pub(crate) async fn load_transcript_preview(
.map_err(std::io::Error::other)?; .map_err(std::io::Error::other)?;
} }
let cwd = thread.cwd.as_path(); let cwd = thread.cwd.as_path();
let inline_visualization_context = codex_home.and_then(|codex_home| { let inline_visualization_context = config.and_then(|config| {
ThreadId::from_string(&thread.id) ThreadId::from_string(&thread.id)
.ok() .ok()
.and_then(|thread_id| InlineVisualizationContext::new(codex_home, thread_id)) .and_then(|thread_id| InlineVisualizationContext::from_config(config, thread_id))
}); });
let mut lines = if thread.history_mode == ThreadHistoryMode::Paginated { let mut lines = if thread.history_mode == ThreadHistoryMode::Paginated {
let mut groups = Vec::new(); let mut groups = Vec::new();
@@ -6452,7 +6451,7 @@ session_picker_view = "dense"
let rendered = thread_to_transcript_cells( let rendered = thread_to_transcript_cells(
thread, thread,
RawReasoningVisibility::Visible, RawReasoningVisibility::Visible,
/*codex_home*/ None, /*config*/ None,
) )
.into_iter() .into_iter()
.flat_map(|cell| cell.transcript_lines(/*width*/ 80)) .flat_map(|cell| cell.transcript_lines(/*width*/ 80))
@@ -6516,7 +6515,7 @@ session_picker_view = "dense"
let hidden = thread_to_transcript_cells( let hidden = thread_to_transcript_cells(
thread.clone(), thread.clone(),
RawReasoningVisibility::Hidden, RawReasoningVisibility::Hidden,
/*codex_home*/ None, /*config*/ None,
) )
.into_iter() .into_iter()
.flat_map(|cell| cell.transcript_lines(/*width*/ 80)) .flat_map(|cell| cell.transcript_lines(/*width*/ 80))
@@ -6526,7 +6525,7 @@ session_picker_view = "dense"
let visible = thread_to_transcript_cells( let visible = thread_to_transcript_cells(
thread, thread,
RawReasoningVisibility::Visible, RawReasoningVisibility::Visible,
/*codex_home*/ None, /*config*/ None,
) )
.into_iter() .into_iter()
.flat_map(|cell| cell.transcript_lines(/*width*/ 80)) .flat_map(|cell| cell.transcript_lines(/*width*/ 80))
@@ -6588,7 +6587,7 @@ session_picker_view = "dense"
let rendered = thread_to_transcript_cells( let rendered = thread_to_transcript_cells(
thread, thread,
RawReasoningVisibility::Visible, RawReasoningVisibility::Visible,
/*codex_home*/ None, /*config*/ None,
) )
.into_iter() .into_iter()
.flat_map(|cell| cell.transcript_lines(/*width*/ 80)) .flat_map(|cell| cell.transcript_lines(/*width*/ 80))
+8 -7
View File
@@ -12,6 +12,7 @@ use crate::history_cell::ReasoningSummaryCell;
use crate::history_cell::UserHistoryCell; use crate::history_cell::UserHistoryCell;
use crate::history_cell::split_reasoning_summary_parts; use crate::history_cell::split_reasoning_summary_parts;
use crate::inline_visualization::InlineVisualizationContext; use crate::inline_visualization::InlineVisualizationContext;
use crate::legacy_core::config::Config;
use crate::multi_agents::sub_agent_activity_summary; use crate::multi_agents::sub_agent_activity_summary;
use codex_app_server_protocol::Thread; use codex_app_server_protocol::Thread;
use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::ThreadItem;
@@ -34,7 +35,7 @@ pub(crate) async fn load_session_transcript(
app_server: &mut AppServerSession, app_server: &mut AppServerSession,
thread_id: ThreadId, thread_id: ThreadId,
raw_reasoning_visibility: RawReasoningVisibility, raw_reasoning_visibility: RawReasoningVisibility,
codex_home: Option<&std::path::Path>, config: Option<&Config>,
) -> std::io::Result<TranscriptCells> { ) -> std::io::Result<TranscriptCells> {
let mut thread = app_server let mut thread = app_server
.thread_read(thread_id, /*include_turns*/ false) .thread_read(thread_id, /*include_turns*/ false)
@@ -53,14 +54,14 @@ pub(crate) async fn load_session_transcript(
Ok(thread_to_transcript_cells( Ok(thread_to_transcript_cells(
thread, thread,
raw_reasoning_visibility, raw_reasoning_visibility,
codex_home, config,
)) ))
} }
pub(crate) fn thread_to_transcript_cells( pub(crate) fn thread_to_transcript_cells(
thread: Thread, thread: Thread,
raw_reasoning_visibility: RawReasoningVisibility, raw_reasoning_visibility: RawReasoningVisibility,
codex_home: Option<&std::path::Path>, config: Option<&Config>,
) -> TranscriptCells { ) -> TranscriptCells {
let cwd = thread.cwd; let cwd = thread.cwd;
let thread_id = ThreadId::from_string(&thread.id).ok(); let thread_id = ThreadId::from_string(&thread.id).ok();
@@ -69,7 +70,7 @@ pub(crate) fn thread_to_transcript_cells(
&cwd, &cwd,
thread.turns.into_iter().flat_map(|turn| turn.items), thread.turns.into_iter().flat_map(|turn| turn.items),
raw_reasoning_visibility, raw_reasoning_visibility,
codex_home, config,
); );
if cells.is_empty() { if cells.is_empty() {
cells.push(Arc::new(PlainHistoryCell::new(vec![ cells.push(Arc::new(PlainHistoryCell::new(vec![
@@ -84,10 +85,10 @@ pub(crate) fn thread_items_to_transcript_cells(
cwd: &AbsolutePathBuf, cwd: &AbsolutePathBuf,
items: impl IntoIterator<Item = ThreadItem>, items: impl IntoIterator<Item = ThreadItem>,
raw_reasoning_visibility: RawReasoningVisibility, raw_reasoning_visibility: RawReasoningVisibility,
codex_home: Option<&std::path::Path>, config: Option<&Config>,
) -> TranscriptCells { ) -> TranscriptCells {
let inline_visualization_context = codex_home.and_then(|codex_home| { let inline_visualization_context = config.and_then(|config| {
thread_id.and_then(|thread_id| InlineVisualizationContext::new(codex_home, thread_id)) thread_id.and_then(|thread_id| InlineVisualizationContext::from_config(config, thread_id))
}); });
let mut cells: TranscriptCells = Vec::new(); let mut cells: TranscriptCells = Vec::new();
for item in items { for item in items {