feat: Set chat name (#4974)

Set chat name with `/name` so they appear in the codex resume page:


https://github.com/user-attachments/assets/c0252bba-3a53-44c7-a740-f4690a3ad405
This commit is contained in:
dedrisian-oai
2025-10-08 16:35:35 -07:00
committed by GitHub
parent b6165aee0c
commit ec238a2c39
13 changed files with 167 additions and 22 deletions

View File

@@ -381,6 +381,9 @@ impl App {
AppEvent::OpenReviewCustomPrompt => {
self.chat_widget.show_review_custom_prompt();
}
AppEvent::SetSessionName(name) => {
self.chat_widget.begin_set_session_name(name);
}
AppEvent::FullScreenApprovalRequest(request) => match request {
ApprovalRequest::ApplyPatch { cwd, changes, .. } => {
let _ = tui.enter_alt_screen();

View File

@@ -85,6 +85,9 @@ pub(crate) enum AppEvent {
/// Open the custom prompt option from the review popup.
OpenReviewCustomPrompt,
/// Begin setting a human-readable name for the current session.
SetSessionName(String),
/// Open the approval popup.
FullScreenApprovalRequest(ApprovalRequest),
}

View File

@@ -25,7 +25,7 @@ pub mod custom_prompt_view;
mod file_search_popup;
mod footer;
mod list_selection_view;
mod prompt_args;
pub mod prompt_args;
pub(crate) use list_selection_view::SelectionViewParams;
mod paste_burst;
pub mod popup_consts;

View File

@@ -1104,6 +1104,7 @@ impl ChatWidget {
return;
}
match cmd {
SlashCommand::Name => self.open_name_popup(),
SlashCommand::New => {
self.app_event_tx.send(AppEvent::NewSession);
}
@@ -1251,6 +1252,29 @@ impl ChatWidget {
return;
}
// Intercept '/name <new name>' as a local rename command (no images allowed).
if image_paths.is_empty()
&& let Some((cmd, rest)) = crate::bottom_pane::prompt_args::parse_slash_name(&text)
&& cmd == "name"
{
let name = rest.trim();
if name.is_empty() {
// Provide a brief usage hint.
self.add_to_history(history_cell::new_info_event(
"Usage: /name <new name>".to_string(),
None,
));
self.request_redraw();
} else {
// Send the rename op; persistence and ack come as an event.
let name_str = name.to_string();
self.codex_op_tx
.send(Op::SetSessionName { name: name_str })
.unwrap_or_else(|e| tracing::error!("failed to send SetSessionName op: {e}"));
}
return;
}
self.capture_ghost_snapshot();
let mut items: Vec<InputItem> = Vec::new();
@@ -1443,6 +1467,13 @@ impl ChatWidget {
self.app_event_tx
.send(crate::app_event::AppEvent::ConversationHistory(ev));
}
EventMsg::SessionRenamed(ev) => {
self.add_to_history(history_cell::new_info_event(
format!("Named this chat: {}", ev.name),
None,
));
self.request_redraw();
}
EventMsg::EnteredReviewMode(review_request) => {
self.on_entered_review_mode(review_request)
}
@@ -2081,6 +2112,33 @@ impl ChatWidget {
self.bottom_pane.show_view(Box::new(view));
}
pub(crate) fn open_name_popup(&mut self) {
let tx = self.app_event_tx.clone();
let view = CustomPromptView::new(
"Name this chat".to_string(),
"Type a name and press Enter".to_string(),
None,
Box::new(move |name: String| {
let trimmed = name.trim().to_string();
if trimmed.is_empty() {
return;
}
tx.send(AppEvent::SetSessionName(trimmed));
}),
);
self.bottom_pane.show_view(Box::new(view));
}
pub(crate) fn begin_set_session_name(&mut self, name: String) {
let trimmed = name.trim().to_string();
if trimmed.is_empty() {
return;
}
self.codex_op_tx
.send(Op::SetSessionName { name: trimmed })
.unwrap_or_else(|e| tracing::error!("failed to send SetSessionName op: {e}"));
}
/// Programmatically submit a user text message as if typed in the
/// composer. The text will be added to conversation history and sent to
/// the agent.

View File

@@ -167,6 +167,7 @@ struct PickerState {
next_search_token: usize,
page_loader: PageLoader,
view_rows: Option<usize>,
// No additional per-path state; names are embedded in rollouts.
}
struct PaginationState {
@@ -586,9 +587,14 @@ fn head_to_row(item: &ConversationItem) -> Row {
.and_then(parse_timestamp_str)
.or(created_at);
let preview = preview_from_head(&item.head)
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
let preview = item
.name
.clone()
.or_else(|| {
preview_from_head(&item.head)
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
})
.unwrap_or_else(|| String::from("(no message yet)"));
Row {
@@ -958,6 +964,7 @@ mod tests {
path: PathBuf::from(path),
head: head_with_ts_and_user_text(ts, &[preview]),
tail: Vec::new(),
name: None,
created_at: Some(ts.to_string()),
updated_at: Some(ts.to_string()),
}
@@ -1020,6 +1027,7 @@ mod tests {
path: PathBuf::from("/tmp/a.jsonl"),
head: head_with_ts_and_user_text("2025-01-01T00:00:00Z", &["A"]),
tail: Vec::new(),
name: None,
created_at: Some("2025-01-01T00:00:00Z".into()),
updated_at: Some("2025-01-01T00:00:00Z".into()),
};
@@ -1027,6 +1035,7 @@ mod tests {
path: PathBuf::from("/tmp/b.jsonl"),
head: head_with_ts_and_user_text("2025-01-02T00:00:00Z", &["B"]),
tail: Vec::new(),
name: None,
created_at: Some("2025-01-02T00:00:00Z".into()),
updated_at: Some("2025-01-02T00:00:00Z".into()),
};
@@ -1055,6 +1064,7 @@ mod tests {
path: PathBuf::from("/tmp/a.jsonl"),
head,
tail,
name: None,
created_at: Some("2025-01-01T00:00:00Z".into()),
updated_at: Some("2025-01-01T01:00:00Z".into()),
};

View File

@@ -15,6 +15,7 @@ pub enum SlashCommand {
Model,
Approvals,
Review,
Name,
New,
Init,
Compact,
@@ -33,6 +34,7 @@ impl SlashCommand {
/// User-visible description shown in the popup.
pub fn description(self) -> &'static str {
match self {
SlashCommand::Name => "set a name for this chat",
SlashCommand::New => "start a new chat during a conversation",
SlashCommand::Init => "create an AGENTS.md file with instructions for Codex",
SlashCommand::Compact => "summarize conversation to prevent hitting the context limit",
@@ -60,6 +62,8 @@ impl SlashCommand {
/// Whether this command can be run while a task is in progress.
pub fn available_during_task(self) -> bool {
match self {
// Naming is a local UI action; allow during tasks.
SlashCommand::Name => true,
SlashCommand::New
| SlashCommand::Init
| SlashCommand::Compact