mirror of
https://github.com/openai/codex.git
synced 2026-11-02 19:43:24 +00:00
tui session load
This commit is contained in:
Generated
+1
@@ -592,6 +592,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -34,3 +34,4 @@ tokio = { version = "1", features = [
|
||||
] }
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = "0.3.19"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
|
||||
@@ -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<PathBuf>) -> 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?;
|
||||
|
||||
@@ -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;
|
||||
|
||||
+15
-2
@@ -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<ChatWidgetArgs>,
|
||||
|
||||
/// Session ID reported by the backend; used for resuming the session.
|
||||
session_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
@@ -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<ResponseItem>) {
|
||||
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<Uuid> {
|
||||
self.session_id
|
||||
|
||||
@@ -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<ResponseItem>) {
|
||||
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::<Vec<_>>()
|
||||
.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::<Vec<_>>()
|
||||
.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 {
|
||||
|
||||
@@ -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<Uuid>,
|
||||
|
||||
/// Optional user prompt to start the session.
|
||||
pub prompt: Option<String>,
|
||||
|
||||
|
||||
+43
-2
@@ -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<Vec<ResponseItem>> {
|
||||
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::<ResponseItem>(&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."
|
||||
|
||||
Reference in New Issue
Block a user