From bc765cd8b25ed543b2b15657e0ddb40f03b77891 Mon Sep 17 00:00:00 2001 From: pfigs Date: Mon, 11 May 2026 14:25:57 +0300 Subject: [PATCH 1/3] feat(gui): delete completed orchestrator features from the sidebar Adds a click-to-confirm "del" button on completed feature rows (state in done/completed/complete/merged/shipped). First click arms the row (label flips to "confirm"); second click executes: - git worktree remove --force - git branch -D - rm -rf All three are best-effort and logged on failure; one failing step doesn't block the others. Assisted-By: Claude Code Pedro Silva --- src/gui/orchestrator_sidebar.rs | 134 ++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/src/gui/orchestrator_sidebar.rs b/src/gui/orchestrator_sidebar.rs index fe8c1ac..c133466 100644 --- a/src/gui/orchestrator_sidebar.rs +++ b/src/gui/orchestrator_sidebar.rs @@ -9,6 +9,10 @@ use super::theme; pub struct OrchestratorState { pub features: Vec, pub show_completed: bool, + /// feature_slug that's one click away from being deleted. + /// Set on first trash-button click, cleared on second click + /// (which executes) or when any other row is interacted with. + pub pending_delete: Option, } impl OrchestratorState { @@ -16,6 +20,7 @@ impl OrchestratorState { Self { features: Vec::new(), show_completed: false, + pending_delete: None, } } } @@ -219,6 +224,37 @@ impl ClaudioApp { ); } + if feature.is_completed() { + let is_pending = + self.orchestrator.pending_delete.as_deref() == Some(&feature.feature_slug); + let (label, color, bg) = if is_pending { + ("confirm", theme::CRUST, theme::RED) + } else { + ("del", theme::OVERLAY0, theme::SURFACE0) + }; + let del_id: SharedString = format!("orch-del-{}", feature.feature_slug).into(); + let slug_for_click = feature.feature_slug.clone(); + top = top.child( + div() + .id(del_id) + .child(label) + .text_color(rgb(color)) + .text_size(px(10.0)) + .px(px(4.0)) + .rounded(px(2.0)) + .bg(rgb(bg)) + .cursor_pointer() + .hover(|s| s.bg(rgb(theme::RED)).text_color(rgb(theme::CRUST))) + .on_mouse_down( + MouseButton::Left, + cx.listener(move |app, _ev: &MouseDownEvent, _window, cx| { + cx.stop_propagation(); + app.handle_orchestrator_delete_click(&slug_for_click, cx); + }), + ), + ); + } + row = row.child(top).child( div() .child(log_preview) @@ -293,4 +329,102 @@ impl ClaudioApp { }) .detach(); } + + /// First click: arm the row for deletion (changes label to "confirm"). + /// Second click on the same row: execute the delete. + /// Clicking a different row's button arms that row instead. + pub fn handle_orchestrator_delete_click( + &mut self, + feature_slug: &str, + cx: &mut Context, + ) { + let armed = self.orchestrator.pending_delete.as_deref() == Some(feature_slug); + if armed { + self.execute_orchestrator_delete(feature_slug, cx); + } else { + self.orchestrator.pending_delete = Some(feature_slug.to_string()); + cx.notify(); + } + } + + fn execute_orchestrator_delete(&mut self, feature_slug: &str, cx: &mut Context) { + let Some(feature) = self + .orchestrator + .features + .iter() + .find(|f| f.feature_slug == feature_slug) + .cloned() + else { + return; + }; + + let feature_dir = feature.status_path.parent().map(|p| p.to_path_buf()); + let worktree = feature.worktree.clone(); + let branch = feature.branch.clone(); + let slug_for_log = feature_slug.to_string(); + + cx.background_executor() + .spawn(async move { + // Worktree dir convention is `/.worktree/`, so the + // parent of the parent is the repo. `git worktree remove` from + // any worktree of the same repo works since .git is shared. + let repo = worktree + .as_ref() + .and_then(|w| w.parent()) + .and_then(|p| p.parent()) + .map(|p| p.to_path_buf()); + + if let (Some(ref wt), Some(ref repo)) = (worktree.as_ref(), repo.as_ref()) { + if wt.exists() { + let status = std::process::Command::new("git") + .args(["worktree", "remove", "--force"]) + .arg(wt) + .current_dir(repo) + .status(); + match status { + Ok(s) if s.success() => { + tracing::info!( + "orchestrator: removed worktree {} for {slug_for_log}", + wt.display() + ); + } + Ok(s) => tracing::warn!( + "orchestrator: git worktree remove for {slug_for_log} exited {s}" + ), + Err(e) => tracing::warn!( + "orchestrator: git worktree remove for {slug_for_log} failed: {e}" + ), + } + } + } + + if let (Some(ref br), Some(ref repo)) = (branch.as_ref(), repo.as_ref()) { + let status = std::process::Command::new("git") + .args(["branch", "-D", br]) + .current_dir(repo) + .status(); + if let Err(e) = status { + tracing::warn!( + "orchestrator: git branch -D {br} for {slug_for_log} failed: {e}" + ); + } + } + + if let Some(ref dir) = feature_dir { + if let Err(e) = std::fs::remove_dir_all(dir) { + tracing::warn!( + "orchestrator: rm -rf {} for {slug_for_log} failed: {e}", + dir.display() + ); + } + } + }) + .detach(); + + self.orchestrator.pending_delete = None; + self.orchestrator + .features + .retain(|f| f.feature_slug != feature_slug); + cx.notify(); + } } From 51204dac4cf02810a7e6c45b44a5468bf0afa042 Mon Sep 17 00:00:00 2001 From: pfigs Date: Wed, 13 May 2026 08:40:22 +0300 Subject: [PATCH 2/3] ci: satisfy clippy 1.95 lints and add ruff dev dep Clippy `-D warnings` was tripping on three lints surfaced by stable Rust 1.95 (new_without_default, module_inception, double_ended_iterator_last). Python lint job was failing because `ruff` was not declared as a dependency of ml_service. Assisted-By: Claude Code Pedro Silva --- ml_service/pyproject.toml | 3 +++ ml_service/src/ok_claude_ml/server.py | 3 --- ml_service/src/ok_claude_ml/tts.py | 2 -- src/gui/orchestrator_index.rs | 3 +-- src/session/manager.rs | 6 ++++++ src/session/mod.rs | 1 + 6 files changed, 11 insertions(+), 7 deletions(-) diff --git a/ml_service/pyproject.toml b/ml_service/pyproject.toml index 21e7029..203b7de 100644 --- a/ml_service/pyproject.toml +++ b/ml_service/pyproject.toml @@ -15,6 +15,9 @@ dependencies = [ [project.scripts] ok-claude-ml = "ok_claude_ml.server:main" +[dependency-groups] +dev = ["ruff>=0.6"] + [build-system] requires = ["uv_build>=0.9.9,<0.10.0"] build-backend = "uv_build" diff --git a/ml_service/src/ok_claude_ml/server.py b/ml_service/src/ok_claude_ml/server.py index 7c6aadc..bee0a04 100644 --- a/ml_service/src/ok_claude_ml/server.py +++ b/ml_service/src/ok_claude_ml/server.py @@ -4,15 +4,12 @@ import logging import os import signal -import struct -import sys from .protocol import ( MSG_PING, MSG_SYNTHESIZE, MSG_TRANSCRIBE, MSG_VAD_FEED, - HEADER_SIZE, read_frame, write_error, write_frame, diff --git a/ml_service/src/ok_claude_ml/tts.py b/ml_service/src/ok_claude_ml/tts.py index ed8e15e..bfcde39 100644 --- a/ml_service/src/ok_claude_ml/tts.py +++ b/ml_service/src/ok_claude_ml/tts.py @@ -38,8 +38,6 @@ def sample_rate(self) -> int: @staticmethod def _default_model_path() -> str: - import subprocess - import shutil # Try to find a piper model in common locations for path in [ diff --git a/src/gui/orchestrator_index.rs b/src/gui/orchestrator_index.rs index 5bc10fa..c2800f0 100644 --- a/src/gui/orchestrator_index.rs +++ b/src/gui/orchestrator_index.rs @@ -83,8 +83,7 @@ pub fn parse_status_text(text: &str, path: &Path) -> Result { let last_log_line = stripped .body .lines() - .filter(|l| !l.trim().is_empty()) - .last() + .rfind(|l| !l.trim().is_empty()) .map(|l| l.trim_start_matches("- ").trim().to_string()); let (repo, feature_slug) = derive_repo_and_feature(path) diff --git a/src/session/manager.rs b/src/session/manager.rs index 685b1b3..18abac6 100644 --- a/src/session/manager.rs +++ b/src/session/manager.rs @@ -10,6 +10,12 @@ pub struct SessionManager { focused_id: Option, } +impl Default for SessionManager { + fn default() -> Self { + Self::new() + } +} + impl SessionManager { pub fn new() -> Self { Self { diff --git a/src/session/mod.rs b/src/session/mod.rs index 561cf19..0c6d31d 100644 --- a/src/session/mod.rs +++ b/src/session/mod.rs @@ -1,3 +1,4 @@ pub mod manager; pub mod mode; +#[allow(clippy::module_inception)] pub mod session; From bf8120e56d2454bd2fc2f8e64a705120cbad4b67 Mon Sep 17 00:00:00 2001 From: pfigs Date: Wed, 3 Jun 2026 16:54:21 +0300 Subject: [PATCH 3/3] fix: collapse nested if into let-chains for clippy Co-Authored-By: Claude Opus 4.8 (1M context) --- src/gui/orchestrator_sidebar.rs | 54 ++++++++++++++++----------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/gui/orchestrator_sidebar.rs b/src/gui/orchestrator_sidebar.rs index c133466..ac08489 100644 --- a/src/gui/orchestrator_sidebar.rs +++ b/src/gui/orchestrator_sidebar.rs @@ -374,27 +374,27 @@ impl ClaudioApp { .and_then(|p| p.parent()) .map(|p| p.to_path_buf()); - if let (Some(ref wt), Some(ref repo)) = (worktree.as_ref(), repo.as_ref()) { - if wt.exists() { - let status = std::process::Command::new("git") - .args(["worktree", "remove", "--force"]) - .arg(wt) - .current_dir(repo) - .status(); - match status { - Ok(s) if s.success() => { - tracing::info!( - "orchestrator: removed worktree {} for {slug_for_log}", - wt.display() - ); - } - Ok(s) => tracing::warn!( - "orchestrator: git worktree remove for {slug_for_log} exited {s}" - ), - Err(e) => tracing::warn!( - "orchestrator: git worktree remove for {slug_for_log} failed: {e}" - ), + if let (Some(ref wt), Some(ref repo)) = (worktree.as_ref(), repo.as_ref()) + && wt.exists() + { + let status = std::process::Command::new("git") + .args(["worktree", "remove", "--force"]) + .arg(wt) + .current_dir(repo) + .status(); + match status { + Ok(s) if s.success() => { + tracing::info!( + "orchestrator: removed worktree {} for {slug_for_log}", + wt.display() + ); } + Ok(s) => tracing::warn!( + "orchestrator: git worktree remove for {slug_for_log} exited {s}" + ), + Err(e) => tracing::warn!( + "orchestrator: git worktree remove for {slug_for_log} failed: {e}" + ), } } @@ -410,13 +410,13 @@ impl ClaudioApp { } } - if let Some(ref dir) = feature_dir { - if let Err(e) = std::fs::remove_dir_all(dir) { - tracing::warn!( - "orchestrator: rm -rf {} for {slug_for_log} failed: {e}", - dir.display() - ); - } + if let Some(ref dir) = feature_dir + && let Err(e) = std::fs::remove_dir_all(dir) + { + tracing::warn!( + "orchestrator: rm -rf {} for {slug_for_log} failed: {e}", + dir.display() + ); } }) .detach();