feat(v0.8.44): session picker inline rename + checklist sidebar refresh

#1600: 'r' keybinding in session picker for inline rename.
Enter inline rename mode, type new title, Enter to confirm, Esc to cancel.
Updates the saved session metadata and refreshes the picker list.

#1787: checklist_write, checklist_update, update_plan now trigger
immediate Work sidebar refresh (previously only todo_write did).
This commit is contained in:
Hunter Bown
2026-05-24 15:19:54 -05:00
parent b9d04547ed
commit 1364ebb7ca
2 changed files with 88 additions and 5 deletions
+85 -5
View File
@@ -58,6 +58,8 @@ pub struct SessionPickerView {
preview_cache: HashMap<String, Vec<String>>,
current_preview: Vec<String>,
confirm_delete: bool,
rename_mode: bool,
rename_input: String,
status: Option<String>,
/// Canonical workspace path used as the per-project scope filter
/// (#1395). `None` opts out of scoping (e.g. when the caller can't
@@ -93,6 +95,8 @@ impl SessionPickerView {
preview_cache: HashMap::new(),
current_preview: Vec::new(),
confirm_delete: false,
rename_mode: false,
rename_input: String::new(),
status: None,
workspace_scope: Some(canonical_or_self(workspace.to_path_buf())),
show_all_workspaces: false,
@@ -311,6 +315,44 @@ impl SessionPickerView {
})
}
fn rename_selected(&mut self, new_title: &str) -> ViewAction {
let Some(session) = self.selected_session().cloned() else {
self.status = Some("No session selected".to_string());
return ViewAction::None;
};
if new_title.is_empty() || new_title.len() > 100 {
self.status = Some("Title must be 1100 characters".to_string());
return ViewAction::None;
}
let manager = match SessionManager::default_location() {
Ok(m) => m,
Err(e) => {
self.status = Some(format!("Could not open sessions: {e}"));
return ViewAction::None;
}
};
let mut saved = match manager.load_session(&session.id) {
Ok(s) => s,
Err(e) => {
self.status = Some(format!("Could not load session: {e}"));
return ViewAction::None;
}
};
saved.metadata.title = new_title.to_string();
if let Err(e) = manager.save_session(&saved) {
self.status = Some(format!("Rename failed: {e}"));
return ViewAction::None;
}
// Update our local metadata cache.
if let Some(meta) = self.sessions.iter_mut().find(|s| s.id == session.id) {
meta.title = new_title.to_string();
}
self.apply_sort_and_filter();
self.refresh_preview();
self.status = Some(format!("Renamed to \"{new_title}\""));
ViewAction::None
}
fn refresh_preview(&mut self) {
let Some(session) = self.selected_session() else {
self.current_preview = vec!["No sessions found.".to_string()];
@@ -401,6 +443,32 @@ impl ModalView for SessionPickerView {
}
}
if self.rename_mode {
match key.code {
KeyCode::Enter => {
self.rename_mode = false;
let new_title = self.rename_input.trim().to_string();
self.rename_input.clear();
return self.rename_selected(&new_title);
}
KeyCode::Esc => {
self.rename_mode = false;
self.rename_input.clear();
self.status = Some("Rename cancelled".to_string());
return ViewAction::None;
}
KeyCode::Backspace => {
self.rename_input.pop();
return ViewAction::None;
}
KeyCode::Char(c) if !c.is_control() => {
self.rename_input.push(c);
return ViewAction::None;
}
_ => return ViewAction::None,
}
}
match key.code {
KeyCode::Esc | KeyCode::Char('q') => ViewAction::Close,
KeyCode::Up | KeyCode::Char('k') => {
@@ -438,6 +506,12 @@ impl ModalView for SessionPickerView {
self.toggle_all_workspaces();
ViewAction::None
}
KeyCode::Char('r') | KeyCode::Char('R') => {
self.rename_mode = true;
self.rename_input.clear();
self.status = Some("New title: ".to_string());
ViewAction::None
}
KeyCode::Char('d') | KeyCode::Char('D') => {
self.confirm_delete = true;
self.status = Some("Delete session? (y/n)".to_string());
@@ -505,6 +579,8 @@ impl ModalView for SessionPickerView {
&self.search_input,
self.sort_label(),
self.confirm_delete,
self.rename_mode,
&self.rename_input,
self.status.as_deref(),
);
let list = Paragraph::new(list_lines)
@@ -539,14 +615,18 @@ fn build_list_lines(
search_input: &str,
sort_label: &str,
confirm_delete: bool,
rename_mode: bool,
rename_input: &str,
status: Option<&str>,
) -> Vec<Line<'static>> {
let mut lines = Vec::new();
let header = if search_mode {
format!("/{search_input}")
} else if rename_mode {
format!("New title: {rename_input}_")
} else {
format!(
"1-9 history | PgUp/PgDn scroll | Enter resume | / search | s sort | a all | d delete | Sort: {sort_label}"
"1-9 history | PgUp/PgDn scroll | Enter resume | / search | s sort | r rename | a all | d delete | Sort: {sort_label}"
)
};
lines.push(Line::from(Span::styled(
@@ -987,7 +1067,7 @@ mod tests {
"A very long title that should be truncated by the list pane width",
)];
let width = 24;
let lines = build_list_lines(&sessions, 0, width, 0, 5, false, "", "recent", false, None);
let lines = build_list_lines(&sessions, 0, width, 0, 5, false, "", "recent", false, false, "", None);
for line in lines {
let rendered_width: usize = line.spans.iter().map(|span| span.content.width()).sum();
@@ -1004,7 +1084,7 @@ mod tests {
test_session(1, "first session"),
test_session(2, "second session"),
];
let lines = build_list_lines(&sessions, 1, 80, 0, 5, false, "", "recent", false, None);
let lines = build_list_lines(&sessions, 1, 80, 0, 5, false, "", "recent", false, false, "", None);
let selected_line = lines
.iter()
@@ -1029,7 +1109,7 @@ mod tests {
let mut forked = test_session(1, "forked path");
forked.parent_session_id = Some("parent-session-abcdef".to_string());
forked.forked_from_message_count = Some(3);
let lines = build_list_lines(&[forked], 0, 120, 0, 5, false, "", "recent", false, None);
let lines = build_list_lines(&[forked], 0, 120, 0, 5, false, "", "recent", false, false, "", None);
let rendered = lines
.iter()
@@ -1046,7 +1126,7 @@ mod tests {
test_session(1, "first session"),
test_session(2, "second session"),
];
let lines = build_list_lines(&sessions, 0, 80, 0, 5, false, "", "recent", false, None);
let lines = build_list_lines(&sessions, 0, 80, 0, 5, false, "", "recent", false, false, "", None);
let rendered = lines
.iter()
+3
View File
@@ -1297,6 +1297,9 @@ async fn run_event_loop(
| "agent_close"
| "agent_cancel"
| "todo_write"
| "checklist_write"
| "checklist_update"
| "update_plan"
| "task_shell_start"
| "exec_shell"
) {