@tryinget/pi-activity-strip 0.4.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 +136 -157
- package/bin/pi-activity-strip-claude-hook.mjs +71 -0
- package/bin/pi-activity-strip.mjs +50 -60
- package/native/bin/linux-x64-gnu/artifact.json +21 -0
- package/native/bin/linux-x64-gnu/pi-activity-strip-panel +0 -0
- package/native/panel/Cargo.lock +1045 -0
- package/native/panel/Cargo.toml +23 -0
- package/native/panel/src/app.rs +606 -0
- package/native/panel/src/card_view.rs +602 -0
- package/native/panel/src/main.rs +31 -0
- package/native/panel/src/protocol.rs +297 -0
- package/native/panel/src/runtime.rs +151 -0
- package/native/panel/src/style.css +250 -0
- package/package.json +16 -4
- package/src/broker/server.mjs +43 -7
- package/src/broker/session-store.mjs +41 -13
- package/src/client/broker-client.mjs +10 -3
- package/src/client/session-telemetry.mjs +54 -14
- package/src/common/activity-order.mjs +15 -9
- package/src/common/agent-identity.mjs +163 -0
- package/src/common/ak-tasks.mjs +354 -0
- package/src/common/alignment-controller.mjs +24 -42
- package/src/common/card-display.mjs +5 -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 +28 -13
- package/src/common/constants.mjs +10 -28
- package/src/common/contracts.d.ts +68 -4
- package/src/common/contracts.ts +105 -4
- package/src/common/ghostty-present.mjs +100 -0
- package/src/common/ghostty-theme.mjs +149 -0
- package/src/common/niri-focus.mjs +209 -140
- package/src/common/protocol.mjs +48 -16
- package/src/common/session-cards.mjs +105 -0
- package/src/common/status-report.mjs +53 -1
- package/src/common/surface-bindings.mjs +234 -0
- package/src/common/telemetry.mjs +8 -1
- package/src/common/terminal-identity.mjs +212 -0
- 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 +499 -0
- package/src/native/panel-projection.mjs +161 -0
- 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/{electron/niri-workspace-events.mjs → native/workspace-events.mjs} +20 -2
- package/src/common/electron.mjs +0 -57
- package/src/electron/main.mjs +0 -499
- package/src/electron/niri-native-window-runtime.mjs +0 -178
- package/src/electron/preload.cjs +0 -50
- package/src/electron/renderer-visibility-runtime.mjs +0 -107
- package/src/electron/workspace-view-runtime.mjs +0 -360
- package/src/ui/strip-html.mjs +0 -500
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
use crate::app::AppMsg;
|
|
2
|
+
use crate::protocol::{AkTask, Card};
|
|
3
|
+
use relm4::Sender;
|
|
4
|
+
use relm4::gtk;
|
|
5
|
+
use relm4::gtk::gdk;
|
|
6
|
+
use relm4::gtk::glib;
|
|
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;
|
|
13
|
+
|
|
14
|
+
pub struct CardView {
|
|
15
|
+
pub root: gtk::Button,
|
|
16
|
+
phase: gtk::Label,
|
|
17
|
+
repo: gtk::Label,
|
|
18
|
+
state: gtk::Label,
|
|
19
|
+
tool: gtk::Label,
|
|
20
|
+
elapsed: gtk::Label,
|
|
21
|
+
detail: gtk::Label,
|
|
22
|
+
prompt: gtk::Label,
|
|
23
|
+
reply: gtk::Label,
|
|
24
|
+
path: gtk::Label,
|
|
25
|
+
pid: gtk::Label,
|
|
26
|
+
agent: gtk::Label,
|
|
27
|
+
task: gtk::Label,
|
|
28
|
+
activation: gtk::Label,
|
|
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,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
impl CardView {
|
|
37
|
+
pub fn new(id: &str, sender: &Sender<AppMsg>) -> Self {
|
|
38
|
+
let root = gtk::Button::new();
|
|
39
|
+
root.add_css_class("card");
|
|
40
|
+
root.set_focusable(true);
|
|
41
|
+
root.set_can_focus(true);
|
|
42
|
+
root.set_width_request(224);
|
|
43
|
+
root.set_height_request(60);
|
|
44
|
+
|
|
45
|
+
let content = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
|
46
|
+
let header = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
|
47
|
+
let label_box = gtk::Box::new(gtk::Orientation::Vertical, 3);
|
|
48
|
+
label_box.set_hexpand(true);
|
|
49
|
+
let repo = label("card-repo", gtk::Align::Start);
|
|
50
|
+
let phase = label("card-phase", gtk::Align::Start);
|
|
51
|
+
label_box.append(&repo);
|
|
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);
|
|
56
|
+
let state = label("card-state", gtk::Align::End);
|
|
57
|
+
header.append(&label_box);
|
|
58
|
+
header.append(&ak_row);
|
|
59
|
+
header.append(&state);
|
|
60
|
+
|
|
61
|
+
let footer = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
|
62
|
+
footer.set_margin_top(5);
|
|
63
|
+
let tool = label("card-tool", gtk::Align::Start);
|
|
64
|
+
tool.set_hexpand(true);
|
|
65
|
+
let elapsed = label("card-elapsed", gtk::Align::End);
|
|
66
|
+
footer.append(&tool);
|
|
67
|
+
footer.append(&elapsed);
|
|
68
|
+
|
|
69
|
+
let inspector = gtk::Grid::new();
|
|
70
|
+
inspector.add_css_class("inspector");
|
|
71
|
+
inspector.set_row_spacing(6);
|
|
72
|
+
inspector.set_column_spacing(8);
|
|
73
|
+
inspector.set_margin_top(10);
|
|
74
|
+
inspector.set_visible(false);
|
|
75
|
+
let detail = inspector_row(&inspector, 0, "detail");
|
|
76
|
+
let prompt = inspector_row(&inspector, 1, "prompt");
|
|
77
|
+
let reply = inspector_row(&inspector, 2, "reply");
|
|
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");
|
|
82
|
+
let activation = label("activation", gtk::Align::Start);
|
|
83
|
+
activation.set_visible(false);
|
|
84
|
+
inspector.attach(&activation, 0, 7, 2, 1);
|
|
85
|
+
|
|
86
|
+
content.append(&header);
|
|
87
|
+
content.append(&footer);
|
|
88
|
+
content.append(&inspector);
|
|
89
|
+
root.set_child(Some(&content));
|
|
90
|
+
|
|
91
|
+
let card_id = id.to_owned();
|
|
92
|
+
let tx = sender.clone();
|
|
93
|
+
root.connect_clicked(move |_| {
|
|
94
|
+
let _ = tx.send(AppMsg::Activate(card_id.clone()));
|
|
95
|
+
});
|
|
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
|
+
|
|
127
|
+
let motion = gtk::EventControllerMotion::new();
|
|
128
|
+
let card_id = id.to_owned();
|
|
129
|
+
let tx = sender.clone();
|
|
130
|
+
motion.connect_enter(move |_, _, _| {
|
|
131
|
+
let _ = tx.send(AppMsg::Hover(card_id.clone(), true));
|
|
132
|
+
});
|
|
133
|
+
let card_id = id.to_owned();
|
|
134
|
+
let tx = sender.clone();
|
|
135
|
+
motion.connect_leave(move |_| {
|
|
136
|
+
let _ = tx.send(AppMsg::Hover(card_id.clone(), false));
|
|
137
|
+
});
|
|
138
|
+
root.add_controller(motion);
|
|
139
|
+
|
|
140
|
+
let focus = gtk::EventControllerFocus::new();
|
|
141
|
+
let card_id = id.to_owned();
|
|
142
|
+
let tx = sender.clone();
|
|
143
|
+
focus.connect_enter(move |_| {
|
|
144
|
+
let _ = tx.send(AppMsg::Focus(card_id.clone(), true));
|
|
145
|
+
});
|
|
146
|
+
let card_id = id.to_owned();
|
|
147
|
+
let tx = sender.clone();
|
|
148
|
+
focus.connect_leave(move |_| {
|
|
149
|
+
let _ = tx.send(AppMsg::Focus(card_id.clone(), false));
|
|
150
|
+
});
|
|
151
|
+
root.add_controller(focus);
|
|
152
|
+
|
|
153
|
+
let keys = gtk::EventControllerKey::new();
|
|
154
|
+
let card_id = id.to_owned();
|
|
155
|
+
let tx = sender.clone();
|
|
156
|
+
keys.connect_key_pressed(move |_, key, _, modifiers| {
|
|
157
|
+
let direction = if key == gdk::Key::Left {
|
|
158
|
+
-1
|
|
159
|
+
} else if key == gdk::Key::Right {
|
|
160
|
+
1
|
|
161
|
+
} else if key == gdk::Key::Escape {
|
|
162
|
+
let _ = tx.send(AppMsg::Collapse);
|
|
163
|
+
return glib::Propagation::Stop;
|
|
164
|
+
} else if key == gdk::Key::space || key == gdk::Key::KP_Space {
|
|
165
|
+
return glib::Propagation::Stop;
|
|
166
|
+
} else {
|
|
167
|
+
return glib::Propagation::Proceed;
|
|
168
|
+
};
|
|
169
|
+
let manual = modifiers.contains(gdk::ModifierType::SHIFT_MASK);
|
|
170
|
+
let _ = tx.send(AppMsg::Navigate(card_id.clone(), direction, manual));
|
|
171
|
+
glib::Propagation::Stop
|
|
172
|
+
});
|
|
173
|
+
root.add_controller(keys);
|
|
174
|
+
|
|
175
|
+
Self {
|
|
176
|
+
root,
|
|
177
|
+
phase,
|
|
178
|
+
repo,
|
|
179
|
+
state,
|
|
180
|
+
tool,
|
|
181
|
+
elapsed,
|
|
182
|
+
detail,
|
|
183
|
+
prompt,
|
|
184
|
+
reply,
|
|
185
|
+
path,
|
|
186
|
+
pid,
|
|
187
|
+
agent,
|
|
188
|
+
task,
|
|
189
|
+
activation,
|
|
190
|
+
inspector,
|
|
191
|
+
ak_row,
|
|
192
|
+
ak_chips,
|
|
193
|
+
ak_clickable,
|
|
194
|
+
ak_overflow,
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
pub fn update(&self, card: &Card, focused: bool, duplicate_label: bool, now_ms: i64) {
|
|
199
|
+
let hidden_tab = card.hidden_tab();
|
|
200
|
+
self.repo.set_text(&display_repo(card, duplicate_label));
|
|
201
|
+
self.phase.set_text(text_or(&card.phase, "Idle"));
|
|
202
|
+
let real_event_at = if card.last_event_at > 0 {
|
|
203
|
+
card.last_event_at
|
|
204
|
+
} else {
|
|
205
|
+
card.updated_at
|
|
206
|
+
};
|
|
207
|
+
let stalled = card.stalled(now_ms);
|
|
208
|
+
self.state.set_text(if stalled {
|
|
209
|
+
"stalled"
|
|
210
|
+
} else {
|
|
211
|
+
state_label(&card.state)
|
|
212
|
+
});
|
|
213
|
+
self.tool.set_text(text_or(
|
|
214
|
+
if card.tool_name.is_empty() {
|
|
215
|
+
&card.tool_target
|
|
216
|
+
} else {
|
|
217
|
+
&card.tool_name
|
|
218
|
+
},
|
|
219
|
+
"monitoring",
|
|
220
|
+
));
|
|
221
|
+
self.elapsed.set_text(&format!(
|
|
222
|
+
"{} · {}",
|
|
223
|
+
duration(now_ms, card.agent_started_at.unwrap_or(card.started_at)),
|
|
224
|
+
duration(now_ms, real_event_at)
|
|
225
|
+
));
|
|
226
|
+
self.detail.set_text(text_or(&card.detail, "Ready"));
|
|
227
|
+
self.prompt
|
|
228
|
+
.set_text(text_or(&card.last_prompt_preview, "—"));
|
|
229
|
+
self.reply.set_text(text_or(&card.assistant_preview, "—"));
|
|
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);
|
|
237
|
+
|
|
238
|
+
for class in [
|
|
239
|
+
"state-idle",
|
|
240
|
+
"state-thinking",
|
|
241
|
+
"state-tool",
|
|
242
|
+
"state-waiting",
|
|
243
|
+
"state-success",
|
|
244
|
+
"state-error",
|
|
245
|
+
"current",
|
|
246
|
+
"stalled",
|
|
247
|
+
"hidden-tab",
|
|
248
|
+
] {
|
|
249
|
+
self.root.remove_css_class(class);
|
|
250
|
+
}
|
|
251
|
+
if hidden_tab {
|
|
252
|
+
self.root.add_css_class("hidden-tab");
|
|
253
|
+
}
|
|
254
|
+
self.root.add_css_class(&format!(
|
|
255
|
+
"state-{}",
|
|
256
|
+
if stalled {
|
|
257
|
+
"waiting"
|
|
258
|
+
} else {
|
|
259
|
+
state_class(&card.state)
|
|
260
|
+
}
|
|
261
|
+
));
|
|
262
|
+
if focused {
|
|
263
|
+
self.root.add_css_class("current");
|
|
264
|
+
}
|
|
265
|
+
if stalled {
|
|
266
|
+
self.root.add_css_class("stalled");
|
|
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);
|
|
274
|
+
self.root.set_tooltip_text(Some(&format!(
|
|
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
|
|
281
|
+
)));
|
|
282
|
+
let accessible_label = format!(
|
|
283
|
+
"{}, {}, {}{}",
|
|
284
|
+
text_or(&card.repo_label, "Pi session"),
|
|
285
|
+
if stalled {
|
|
286
|
+
"stalled"
|
|
287
|
+
} else {
|
|
288
|
+
text_or(&card.phase, "idle")
|
|
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
|
|
296
|
+
);
|
|
297
|
+
self.root
|
|
298
|
+
.update_property(&[gtk::accessible::Property::Label(&accessible_label)]);
|
|
299
|
+
self.root
|
|
300
|
+
.update_state(&[gtk::accessible::State::Selected(Some(focused))]);
|
|
301
|
+
}
|
|
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
|
+
|
|
337
|
+
pub fn set_expanded(&self, expanded: bool) {
|
|
338
|
+
self.root
|
|
339
|
+
.update_state(&[gtk::accessible::State::Expanded(Some(expanded))]);
|
|
340
|
+
self.root
|
|
341
|
+
.set_height_request(if expanded { 252 } else { 60 });
|
|
342
|
+
self.inspector.set_visible(expanded);
|
|
343
|
+
if expanded {
|
|
344
|
+
self.root.add_css_class("open");
|
|
345
|
+
} else {
|
|
346
|
+
self.root.remove_css_class("open");
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
pub fn set_activation(&self, ok: bool, message: &str) {
|
|
351
|
+
self.activation.set_text(message);
|
|
352
|
+
self.activation.set_visible(true);
|
|
353
|
+
self.activation.remove_css_class("success");
|
|
354
|
+
self.activation.remove_css_class("error");
|
|
355
|
+
self.activation
|
|
356
|
+
.add_css_class(if ok { "success" } else { "error" });
|
|
357
|
+
self.root
|
|
358
|
+
.update_property(&[gtk::accessible::Property::Description(message)]);
|
|
359
|
+
self.root
|
|
360
|
+
.announce(message, gtk::AccessibleAnnouncementPriority::Medium);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
fn label(class: &str, align: gtk::Align) -> gtk::Label {
|
|
365
|
+
let value = gtk::Label::new(None);
|
|
366
|
+
value.add_css_class(class);
|
|
367
|
+
value.set_halign(align);
|
|
368
|
+
value.set_ellipsize(relm4::gtk::pango::EllipsizeMode::End);
|
|
369
|
+
value.set_xalign(if align == gtk::Align::End { 1.0 } else { 0.0 });
|
|
370
|
+
value
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
fn inspector_row(grid: >k::Grid, row: i32, key: &str) -> gtk::Label {
|
|
374
|
+
let key_label = label("inspector-key", gtk::Align::Start);
|
|
375
|
+
key_label.set_text(key);
|
|
376
|
+
key_label.set_width_chars(7);
|
|
377
|
+
let value = label("inspector-value", gtk::Align::Start);
|
|
378
|
+
value.set_wrap(true);
|
|
379
|
+
value.set_lines(2);
|
|
380
|
+
value.set_hexpand(true);
|
|
381
|
+
grid.attach(&key_label, 0, row, 1, 1);
|
|
382
|
+
grid.attach(&value, 1, row, 1, 1);
|
|
383
|
+
value
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
fn display_repo(card: &Card, duplicate: bool) -> String {
|
|
387
|
+
let marker = if card.hidden_tab() { "⧉ " } else { "" };
|
|
388
|
+
if duplicate && card.pid > 0 {
|
|
389
|
+
format!(
|
|
390
|
+
"{}{} · {:04}",
|
|
391
|
+
marker,
|
|
392
|
+
text_or(&card.repo_label, "Pi session"),
|
|
393
|
+
card.pid % 10_000
|
|
394
|
+
)
|
|
395
|
+
} else {
|
|
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
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
fn state_label(state: &str) -> &str {
|
|
414
|
+
match state {
|
|
415
|
+
"success" => "done",
|
|
416
|
+
"thinking" | "tool" | "waiting" | "error" | "idle" => state,
|
|
417
|
+
_ => "idle",
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
fn state_class(state: &str) -> &str {
|
|
422
|
+
match state {
|
|
423
|
+
"thinking" | "tool" | "waiting" | "success" | "error" => state,
|
|
424
|
+
_ => "idle",
|
|
425
|
+
}
|
|
426
|
+
}
|
|
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
|
+
|
|
503
|
+
fn text_or<'a>(value: &'a str, fallback: &'a str) -> &'a str {
|
|
504
|
+
if value.is_empty() { fallback } else { value }
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
fn duration(now_ms: i64, anchor_ms: i64) -> String {
|
|
508
|
+
let seconds = now_ms.saturating_sub(anchor_ms.max(1)) / 1000;
|
|
509
|
+
format!("{}:{:02}", seconds / 60, seconds % 60)
|
|
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
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
mod app;
|
|
2
|
+
mod card_view;
|
|
3
|
+
mod protocol;
|
|
4
|
+
mod runtime;
|
|
5
|
+
|
|
6
|
+
use app::{App, AppInit};
|
|
7
|
+
use relm4::RelmApp;
|
|
8
|
+
|
|
9
|
+
#[cfg(target_os = "linux")]
|
|
10
|
+
fn bind_lifetime_to_parent() {
|
|
11
|
+
unsafe {
|
|
12
|
+
libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM);
|
|
13
|
+
if libc::getppid() == 1 {
|
|
14
|
+
std::process::exit(1);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#[cfg(not(target_os = "linux"))]
|
|
20
|
+
fn bind_lifetime_to_parent() {}
|
|
21
|
+
|
|
22
|
+
fn main() {
|
|
23
|
+
bind_lifetime_to_parent();
|
|
24
|
+
let demo = std::env::var("PI_ACTIVITY_STRIP_PANEL_DEMO").as_deref() == Ok("1");
|
|
25
|
+
let click_through = std::env::var("PI_ACTIVITY_STRIP_CLICK_THROUGH").as_deref() == Ok("1");
|
|
26
|
+
let app = RelmApp::new("com.tryinget.pi-activity-strip.panel").visible_on_activate(false);
|
|
27
|
+
app.run::<App>(AppInit {
|
|
28
|
+
demo,
|
|
29
|
+
click_through,
|
|
30
|
+
});
|
|
31
|
+
}
|