changing attribute to name + support command with arg

This commit is contained in:
pap
2026-01-09 11:37:17 +00:00
parent 7243698122
commit 9f1ca04358
9 changed files with 61 additions and 50 deletions
+1 -1
View File
@@ -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,
+8 -8
View File
@@ -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),
}),
};
+27 -28
View File
@@ -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()
);
+1 -1
View File
@@ -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,
+4 -4
View File
@@ -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);
}
+9 -3
View File
@@ -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);
}
+9 -3
View File
@@ -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 }));
}),
);