Skip to content

Commit 32e6caa

Browse files
committed
Add RequestAttention DBus method for tabs needing input
Long-running interactive programs in a tab (notably AI agents) can now ask the terminal to flag the tab when they need user input. This reuses the existing tab-state + freedesktop-notification plumbing used for process completion: a new TabState.ATTENTION shows a dialog-question-symbolic icon and sets Adw.TabPage.needs_attention, and the indicator clears when the user focuses the tab. Triggered from the tab via: dbus-send --type=method_call --session \ --dest=io.elementary.terminal /io/elementary/terminal \ io.elementary.terminal.RequestAttention \ string:"$PANTHEON_TERMINAL_ID" string:"reason"
1 parent 532407c commit 32e6caa

5 files changed

Lines changed: 87 additions & 1 deletion

File tree

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,17 @@ Terminal implements process completion notifications. They are enabled for BASH
3838
builtin . /usr/share/io.elementary.terminal/enable-zsh-completion-notifications || builtin true
3939

4040
DISTRIBUTORS: depending on the policy of your distribution, either inform the user about this via the default mechanism for your distribution (for DIY distros like Arch), or add that line to `/etc/zshrc` automatically on installation (for preconfigured distros like Ubuntu).
41+
42+
## Attention Requests
43+
44+
Long-running interactive programs (for example AI agents) can ask the terminal to flag a tab when they need user input. The terminal marks the tab with an attention indicator and a `dialog-question-symbolic` icon, and posts a desktop notification if the window is not focused or the tab is not selected. The indicator clears automatically when the user focuses the tab.
45+
46+
Each tab exports its identifier as `PANTHEON_TERMINAL_ID`. A program can request attention with a single DBus call:
47+
48+
dbus-send --type=method_call --session \
49+
--dest=io.elementary.terminal /io/elementary/terminal \
50+
io.elementary.terminal.RequestAttention \
51+
string:"$PANTHEON_TERMINAL_ID" \
52+
string:"Waiting for confirmation"
53+
54+
The second string is shown as the notification body and may be empty.

src/Application.vala

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,53 @@ public class Terminal.Application : Gtk.Application {
154154
var dbus = new DBus ();
155155
dbus_id = connection.register_object (object_path, dbus);
156156

157+
dbus.attention_requested.connect ((id, message) => {
158+
TerminalWidget terminal = null;
159+
160+
foreach (var window in (List<MainWindow>) get_windows ()) {
161+
if (terminal != null) {
162+
break;
163+
}
164+
165+
terminal = window.get_terminal (id);
166+
}
167+
168+
if (terminal == null) {
169+
return;
170+
}
171+
172+
unowned var window = (MainWindow) terminal.root;
173+
if (terminal != window.current_terminal || !window.is_active) {
174+
terminal.tab_state = ATTENTION;
175+
}
176+
177+
if (window.is_active && terminal == window.current_terminal) {
178+
return;
179+
}
180+
181+
var notification_title = _("Terminal needs your attention");
182+
var notification = new Notification (notification_title);
183+
if (message.length > 0) {
184+
notification.set_body (message);
185+
}
186+
notification.set_icon (TerminalWidget.TabState.ATTENTION.to_icon ());
187+
notification.set_default_action_and_target_value ("app.process-finished", new Variant.string (id));
188+
send_notification ("attention-requested-%s".printf (id), notification);
189+
190+
ulong tab_change_handler = 0;
191+
ulong focus_in_handler = 0;
192+
193+
tab_change_handler = window.notify["current-terminal"].connect ((obj, pspec) => {
194+
withdraw_attention_for_terminal ((MainWindow) obj, terminal, id, tab_change_handler, focus_in_handler);
195+
});
196+
197+
focus_in_handler = window.notify["is-active"].connect ((obj, pspec) => {
198+
if (((MainWindow) obj).is_active) {
199+
withdraw_attention_for_terminal ((MainWindow) obj, terminal, id, tab_change_handler, focus_in_handler);
200+
}
201+
});
202+
});
203+
157204
dbus.finished_process.connect ((id, process, exit_status) => {
158205
TerminalWidget terminal = null;
159206

@@ -228,6 +275,20 @@ public class Terminal.Application : Gtk.Application {
228275
window.disconnect (focus_in_handler);
229276
}
230277

278+
private void withdraw_attention_for_terminal (MainWindow window, TerminalWidget terminal, string id, ulong tab_change_handler, ulong focus_in_handler) {
279+
if (window.current_terminal != terminal) {
280+
return;
281+
}
282+
283+
if (terminal.tab_state == ATTENTION) {
284+
terminal.tab_state = NONE;
285+
}
286+
withdraw_notification ("attention-requested-%s".printf (id));
287+
288+
window.disconnect (tab_change_handler);
289+
window.disconnect (focus_in_handler);
290+
}
291+
231292
protected override void startup () {
232293
base.startup ();
233294
Granite.init ();

src/DBus.vala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,18 @@ namespace Terminal {
2323
[DBus (visible = false)]
2424
public signal void finished_process (string terminal_id, string process, int exit_status);
2525

26+
[DBus (visible = false)]
27+
public signal void attention_requested (string terminal_id, string message);
28+
2629
public DBus () {
2730
}
2831

2932
public void process_finished (string terminal_id, string process, int exit_status) throws GLib.Error {
3033
finished_process (terminal_id, process, exit_status);
3134
}
35+
36+
public void request_attention (string terminal_id, string message) throws GLib.Error {
37+
attention_requested (terminal_id, message);
38+
}
3239
}
3340
}

src/Widgets/TerminalView.vala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ public class Terminal.TerminalView : Granite.Bin {
180180

181181
terminal_widget.notify["tab-state"].connect (() => {
182182
tab.icon = terminal_widget.tab_state.to_icon ();
183+
tab.needs_attention = terminal_widget.tab_state == ATTENTION;
183184
});
184185

185186
// Set correct label now to avoid race when spawning shell

src/Widgets/TerminalWidget.vala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ namespace Terminal {
99
NONE,
1010
WORKING,
1111
COMPLETED,
12-
ERROR;
12+
ERROR,
13+
ATTENTION;
1314

1415
public GLib.Icon? to_icon () {
1516
switch (this) {
@@ -18,6 +19,8 @@ namespace Terminal {
1819
return new GLib.ThemedIcon ("process-completed-symbolic");
1920
case ERROR:
2021
return new GLib.ThemedIcon ("process-error-symbolic");
22+
case ATTENTION:
23+
return new GLib.ThemedIcon ("dialog-question-symbolic");
2124
default:
2225
return null;
2326
}

0 commit comments

Comments
 (0)