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:
@@ -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();
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()),
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user