mirror of
https://github.com/openai/codex.git
synced 2026-05-23 20:44:50 +00:00
## Why The TUI still had a few low-risk dependencies flowing through the transitional `legacy_core` namespace after the app-server migration. These helpers either already have clearer non-core owners or are presentation logic that does not belong in `codex-core`, so moving them out reduces the compatibility surface without changing product behavior. ## What changed This is a low-risk change, almost completely mechanical in nature. - Route TUI Codex-home lookup through `codex-utils-home-dir`, use `Config::log_dir` directly, and call `codex-sandboxing::system_bwrap_warning` without going through `legacy_core`. - Move shared `codex resume` hint formatting from `codex-core` into `codex-utils-cli`. - Update CLI and TUI call sites to use the shared CLI utility, and keep the resume-command behavior covered by tests in its new home. ## Verification - `cargo test -p codex-utils-cli` - `cargo test -p codex-utils-cli resume_command`
240 lines
6.6 KiB
Rust
240 lines
6.6 KiB
Rust
use std::fs::File;
|
||
use std::fs::OpenOptions;
|
||
use std::io::Write;
|
||
use std::path::PathBuf;
|
||
use std::sync::LazyLock;
|
||
use std::sync::Mutex;
|
||
use std::sync::OnceLock;
|
||
|
||
use crate::app_command::AppCommand;
|
||
use crate::legacy_core::config::Config;
|
||
use serde::Serialize;
|
||
use serde_json::json;
|
||
|
||
use crate::app_event::AppEvent;
|
||
|
||
static LOGGER: LazyLock<SessionLogger> = LazyLock::new(SessionLogger::new);
|
||
|
||
struct SessionLogger {
|
||
file: OnceLock<Mutex<File>>,
|
||
}
|
||
|
||
impl SessionLogger {
|
||
fn new() -> Self {
|
||
Self {
|
||
file: OnceLock::new(),
|
||
}
|
||
}
|
||
|
||
fn open(&self, path: PathBuf) -> std::io::Result<()> {
|
||
let mut opts = OpenOptions::new();
|
||
opts.create(true).truncate(true).write(true);
|
||
|
||
#[cfg(unix)]
|
||
{
|
||
use std::os::unix::fs::OpenOptionsExt;
|
||
opts.mode(0o600);
|
||
}
|
||
|
||
let file = opts.open(path)?;
|
||
self.file.get_or_init(|| Mutex::new(file));
|
||
Ok(())
|
||
}
|
||
|
||
fn write_json_line(&self, value: serde_json::Value) {
|
||
let Some(mutex) = self.file.get() else {
|
||
return;
|
||
};
|
||
let mut guard = match mutex.lock() {
|
||
Ok(g) => g,
|
||
Err(poisoned) => poisoned.into_inner(),
|
||
};
|
||
match serde_json::to_string(&value) {
|
||
Ok(serialized) => {
|
||
if let Err(e) = guard.write_all(serialized.as_bytes()) {
|
||
tracing::warn!("session log write error: {}", e);
|
||
return;
|
||
}
|
||
if let Err(e) = guard.write_all(b"\n") {
|
||
tracing::warn!("session log write error: {}", e);
|
||
return;
|
||
}
|
||
if let Err(e) = guard.flush() {
|
||
tracing::warn!("session log flush error: {}", e);
|
||
}
|
||
}
|
||
Err(e) => tracing::warn!("session log serialize error: {}", e),
|
||
}
|
||
}
|
||
|
||
fn is_enabled(&self) -> bool {
|
||
self.file.get().is_some()
|
||
}
|
||
}
|
||
|
||
fn now_ts() -> String {
|
||
// RFC3339 for readability; consumers can parse as needed.
|
||
chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
|
||
}
|
||
|
||
pub(crate) fn maybe_init(config: &Config) {
|
||
let enabled = std::env::var("CODEX_TUI_RECORD_SESSION")
|
||
.map(|v| matches!(v.as_str(), "1" | "true" | "TRUE" | "yes" | "YES"))
|
||
.unwrap_or(false);
|
||
if !enabled {
|
||
return;
|
||
}
|
||
|
||
let path = if let Ok(path) = std::env::var("CODEX_TUI_SESSION_LOG_PATH") {
|
||
PathBuf::from(path)
|
||
} else {
|
||
let mut p = config.log_dir.clone();
|
||
let filename = format!(
|
||
"session-{}.jsonl",
|
||
chrono::Utc::now().format("%Y%m%dT%H%M%SZ")
|
||
);
|
||
p.push(filename);
|
||
p
|
||
};
|
||
|
||
if let Err(e) = LOGGER.open(path.clone()) {
|
||
tracing::error!("failed to open session log {:?}: {}", path, e);
|
||
return;
|
||
}
|
||
|
||
// Write a header record so we can attach context.
|
||
let header = json!({
|
||
"ts": now_ts(),
|
||
"dir": "meta",
|
||
"kind": "session_start",
|
||
"cwd": config.cwd,
|
||
"model": config.model,
|
||
"model_provider_id": config.model_provider_id,
|
||
"model_provider_name": config.model_provider.name,
|
||
});
|
||
LOGGER.write_json_line(header);
|
||
}
|
||
|
||
pub(crate) fn log_inbound_app_event(event: &AppEvent) {
|
||
// Log only if enabled
|
||
if !LOGGER.is_enabled() {
|
||
return;
|
||
}
|
||
|
||
match event {
|
||
AppEvent::NewSession => {
|
||
let value = json!({
|
||
"ts": now_ts(),
|
||
"dir": "to_tui",
|
||
"kind": "new_session",
|
||
});
|
||
LOGGER.write_json_line(value);
|
||
}
|
||
AppEvent::ClearUi => {
|
||
let value = json!({
|
||
"ts": now_ts(),
|
||
"dir": "to_tui",
|
||
"kind": "clear_ui",
|
||
});
|
||
LOGGER.write_json_line(value);
|
||
}
|
||
AppEvent::InsertHistoryCell(cell) => {
|
||
let value = json!({
|
||
"ts": now_ts(),
|
||
"dir": "to_tui",
|
||
"kind": "insert_history_cell",
|
||
"lines": cell.transcript_lines(u16::MAX).len(),
|
||
});
|
||
LOGGER.write_json_line(value);
|
||
}
|
||
AppEvent::StartFileSearch(query) => {
|
||
let value = json!({
|
||
"ts": now_ts(),
|
||
"dir": "to_tui",
|
||
"kind": "file_search_start",
|
||
"query": query,
|
||
});
|
||
LOGGER.write_json_line(value);
|
||
}
|
||
AppEvent::FileSearchResult { query, matches } => {
|
||
let value = json!({
|
||
"ts": now_ts(),
|
||
"dir": "to_tui",
|
||
"kind": "file_search_result",
|
||
"query": query,
|
||
"matches": matches.len(),
|
||
});
|
||
LOGGER.write_json_line(value);
|
||
}
|
||
AppEvent::PetPreviewLoaded { request_id, result } => {
|
||
let value = json!({
|
||
"ts": now_ts(),
|
||
"dir": "to_tui",
|
||
"kind": "app_event",
|
||
"variant": "PetPreviewLoaded",
|
||
"request_id": request_id,
|
||
"ok": result.is_ok(),
|
||
});
|
||
LOGGER.write_json_line(value);
|
||
}
|
||
AppEvent::PetSelectionLoaded {
|
||
request_id,
|
||
pet_id,
|
||
result,
|
||
} => {
|
||
let value = json!({
|
||
"ts": now_ts(),
|
||
"dir": "to_tui",
|
||
"kind": "app_event",
|
||
"variant": "PetSelectionLoaded",
|
||
"request_id": request_id,
|
||
"pet_id": pet_id,
|
||
"ok": result.is_ok(),
|
||
});
|
||
LOGGER.write_json_line(value);
|
||
}
|
||
// Noise or control flow – record variant only
|
||
other => {
|
||
let value = json!({
|
||
"ts": now_ts(),
|
||
"dir": "to_tui",
|
||
"kind": "app_event",
|
||
"variant": format!("{other:?}").split('(').next().unwrap_or("app_event"),
|
||
});
|
||
LOGGER.write_json_line(value);
|
||
}
|
||
}
|
||
}
|
||
|
||
pub(crate) fn log_outbound_op(op: &AppCommand) {
|
||
if !LOGGER.is_enabled() {
|
||
return;
|
||
}
|
||
write_record("from_tui", "op", op);
|
||
}
|
||
|
||
pub(crate) fn log_session_end() {
|
||
if !LOGGER.is_enabled() {
|
||
return;
|
||
}
|
||
let value = json!({
|
||
"ts": now_ts(),
|
||
"dir": "meta",
|
||
"kind": "session_end",
|
||
});
|
||
LOGGER.write_json_line(value);
|
||
}
|
||
|
||
fn write_record<T>(dir: &str, kind: &str, obj: &T)
|
||
where
|
||
T: Serialize,
|
||
{
|
||
let value = json!({
|
||
"ts": now_ts(),
|
||
"dir": dir,
|
||
"kind": kind,
|
||
"payload": obj,
|
||
});
|
||
LOGGER.write_json_line(value);
|
||
}
|