Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions ml_service/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
134 changes: 134 additions & 0 deletions src/gui/orchestrator_sidebar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@ use super::theme;
pub struct OrchestratorState {
pub features: Vec<FeatureSummary>,
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<String>,
}

impl OrchestratorState {
pub fn new() -> Self {
Self {
features: Vec::new(),
show_completed: false,
pending_delete: None,
}
}
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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<Self>,
) {
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<Self>) {
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 `<repo>/.worktree/<name>`, 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())
&& 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
&& 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();
}
}
Loading