use crate::core::event::{Event, EventKind};
use crate::visualization::TraceDetail;
use serde_json::{Value, json};
const MAX_COMMAND_CHARS: usize = 600;
const MAX_PROMPT_CHARS: usize = 8_000;
pub(super) fn prepare(detail: &mut TraceDetail) {
detail.prompt = prompt_from_events(&detail.events)
.or_else(|| prompt_from_trace(&detail.session.trace_path));
detail.events.iter_mut().for_each(compact_event);
}
fn compact_event(event: &mut Event) {
event.payload = event_summary(event)
.map(|summary| json!({"summary": summary}))
.unwrap_or(Value::Null);
}
fn event_summary(event: &Event) -> Option<String> {
(event.kind == EventKind::ToolCall)
.then(|| payload_summary(&event.payload))
.flatten()
.map(|value| compact_text(&value, MAX_COMMAND_CHARS))
}
fn payload_summary(payload: &Value) -> Option<String> {
pointer_text(
payload,
&["/tool_input/command", "/input/command", "/command", "/cmd"],
)
.or_else(|| argument_summary(payload))
.or_else(|| pointer_text(payload, &["/tool_input/file_path", "/input/path", "/path"]))
}
fn argument_summary(payload: &Value) -> Option<String> {
let raw = pointer_text(payload, &["/arguments", "/function/arguments"])?;
let parsed: Value = serde_json::from_str(&raw).ok()?;
pointer_text(&parsed, &["/cmd", "/command", "/path", "/file_path"])
}
fn pointer_text(value: &Value, pointers: &[&str]) -> Option<String> {
pointers
.iter()
.find_map(|pointer| value.pointer(pointer)?.as_str().map(ToOwned::to_owned))
}
fn prompt_from_events(events: &[Event]) -> Option<String> {
events
.iter()
.rev()
.find_map(|event| prompt_from_value(&event.payload))
}
fn prompt_from_trace(raw: &str) -> Option<String> {
super::prompt_cache::from_trace(raw)
}
pub(super) fn prompt_from_line(line: &str) -> Option<String> {
let value: Value = serde_json::from_str(line).ok()?;
value
.get("payload")
.and_then(prompt_from_value)
.or_else(|| prompt_from_value(&value))
}
fn prompt_from_value(value: &Value) -> Option<String> {
direct_prompt(value)
.or_else(|| user_message(value))
.map(|value| compact_text(&value, MAX_PROMPT_CHARS))
}
fn direct_prompt(value: &Value) -> Option<String> {
pointer_text(value, &["/prompt", "/user_prompt"])
.as_deref()
.and_then(clean_prompt)
}
fn user_message(value: &Value) -> Option<String> {
let message = value.get("message").unwrap_or(value);
(message.get("role")?.as_str()? == "user")
.then(|| content_prompt(message.get("content")?))
.flatten()
}
fn content_prompt(content: &Value) -> Option<String> {
content.as_str().and_then(clean_prompt).or_else(|| {
content
.as_array()?
.iter()
.rev()
.find_map(|part| part.get("text")?.as_str().and_then(clean_prompt))
})
}
fn clean_prompt(raw: &str) -> Option<String> {
let value = objective(raw).unwrap_or(raw).trim();
(!value.is_empty() && !ignored_context(value)).then(|| value.to_string())
}
fn objective(raw: &str) -> Option<&str> {
tagged(raw, "<objective>", "</objective>")
.or_else(|| tagged(raw, "<untrusted_objective>", "</untrusted_objective>"))
}
fn tagged<'a>(raw: &'a str, open: &str, close: &str) -> Option<&'a str> {
raw.split_once(open)?.1.split_once(close).map(|pair| pair.0)
}
fn ignored_context(value: &str) -> bool {
value.starts_with("<environment_context>") || value.starts_with("# AGENTS.md instructions")
}
fn compact_text(value: &str, limit: usize) -> String {
let normalized = value.split_whitespace().collect::<Vec<_>>().join(" ");
let truncated = normalized.chars().take(limit).collect::<String>();
if normalized.chars().count() > limit {
format!("{truncated}...")
} else {
truncated
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::OsString;
use std::path::PathBuf;
#[test]
fn trusted_trace_returns_latest_user_prompt() {
let _guard = crate::core::paths::test_lock::global().lock().unwrap();
let (temp, path, _home) = trace_fixture();
let line = json!({"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"<environment_context>hidden</environment_context>"},{"type":"input_text","text":"Show commands"}]}});
let filler = format!(
"{}\n",
json!({"type":"event_msg","payload":{"type":"noise"}})
)
.repeat(10_000);
std::fs::write(&path, format!("{line}\n{filler}")).unwrap();
assert_eq!(
prompt_from_trace(path.to_str().unwrap()).as_deref(),
Some("Show commands")
);
drop(temp);
}
#[test]
fn codex_arguments_expose_command_only() {
let payload = json!({"arguments":"{\"cmd\":\"rg parser src\",\"max_output_tokens\":1000}"});
assert_eq!(payload_summary(&payload).as_deref(), Some("rg parser src"));
}
#[test]
fn objective_wrapper_returns_user_objective() {
let wrapped =
"<codex_internal_context><objective>Fix identity</objective></codex_internal_context>";
assert_eq!(clean_prompt(wrapped).as_deref(), Some("Fix identity"));
}
#[test]
fn goal_update_wrapper_returns_user_objective() {
let wrapped = "<codex_internal_context><untrusted_objective>Show prompt</untrusted_objective>Budget: hidden</codex_internal_context>";
assert_eq!(clean_prompt(wrapped).as_deref(), Some("Show prompt"));
}
fn trace_fixture() -> (tempfile::TempDir, PathBuf, HomeGuard) {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join(".codex/sessions/session.jsonl");
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
let home = HomeGuard::set(temp.path().into());
(temp, path, home)
}
struct HomeGuard(Option<OsString>);
impl HomeGuard {
fn set(value: OsString) -> Self {
let old = std::env::var_os("HOME");
unsafe { std::env::set_var("HOME", value) };
Self(old)
}
}
impl Drop for HomeGuard {
fn drop(&mut self) {
match self.0.take() {
Some(value) => unsafe { std::env::set_var("HOME", value) },
None => unsafe { std::env::remove_var("HOME") },
}
}
}
}