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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# nix build symlink
/result
# tooling caches (pre-commit, etc.)
/.cache/
5 changes: 4 additions & 1 deletion FEATURES.org
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
7 changes: 4 additions & 3 deletions docs/a11y-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ Launch `./build/gattn` from another shell. With Orca on:
Expected: `<name>, <state>, <cwd-basename>` per row.
- **State-change**: kick a claude session; when it stops working, Orca
should say `<name> 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
Expand Down
76 changes: 55 additions & 21 deletions src/ui/sidebar.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 *
Expand Down Expand Up @@ -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);
Expand All @@ -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. */
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
Loading