diff --git a/src/tui/app.rs b/src/tui/app.rs index 2edcd26..9469055 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -636,8 +636,6 @@ pub struct App { pub rebase_branch_list_state: ListState, pub pending_branch_command: Option, pub rebase_branch_search: TextArea<'static>, - // Delete mode chooser - pub archive_mode_index: usize, // Restart task wizard pub restart_wizard: Option, pub should_restart: bool, @@ -866,7 +864,6 @@ impl App { rebase_branch_list_state: ListState::default(), pending_branch_command: None, rebase_branch_search: Self::create_plain_editor(), - archive_mode_index: 0, restart_wizard: None, should_restart: false, last_pr_poll: Instant::now(), @@ -1413,6 +1410,62 @@ impl App { } } + fn open_project_pm_chat(&mut self) { + let Some(project_name) = self.current_project.clone() else { + return; + }; + if project_name == "(unassigned)" || self.popup.is_some() { + return; + } + + match use_cases::open_pm_popup(&self.config, &project_name) { + Ok(child) => { + tracing::info!(project = %project_name, "opened PM popup"); + self.popup = Some(ActivePopup { child }); + } + Err(e) => { + tracing::error!(project = %project_name, error = %e, "failed to open PM popup"); + self.set_status(format!("Failed to open PM chat: {e}")); + } + } + } + + fn start_project_respawn_confirm(&mut self) { + let Some(project_name) = self.current_project.clone() else { + return; + }; + if project_name == "(unassigned)" || self.respawn_in_progress.is_some() { + return; + } + + self.respawn_confirm_target = Some(project_name); + self.respawn_confirm_index = 0; + self.respawn_confirm_is_chief_of_staff = false; + self.respawn_confirm_return_view = View::TaskList; + self.view = View::RespawnConfirm; + } + + fn start_focused_archive_confirm(&mut self) { + let has_selection = match self.project_pane_focus { + ProjectPaneFocus::Tasks => !self.tasks.is_empty(), + ProjectPaneFocus::Assistants => self.selected_assistant().is_some(), + }; + if has_selection { + self.view = View::DeleteConfirm; + } + } + + fn archive_focused_project_row(&mut self) -> Result<()> { + match self.project_pane_focus { + ProjectPaneFocus::Tasks => self.archive_task(false)?, + ProjectPaneFocus::Assistants => { + self.archive_selected_assistant(); + self.view = View::TaskList; + } + } + Ok(()) + } + fn open_archive(&mut self, kind: ArchiveKind) { self.archive_kind = kind; match kind { @@ -1662,43 +1715,6 @@ impl App { Ok(()) } - fn fully_delete_task(&mut self) -> Result<()> { - if self.tasks.is_empty() { - return Ok(()); - } - - let task = self.tasks.remove(self.selected_index); - let task_id = task.meta.task_id(); - - tracing::info!(task_id = %task_id, "TUI: full delete requested"); - self.log_output(format!("Fully deleting task {}...", task_id)); - - // Kill tmux sessions for all repos (side effect) - if task.meta.has_repos() { - for repo in &task.meta.repos { - let _ = Tmux::kill_session(&repo.tmux_session); - } - } - // Also kill the parent-dir session (used for repo-inspector in multi-repo tasks) - if task.meta.is_multi_repo() { - let parent_session = Config::tmux_session_name(&task.meta.name, &task.meta.branch_name); - let _ = Tmux::kill_session(&parent_session); - } - self.log_output(" Killed tmux session(s)".to_string()); - - // Delegate business logic to use_cases - use_cases::fully_delete_task(&self.config, task)?; - self.log_output(" Deleted task".to_string()); - - if self.selected_index >= self.tasks.len() && !self.tasks.is_empty() { - self.selected_index = self.tasks.len() - 1; - } - - self.set_status(format!("Deleted: {}", task_id)); - self.view = View::TaskList; - Ok(()) - } - fn start_feedback(&mut self) { // Clear the feedback editor and start in insert mode self.feedback_editor = VimTextArea::new(); @@ -2872,14 +2888,8 @@ impl App { KeyCode::Char('s') => { self.stop_task()?; } - KeyCode::Char('A') => { - self.archive_task(false)?; - } KeyCode::Char('d') => { - if !self.tasks.is_empty() { - self.archive_mode_index = 0; - self.view = View::DeleteConfirm; - } + self.start_focused_archive_confirm(); } KeyCode::Char('f') => { if !self.tasks.is_empty() { @@ -2939,22 +2949,8 @@ impl App { self.toggle_hold()?; } KeyCode::Char('c') => { - if let Some(ref project_name) = self.current_project.clone() { - if project_name != "(unassigned)" { - if self.popup.is_some() { - return Ok(false); - } - match use_cases::open_pm_popup(&self.config, project_name) { - Ok(child) => { - tracing::info!(project = %project_name, "opened PM popup"); - self.popup = Some(ActivePopup { child }); - } - Err(e) => { - tracing::error!(project = %project_name, error = %e, "failed to open PM popup"); - self.set_status(format!("Failed to open PM chat: {e}")); - } - } - } + if self.current_project.is_some() { + self.open_project_pm_chat(); } else { let is_owned = self .selected_task() @@ -2979,15 +2975,7 @@ impl App { self.open_archive(ArchiveKind::Tasks); } KeyCode::Char('e') => { - if let Some(ref project_name) = self.current_project.clone() { - if project_name != "(unassigned)" && self.respawn_in_progress.is_none() { - self.respawn_confirm_target = Some(project_name.clone()); - self.respawn_confirm_index = 0; - self.respawn_confirm_is_chief_of_staff = false; - self.respawn_confirm_return_view = View::TaskList; - self.view = View::RespawnConfirm; - } - } + self.start_project_respawn_confirm(); } _ => {} } @@ -3016,12 +3004,18 @@ impl App { KeyCode::Char('n') => { self.start_assistant_wizard(); } - KeyCode::Char('A') | KeyCode::Char('d') => { - self.archive_selected_assistant(); + KeyCode::Char('d') => { + self.start_focused_archive_confirm(); } KeyCode::Char('z') => { self.open_archive(ArchiveKind::Assistants); } + KeyCode::Char('c') => { + self.open_project_pm_chat(); + } + KeyCode::Char('e') => { + self.start_project_respawn_confirm(); + } KeyCode::Char('s') | KeyCode::Char('f') | KeyCode::Char('x') @@ -3029,9 +3023,7 @@ impl App { | KeyCode::Char('a') | KeyCode::Char('o') | KeyCode::Char('r') - | KeyCode::Char('h') - | KeyCode::Char('c') - | KeyCode::Char('e') => { + | KeyCode::Char('h') => { self.set_status("Switch to Tasks for task actions".to_string()); } _ => {} @@ -4411,17 +4403,7 @@ impl App { fn handle_delete_confirm_event(&mut self, event: Event) -> Result { if let Event::Key(key) = event { match key.code { - KeyCode::Char('j') | KeyCode::Down => { - self.archive_mode_index = (self.archive_mode_index + 1) % 3; - } - KeyCode::Char('k') | KeyCode::Up => { - self.archive_mode_index = (self.archive_mode_index + 2) % 3; - } - KeyCode::Enter => match self.archive_mode_index { - 0 => self.archive_task(false)?, - 1 => self.archive_task(true)?, - _ => self.fully_delete_task()?, - }, + KeyCode::Enter => self.archive_focused_project_row()?, KeyCode::Esc | KeyCode::Char('q') => { self.view = View::TaskList; } diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 5461453..efa85dd 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -136,7 +136,7 @@ pub fn draw(f: &mut Frame, app: &mut App) { View::Preview => draw_preview(f, app, chunks[0]), View::DeleteConfirm => { draw_project_detail(f, app, chunks[0]); - draw_delete_confirm(f, app, app.archive_retention_days); + draw_delete_confirm(f, app); } View::Feedback => { draw_preview(f, app, chunks[0]); @@ -2100,103 +2100,60 @@ fn draw_feedback(f: &mut Frame, app: &mut App) { f.render_widget(&app.feedback_editor.textarea, chunks[1]); } -fn draw_delete_confirm(f: &mut Frame, app: &App, retention_days: u64) { - let area = centered_rect(55, 55, f.area()); +fn draw_delete_confirm(f: &mut Frame, app: &App) { + let area = centered_rect(52, 28, f.area()); f.render_widget(Clear, area); - let task_id = app - .selected_task() - .map(|t| t.meta.task_id()) - .unwrap_or_else(|| "unknown".to_string()); - - let sel = app.archive_mode_index; - - let archive_style = if sel == 0 { - Style::default() - .fg(Color::White) - .bg(Color::Rgb(30, 40, 60)) - .add_modifier(Modifier::BOLD) - } else { - Style::default().fg(Color::Gray) - }; - let save_style = if sel == 1 { - Style::default() - .fg(Color::White) - .bg(Color::Rgb(20, 50, 40)) - .add_modifier(Modifier::BOLD) - } else { - Style::default().fg(Color::Gray) - }; - let delete_style = if sel == 2 { - Style::default() - .fg(Color::White) - .bg(Color::Rgb(60, 20, 20)) - .add_modifier(Modifier::BOLD) - } else { - Style::default().fg(Color::Gray) + let (question, subject) = match app.project_pane_focus { + ProjectPaneFocus::Tasks => ( + "Archive this task?", + app.selected_task() + .map(|t| t.meta.task_id()) + .unwrap_or_else(|| "unknown".to_string()), + ), + ProjectPaneFocus::Assistants => ( + "Archive this assistant?", + app.assistants + .get(app.assistant_list_index) + .map(|assistant| format!("{}--{}", assistant.meta.project, assistant.meta.name)) + .unwrap_or_else(|| "unknown".to_string()), + ), }; - let archive_prefix = if sel == 0 { "▸ " } else { " " }; - let save_prefix = if sel == 1 { "▸ " } else { " " }; - let delete_prefix = if sel == 2 { "▸ " } else { " " }; - let text = vec![ Line::from(""), Line::from(Span::styled( - format!(" Archive task '{}'?", task_id), + format!(" {question}"), Style::default() .fg(Color::White) .add_modifier(Modifier::BOLD), )), Line::from(""), Line::from(Span::styled( - format!("{}Archive", archive_prefix), - archive_style, + format!(" {subject}"), + Style::default().fg(Color::LightCyan), )), + Line::from(""), Line::from(Span::styled( - " Kill tmux, remove worktree + branch,", + " This moves the item to the archive. Permanent delete remains", Style::default().fg(Color::LightBlue), )), Line::from(Span::styled( - format!( - " keep task files. Auto-purged after {} days.", - retention_days - ), + " available from the archive view.", Style::default().fg(Color::LightBlue), )), Line::from(""), Line::from(Span::styled( - format!("{}Archive & Save", save_prefix), - save_style, - )), - Line::from(Span::styled( - " Same as Archive, but will NOT be auto-purged.", - Style::default().fg(Color::LightCyan), - )), - Line::from(Span::styled( - " Use for tasks you want to keep permanently.", - Style::default().fg(Color::LightCyan), - )), - Line::from(""), - Line::from(Span::styled( - format!("{}Delete", delete_prefix), - delete_style, - )), - Line::from(Span::styled( - " Kill tmux, remove worktree, delete branches", - Style::default().fg(Color::LightRed), - )), - Line::from(Span::styled( - " and task files. Irreversible.", - Style::default().fg(Color::LightRed), + " [Enter] archive [Esc] cancel", + Style::default().fg(Color::DarkGray), )), ]; let popup = Paragraph::new(text).block( Block::default() .title(Span::styled( - " Remove Task ", + " Archive ", Style::default() .fg(Color::LightBlue) .add_modifier(Modifier::BOLD), @@ -2585,14 +2542,34 @@ fn draw_status_bar(f: &mut Frame, app: &App, area: Rect) { Span::styled(" new ", Style::default().fg(Color::DarkGray)), ]; if app.project_pane_focus == ProjectPaneFocus::Assistants { + if app + .current_project + .as_deref() + .is_some_and(|p| p != "(unassigned)") + { + spans.extend([ + Span::styled("c", Style::default().fg(Color::LightYellow)), + Span::styled(" PM chat ", Style::default().fg(Color::DarkGray)), + ]); + } spans.extend([ Span::styled("enter", Style::default().fg(Color::LightGreen)), Span::styled(" attach ", Style::default().fg(Color::DarkGray)), - Span::styled("A/d", Style::default().fg(Color::LightRed)), + Span::styled("d", Style::default().fg(Color::LightRed)), Span::styled(" archive ", Style::default().fg(Color::DarkGray)), Span::styled("z", Style::default().fg(Color::LightYellow)), Span::styled(" archived ", Style::default().fg(Color::DarkGray)), ]); + if app + .current_project + .as_deref() + .is_some_and(|p| p != "(unassigned)") + { + spans.extend([ + Span::styled("e", Style::default().fg(Color::LightMagenta)), + Span::styled(" respawn ", Style::default().fg(Color::DarkGray)), + ]); + } if app.current_project.is_some() { spans.extend([ Span::styled("q", Style::default().fg(Color::LightCyan)), @@ -2647,7 +2624,8 @@ fn draw_status_bar(f: &mut Frame, app: &App, area: Rect) { Style::default().fg(Color::DarkGray), )); } - if task.meta.review_addressed + if app.current_project.is_none() + && task.meta.review_addressed && task.meta.linked_pr.as_ref().is_some_and(|pr| pr.owned) { spans.push(Span::styled("c", Style::default().fg(Color::LightGreen))); @@ -2673,10 +2651,8 @@ fn draw_status_bar(f: &mut Frame, app: &App, area: Rect) { Span::styled(" feedback ", Style::default().fg(Color::DarkGray)), Span::styled("x", Style::default().fg(Color::LightMagenta)), Span::styled(" cmd ", Style::default().fg(Color::DarkGray)), - Span::styled("A", Style::default().fg(Color::LightRed)), - Span::styled(" archive ", Style::default().fg(Color::DarkGray)), Span::styled("d", Style::default().fg(Color::LightRed)), - Span::styled(" del ", Style::default().fg(Color::DarkGray)), + Span::styled(" archive ", Style::default().fg(Color::DarkGray)), ]); } if app @@ -2691,7 +2667,7 @@ fn draw_status_bar(f: &mut Frame, app: &App, area: Rect) { } spans.extend([ Span::styled("z", Style::default().fg(Color::LightYellow)), - Span::styled(" archive ", Style::default().fg(Color::DarkGray)), + Span::styled(" archived ", Style::default().fg(Color::DarkGray)), ]); if app.current_project.is_some() { spans.extend([ @@ -2790,10 +2766,8 @@ fn draw_status_bar(f: &mut Frame, app: &App, area: Rect) { } View::DeleteConfirm => { vec![ - Span::styled("j/k", Style::default().fg(Color::LightCyan)), - Span::styled(" nav ", Style::default().fg(Color::DarkGray)), Span::styled("Enter", Style::default().fg(Color::LightGreen)), - Span::styled(" confirm ", Style::default().fg(Color::DarkGray)), + Span::styled(" archive ", Style::default().fg(Color::DarkGray)), Span::styled("Esc/q", Style::default().fg(Color::LightRed)), Span::styled(" cancel", Style::default().fg(Color::DarkGray)), ]