mirror of
https://github.com/openai/codex.git
synced 2026-11-03 19:53:25 +00:00
changing attribute to name + support command with arg
This commit is contained in:
@@ -44,7 +44,7 @@ pub fn create_fake_rollout(
|
||||
id: conversation_id,
|
||||
timestamp: meta_rfc3339.to_string(),
|
||||
cwd: PathBuf::from("/"),
|
||||
title: None,
|
||||
name: None,
|
||||
originator: "codex".to_string(),
|
||||
cli_version: "0.0.0".to_string(),
|
||||
instructions: None,
|
||||
|
||||
@@ -1699,8 +1699,8 @@ async fn submission_loop(sess: Arc<Session>, config: Arc<Config>, rx_sub: Receiv
|
||||
Op::ThreadRollback { num_turns } => {
|
||||
handlers::thread_rollback(&sess, sub.id.clone(), num_turns).await;
|
||||
}
|
||||
Op::SetSessionTitle { title } => {
|
||||
handlers::set_session_title(&sess, sub.id.clone(), title).await;
|
||||
Op::SetSessionName { name } => {
|
||||
handlers::set_session_name(&sess, sub.id.clone(), name).await;
|
||||
}
|
||||
Op::RunUserShellCommand { command } => {
|
||||
handlers::run_user_shell_command(
|
||||
@@ -2125,13 +2125,13 @@ mod handlers {
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn set_session_title(sess: &Arc<Session>, sub_id: String, title: String) {
|
||||
let title = title.trim().to_string();
|
||||
if title.is_empty() {
|
||||
pub async fn set_session_name(sess: &Arc<Session>, sub_id: String, name: String) {
|
||||
let name = name.trim().to_string();
|
||||
if name.is_empty() {
|
||||
let event = Event {
|
||||
id: sub_id,
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
message: "Session title cannot be empty.".to_string(),
|
||||
message: "Session name cannot be empty.".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::BadRequest),
|
||||
}),
|
||||
};
|
||||
@@ -2155,11 +2155,11 @@ mod handlers {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Err(e) = recorder.set_session_title(title).await {
|
||||
if let Err(e) = recorder.set_session_name(name).await {
|
||||
let event = Event {
|
||||
id: sub_id,
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
message: format!("Failed to set session title: {e}"),
|
||||
message: format!("Failed to set session name: {e}"),
|
||||
codex_error_info: Some(CodexErrorInfo::Other),
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -67,9 +67,9 @@ enum RolloutCmd {
|
||||
Flush {
|
||||
ack: oneshot::Sender<()>,
|
||||
},
|
||||
/// Rewrite the first SessionMeta line in the rollout file to include a title.
|
||||
SetSessionTitle {
|
||||
title: String,
|
||||
/// Rewrite the first SessionMeta line in the rollout file to include a name.
|
||||
SetSessionName {
|
||||
name: String,
|
||||
ack: oneshot::Sender<std::io::Result<()>>,
|
||||
},
|
||||
Shutdown {
|
||||
@@ -148,7 +148,7 @@ impl RolloutRecorder {
|
||||
id: session_id,
|
||||
timestamp,
|
||||
cwd: config.cwd.clone(),
|
||||
title: None,
|
||||
name: None,
|
||||
originator: originator().value.clone(),
|
||||
cli_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
instructions,
|
||||
@@ -213,14 +213,14 @@ impl RolloutRecorder {
|
||||
.map_err(|e| IoError::other(format!("failed waiting for rollout flush: {e}")))
|
||||
}
|
||||
|
||||
pub async fn set_session_title(&self, title: String) -> std::io::Result<()> {
|
||||
pub async fn set_session_name(&self, name: String) -> std::io::Result<()> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.tx
|
||||
.send(RolloutCmd::SetSessionTitle { title, ack: tx })
|
||||
.send(RolloutCmd::SetSessionName { name, ack: tx })
|
||||
.await
|
||||
.map_err(|e| IoError::other(format!("failed to queue session title update: {e}")))?;
|
||||
.map_err(|e| IoError::other(format!("failed to queue session name update: {e}")))?;
|
||||
rx.await
|
||||
.map_err(|e| IoError::other(format!("failed waiting for session title update: {e}")))?
|
||||
.map_err(|e| IoError::other(format!("failed waiting for session name update: {e}")))?
|
||||
}
|
||||
|
||||
pub async fn get_rollout_history(path: &Path) -> std::io::Result<InitialHistory> {
|
||||
@@ -400,8 +400,8 @@ async fn rollout_writer(
|
||||
}
|
||||
let _ = ack.send(());
|
||||
}
|
||||
RolloutCmd::SetSessionTitle { title, ack } => {
|
||||
let result = rewrite_session_title(&mut writer, &rollout_path, &title).await;
|
||||
RolloutCmd::SetSessionName { name, ack } => {
|
||||
let result = rewrite_session_name(&mut writer, &rollout_path, &name).await;
|
||||
let _ = ack.send(result);
|
||||
}
|
||||
RolloutCmd::Shutdown { ack } => {
|
||||
@@ -413,10 +413,10 @@ async fn rollout_writer(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn rewrite_session_title(
|
||||
async fn rewrite_session_name(
|
||||
writer: &mut JsonlWriter,
|
||||
rollout_path: &Path,
|
||||
title: &str,
|
||||
name: &str,
|
||||
) -> std::io::Result<()> {
|
||||
// Flush and close the writer's file handle before swapping the on-disk file,
|
||||
// otherwise subsequent appends would keep writing to the old inode/handle.
|
||||
@@ -424,7 +424,7 @@ async fn rewrite_session_title(
|
||||
|
||||
// Compute the rewritten contents first so any read/parse/legacy-format errors
|
||||
// don't disturb the active writer handle.
|
||||
let rewritten_contents = rewrite_first_session_meta_line_title(rollout_path, title).await?;
|
||||
let rewritten_contents = rewrite_first_session_meta_line_name(rollout_path, name).await?;
|
||||
|
||||
// Close the active handle using a portable placeholder.
|
||||
let placeholder = tokio::fs::File::from_std(tempfile::tempfile()?);
|
||||
@@ -456,9 +456,9 @@ async fn rewrite_session_title(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn rewrite_first_session_meta_line_title(
|
||||
async fn rewrite_first_session_meta_line_name(
|
||||
rollout_path: &Path,
|
||||
title: &str,
|
||||
name: &str,
|
||||
) -> std::io::Result<String> {
|
||||
let text = tokio::fs::read_to_string(rollout_path).await?;
|
||||
let mut rewritten = false;
|
||||
@@ -469,7 +469,7 @@ async fn rewrite_first_session_meta_line_title(
|
||||
let mut out = String::with_capacity(text.len() + 32);
|
||||
for line in text.lines() {
|
||||
if !rewritten && !line.trim().is_empty() {
|
||||
out.push_str(&rewrite_session_meta_line_title(line, title)?);
|
||||
out.push_str(&rewrite_session_meta_line_name(line, name)?);
|
||||
rewritten = true;
|
||||
} else {
|
||||
out.push_str(line);
|
||||
@@ -479,22 +479,22 @@ async fn rewrite_first_session_meta_line_title(
|
||||
|
||||
if !rewritten {
|
||||
return Err(IoError::other(
|
||||
"failed to set session title: rollout has no SessionMeta line",
|
||||
"failed to set session name: rollout has no SessionMeta line",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn rewrite_session_meta_line_title(line: &str, title: &str) -> std::io::Result<String> {
|
||||
fn rewrite_session_meta_line_name(line: &str, name: &str) -> std::io::Result<String> {
|
||||
let mut rollout_line = serde_json::from_str::<RolloutLine>(line).map_err(IoError::other)?;
|
||||
let RolloutItem::SessionMeta(meta_line) = &mut rollout_line.item else {
|
||||
return Err(IoError::other(
|
||||
"failed to set session title: rollout has no SessionMeta line",
|
||||
"failed to set session name: rollout has no SessionMeta line",
|
||||
));
|
||||
};
|
||||
|
||||
meta_line.meta.title = Some(title.to_string());
|
||||
meta_line.meta.name = Some(name.to_string());
|
||||
serde_json::to_string(&rollout_line).map_err(IoError::other)
|
||||
}
|
||||
|
||||
@@ -556,16 +556,15 @@ impl JsonlWriter {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_protocol::ThreadId;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_session_title_rewrites_first_session_meta_line() -> std::io::Result<()> {
|
||||
async fn set_session_name_rewrites_first_session_meta_line() -> std::io::Result<()> {
|
||||
let config = crate::config::test_config();
|
||||
|
||||
let conversation_id =
|
||||
ConversationId::from_string(&Uuid::new_v4().to_string()).expect("uuid should parse");
|
||||
let conversation_id = ThreadId::new();
|
||||
let recorder = RolloutRecorder::new(
|
||||
&config,
|
||||
RolloutRecorderParams::new(conversation_id, None, SessionSource::Cli),
|
||||
@@ -573,7 +572,7 @@ mod tests {
|
||||
.await?;
|
||||
|
||||
recorder
|
||||
.set_session_title("My Session Title".to_string())
|
||||
.set_session_name("My Session Name".to_string())
|
||||
.await?;
|
||||
|
||||
let text = tokio::fs::read_to_string(&recorder.rollout_path).await?;
|
||||
@@ -582,12 +581,12 @@ mod tests {
|
||||
let RolloutItem::SessionMeta(meta_line) = rollout_line.item else {
|
||||
panic!("expected SessionMeta as first rollout line");
|
||||
};
|
||||
assert_eq!(meta_line.meta.title.as_deref(), Some("My Session Title"));
|
||||
assert_eq!(meta_line.meta.name.as_deref(), Some("My Session Name"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_session_title_failure_does_not_redirect_future_writes() -> std::io::Result<()> {
|
||||
async fn set_session_name_failure_does_not_redirect_future_writes() -> std::io::Result<()> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let rollout_path = dir.path().join("rollout.jsonl");
|
||||
|
||||
@@ -601,7 +600,7 @@ mod tests {
|
||||
let mut writer = JsonlWriter { file };
|
||||
|
||||
assert!(
|
||||
rewrite_session_title(&mut writer, &rollout_path, "title")
|
||||
rewrite_session_name(&mut writer, &rollout_path, "name")
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
@@ -588,7 +588,7 @@ async fn test_updated_at_uses_file_mtime() -> Result<()> {
|
||||
timestamp: ts.to_string(),
|
||||
instructions: None,
|
||||
cwd: ".".into(),
|
||||
title: None,
|
||||
name: None,
|
||||
originator: "test_originator".into(),
|
||||
cli_version: "test_version".into(),
|
||||
source: SessionSource::VSCode,
|
||||
|
||||
@@ -207,10 +207,10 @@ pub enum Op {
|
||||
/// to generate a summary which will be returned as an AgentMessage event.
|
||||
Compact,
|
||||
|
||||
/// Set a user-facing session title in the persisted rollout metadata.
|
||||
/// Set a user-facing session name in the persisted rollout metadata.
|
||||
/// This is a local-only operation handled by codex-core; it does not
|
||||
/// involve the model.
|
||||
SetSessionTitle { title: String },
|
||||
SetSessionName { name: String },
|
||||
|
||||
/// Request Codex to undo a turn (turn are stacked so it is the same effect as CMD + Z).
|
||||
Undo,
|
||||
@@ -1297,7 +1297,7 @@ pub struct SessionMeta {
|
||||
pub timestamp: String,
|
||||
pub cwd: PathBuf,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
pub name: Option<String>,
|
||||
pub originator: String,
|
||||
pub cli_version: String,
|
||||
pub instructions: Option<String>,
|
||||
@@ -1312,7 +1312,7 @@ impl Default for SessionMeta {
|
||||
id: ThreadId::default(),
|
||||
timestamp: String::new(),
|
||||
cwd: PathBuf::new(),
|
||||
title: None,
|
||||
name: None,
|
||||
originator: String::new(),
|
||||
cli_version: String::new(),
|
||||
instructions: None,
|
||||
|
||||
@@ -1327,7 +1327,7 @@ impl ChatComposer {
|
||||
&& let Some((_n, cmd)) = built_in_slash_commands()
|
||||
.into_iter()
|
||||
.find(|(command_name, _)| *command_name == name)
|
||||
&& cmd == SlashCommand::Review
|
||||
&& matches!(cmd, SlashCommand::Review | SlashCommand::Rename)
|
||||
{
|
||||
return (InputResult::CommandWithArgs(cmd, rest.to_string()), true);
|
||||
}
|
||||
|
||||
@@ -1872,6 +1872,12 @@ impl ChatWidget {
|
||||
|
||||
let trimmed = args.trim();
|
||||
match cmd {
|
||||
SlashCommand::Rename if !trimmed.is_empty() => {
|
||||
let name = trimmed.to_string();
|
||||
self.add_info_message(format!("Session renamed to \"{name}\""), None);
|
||||
self.app_event_tx
|
||||
.send(AppEvent::CodexOp(Op::SetSessionName { name }));
|
||||
}
|
||||
SlashCommand::Review if !trimmed.is_empty() => {
|
||||
self.submit_op(Op::Review {
|
||||
review_request: ReviewRequest {
|
||||
@@ -1892,11 +1898,11 @@ impl ChatWidget {
|
||||
"Rename session".to_string(),
|
||||
"Type a new name and press Enter".to_string(),
|
||||
None,
|
||||
Box::new(move |title: String| {
|
||||
Box::new(move |name: String| {
|
||||
tx.send(AppEvent::InsertHistoryCell(Box::new(
|
||||
history_cell::new_info_event(format!("Session renamed to \"{title}\""), None),
|
||||
history_cell::new_info_event(format!("Session renamed to \"{name}\""), None),
|
||||
)));
|
||||
tx.send(AppEvent::CodexOp(Op::SetSessionTitle { title }));
|
||||
tx.send(AppEvent::CodexOp(Op::SetSessionName { name }));
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -1244,7 +1244,7 @@ impl ChatComposer {
|
||||
&& let Some((_n, cmd)) = built_in_slash_commands()
|
||||
.into_iter()
|
||||
.find(|(command_name, _)| *command_name == name)
|
||||
&& cmd == SlashCommand::Review
|
||||
&& matches!(cmd, SlashCommand::Review | SlashCommand::Rename)
|
||||
{
|
||||
return (InputResult::CommandWithArgs(cmd, rest.to_string()), true);
|
||||
}
|
||||
|
||||
@@ -1699,6 +1699,12 @@ impl ChatWidget {
|
||||
|
||||
let trimmed = args.trim();
|
||||
match cmd {
|
||||
SlashCommand::Rename if !trimmed.is_empty() => {
|
||||
let name = trimmed.to_string();
|
||||
self.add_info_message(format!("Session renamed to \"{name}\""), None);
|
||||
self.app_event_tx
|
||||
.send(AppEvent::CodexOp(Op::SetSessionName { name }));
|
||||
}
|
||||
SlashCommand::Review if !trimmed.is_empty() => {
|
||||
self.submit_op(Op::Review {
|
||||
review_request: ReviewRequest {
|
||||
@@ -1719,11 +1725,11 @@ impl ChatWidget {
|
||||
"Rename session".to_string(),
|
||||
"Type a new name and press Enter".to_string(),
|
||||
None,
|
||||
Box::new(move |title: String| {
|
||||
Box::new(move |name: String| {
|
||||
tx.send(AppEvent::InsertHistoryCell(Box::new(
|
||||
history_cell::new_info_event(format!("Session renamed to \"{title}\""), None),
|
||||
history_cell::new_info_event(format!("Session renamed to \"{name}\""), None),
|
||||
)));
|
||||
tx.send(AppEvent::CodexOp(Op::SetSessionTitle { title }));
|
||||
tx.send(AppEvent::CodexOp(Op::SetSessionName { name }));
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user