@tryinget/pi-activity-strip 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -6
- package/bin/pi-activity-strip-claude-hook.mjs +71 -0
- package/bin/pi-activity-strip.mjs +22 -1
- package/native/bin/linux-x64-gnu/artifact.json +2 -2
- package/native/bin/linux-x64-gnu/pi-activity-strip-panel +0 -0
- package/native/panel/src/app.rs +20 -20
- package/native/panel/src/card_view.rs +308 -9
- package/native/panel/src/protocol.rs +63 -0
- package/native/panel/src/runtime.rs +50 -9
- package/native/panel/src/style.css +134 -60
- package/package.json +4 -4
- package/src/common/agent-identity.mjs +163 -0
- package/src/common/ak-tasks.mjs +354 -0
- package/src/common/claude-events.mjs +190 -0
- package/src/common/claude-hook-config.mjs +56 -0
- package/src/common/claude-transcript.mjs +163 -0
- package/src/common/codex-transcript.mjs +153 -0
- package/src/common/compatibility.mjs +13 -0
- package/src/common/contracts.d.ts +26 -0
- package/src/common/contracts.ts +64 -1
- package/src/common/ghostty-present.mjs +100 -0
- package/src/common/ghostty-theme.mjs +149 -0
- package/src/common/niri-focus.mjs +179 -110
- package/src/common/session-cards.mjs +5 -2
- package/src/common/status-report.mjs +51 -0
- package/src/common/surface-bindings.mjs +234 -0
- package/src/common/terminal-identity.mjs +75 -12
- package/src/common/window-placement.mjs +59 -0
- package/src/common/workspace-view.mjs +161 -0
- package/src/native/agent-discovery.mjs +294 -0
- package/src/native/ak-runtime.mjs +164 -0
- package/src/native/codex-discovery.mjs +128 -0
- package/src/native/ghostty-tab-inventory.py +99 -0
- package/src/native/height-repair.mjs +154 -0
- package/src/native/main.mjs +120 -14
- package/src/native/panel-projection.mjs +67 -5
- package/src/native/placement.mjs +225 -0
- package/src/native/tab-inventory.mjs +188 -0
- package/src/native/theme-runtime.mjs +72 -0
- package/src/native/theme-source.mjs +197 -0
- package/src/native/workspace-events.mjs +16 -1
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
use crate::app::AppMsg;
|
|
2
|
-
use crate::protocol::Card;
|
|
2
|
+
use crate::protocol::{AkTask, Card};
|
|
3
3
|
use relm4::Sender;
|
|
4
4
|
use relm4::gtk;
|
|
5
5
|
use relm4::gtk::gdk;
|
|
6
6
|
use relm4::gtk::glib;
|
|
7
7
|
use relm4::gtk::prelude::*;
|
|
8
|
+
use std::cell::Cell;
|
|
9
|
+
use std::rc::Rc;
|
|
10
|
+
|
|
11
|
+
/// Rendered AK chips per card: at most two live-claim buttons plus two state badges.
|
|
12
|
+
const AK_CHIP_SLOTS: usize = 4;
|
|
8
13
|
|
|
9
14
|
pub struct CardView {
|
|
10
15
|
pub root: gtk::Button,
|
|
@@ -17,8 +22,15 @@ pub struct CardView {
|
|
|
17
22
|
prompt: gtk::Label,
|
|
18
23
|
reply: gtk::Label,
|
|
19
24
|
path: gtk::Label,
|
|
25
|
+
pid: gtk::Label,
|
|
26
|
+
agent: gtk::Label,
|
|
27
|
+
task: gtk::Label,
|
|
20
28
|
activation: gtk::Label,
|
|
21
29
|
inspector: gtk::Grid,
|
|
30
|
+
ak_row: gtk::Box,
|
|
31
|
+
ak_chips: Vec<gtk::Label>,
|
|
32
|
+
ak_clickable: Vec<Rc<Cell<bool>>>,
|
|
33
|
+
ak_overflow: gtk::Label,
|
|
22
34
|
}
|
|
23
35
|
|
|
24
36
|
impl CardView {
|
|
@@ -38,8 +50,12 @@ impl CardView {
|
|
|
38
50
|
let phase = label("card-phase", gtk::Align::Start);
|
|
39
51
|
label_box.append(&repo);
|
|
40
52
|
label_box.append(&phase);
|
|
53
|
+
let ak_row = gtk::Box::new(gtk::Orientation::Horizontal, 4);
|
|
54
|
+
ak_row.set_valign(gtk::Align::Center);
|
|
55
|
+
ak_row.set_visible(false);
|
|
41
56
|
let state = label("card-state", gtk::Align::End);
|
|
42
57
|
header.append(&label_box);
|
|
58
|
+
header.append(&ak_row);
|
|
43
59
|
header.append(&state);
|
|
44
60
|
|
|
45
61
|
let footer = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
|
@@ -60,9 +76,12 @@ impl CardView {
|
|
|
60
76
|
let prompt = inspector_row(&inspector, 1, "prompt");
|
|
61
77
|
let reply = inspector_row(&inspector, 2, "reply");
|
|
62
78
|
let path = inspector_row(&inspector, 3, "path");
|
|
79
|
+
let pid = inspector_row(&inspector, 4, "pid");
|
|
80
|
+
let agent = inspector_row(&inspector, 5, "agent");
|
|
81
|
+
let task = inspector_row(&inspector, 6, "task");
|
|
63
82
|
let activation = label("activation", gtk::Align::Start);
|
|
64
83
|
activation.set_visible(false);
|
|
65
|
-
inspector.attach(&activation, 0,
|
|
84
|
+
inspector.attach(&activation, 0, 7, 2, 1);
|
|
66
85
|
|
|
67
86
|
content.append(&header);
|
|
68
87
|
content.append(&footer);
|
|
@@ -75,6 +94,36 @@ impl CardView {
|
|
|
75
94
|
let _ = tx.send(AppMsg::Activate(card_id.clone()));
|
|
76
95
|
});
|
|
77
96
|
|
|
97
|
+
// AK task chips: an `active` chip reuses the card's own activation, so clicking it focuses
|
|
98
|
+
// the exact Ghostty window of the session holding that task's live claim. Badge states stop
|
|
99
|
+
// propagation instead, so a non-actionable reference never fires anything.
|
|
100
|
+
let mut ak_chips = Vec::with_capacity(AK_CHIP_SLOTS);
|
|
101
|
+
let mut ak_clickable = Vec::with_capacity(AK_CHIP_SLOTS);
|
|
102
|
+
for _slot in 0..AK_CHIP_SLOTS {
|
|
103
|
+
let chip = label("ak-task", gtk::Align::End);
|
|
104
|
+
chip.set_ellipsize(relm4::gtk::pango::EllipsizeMode::End);
|
|
105
|
+
chip.set_max_width_chars(20);
|
|
106
|
+
chip.set_visible(false);
|
|
107
|
+
let clickable = Rc::new(Cell::new(false));
|
|
108
|
+
let click = gtk::GestureClick::new();
|
|
109
|
+
let card_id = id.to_owned();
|
|
110
|
+
let tx = sender.clone();
|
|
111
|
+
let chip_clickable = Rc::clone(&clickable);
|
|
112
|
+
click.connect_released(move |gesture, _, _, _| {
|
|
113
|
+
if chip_clickable.get() {
|
|
114
|
+
let _ = tx.send(AppMsg::Activate(card_id.clone()));
|
|
115
|
+
}
|
|
116
|
+
gesture.set_state(gtk::EventSequenceState::Claimed);
|
|
117
|
+
});
|
|
118
|
+
chip.add_controller(click);
|
|
119
|
+
ak_row.append(&chip);
|
|
120
|
+
ak_chips.push(chip);
|
|
121
|
+
ak_clickable.push(clickable);
|
|
122
|
+
}
|
|
123
|
+
let ak_overflow = label("ak-overflow", gtk::Align::End);
|
|
124
|
+
ak_overflow.set_visible(false);
|
|
125
|
+
ak_row.append(&ak_overflow);
|
|
126
|
+
|
|
78
127
|
let motion = gtk::EventControllerMotion::new();
|
|
79
128
|
let card_id = id.to_owned();
|
|
80
129
|
let tx = sender.clone();
|
|
@@ -134,12 +183,20 @@ impl CardView {
|
|
|
134
183
|
prompt,
|
|
135
184
|
reply,
|
|
136
185
|
path,
|
|
186
|
+
pid,
|
|
187
|
+
agent,
|
|
188
|
+
task,
|
|
137
189
|
activation,
|
|
138
190
|
inspector,
|
|
191
|
+
ak_row,
|
|
192
|
+
ak_chips,
|
|
193
|
+
ak_clickable,
|
|
194
|
+
ak_overflow,
|
|
139
195
|
}
|
|
140
196
|
}
|
|
141
197
|
|
|
142
198
|
pub fn update(&self, card: &Card, focused: bool, duplicate_label: bool, now_ms: i64) {
|
|
199
|
+
let hidden_tab = card.hidden_tab();
|
|
143
200
|
self.repo.set_text(&display_repo(card, duplicate_label));
|
|
144
201
|
self.phase.set_text(text_or(&card.phase, "Idle"));
|
|
145
202
|
let real_event_at = if card.last_event_at > 0 {
|
|
@@ -171,6 +228,12 @@ impl CardView {
|
|
|
171
228
|
.set_text(text_or(&card.last_prompt_preview, "—"));
|
|
172
229
|
self.reply.set_text(text_or(&card.assistant_preview, "—"));
|
|
173
230
|
self.path.set_text(text_or(&card.cwd, "—"));
|
|
231
|
+
self.pid.set_text(&pid_row(card, hidden_tab));
|
|
232
|
+
self.agent
|
|
233
|
+
.set_text(text_or(&card.agent_label, "Pi session"));
|
|
234
|
+
self.task
|
|
235
|
+
.set_text(&ak_inspector_text(&card.ak_tasks, card.ak_task_overflow));
|
|
236
|
+
self.apply_ak_tasks(card);
|
|
174
237
|
|
|
175
238
|
for class in [
|
|
176
239
|
"state-idle",
|
|
@@ -181,9 +244,13 @@ impl CardView {
|
|
|
181
244
|
"state-error",
|
|
182
245
|
"current",
|
|
183
246
|
"stalled",
|
|
247
|
+
"hidden-tab",
|
|
184
248
|
] {
|
|
185
249
|
self.root.remove_css_class(class);
|
|
186
250
|
}
|
|
251
|
+
if hidden_tab {
|
|
252
|
+
self.root.add_css_class("hidden-tab");
|
|
253
|
+
}
|
|
187
254
|
self.root.add_css_class(&format!(
|
|
188
255
|
"state-{}",
|
|
189
256
|
if stalled {
|
|
@@ -198,18 +265,34 @@ impl CardView {
|
|
|
198
265
|
if stalled {
|
|
199
266
|
self.root.add_css_class("stalled");
|
|
200
267
|
}
|
|
268
|
+
let pid_note = if card.pid > 0 {
|
|
269
|
+
format!("pid {}", card.pid)
|
|
270
|
+
} else {
|
|
271
|
+
"no pid".to_owned()
|
|
272
|
+
};
|
|
273
|
+
let ak_note = ak_accessible_note(&card.ak_tasks);
|
|
201
274
|
self.root.set_tooltip_text(Some(&format!(
|
|
202
|
-
"
|
|
203
|
-
|
|
275
|
+
"{} {} ({}{}){}",
|
|
276
|
+
if hidden_tab { "Present" } else { "Focus" },
|
|
277
|
+
text_or(&card.repo_label, "Pi session"),
|
|
278
|
+
pid_note,
|
|
279
|
+
if hidden_tab { ", hidden tab" } else { "" },
|
|
280
|
+
ak_note
|
|
204
281
|
)));
|
|
205
282
|
let accessible_label = format!(
|
|
206
|
-
"{}, {},
|
|
283
|
+
"{}, {}, {}{}",
|
|
207
284
|
text_or(&card.repo_label, "Pi session"),
|
|
208
285
|
if stalled {
|
|
209
286
|
"stalled"
|
|
210
287
|
} else {
|
|
211
288
|
text_or(&card.phase, "idle")
|
|
212
|
-
}
|
|
289
|
+
},
|
|
290
|
+
if hidden_tab {
|
|
291
|
+
"hidden tab, press Enter to present it in its Ghostty window"
|
|
292
|
+
} else {
|
|
293
|
+
"press Enter to focus its Ghostty window"
|
|
294
|
+
},
|
|
295
|
+
ak_note
|
|
213
296
|
);
|
|
214
297
|
self.root
|
|
215
298
|
.update_property(&[gtk::accessible::Property::Label(&accessible_label)]);
|
|
@@ -217,11 +300,45 @@ impl CardView {
|
|
|
217
300
|
.update_state(&[gtk::accessible::State::Selected(Some(focused))]);
|
|
218
301
|
}
|
|
219
302
|
|
|
303
|
+
fn apply_ak_tasks(&self, card: &Card) {
|
|
304
|
+
let chips = &card.ak_tasks;
|
|
305
|
+
for (index, chip) in self.ak_chips.iter().enumerate() {
|
|
306
|
+
let clickable = self.ak_clickable.get(index).map(Rc::as_ref);
|
|
307
|
+
match (chips.get(index), clickable) {
|
|
308
|
+
(Some(task), Some(flag)) => {
|
|
309
|
+
chip.set_visible(true);
|
|
310
|
+
chip.set_text(&ak_chip_text(task));
|
|
311
|
+
for class in ["ak-active", "ak-deferred", "ak-orphaned"] {
|
|
312
|
+
chip.remove_css_class(class);
|
|
313
|
+
}
|
|
314
|
+
chip.add_css_class(ak_chip_class(&task.state));
|
|
315
|
+
chip.set_tooltip_text(Some(&ak_chip_tooltip(task)));
|
|
316
|
+
flag.set(task.is_active());
|
|
317
|
+
}
|
|
318
|
+
_ => {
|
|
319
|
+
chip.set_visible(false);
|
|
320
|
+
chip.set_tooltip_text(None);
|
|
321
|
+
if let Some(flag) = clickable {
|
|
322
|
+
flag.set(false);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
self.ak_row.set_visible(!chips.is_empty());
|
|
328
|
+
if card.ak_task_overflow > 0 {
|
|
329
|
+
self.ak_overflow.set_visible(true);
|
|
330
|
+
self.ak_overflow
|
|
331
|
+
.set_text(&format!("+{} more", card.ak_task_overflow));
|
|
332
|
+
} else {
|
|
333
|
+
self.ak_overflow.set_visible(false);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
220
337
|
pub fn set_expanded(&self, expanded: bool) {
|
|
221
338
|
self.root
|
|
222
339
|
.update_state(&[gtk::accessible::State::Expanded(Some(expanded))]);
|
|
223
340
|
self.root
|
|
224
|
-
.set_height_request(if expanded {
|
|
341
|
+
.set_height_request(if expanded { 252 } else { 60 });
|
|
225
342
|
self.inspector.set_visible(expanded);
|
|
226
343
|
if expanded {
|
|
227
344
|
self.root.add_css_class("open");
|
|
@@ -267,14 +384,29 @@ fn inspector_row(grid: >k::Grid, row: i32, key: &str) -> gtk::Label {
|
|
|
267
384
|
}
|
|
268
385
|
|
|
269
386
|
fn display_repo(card: &Card, duplicate: bool) -> String {
|
|
387
|
+
let marker = if card.hidden_tab() { "⧉ " } else { "" };
|
|
270
388
|
if duplicate && card.pid > 0 {
|
|
271
389
|
format!(
|
|
272
|
-
"{} · {:04}",
|
|
390
|
+
"{}{} · {:04}",
|
|
391
|
+
marker,
|
|
273
392
|
text_or(&card.repo_label, "Pi session"),
|
|
274
393
|
card.pid % 10_000
|
|
275
394
|
)
|
|
276
395
|
} else {
|
|
277
|
-
text_or(&card.repo_label, "Pi session")
|
|
396
|
+
format!("{}{}", marker, text_or(&card.repo_label, "Pi session"))
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
fn pid_row(card: &Card, hidden_tab: bool) -> String {
|
|
401
|
+
let pid = if card.pid > 0 {
|
|
402
|
+
card.pid.to_string()
|
|
403
|
+
} else {
|
|
404
|
+
"—".to_owned()
|
|
405
|
+
};
|
|
406
|
+
if hidden_tab {
|
|
407
|
+
format!("{pid} · hidden tab")
|
|
408
|
+
} else {
|
|
409
|
+
pid
|
|
278
410
|
}
|
|
279
411
|
}
|
|
280
412
|
|
|
@@ -293,6 +425,81 @@ fn state_class(state: &str) -> &str {
|
|
|
293
425
|
}
|
|
294
426
|
}
|
|
295
427
|
|
|
428
|
+
/// CSS class for one AK chip state. Unknown states render as a muted, non-clickable chip.
|
|
429
|
+
fn ak_chip_class(state: &str) -> &'static str {
|
|
430
|
+
match state {
|
|
431
|
+
"active" => "ak-active",
|
|
432
|
+
"deferred" => "ak-deferred",
|
|
433
|
+
"orphaned" => "ak-orphaned",
|
|
434
|
+
_ => "ak-unknown",
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/// Compact chip text: task id plus as much title as the pill can hold.
|
|
439
|
+
fn ak_chip_text(task: &AkTask) -> String {
|
|
440
|
+
format!("AK #{} · {}", task.id, task.title)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
fn ak_chip_tooltip(task: &AkTask) -> String {
|
|
444
|
+
match task.state.as_str() {
|
|
445
|
+
"active" => format!(
|
|
446
|
+
"AK task #{} — {}. Click to focus the claiming session's Ghostty window.",
|
|
447
|
+
task.id, task.title
|
|
448
|
+
),
|
|
449
|
+
"orphaned" => format!(
|
|
450
|
+
"AK task #{} — {}. Claim outlives its session (lease not expired); not clickable.",
|
|
451
|
+
task.id, task.title
|
|
452
|
+
),
|
|
453
|
+
"deferred" => format!(
|
|
454
|
+
"AK task #{} — {}. Deferred by AK; not clickable.",
|
|
455
|
+
task.id, task.title
|
|
456
|
+
),
|
|
457
|
+
_ => format!("AK task #{} — {}", task.id, task.title),
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/// Inspector row: every joined reference with its state, plus the overflow count.
|
|
462
|
+
fn ak_inspector_text(tasks: &[AkTask], overflow: u32) -> String {
|
|
463
|
+
if tasks.is_empty() && overflow == 0 {
|
|
464
|
+
return "—".to_owned();
|
|
465
|
+
}
|
|
466
|
+
let mut lines: Vec<String> = tasks
|
|
467
|
+
.iter()
|
|
468
|
+
.map(|task| {
|
|
469
|
+
format!(
|
|
470
|
+
"#{} [{}] {}",
|
|
471
|
+
task.id,
|
|
472
|
+
match task.state.as_str() {
|
|
473
|
+
"active" => "live claim",
|
|
474
|
+
"orphaned" => "claim outlives session",
|
|
475
|
+
"deferred" => "deferred",
|
|
476
|
+
_ => "unknown",
|
|
477
|
+
},
|
|
478
|
+
task.title
|
|
479
|
+
)
|
|
480
|
+
})
|
|
481
|
+
.collect();
|
|
482
|
+
if overflow > 0 {
|
|
483
|
+
lines.push(format!("+{overflow} more"));
|
|
484
|
+
}
|
|
485
|
+
lines.join("\n")
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/// Appended to the card's tooltip and accessible label when AK references exist.
|
|
489
|
+
fn ak_accessible_note(tasks: &[AkTask]) -> String {
|
|
490
|
+
match tasks.first() {
|
|
491
|
+
Some(task) if task.is_active() => format!(
|
|
492
|
+
". AK task #{} {} (click the chip to focus the claiming session)",
|
|
493
|
+
task.id, task.title
|
|
494
|
+
),
|
|
495
|
+
Some(task) => format!(
|
|
496
|
+
". AK task #{} {} (badge only, not clickable)",
|
|
497
|
+
task.id, task.title
|
|
498
|
+
),
|
|
499
|
+
None => String::new(),
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
296
503
|
fn text_or<'a>(value: &'a str, fallback: &'a str) -> &'a str {
|
|
297
504
|
if value.is_empty() { fallback } else { value }
|
|
298
505
|
}
|
|
@@ -301,3 +508,95 @@ fn duration(now_ms: i64, anchor_ms: i64) -> String {
|
|
|
301
508
|
let seconds = now_ms.saturating_sub(anchor_ms.max(1)) / 1000;
|
|
302
509
|
format!("{}:{:02}", seconds / 60, seconds % 60)
|
|
303
510
|
}
|
|
511
|
+
|
|
512
|
+
#[cfg(test)]
|
|
513
|
+
mod tests {
|
|
514
|
+
use super::*;
|
|
515
|
+
|
|
516
|
+
fn card(json: serde_json::Value) -> Card {
|
|
517
|
+
serde_json::from_value(json).expect("card")
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
#[test]
|
|
521
|
+
fn hidden_tabs_carry_a_marker_and_pid_note() {
|
|
522
|
+
let agent = card(serde_json::json!({ "agentLabel": "Claude Code" }));
|
|
523
|
+
assert_eq!(agent.agent_label, "Claude Code");
|
|
524
|
+
assert_eq!(text_or(&agent.agent_label, "Pi session"), "Claude Code");
|
|
525
|
+
assert_eq!(
|
|
526
|
+
text_or(&card(serde_json::json!({})).agent_label, "Pi session"),
|
|
527
|
+
"Pi session"
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
let hidden = card(serde_json::json!({
|
|
531
|
+
"repoLabel": "dspx", "processId": 12345, "surfaceVisible": false
|
|
532
|
+
}));
|
|
533
|
+
assert!(hidden.hidden_tab());
|
|
534
|
+
assert_eq!(display_repo(&hidden, false), "⧉ dspx");
|
|
535
|
+
assert_eq!(display_repo(&hidden, true), "⧉ dspx · 2345");
|
|
536
|
+
assert_eq!(pid_row(&hidden, true), "12345 · hidden tab");
|
|
537
|
+
|
|
538
|
+
let visible = card(serde_json::json!({ "repoLabel": "dspx", "surfaceVisible": true }));
|
|
539
|
+
assert!(!visible.hidden_tab());
|
|
540
|
+
assert_eq!(display_repo(&visible, false), "dspx");
|
|
541
|
+
assert_eq!(pid_row(&visible, false), "—");
|
|
542
|
+
|
|
543
|
+
let legacy = card(serde_json::json!({ "repoLabel": "dspx" }));
|
|
544
|
+
assert!(
|
|
545
|
+
!legacy.hidden_tab(),
|
|
546
|
+
"controllers without placement never mark tabs hidden"
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
#[test]
|
|
551
|
+
fn ak_chip_states_map_to_classes_and_only_active_is_clickable() {
|
|
552
|
+
let active = AkTask {
|
|
553
|
+
id: 5701,
|
|
554
|
+
title: "Show clickable AK-task references".into(),
|
|
555
|
+
state: "active".into(),
|
|
556
|
+
};
|
|
557
|
+
let orphaned = AkTask {
|
|
558
|
+
state: "orphaned".into(),
|
|
559
|
+
..active.clone()
|
|
560
|
+
};
|
|
561
|
+
let deferred = AkTask {
|
|
562
|
+
state: "deferred".into(),
|
|
563
|
+
..active.clone()
|
|
564
|
+
};
|
|
565
|
+
assert_eq!(ak_chip_class("active"), "ak-active");
|
|
566
|
+
assert_eq!(ak_chip_class("orphaned"), "ak-orphaned");
|
|
567
|
+
assert_eq!(ak_chip_class("deferred"), "ak-deferred");
|
|
568
|
+
assert_eq!(ak_chip_class("surprise"), "ak-unknown");
|
|
569
|
+
assert!(active.is_active());
|
|
570
|
+
assert!(!orphaned.is_active());
|
|
571
|
+
assert!(!deferred.is_active());
|
|
572
|
+
assert_eq!(
|
|
573
|
+
ak_chip_text(&active),
|
|
574
|
+
"AK #5701 · Show clickable AK-task references"
|
|
575
|
+
);
|
|
576
|
+
assert!(ak_chip_tooltip(&orphaned).contains("not clickable"));
|
|
577
|
+
assert!(ak_chip_tooltip(&active).contains("focus the claiming session"));
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
#[test]
|
|
581
|
+
fn ak_inspector_lists_every_reference_with_overflow() {
|
|
582
|
+
let tasks = vec![
|
|
583
|
+
AkTask {
|
|
584
|
+
id: 5701,
|
|
585
|
+
title: "Live claim task".into(),
|
|
586
|
+
state: "active".into(),
|
|
587
|
+
},
|
|
588
|
+
AkTask {
|
|
589
|
+
id: 4220,
|
|
590
|
+
title: "Deferred task".into(),
|
|
591
|
+
state: "deferred".into(),
|
|
592
|
+
},
|
|
593
|
+
];
|
|
594
|
+
let text = ak_inspector_text(&tasks, 3);
|
|
595
|
+
assert!(text.contains("#5701 [live claim] Live claim task"));
|
|
596
|
+
assert!(text.contains("#4220 [deferred] Deferred task"));
|
|
597
|
+
assert!(text.contains("+3 more"));
|
|
598
|
+
assert_eq!(ak_inspector_text(&[], 0), "—");
|
|
599
|
+
assert!(ak_accessible_note(&tasks).starts_with(". AK task #5701"));
|
|
600
|
+
assert_eq!(ak_accessible_note(&[]), "");
|
|
601
|
+
}
|
|
602
|
+
}
|
|
@@ -3,6 +3,25 @@ use serde_json::{Value, json};
|
|
|
3
3
|
use std::io::{self, Write};
|
|
4
4
|
use std::sync::{Mutex, OnceLock};
|
|
5
5
|
|
|
6
|
+
#[derive(Clone, Debug, Deserialize)]
|
|
7
|
+
#[serde(rename_all = "camelCase")]
|
|
8
|
+
pub struct AkTask {
|
|
9
|
+
#[serde(default)]
|
|
10
|
+
pub id: i64,
|
|
11
|
+
#[serde(default)]
|
|
12
|
+
pub title: String,
|
|
13
|
+
#[serde(default)]
|
|
14
|
+
pub state: String,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
impl AkTask {
|
|
18
|
+
/// Only an `active` chip is a button: the card's own session holds a live claim, so clicking
|
|
19
|
+
/// it focuses that session's Ghostty window. Every other state is a badge and never fires.
|
|
20
|
+
pub fn is_active(&self) -> bool {
|
|
21
|
+
self.state == "active"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
6
25
|
#[derive(Clone, Debug, Deserialize)]
|
|
7
26
|
#[serde(rename_all = "camelCase")]
|
|
8
27
|
pub struct Card {
|
|
@@ -40,6 +59,20 @@ pub struct Card {
|
|
|
40
59
|
pub agent_active: bool,
|
|
41
60
|
#[serde(default, rename = "processId")]
|
|
42
61
|
pub pid: i64,
|
|
62
|
+
/// Set for tabs running an agent other than Pi, which publish no telemetry of their own.
|
|
63
|
+
#[serde(default)]
|
|
64
|
+
pub agent_label: String,
|
|
65
|
+
/// Read-only AK task references joined onto this card by the controller. `active` chips are
|
|
66
|
+
/// clickable and focus this card's terminal; the rest are inert badges.
|
|
67
|
+
#[serde(default)]
|
|
68
|
+
pub ak_tasks: Vec<AkTask>,
|
|
69
|
+
/// How many further AK references exist beyond the rendered chips.
|
|
70
|
+
#[serde(default)]
|
|
71
|
+
pub ak_task_overflow: u32,
|
|
72
|
+
/// `Some(false)` marks a tab hidden behind another tab of its Ghostty window; the controller
|
|
73
|
+
/// placed it through its host process, so activation presents the tab before focusing.
|
|
74
|
+
#[serde(default)]
|
|
75
|
+
pub surface_visible: Option<bool>,
|
|
43
76
|
}
|
|
44
77
|
|
|
45
78
|
impl Card {
|
|
@@ -51,6 +84,10 @@ impl Card {
|
|
|
51
84
|
}
|
|
52
85
|
}
|
|
53
86
|
|
|
87
|
+
pub fn hidden_tab(&self) -> bool {
|
|
88
|
+
self.surface_visible == Some(false)
|
|
89
|
+
}
|
|
90
|
+
|
|
54
91
|
pub fn active(&self) -> bool {
|
|
55
92
|
self.agent_active || matches!(self.state.as_str(), "thinking" | "tool" | "waiting")
|
|
56
93
|
}
|
|
@@ -159,6 +196,12 @@ pub fn demo_view() -> ViewMessage {
|
|
|
159
196
|
last_event_at: now - 1_000,
|
|
160
197
|
updated_at: now,
|
|
161
198
|
agent_active: true,
|
|
199
|
+
ak_tasks: vec![AkTask {
|
|
200
|
+
id: 5701,
|
|
201
|
+
title: "Show clickable AK-task references on the activity ribbon".into(),
|
|
202
|
+
state: "active".into(),
|
|
203
|
+
}],
|
|
204
|
+
ak_task_overflow: 1,
|
|
162
205
|
..empty_card()
|
|
163
206
|
},
|
|
164
207
|
],
|
|
@@ -184,6 +227,10 @@ fn empty_card() -> Card {
|
|
|
184
227
|
updated_at: 0,
|
|
185
228
|
agent_active: false,
|
|
186
229
|
pid: 0,
|
|
230
|
+
agent_label: String::new(),
|
|
231
|
+
ak_tasks: Vec::new(),
|
|
232
|
+
ak_task_overflow: 0,
|
|
233
|
+
surface_visible: None,
|
|
187
234
|
}
|
|
188
235
|
}
|
|
189
236
|
|
|
@@ -202,6 +249,22 @@ mod tests {
|
|
|
202
249
|
assert!(view.sessions[0].active());
|
|
203
250
|
assert_eq!(view.sessions[0].agent_started_at, None);
|
|
204
251
|
assert_eq!(view.sessions[0].pid, 4242);
|
|
252
|
+
assert!(view.sessions[0].ak_tasks.is_empty());
|
|
253
|
+
assert_eq!(view.sessions[0].ak_task_overflow, 0);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
#[test]
|
|
257
|
+
fn ak_task_chips_parse_with_states() {
|
|
258
|
+
let view: ViewMessage = serde_json::from_str(
|
|
259
|
+
r#"{"protocol":1,"type":"view","revision":8,"sessions":[{"cardId":"card-a","akTasks":[{"id":5701,"title":"Show clickable AK-task references","state":"active"},{"id":5432,"title":"Preserve safe NEXUS recovery reasons","state":"orphaned"},{"id":4220,"title":"Control Decision 77 epoch admissions","state":"deferred"}],"akTaskOverflow":3}]}"#,
|
|
260
|
+
)
|
|
261
|
+
.expect("view should parse");
|
|
262
|
+
let card = &view.sessions[0];
|
|
263
|
+
assert_eq!(card.ak_tasks.len(), 3);
|
|
264
|
+
assert!(card.ak_tasks[0].is_active());
|
|
265
|
+
assert!(!card.ak_tasks[1].is_active());
|
|
266
|
+
assert!(!card.ak_tasks[2].is_active());
|
|
267
|
+
assert_eq!(card.ak_task_overflow, 3);
|
|
205
268
|
}
|
|
206
269
|
|
|
207
270
|
#[test]
|
|
@@ -5,17 +5,44 @@ use std::collections::{HashMap, HashSet};
|
|
|
5
5
|
use std::io::{BufRead, BufReader};
|
|
6
6
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
7
7
|
|
|
8
|
+
/// Colour definitions used until the controller reports what Ghostty is showing.
|
|
9
|
+
const FALLBACK_DEFINITIONS: &str = concat!(
|
|
10
|
+
"@define-color pi_background #1e2326;\n",
|
|
11
|
+
"@define-color pi_foreground #d3c6aa;\n",
|
|
12
|
+
"@define-color pi_accent #7fbbb3;\n",
|
|
13
|
+
"@define-color pi_thinking #7fbbb3;\n",
|
|
14
|
+
"@define-color pi_tool #dbbc7f;\n",
|
|
15
|
+
"@define-color pi_waiting #e69875;\n",
|
|
16
|
+
"@define-color pi_success #a7c080;\n",
|
|
17
|
+
"@define-color pi_error #e67e80;\n",
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
thread_local! {
|
|
21
|
+
static THEME_PROVIDER: gtk::CssProvider = gtk::CssProvider::new();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// The stylesheet derives every shade from a handful of named colours, so following the terminal's
|
|
25
|
+
/// theme is a matter of redefining those names and reloading one provider.
|
|
8
26
|
#[allow(deprecated)]
|
|
9
27
|
pub fn install_css() {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
28
|
+
apply_theme(FALLBACK_DEFINITIONS);
|
|
29
|
+
THEME_PROVIDER.with(|provider| {
|
|
30
|
+
if let Some(display) = gtk::gdk::Display::default() {
|
|
31
|
+
gtk::style_context_add_provider_for_display(
|
|
32
|
+
&display,
|
|
33
|
+
provider,
|
|
34
|
+
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Replace the active colour definitions. Definitions must sit in the same provider as the rules
|
|
41
|
+
/// that reference them, so both are loaded together.
|
|
42
|
+
#[allow(deprecated)]
|
|
43
|
+
pub fn apply_theme(definitions: &str) {
|
|
44
|
+
let css = format!("{definitions}\n{}", include_str!("style.css"));
|
|
45
|
+
THEME_PROVIDER.with(|provider| provider.load_from_data(&css));
|
|
19
46
|
}
|
|
20
47
|
|
|
21
48
|
pub fn start_input_reader(sender: relm4::Sender<AppMsg>) {
|
|
@@ -53,6 +80,20 @@ fn consume_line(sender: &relm4::Sender<AppMsg>, line: &str) {
|
|
|
53
80
|
let _ = sender.send(AppMsg::InputError(format!("invalid view message: {error}")));
|
|
54
81
|
}
|
|
55
82
|
},
|
|
83
|
+
Some("theme") => {
|
|
84
|
+
let definitions = value
|
|
85
|
+
.get("definitions")
|
|
86
|
+
.and_then(|item| item.as_str())
|
|
87
|
+
.unwrap_or_default()
|
|
88
|
+
.to_owned();
|
|
89
|
+
if definitions.is_empty() {
|
|
90
|
+
let _ = sender.send(AppMsg::InputError(
|
|
91
|
+
"theme message carried no colours".into(),
|
|
92
|
+
));
|
|
93
|
+
} else {
|
|
94
|
+
let _ = sender.send(AppMsg::Theme(definitions));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
56
97
|
Some("focus-strip") => {
|
|
57
98
|
let _ = sender.send(AppMsg::FocusStrip);
|
|
58
99
|
}
|