From 9dcf0841474ff140897a86f7fe91e7af15cd4e27 Mon Sep 17 00:00:00 2001 From: mmxgn <> Date: Wed, 12 Aug 2026 16:20:11 +0200 Subject: [PATCH 1/2] feat: terminate and force-kill buttons on spawned-process rows Child rows (parent_id != 0) get the same hover-revealed action overlay as session rows, with two buttons: SIGTERM to end the process gracefully and SIGKILL when it won't go quietly. Row teardown is already handled by poll_children noticing the PID is gone. Closes #12 --- FEATURES.org | 5 ++- README.md | 2 ++ docs/a11y-testing.md | 7 ++-- src/ui/sidebar.c | 76 ++++++++++++++++++++++++++++++++------------ 4 files changed, 65 insertions(+), 25 deletions(-) diff --git a/FEATURES.org b/FEATURES.org index b74f229..f073530 100644 --- a/FEATURES.org +++ b/FEATURES.org @@ -26,7 +26,10 @@ VTE regex on URLs (http/https/ftp/file/www.) and absolute paths; hover shows poi * TODO I want, in the Diff menu, to have a top "bar" with 1) the current commit message, (or uncommited on unstaged) but also some transport-like buttons (e.g. << >> ) that let me navigate (and ask questions and use) previous commits in the history. * TODO Add an icon for when claude is compacting discussion * TODO I want to be able to open ssh sessions/w claude to a machine in the same way. Add support for remote claude sessions with the same/or as similar as possible functionality without sacrificing too much performance (maybe a remote session should have a different icon) -* TODO Sub-agents should be killed +* DONE Sub-agents should be killed +Child-process rows (parent_id != 0) get two hover-revealed buttons in make_row: +edit-delete-symbolic sends SIGKILL, window-close-symbolic sends SIGTERM. The row +is torn down by the existing poll_children -> on_child_exited path. * TODO Hooks for Claude for getting things such as information about agents * TODO New session / open session functionality lets you type in paths directly Could be emacs like, like ~/claude:~ opens a claude session, or ~/ssh:~ opens an ssh session diff --git a/README.md b/README.md index 978eb36..7d9540b 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,8 @@ Everything is reachable without a mouse: When a Claude session spawns sub-agents (parallel tasks), they appear indented under their parent in the sidebar with a robot icon. Their state dots follow the same colour scheme. Long executable paths are shortened to `/.../name` to keep the sidebar readable. Sub-agents that block on a question turn red, making it easy to spot which one needs a decision — use `Ctrl+Shift+A` to jump straight to it. +Hovering a spawned process (a sub-agent, or any subprocess of a shell) reveals two buttons: terminate it gracefully (`SIGTERM`), or force-kill it (`SIGKILL`) when it will not go quietly. The row disappears once the process is gone. + ## Install Grab the latest asset from the [releases page](https://github.com/mmxgn/gattn/releases/latest). diff --git a/docs/a11y-testing.md b/docs/a11y-testing.md index e37370d..69d01ec 100644 --- a/docs/a11y-testing.md +++ b/docs/a11y-testing.md @@ -18,9 +18,10 @@ Launch `./build/gattn` from another shell. With Orca on: Expected: `, , ` per row. - **State-change**: kick a claude session; when it stops working, Orca should say ` needs input` even without focus. -- **Icon buttons**: Tab into the row's inline buttons. Tooltips - (`Open folder`, `Open shell here`, `Show diff`, `Close session`) - should be spoken. +- **Icon buttons**: Tab into the row's inline buttons. Tooltips should be + spoken. Session rows: `Open folder`, `Open shell here`, `Fork session`, + `Close session`. Spawned-process rows: `Force kill process`, + `Terminate process`. - **Search bar**: `Ctrl+F`, type — placeholder + echoes should be spoken. ## 3. Keyboard-only diff --git a/src/ui/sidebar.c b/src/ui/sidebar.c index 52a904b..23d29f5 100644 --- a/src/ui/sidebar.c +++ b/src/ui/sidebar.c @@ -848,6 +848,28 @@ on_close_btn_clicked(GtkButton *btn, gpointer data) kill(s->pid, SIGHUP); } +/* Child-row buttons: graceful first, unblockable second. The row itself is torn + down by poll_children noticing the PID is gone, so there's nothing to clean up + here. */ + +static void +on_stop_btn_clicked(GtkButton *btn, gpointer data) +{ + (void)btn; + Session *s = data; + if (s->pid > 0) + kill(s->pid, SIGTERM); +} + +static void +on_kill_btn_clicked(GtkButton *btn, gpointer data) +{ + (void)btn; + Session *s = data; + if (s->pid > 0) + kill(s->pid, SIGKILL); +} + /* -- row builder -- */ static GtkWidget * @@ -975,28 +997,35 @@ make_row(Session *s) gtk_box_append(GTK_BOX(box), labels); } - GtkWidget *actions = NULL; - GtkWidget *content = box; - if (!s->parent_id) { - actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + GtkWidget *actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + if (s->parent_id) { + /* Spawned process: only the two ways to end it. edit-delete-symbolic is a + circled X; process-stop-symbolic would be a bare X, indistinguishable + from the terminate button sitting next to it. */ + gtk_box_append(GTK_BOX(actions), make_icon_btn("edit-delete-symbolic", "Force kill process", + G_CALLBACK(on_kill_btn_clicked), s)); + gtk_box_append(GTK_BOX(actions), make_icon_btn("window-close-symbolic", "Terminate process", + G_CALLBACK(on_stop_btn_clicked), s)); + } else { gtk_box_append(GTK_BOX(actions), make_icon_btn("folder-open-symbolic", "Open folder", G_CALLBACK(on_folder_btn_clicked), s)); gtk_box_append(GTK_BOX(actions), make_icon_btn("utilities-terminal-symbolic", "Open shell here", G_CALLBACK(on_terminal_btn_clicked), s)); - gtk_box_append(GTK_BOX(actions), make_icon_btn("media-playlist-shuffle-symbolic", "Fork session", - G_CALLBACK(on_fork_btn_clicked), s)); + gtk_box_append(GTK_BOX(actions), + make_icon_btn("media-playlist-shuffle-symbolic", "Fork session", + G_CALLBACK(on_fork_btn_clicked), s)); gtk_box_append(GTK_BOX(actions), make_icon_btn("window-close-symbolic", "Close session", G_CALLBACK(on_close_btn_clicked), s)); - /* Actions float on top of the row via GtkOverlay so they don't steal - horizontal space from the labels. Hover fades them in over the branch. */ - gtk_widget_set_halign(actions, GTK_ALIGN_END); - gtk_widget_set_valign(actions, GTK_ALIGN_CENTER); - gtk_widget_set_margin_end(actions, 4); - content = gtk_overlay_new(); - gtk_overlay_set_child(GTK_OVERLAY(content), box); - gtk_overlay_add_overlay(GTK_OVERLAY(content), actions); } + /* Actions float on top of the row via GtkOverlay so they don't steal + horizontal space from the labels. Hover fades them in over the branch. */ + gtk_widget_set_halign(actions, GTK_ALIGN_END); + gtk_widget_set_valign(actions, GTK_ALIGN_CENTER); + gtk_widget_set_margin_end(actions, 4); + GtkWidget *content = gtk_overlay_new(); + gtk_overlay_set_child(GTK_OVERLAY(content), box); + gtk_overlay_add_overlay(GTK_OVERLAY(content), actions); GtkWidget *row = gtk_list_box_row_new(); gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), content); @@ -1011,7 +1040,8 @@ make_row(Session *s) g_object_set_data(G_OBJECT(row), "gattn-row-cwd", s->cwd_label); g_object_set_data(G_OBJECT(row), "gattn-row-branch-sep", s->branch_sep); g_object_set_data(G_OBJECT(row), "gattn-row-branch", s->branch_label); - g_object_set_data(G_OBJECT(row), "gattn-row-labels", labels); + /* Robot rows have no vbox, so the name label itself is what gets pushed in. */ + g_object_set_data(G_OBJECT(row), "gattn-row-labels", labels ? labels : name_lbl); session_refresh_a11y(s); /* Hover / focus reveal for the action buttons: hidden by default. */ @@ -1123,10 +1153,13 @@ apply_row_visibility(GtkListBoxRow *row) } /* Push the labels' trailing edge in when actions are revealed so the name - ellipsizes before it slides under the icons. 4 icons ~32 px each + margin. */ + ellipsizes before it slides under the icons. ~32 px per icon + margin; + child rows carry 2 buttons, top-level rows 4. */ GtkWidget *labels = g_object_get_data(G_OBJECT(row), "gattn-row-labels"); - if (labels) - gtk_widget_set_margin_end(labels, hover_or_focus ? 136 : 0); + if (labels) { + int push = (s && s->parent_id) ? 72 : 136; + gtk_widget_set_margin_end(labels, hover_or_focus ? push : 0); + } } static void @@ -1239,9 +1272,10 @@ sidebar_add_session(GtkWidget *split, Session *s) if (!r) break; Session *rs = g_object_get_data(G_OBJECT(r), "gattn-session"); - if (rs && (rs->id == s->fork_parent_id - || (rs->is_fork && rs->fork_parent_id == s->fork_parent_id) - || rs->parent_id == s->fork_parent_id)) + if (rs + && (rs->id == s->fork_parent_id + || (rs->is_fork && rs->fork_parent_id == s->fork_parent_id) + || rs->parent_id == s->fork_parent_id)) pos = i + 1; } gtk_list_box_insert(lb, row, pos); From cdb0ab409eedd51751b886fcb95fc4b6535418f6 Mon Sep 17 00:00:00 2001 From: mmxgn <> Date: Wed, 12 Aug 2026 16:21:28 +0200 Subject: [PATCH 2/2] chore: ignore nix result symlink and .cache --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f0cde1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +# nix build symlink +/result +# tooling caches (pre-commit, etc.) +/.cache/