From 65e5d926ab67db78f0638a09f35fcf564a2baa03 Mon Sep 17 00:00:00 2001 From: "Rai (Michael Pokorny)" Date: Sat, 21 Jun 2025 06:38:29 -0700 Subject: [PATCH] tui session load --- codex-rs/Cargo.lock | 1 + codex-rs/cli/Cargo.toml | 1 + codex-rs/cli/src/main.rs | 12 +++++++++ codex-rs/core/src/lib.rs | 1 + codex-rs/tui/src/app.rs | 17 +++++++++++-- codex-rs/tui/src/chatwidget.rs | 40 ++++++++++++++++++++++++++++++ codex-rs/tui/src/cli.rs | 8 ++++++ codex-rs/tui/src/lib.rs | 45 ++++++++++++++++++++++++++++++++-- 8 files changed, 121 insertions(+), 4 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 66b4fa3e00..305275ba71 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -592,6 +592,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "uuid", ] [[package]] diff --git a/codex-rs/cli/Cargo.toml b/codex-rs/cli/Cargo.toml index 78fd08a7d3..30e4f521e4 100644 --- a/codex-rs/cli/Cargo.toml +++ b/codex-rs/cli/Cargo.toml @@ -34,3 +34,4 @@ tokio = { version = "1", features = [ ] } tracing = "0.1.41" tracing-subscriber = "0.3.19" +uuid = { version = "1", features = ["serde", "v4"] } diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 0e9ba01827..aade8a95e1 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -7,6 +7,7 @@ use codex_common::CliConfigOverrides; use codex_exec::Cli as ExecCli; use codex_tui::Cli as TuiCli; use std::path::PathBuf; +use uuid::Uuid; use crate::proto::ProtoCli; @@ -33,6 +34,11 @@ struct MultitoolCli { #[derive(Debug, clap::Subcommand)] enum Subcommand { + /// Resume an existing TUI session by UUID. + Session { + /// UUID of the session to resume + session_id: Uuid, + }, /// Run Codex non-interactively. #[clap(visible_alias = "e")] Exec(ExecCli), @@ -88,6 +94,12 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() prepend_config_flags(&mut tui_cli.config_overrides, cli.config_overrides); codex_tui::run_main(tui_cli, codex_linux_sandbox_exe)?; } + Some(Subcommand::Session { session_id }) => { + let mut tui_cli = cli.interactive; + tui_cli.session = Some(session_id); + prepend_config_flags(&mut tui_cli.config_overrides, cli.config_overrides); + codex_tui::run_main(tui_cli, codex_linux_sandbox_exe)?; + } Some(Subcommand::Exec(mut exec_cli)) => { prepend_config_flags(&mut exec_cli.config_overrides, cli.config_overrides); codex_exec::run_main(exec_cli, codex_linux_sandbox_exe).await?; diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 16cf190588..cc42097523 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -27,6 +27,7 @@ mod model_provider_info; pub use model_provider_info::ModelProviderInfo; pub use model_provider_info::WireApi; mod models; +pub use models::{ContentItem, ReasoningItemReasoningSummary, ResponseItem}; pub mod openai_api_key; mod openai_tools; mod project_doc; diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 8600b9e0c5..15c90df85a 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -19,6 +19,7 @@ use std::path::PathBuf; use std::sync::mpsc::Receiver; use std::sync::mpsc::channel; +use codex_core::ResponseItem; use uuid::Uuid; /// Top-level application state: which full-screen view is currently active. @@ -47,8 +48,6 @@ pub(crate) struct App<'a> { /// Stored parameters needed to instantiate the ChatWidget later, e.g., /// after dismissing the Git-repo warning. chat_args: Option, - - /// Session ID reported by the backend; used for resuming the session. session_id: Option, } @@ -176,6 +175,20 @@ impl<'a> App<'a> { self.app_event_tx.clone() } + /// Override the session ID for this UI instance (useful for session-resume). + pub fn set_session_id(&mut self, id: Uuid) { + self.session_id = Some(id); + } + + /// Replay a previous session transcript into the chat widget. + pub fn replay_items(&mut self, items: Vec) { + if let AppState::Chat { widget } = &mut self.app_state { + widget.replay_items(items); + } + } + + /// Override the session ID for this UI instance (useful for session-resume). + /// Returns the session ID assigned by the backend for this session, if available. pub fn session_id(&self) -> Option { self.session_id diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 5811d74927..667f82b788 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -37,6 +37,7 @@ use crate::bottom_pane::InputResult; use crate::conversation_history_widget::ConversationHistoryWidget; use crate::history_cell::PatchEventType; use crate::user_approval_widget::ApprovalRequest; +use codex_core::{ContentItem, ReasoningItemReasoningSummary, ResponseItem}; pub(crate) struct ChatWidget<'a> { app_event_tx: AppEventSender, @@ -208,6 +209,45 @@ impl ChatWidget<'_> { self.conversation_history.scroll_to_bottom(); } + /// Replay a previous session transcript into the conversation history. + pub fn replay_items(&mut self, items: Vec) { + for item in items { + match item { + ResponseItem::Message { role, content } => { + let text = content + .into_iter() + .filter_map(|ci| match ci { + ContentItem::InputText { text } | ContentItem::OutputText { text } => { + Some(text) + } + _ => None, + }) + .collect::>() + .join(""); + if role.eq_ignore_ascii_case("user") { + self.conversation_history.add_user_message(text); + } else { + self.conversation_history + .add_agent_message(&self.config, text); + } + } + ResponseItem::Reasoning { summary, .. } => { + let text = summary + .into_iter() + .filter_map(|s| match s { + ReasoningItemReasoningSummary::SummaryText { text } => Some(text), + }) + .collect::>() + .join(" "); + self.conversation_history + .add_agent_reasoning(&self.config, text); + } + _ => {} + } + } + self.conversation_history.scroll_to_bottom(); + } + pub(crate) fn handle_codex_event(&mut self, event: Event) { let Event { id, msg } = event; match msg { diff --git a/codex-rs/tui/src/cli.rs b/codex-rs/tui/src/cli.rs index 4abd684144..0bfd941d86 100644 --- a/codex-rs/tui/src/cli.rs +++ b/codex-rs/tui/src/cli.rs @@ -3,10 +3,18 @@ use codex_common::ApprovalModeCliArg; use codex_common::CliConfigOverrides; use codex_common::SandboxPermissionOption; use std::path::PathBuf; +use uuid::Uuid; +/// Interactive TUI for chatting with Codex. +/// +/// If a `--session` UUID is provided, resumes the existing session. #[derive(Parser, Debug)] #[command(version)] pub struct Cli { + /// Resume a previous session by its UUID. + #[arg(long, value_name = "SESSION_ID")] + pub session: Option, + /// Optional user prompt to start the session. pub prompt: Option, diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index e594ad7788..2f4116204d 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -3,6 +3,7 @@ // alternate‑screen mode starts; that file opts‑out locally via `allow`. #![deny(clippy::print_stdout, clippy::print_stderr)] use app::App; +use codex_core::ResponseItem; use codex_core::config::Config; use codex_core::config::ConfigOverrides; use codex_core::openai_api_key::OPENAI_API_KEY_ENV_VAR; @@ -13,11 +14,14 @@ use codex_core::protocol::SandboxPolicy; use codex_core::util::is_inside_git_repo; use codex_login::try_read_openai_api_key; use log_layer::TuiLogLayer; -use std::fs::OpenOptions; +use serde_json; +use std::fs::{self, File, OpenOptions}; +use std::io::{BufRead, BufReader}; use std::path::PathBuf; use tracing_appender::non_blocking; use tracing_subscriber::EnvFilter; use tracing_subscriber::prelude::*; +use uuid::Uuid; mod app; mod app_event; @@ -175,7 +179,12 @@ fn run_ratatui_app( let (mut terminal, mut mouse_capture) = tui::init(&config)?; terminal.clear()?; - let Cli { prompt, images, .. } = cli; + let Cli { + prompt, + images, + session, + .. + } = cli; let mut app = App::new( config.clone(), prompt, @@ -183,6 +192,14 @@ fn run_ratatui_app( show_git_warning, images, ); + // If resuming, override the generated session ID so UI hint logic and history use it + if let Some(id) = session { + app.set_session_id(id); + // Attempt to replay past conversation transcript + if let Some(items) = load_rollout_for_session(&config, id) { + app.replay_items(items); + } + } // Bridge log receiver into the AppEvent channel so latest log lines update the UI. { @@ -205,6 +222,30 @@ fn run_ratatui_app( app_result } +/// Load and parse a previous session's rollout JSONL file. +fn load_rollout_for_session(config: &Config, session_id: Uuid) -> Option> { + let dir = config.codex_home.join("sessions"); + let target = session_id.to_string(); + for entry in fs::read_dir(&dir).ok()? { + let path = entry.ok()?.path(); + if let Some(fname) = path.file_name().and_then(|s| s.to_str()) { + if fname.starts_with("rollout-") && fname.contains(&target) && fname.ends_with(".jsonl") + { + let file = File::open(path).ok()?; + let reader = BufReader::new(file); + let mut items = Vec::new(); + for line in reader.lines().flatten() { + if let Ok(item) = serde_json::from_str::(&line) { + items.push(item); + } + } + return Some(items); + } + } + } + None +} + #[expect( clippy::print_stderr, reason = "TUI should no longer be displayed, so we can write to stderr."