@tryinget/pi-activity-strip 0.8.0 → 0.10.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 +59 -21
- package/bin/pi-activity-strip.mjs +32 -4
- package/native/bin/linux-x64-gnu/artifact.json +4 -2
- package/native/bin/linux-x64-gnu/pi-activity-strip-panel +0 -0
- package/native/panel/src/app.rs +1141 -84
- package/native/panel/src/app_gui_tests.rs +730 -0
- package/native/panel/src/app_hover_gui_tests.rs +917 -0
- package/native/panel/src/card_view.rs +19 -4
- package/native/panel/src/directory_view.rs +471 -0
- package/native/panel/src/hints.rs +65 -0
- package/native/panel/src/hover-lifecycle-fixtures.json +30 -0
- package/native/panel/src/hover_keyboard.rs +237 -0
- package/native/panel/src/interaction.rs +156 -0
- package/native/panel/src/keyboard-observation-fixtures.json +241 -0
- package/native/panel/src/keyboard_observation.rs +70 -0
- package/native/panel/src/main.rs +5 -0
- package/native/panel/src/protocol.rs +93 -8
- package/native/panel/src/runtime.rs +85 -12
- package/native/panel/src/style.css +85 -0
- package/package.json +6 -7
- package/src/broker/server.mjs +31 -14
- package/src/client/launcher.mjs +3 -7
- package/src/client/runtime-lock.mjs +2 -1
- package/src/common/agent-identity.mjs +6 -1
- package/src/common/contracts.d.ts +2 -0
- package/src/common/contracts.ts +39 -0
- package/src/common/keyboard-observation.mjs +35 -0
- package/src/common/overlay-readiness.mjs +11 -0
- package/src/common/presence-apps.mjs +57 -0
- package/src/common/recovery-identity.mjs +85 -0
- package/src/common/recovery-manifest.mjs +417 -0
- package/src/common/recovery-types.ts +75 -0
- package/src/common/ribbon-status.mjs +41 -0
- package/src/common/status-report.mjs +37 -2
- package/src/common/workspace-view.mjs +118 -0
- package/src/native/ak-runtime.mjs +50 -9
- package/src/native/focus-visibility.mjs +67 -0
- package/src/native/ghostty-tab-inventory.py +40 -23
- package/src/native/hover-keyboard.mjs +32 -0
- package/src/native/interaction-context.mjs +45 -0
- package/src/native/keyboard-entry.mjs +32 -0
- package/src/native/main.mjs +91 -42
- package/src/native/panel-binary.mjs +25 -8
- package/src/native/panel-neutral-source.mjs +53 -0
- package/src/native/panel-presentation.mjs +137 -0
- package/src/native/panel-projection.mjs +354 -18
- package/src/native/panel-protocol.mjs +23 -0
- package/src/native/panel-window-jump.mjs +90 -0
- package/src/native/placement.mjs +83 -20
- package/src/native/recovery-inventory.mjs +209 -0
- package/src/native/recovery-proc.mjs +178 -0
- package/src/native/recovery-sources.mjs +358 -0
- package/src/native/runtime-diagnostics.mjs +242 -0
- package/src/native/strip-hotkey-kdl.mjs +252 -0
- package/src/native/strip-hotkey.mjs +62 -0
- package/src/native/tab-inventory.mjs +62 -11
- package/src/native/window-jump.mjs +43 -0
- package/src/native/workspace-events.mjs +123 -2
package/native/panel/src/main.rs
CHANGED
|
@@ -134,20 +134,57 @@ pub struct ViewMessage {
|
|
|
134
134
|
#[serde(rename = "type")]
|
|
135
135
|
pub message_type: String,
|
|
136
136
|
pub revision: u64,
|
|
137
|
+
/// Changes only when exact controller admission/visibility context changes, not on telemetry.
|
|
138
|
+
#[serde(default)]
|
|
139
|
+
pub interaction_epoch: u64,
|
|
137
140
|
#[serde(default)]
|
|
138
141
|
pub visible: bool,
|
|
139
142
|
#[serde(default)]
|
|
140
143
|
pub focused_card_id: Option<String>,
|
|
141
144
|
#[serde(default)]
|
|
142
145
|
pub sessions: Vec<Card>,
|
|
146
|
+
/// Every agent window on every workspace, for the brand block's window list.
|
|
147
|
+
#[serde(default)]
|
|
148
|
+
pub windows: Vec<WindowEntry>,
|
|
149
|
+
/// The compositor shortcut that enters keyboard mode, as the operator's config binds it.
|
|
150
|
+
#[serde(default)]
|
|
151
|
+
pub hotkey: Option<String>,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// One entry of the window list: the card's first line and the Niri window number to jump to.
|
|
155
|
+
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
|
156
|
+
#[serde(rename_all = "camelCase")]
|
|
157
|
+
pub struct WindowEntry {
|
|
158
|
+
#[serde(default)]
|
|
159
|
+
pub card_id: String,
|
|
160
|
+
#[serde(default)]
|
|
161
|
+
pub label: String,
|
|
162
|
+
pub window_id: i64,
|
|
163
|
+
/// Niri's workspace id: the grouping key, since workspace numbers repeat across monitors.
|
|
164
|
+
#[serde(default, deserialize_with = "lenient_integer")]
|
|
165
|
+
pub workspace_id: Option<i64>,
|
|
166
|
+
#[serde(default, deserialize_with = "lenient_integer")]
|
|
167
|
+
pub workspace_idx: Option<i64>,
|
|
168
|
+
#[serde(default)]
|
|
169
|
+
pub workspace_name: Option<String>,
|
|
170
|
+
#[serde(default)]
|
|
171
|
+
pub hidden_tab: bool,
|
|
172
|
+
#[serde(default)]
|
|
173
|
+
pub current: bool,
|
|
143
174
|
}
|
|
144
175
|
|
|
145
176
|
fn protocol_version() -> u8 {
|
|
146
|
-
|
|
177
|
+
2
|
|
147
178
|
}
|
|
148
179
|
|
|
149
180
|
static OUTPUT: OnceLock<Mutex<io::Stdout>> = OnceLock::new();
|
|
150
181
|
static READY: AtomicBool = AtomicBool::new(false);
|
|
182
|
+
/// Do not rely on closing a never-mapped GTK window to end its application hold.
|
|
183
|
+
/// A fatal private-protocol mismatch drops the Wayland connection (and any grab) immediately.
|
|
184
|
+
pub fn reject_protocol(message: impl Into<String>) -> ! {
|
|
185
|
+
emit_error(message);
|
|
186
|
+
std::process::exit(1);
|
|
187
|
+
}
|
|
151
188
|
|
|
152
189
|
pub fn emit(event: Value) {
|
|
153
190
|
let stdout = OUTPUT.get_or_init(|| Mutex::new(io::stdout()));
|
|
@@ -167,7 +204,7 @@ pub fn was_ready() -> bool {
|
|
|
167
204
|
pub fn emit_ready() {
|
|
168
205
|
READY.store(true, Ordering::SeqCst);
|
|
169
206
|
emit(json!({
|
|
170
|
-
"protocol":
|
|
207
|
+
"protocol": 2,
|
|
171
208
|
"type": "ready",
|
|
172
209
|
"pid": std::process::id(),
|
|
173
210
|
"namespace": "pi-activity-strip"
|
|
@@ -175,7 +212,7 @@ pub fn emit_ready() {
|
|
|
175
212
|
}
|
|
176
213
|
|
|
177
214
|
pub fn emit_error(message: impl Into<String>) {
|
|
178
|
-
emit(json!({ "protocol":
|
|
215
|
+
emit(json!({ "protocol": 2, "type": "error", "message": message.into() }));
|
|
179
216
|
}
|
|
180
217
|
|
|
181
218
|
pub fn demo_view() -> ViewMessage {
|
|
@@ -184,11 +221,13 @@ pub fn demo_view() -> ViewMessage {
|
|
|
184
221
|
.unwrap_or_default()
|
|
185
222
|
.as_millis() as i64;
|
|
186
223
|
ViewMessage {
|
|
187
|
-
protocol:
|
|
224
|
+
protocol: 2,
|
|
188
225
|
message_type: "view".into(),
|
|
189
226
|
revision: 1,
|
|
227
|
+
interaction_epoch: 1,
|
|
190
228
|
visible: true,
|
|
191
229
|
focused_card_id: Some("demo-tool".into()),
|
|
230
|
+
hotkey: Some("Super+Shift+A".into()),
|
|
192
231
|
sessions: vec![
|
|
193
232
|
Card {
|
|
194
233
|
card_id: "demo-monitor".into(),
|
|
@@ -252,6 +291,33 @@ pub fn demo_view() -> ViewMessage {
|
|
|
252
291
|
..empty_card()
|
|
253
292
|
},
|
|
254
293
|
],
|
|
294
|
+
windows: vec![
|
|
295
|
+
demo_window("demo-monitor", "activity-strip", 36, 2, false, false),
|
|
296
|
+
demo_window("demo-tool", "native-dogfood", 43, 2, false, true),
|
|
297
|
+
demo_window("demo-hidden", "pi-extensions", 43, 2, true, false),
|
|
298
|
+
demo_window("demo-other", "dep-surgeon", 344, 3, false, false),
|
|
299
|
+
demo_window("demo-far", "niri-desktop-continuity", 1048, 5, false, false),
|
|
300
|
+
],
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
fn demo_window(
|
|
305
|
+
card_id: &str,
|
|
306
|
+
label: &str,
|
|
307
|
+
window_id: i64,
|
|
308
|
+
workspace_idx: i64,
|
|
309
|
+
hidden_tab: bool,
|
|
310
|
+
current: bool,
|
|
311
|
+
) -> WindowEntry {
|
|
312
|
+
WindowEntry {
|
|
313
|
+
card_id: card_id.into(),
|
|
314
|
+
label: label.into(),
|
|
315
|
+
window_id,
|
|
316
|
+
workspace_id: Some(workspace_idx + 100),
|
|
317
|
+
workspace_idx: Some(workspace_idx),
|
|
318
|
+
workspace_name: None,
|
|
319
|
+
hidden_tab,
|
|
320
|
+
current,
|
|
255
321
|
}
|
|
256
322
|
}
|
|
257
323
|
|
|
@@ -290,7 +356,7 @@ mod tests {
|
|
|
290
356
|
#[test]
|
|
291
357
|
fn view_messages_accept_current_node_projection_shape() {
|
|
292
358
|
let view: ViewMessage = serde_json::from_str(
|
|
293
|
-
r#"{"protocol":
|
|
359
|
+
r#"{"protocol":2,"type":"view","revision":7,"visible":true,"focusedCardId":"card-a","sessions":[{"cardId":"card-a","sessionId":"session-a","state":"tool","agentActive":true,"agentStartedAt":null,"processId":4242}]}"#,
|
|
294
360
|
)
|
|
295
361
|
.expect("view should parse");
|
|
296
362
|
assert_eq!(view.revision, 7);
|
|
@@ -300,12 +366,31 @@ mod tests {
|
|
|
300
366
|
assert_eq!(view.sessions[0].pid, 4242);
|
|
301
367
|
assert!(view.sessions[0].ak_tasks.is_empty());
|
|
302
368
|
assert_eq!(view.sessions[0].ak_task_overflow, 0);
|
|
369
|
+
assert!(
|
|
370
|
+
view.windows.is_empty(),
|
|
371
|
+
"controllers without a window list still parse"
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
#[test]
|
|
376
|
+
fn view_messages_carry_the_window_list() {
|
|
377
|
+
let view: ViewMessage = serde_json::from_str(
|
|
378
|
+
r#"{"type":"view","revision":1,"windows":[{"cardId":"terminal:a","label":"pi-extensions","windowId":432,"workspaceIdx":"x","workspaceName":null,"hiddenTab":true,"current":false}]}"#,
|
|
379
|
+
)
|
|
380
|
+
.expect("view should parse");
|
|
381
|
+
assert_eq!(view.windows.len(), 1);
|
|
382
|
+
assert_eq!(view.windows[0].window_id, 432);
|
|
383
|
+
assert_eq!(
|
|
384
|
+
view.windows[0].workspace_idx, None,
|
|
385
|
+
"a malformed workspace number drops only that field"
|
|
386
|
+
);
|
|
387
|
+
assert!(view.windows[0].hidden_tab);
|
|
303
388
|
}
|
|
304
389
|
|
|
305
390
|
#[test]
|
|
306
391
|
fn window_and_workspace_numbers_are_optional_card_fields() {
|
|
307
392
|
let view: ViewMessage = serde_json::from_str(
|
|
308
|
-
r#"{"protocol":
|
|
393
|
+
r#"{"protocol":2,"type":"view","revision":9,"sessions":[{"cardId":"card-a","windowId":43,"workspaceIdx":2},{"cardId":"card-b","windowId":null,"workspaceIdx":null},{"cardId":"card-c"}]}"#,
|
|
309
394
|
)
|
|
310
395
|
.expect("view should parse");
|
|
311
396
|
assert_eq!(view.sessions[0].window_id, Some(43));
|
|
@@ -322,7 +407,7 @@ mod tests {
|
|
|
322
407
|
#[test]
|
|
323
408
|
fn a_malformed_window_or_workspace_number_drops_only_that_field() {
|
|
324
409
|
let view: ViewMessage = serde_json::from_str(
|
|
325
|
-
r#"{"protocol":
|
|
410
|
+
r#"{"protocol":2,"type":"view","revision":10,"sessions":[{"cardId":"card-a","windowId":"43","workspaceIdx":2.5},{"cardId":"card-b","windowId":44,"workspaceIdx":3}]}"#,
|
|
326
411
|
)
|
|
327
412
|
.expect("a bad optional field must not reject the whole view");
|
|
328
413
|
assert_eq!(view.sessions.len(), 2);
|
|
@@ -335,7 +420,7 @@ mod tests {
|
|
|
335
420
|
#[test]
|
|
336
421
|
fn ak_task_chips_parse_with_states() {
|
|
337
422
|
let view: ViewMessage = serde_json::from_str(
|
|
338
|
-
r#"{"protocol":
|
|
423
|
+
r#"{"protocol":2,"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}]}"#,
|
|
339
424
|
)
|
|
340
425
|
.expect("view should parse");
|
|
341
426
|
let card = &view.sessions[0];
|
|
@@ -45,20 +45,38 @@ pub fn apply_theme(definitions: &str) {
|
|
|
45
45
|
THEME_PROVIDER.with(|provider| provider.load_from_data(&css));
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/// Read controller messages from stdin on their own thread. The thread is detached on purpose: it
|
|
49
|
+
/// lives exactly as long as stdin, and on EOF it reports the parent gone, which closes the panel.
|
|
48
50
|
pub fn start_input_reader(sender: relm4::Sender<AppMsg>) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
51
|
+
let reader_sender = sender.clone();
|
|
52
|
+
let spawned = std::thread::Builder::new()
|
|
53
|
+
.name("panel-input".into())
|
|
54
|
+
.spawn(move || read_input(reader_sender));
|
|
55
|
+
match spawned {
|
|
56
|
+
Ok(handle) => drop(handle),
|
|
57
|
+
Err(error) => {
|
|
58
|
+
let _ = sender.send(AppMsg::InputError(format!(
|
|
59
|
+
"could not start the panel input reader: {error}"
|
|
60
|
+
)));
|
|
61
|
+
let _ = sender.send(AppMsg::ParentGone);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fn read_input(sender: relm4::Sender<AppMsg>) {
|
|
67
|
+
for line in BufReader::new(std::io::stdin()).lines() {
|
|
68
|
+
match line {
|
|
69
|
+
Ok(line) if !line.trim().is_empty() => {
|
|
70
|
+
consume_line(&sender, &line);
|
|
71
|
+
}
|
|
72
|
+
Ok(_) => {}
|
|
73
|
+
Err(error) => {
|
|
74
|
+
let _ = sender.send(AppMsg::InputError(format!("panel input failed: {error}")));
|
|
75
|
+
break;
|
|
58
76
|
}
|
|
59
77
|
}
|
|
60
|
-
|
|
61
|
-
|
|
78
|
+
}
|
|
79
|
+
let _ = sender.send(AppMsg::ParentGone);
|
|
62
80
|
}
|
|
63
81
|
|
|
64
82
|
fn consume_line(sender: &relm4::Sender<AppMsg>, line: &str) {
|
|
@@ -71,6 +89,12 @@ fn consume_line(sender: &relm4::Sender<AppMsg>, line: &str) {
|
|
|
71
89
|
return;
|
|
72
90
|
}
|
|
73
91
|
};
|
|
92
|
+
if value.get("protocol").and_then(|p| p.as_u64()) != Some(2) {
|
|
93
|
+
crate::protocol::reject_protocol(format!(
|
|
94
|
+
"Native panel private protocol mismatch: expected 2, received {}",
|
|
95
|
+
value.get("protocol").unwrap_or(&serde_json::Value::Null)
|
|
96
|
+
));
|
|
97
|
+
}
|
|
74
98
|
match value.get("type").and_then(|kind| kind.as_str()) {
|
|
75
99
|
Some("view") => match serde_json::from_value::<ViewMessage>(value) {
|
|
76
100
|
Ok(view) => {
|
|
@@ -95,7 +119,40 @@ fn consume_line(sender: &relm4::Sender<AppMsg>, line: &str) {
|
|
|
95
119
|
}
|
|
96
120
|
}
|
|
97
121
|
Some("focus-strip") => {
|
|
98
|
-
let _ = sender.send(AppMsg::FocusStrip
|
|
122
|
+
let _ = sender.send(AppMsg::FocusStrip(
|
|
123
|
+
value
|
|
124
|
+
.get("revision")
|
|
125
|
+
.and_then(|item| item.as_u64())
|
|
126
|
+
.unwrap_or(0),
|
|
127
|
+
value
|
|
128
|
+
.get("epoch")
|
|
129
|
+
.and_then(|item| item.as_u64())
|
|
130
|
+
.unwrap_or(0),
|
|
131
|
+
));
|
|
132
|
+
}
|
|
133
|
+
Some("hover-grant") => {
|
|
134
|
+
let _ = sender.send(AppMsg::HoverGrant(
|
|
135
|
+
value
|
|
136
|
+
.get("token")
|
|
137
|
+
.and_then(|item| item.as_u64())
|
|
138
|
+
.unwrap_or(0),
|
|
139
|
+
value
|
|
140
|
+
.get("epoch")
|
|
141
|
+
.and_then(|item| item.as_u64())
|
|
142
|
+
.unwrap_or(0),
|
|
143
|
+
value
|
|
144
|
+
.get("request")
|
|
145
|
+
.and_then(|item| item.as_u64())
|
|
146
|
+
.unwrap_or(0),
|
|
147
|
+
));
|
|
148
|
+
}
|
|
149
|
+
Some("hover-denied") => {
|
|
150
|
+
let _ = sender.send(AppMsg::HoverDenied(
|
|
151
|
+
value
|
|
152
|
+
.get("token")
|
|
153
|
+
.and_then(|item| item.as_u64())
|
|
154
|
+
.unwrap_or(0),
|
|
155
|
+
));
|
|
99
156
|
}
|
|
100
157
|
Some("collapse") => {
|
|
101
158
|
let _ = sender.send(AppMsg::Collapse);
|
|
@@ -121,6 +178,22 @@ fn consume_line(sender: &relm4::Sender<AppMsg>, line: &str) {
|
|
|
121
178
|
.to_owned();
|
|
122
179
|
let _ = sender.send(AppMsg::ActivationResult(card_id, ok, message));
|
|
123
180
|
}
|
|
181
|
+
Some("jump-result") => {
|
|
182
|
+
let ok = value
|
|
183
|
+
.get("ok")
|
|
184
|
+
.and_then(|item| item.as_bool())
|
|
185
|
+
.unwrap_or(false);
|
|
186
|
+
let message = value
|
|
187
|
+
.get("message")
|
|
188
|
+
.and_then(|item| item.as_str())
|
|
189
|
+
.unwrap_or(if ok { "Jumped." } else { "Nothing moved." })
|
|
190
|
+
.to_owned();
|
|
191
|
+
let window_id = value
|
|
192
|
+
.get("windowId")
|
|
193
|
+
.and_then(|item| item.as_i64())
|
|
194
|
+
.unwrap_or(-1);
|
|
195
|
+
let _ = sender.send(AppMsg::JumpResult(window_id, ok, message));
|
|
196
|
+
}
|
|
124
197
|
Some(kind) => {
|
|
125
198
|
let _ = sender.send(AppMsg::InputError(format!(
|
|
126
199
|
"unsupported panel message: {kind}"
|
|
@@ -265,3 +265,88 @@ window {
|
|
|
265
265
|
font-family: "IBM Plex Mono", "JetBrains Mono", monospace;
|
|
266
266
|
font-size: 8px;
|
|
267
267
|
}
|
|
268
|
+
|
|
269
|
+
/*
|
|
270
|
+
* The brand block's window list: every agent window on every workspace, as its card's first line
|
|
271
|
+
* and its Niri window number. The number is the thing you type, so it carries the accent.
|
|
272
|
+
*/
|
|
273
|
+
.directory {
|
|
274
|
+
border-top: 1px solid alpha(@pi_foreground, 0.12);
|
|
275
|
+
padding-top: 7px;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.jump-line {
|
|
279
|
+
color: @pi_accent;
|
|
280
|
+
font-family: "IBM Plex Mono", "JetBrains Mono", monospace;
|
|
281
|
+
font-size: 10px;
|
|
282
|
+
font-weight: 600;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/* biome-ignore lint/correctness/noUnknownTypeSelector: GTK CSS uses widget node names. */
|
|
286
|
+
.directory-scroller,
|
|
287
|
+
.directory-scroller > viewport {
|
|
288
|
+
background: transparent;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* Same as the card row: scrolls by wheel or touchpad without a visible bar. */
|
|
292
|
+
/* biome-ignore lint/correctness/noUnknownTypeSelector: GTK CSS uses widget node names. */
|
|
293
|
+
.directory-scroller scrollbar {
|
|
294
|
+
opacity: 0;
|
|
295
|
+
min-width: 0;
|
|
296
|
+
min-height: 0;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.directory-header {
|
|
300
|
+
color: alpha(@pi_foreground, 0.5);
|
|
301
|
+
font-family: "IBM Plex Mono", "JetBrains Mono", monospace;
|
|
302
|
+
font-size: 8px;
|
|
303
|
+
letter-spacing: 1px;
|
|
304
|
+
margin-top: 5px;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.directory-row {
|
|
308
|
+
background: transparent;
|
|
309
|
+
border: 1px solid transparent;
|
|
310
|
+
border-radius: 6px;
|
|
311
|
+
color: @pi_foreground;
|
|
312
|
+
min-height: 0;
|
|
313
|
+
padding: 2px 6px;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.directory-row:hover,
|
|
317
|
+
.directory-row:focus-visible {
|
|
318
|
+
background: mix(@pi_background, @pi_foreground, 0.1);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/* The window you are in: the same accent ring as its card. */
|
|
322
|
+
.directory-row.current {
|
|
323
|
+
border-color: @pi_accent;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.directory-label {
|
|
327
|
+
font-size: 11px;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.directory-id {
|
|
331
|
+
color: @pi_accent;
|
|
332
|
+
font-family: "IBM Plex Mono", "JetBrains Mono", monospace;
|
|
333
|
+
font-size: 10px;
|
|
334
|
+
font-weight: 600;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.directory-empty {
|
|
338
|
+
color: alpha(@pi_foreground, 0.5);
|
|
339
|
+
font-size: 10px;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/* Which workspace a row's window is on; quieter than the number you type. */
|
|
343
|
+
.directory-workspace {
|
|
344
|
+
color: alpha(@pi_foreground, 0.5);
|
|
345
|
+
font-family: "IBM Plex Mono", "JetBrains Mono", monospace;
|
|
346
|
+
font-size: 9px;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/* The row Enter would open: a typed name always shows where it leads. */
|
|
350
|
+
.directory-row.chosen {
|
|
351
|
+
background: alpha(@pi_accent, 0.16);
|
|
352
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tryinget/pi-activity-strip",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "pi extension package for activity-strip workflows in monorepo runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"docs:list:json": "node ~/ai-society/core/agent-scripts/scripts/docs-list.mjs --json",
|
|
47
47
|
"release:check": "bash ./scripts/release-check.sh",
|
|
48
48
|
"release:check:quick": "SKIP_PI_SMOKE=1 bash ./scripts/release-check.sh",
|
|
49
|
-
"native:check": "cd native/panel && RUSTUP_TOOLCHAIN=
|
|
49
|
+
"native:check": "cd native/panel && RUSTUP_TOOLCHAIN=1.98.0 cargo fmt --check && RUSTUP_TOOLCHAIN=1.98.0 cargo test --locked",
|
|
50
50
|
"native:build": "bash ./scripts/build-native-panel.sh",
|
|
51
51
|
"strip:open": "node ./bin/pi-activity-strip.mjs open",
|
|
52
52
|
"strip:status": "node ./bin/pi-activity-strip.mjs status",
|
|
@@ -88,13 +88,12 @@
|
|
|
88
88
|
},
|
|
89
89
|
"devDependencies": {
|
|
90
90
|
"@biomejs/biome": "2.4.4",
|
|
91
|
-
"@earendil-works/pi-agent-core": "
|
|
92
|
-
"@earendil-works/pi-ai": "
|
|
93
|
-
"@earendil-works/pi-coding-agent": "
|
|
91
|
+
"@earendil-works/pi-agent-core": "1.4.0",
|
|
92
|
+
"@earendil-works/pi-ai": "1.4.0",
|
|
93
|
+
"@earendil-works/pi-coding-agent": "1.4.0",
|
|
94
94
|
"@types/node": "22.14.0",
|
|
95
|
-
"@typescript/native-preview": "7.0.0-dev.20260417.1",
|
|
96
95
|
"typebox": "1.3.7",
|
|
97
|
-
"typescript": "
|
|
96
|
+
"typescript": "7.0.2"
|
|
98
97
|
},
|
|
99
98
|
"overrides": {
|
|
100
99
|
"fast-xml-parser": "5.7.0"
|
package/src/broker/server.mjs
CHANGED
|
@@ -58,28 +58,45 @@ export class ActivityStripBroker extends EventEmitter {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
async start() {
|
|
61
|
+
// Linux sun_path has 108 bytes including the trailing NUL. Node can silently
|
|
62
|
+
// bind a truncated path, so reject before chmod/unlink can change the filesystem.
|
|
63
|
+
if (Buffer.byteLength(this.socketPath, "utf8") > 107) {
|
|
64
|
+
throw new RangeError("Activity strip socket path must not exceed 107 UTF-8 bytes.");
|
|
65
|
+
}
|
|
66
|
+
|
|
61
67
|
fs.mkdirSync(this.socketDir, { recursive: true, mode: 0o700 });
|
|
62
68
|
fs.chmodSync(this.socketDir, 0o700);
|
|
63
69
|
safeUnlink(this.socketPath);
|
|
64
70
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
this.server.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
let bound = false;
|
|
72
|
+
try {
|
|
73
|
+
await new Promise((resolve, reject) => {
|
|
74
|
+
this.server.once("error", reject);
|
|
75
|
+
this.server.listen(this.socketPath, () => {
|
|
76
|
+
this.server.off("error", reject);
|
|
77
|
+
bound = true;
|
|
71
78
|
resolve(undefined);
|
|
72
|
-
}
|
|
73
|
-
reject(error);
|
|
74
|
-
}
|
|
79
|
+
});
|
|
75
80
|
});
|
|
76
|
-
|
|
81
|
+
fs.chmodSync(this.socketPath, 0o600);
|
|
77
82
|
|
|
78
|
-
|
|
83
|
+
this.tick = setInterval(() => {
|
|
84
|
+
this.emitSnapshot();
|
|
85
|
+
}, ACTIVITY_STRIP_BROADCAST_TICK_MS);
|
|
86
|
+
this.tick.unref?.();
|
|
79
87
|
this.emitSnapshot();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
} catch (error) {
|
|
89
|
+
// Only a successful bind gives us a socket to clean up. On a failed bind,
|
|
90
|
+
// stop() could unlink the address of the listener that beat us to it.
|
|
91
|
+
if (bound) {
|
|
92
|
+
try {
|
|
93
|
+
await this.stop();
|
|
94
|
+
} catch {
|
|
95
|
+
// Preserve the startup failure even if cleanup also fails.
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
83
100
|
}
|
|
84
101
|
|
|
85
102
|
emitSnapshot() {
|
package/src/client/launcher.mjs
CHANGED
|
@@ -8,13 +8,9 @@ import { spawn } from "node:child_process";
|
|
|
8
8
|
import { setTimeout as delay } from "node:timers/promises";
|
|
9
9
|
import { assessActivityStripCompatibility } from "../common/compatibility.mjs";
|
|
10
10
|
import { ACTIVITY_STRIP_START_TIMEOUT_MS } from "../common/constants.mjs";
|
|
11
|
+
import { isOverlayReady } from "../common/overlay-readiness.mjs";
|
|
11
12
|
import { getBrokerStatus } from "./broker-client.mjs";
|
|
12
13
|
|
|
13
|
-
/** @param {import("../common/contracts.ts").BrokerResponse | null | undefined} result */
|
|
14
|
-
function isBrokerReady(result) {
|
|
15
|
-
return Boolean(result?.ok && (!result.runtimeStatus || result.runtimeStatus.state === "ready"));
|
|
16
|
-
}
|
|
17
|
-
|
|
18
14
|
/** @param {import("../common/contracts.ts").BrokerResponse | null | undefined} result */
|
|
19
15
|
function describeBrokerWait(result) {
|
|
20
16
|
if (!result?.ok) return "Activity strip broker did not answer.";
|
|
@@ -64,7 +60,7 @@ export async function ensureActivityStripRunning(binPath, options = {}) {
|
|
|
64
60
|
latestStatus = null;
|
|
65
61
|
}
|
|
66
62
|
|
|
67
|
-
if (
|
|
63
|
+
if (isOverlayReady(latestStatus)) {
|
|
68
64
|
return { ok: true, started: false };
|
|
69
65
|
}
|
|
70
66
|
|
|
@@ -101,7 +97,7 @@ export async function ensureActivityStripRunning(binPath, options = {}) {
|
|
|
101
97
|
} catch {
|
|
102
98
|
latestStatus = null;
|
|
103
99
|
}
|
|
104
|
-
if (
|
|
100
|
+
if (isOverlayReady(latestStatus)) {
|
|
105
101
|
return { ok: true, started };
|
|
106
102
|
}
|
|
107
103
|
if (latestStatus?.runtimeStatus?.state === "error") {
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
ACTIVITY_STRIP_START_TIMEOUT_MS,
|
|
14
14
|
ACTIVITY_STRIP_STOP_TIMEOUT_MS,
|
|
15
15
|
} from "../common/constants.mjs";
|
|
16
|
+
import { isOverlayReady } from "../common/overlay-readiness.mjs";
|
|
16
17
|
import { requestBrokerShutdown } from "./broker-client.mjs";
|
|
17
18
|
|
|
18
19
|
const execFileAsync = promisify(execFile);
|
|
@@ -123,7 +124,7 @@ export async function waitForStartedRuntime({
|
|
|
123
124
|
const deadline = now() + timeoutMs;
|
|
124
125
|
while (true) {
|
|
125
126
|
const status = await getStatus();
|
|
126
|
-
if (
|
|
127
|
+
if (isOverlayReady(status)) {
|
|
127
128
|
return { ok: true, started: controller.exitCode === undefined };
|
|
128
129
|
}
|
|
129
130
|
if (status?.runtimeStatus?.state === "error") return { ok: false, reason: "error", status };
|
|
@@ -21,8 +21,13 @@ export const AGENT_CATALOG = Object.freeze([
|
|
|
21
21
|
{ kind: "opencode", label: "opencode", commands: Object.freeze(["opencode"]) },
|
|
22
22
|
{ kind: "crush", label: "Crush", commands: Object.freeze(["crush"]) },
|
|
23
23
|
{ kind: "goose", label: "Goose", commands: Object.freeze(["goose"]) },
|
|
24
|
-
|
|
24
|
+
// Cursor's CLI installs both names; `agent` is the one its docs now lead with.
|
|
25
|
+
{ kind: "cursor", label: "Cursor Agent", commands: Object.freeze(["cursor-agent", "agent"]) },
|
|
25
26
|
{ kind: "zcode", label: "zcode", commands: Object.freeze(["zcode"]) },
|
|
27
|
+
// The nodecode TUI; its gateway runs outside any terminal, so tab admission never sees it.
|
|
28
|
+
{ kind: "nodecode", label: "Nodecode", commands: Object.freeze(["nodecode"]) },
|
|
29
|
+
// angelx is a Python launcher; the cockpit binary it starts, `angel`, is what holds the tab.
|
|
30
|
+
{ kind: "angel", label: "angelx", commands: Object.freeze(["angel"]) },
|
|
26
31
|
]);
|
|
27
32
|
|
|
28
33
|
/** Pi publishes its own telemetry, so its processes are never discovered this way. */
|
|
@@ -99,6 +99,7 @@ export interface ActivityStripRuntimeStatus {
|
|
|
99
99
|
clickThrough?: boolean;
|
|
100
100
|
panelMoveCount?: number;
|
|
101
101
|
panelActivationCount?: number;
|
|
102
|
+
panelJumpCount?: number;
|
|
102
103
|
panelRestartCount?: number;
|
|
103
104
|
lastMovedCardId?: string | null;
|
|
104
105
|
warnings?: string[];
|
|
@@ -168,6 +169,7 @@ export interface SessionStartContextLike {
|
|
|
168
169
|
}
|
|
169
170
|
|
|
170
171
|
export interface BeforeAgentStartEventLike {
|
|
172
|
+
messageType?: "user" | "custom";
|
|
171
173
|
prompt?: string;
|
|
172
174
|
}
|
|
173
175
|
|
package/src/common/contracts.ts
CHANGED
|
@@ -80,6 +80,17 @@ export interface BrokerSnapshot {
|
|
|
80
80
|
|
|
81
81
|
export type ActivityStripRuntimeState = "starting" | "ready" | "error";
|
|
82
82
|
|
|
83
|
+
/** A last native observation, not current capture/authorization or a cross-clock timer. */
|
|
84
|
+
export interface NativeKeyboardObservation {
|
|
85
|
+
panelPid: number;
|
|
86
|
+
request: number;
|
|
87
|
+
origin: "hover" | "explicit";
|
|
88
|
+
reason: "acquired" | "activity" | "idle-expired" | "source-revoked";
|
|
89
|
+
nativeMs: number;
|
|
90
|
+
deadlineMs: number | null;
|
|
91
|
+
captured: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
83
94
|
export interface ActivityStripRuntimeStatus {
|
|
84
95
|
state: ActivityStripRuntimeState;
|
|
85
96
|
backend?: "native-layer-shell";
|
|
@@ -114,15 +125,42 @@ export interface ActivityStripRuntimeStatus {
|
|
|
114
125
|
akTaskError?: string | null;
|
|
115
126
|
akTaskClaimCount?: number;
|
|
116
127
|
akTaskDeferredCount?: number;
|
|
128
|
+
/** When the deferral list was last read; it is reused for up to AK_DEFERRED_REFRESH_MS (AK7943). */
|
|
129
|
+
akTaskDeferredReadAt?: number | null;
|
|
130
|
+
/** How long that list may be reused (AK_DEFERRED_REFRESH_MS). */
|
|
131
|
+
akTaskDeferredMaxAgeMs?: number;
|
|
117
132
|
akTaskOrphanCount?: number;
|
|
118
133
|
akTaskRenderedCount?: number;
|
|
119
134
|
rendererVisibilityTransitionCount?: number;
|
|
120
135
|
controllerPid?: number;
|
|
121
136
|
panelPid?: number | null;
|
|
122
137
|
panelExpanded?: boolean;
|
|
138
|
+
nativeKeyboardObservation?: NativeKeyboardObservation | null;
|
|
139
|
+
/** Optional observational extension; absent on older controllers, never input/AK authority. */
|
|
140
|
+
ribbonDiagnostics?: {
|
|
141
|
+
controllerStartupSourceSha256: string | null;
|
|
142
|
+
expectedPrivateProtocol: number;
|
|
143
|
+
panelBuild: {
|
|
144
|
+
state: string;
|
|
145
|
+
binarySha256?: string;
|
|
146
|
+
processStartTicks?: string;
|
|
147
|
+
privateProtocol?: number | null;
|
|
148
|
+
receiptedSourceSha256?: string | null;
|
|
149
|
+
};
|
|
150
|
+
visibilityReason: string;
|
|
151
|
+
panelBuildObservedAt?: number | null;
|
|
152
|
+
focus: string;
|
|
153
|
+
contextValid: boolean;
|
|
154
|
+
eligible: boolean;
|
|
155
|
+
interactionEpoch: number;
|
|
156
|
+
keyboardAuthorization: "none" | "hover" | "explicit";
|
|
157
|
+
captureExpiry: string;
|
|
158
|
+
nativeKeyboardObservation?: NativeKeyboardObservation | null;
|
|
159
|
+
};
|
|
123
160
|
clickThrough?: boolean;
|
|
124
161
|
panelMoveCount?: number;
|
|
125
162
|
panelActivationCount?: number;
|
|
163
|
+
panelJumpCount?: number;
|
|
126
164
|
panelRestartCount?: number;
|
|
127
165
|
lastMovedCardId?: string | null;
|
|
128
166
|
warnings?: string[];
|
|
@@ -198,6 +236,7 @@ export interface SessionStartContextLike {
|
|
|
198
236
|
}
|
|
199
237
|
|
|
200
238
|
export interface BeforeAgentStartEventLike {
|
|
239
|
+
messageType?: "user" | "custom";
|
|
201
240
|
prompt?: string;
|
|
202
241
|
}
|
|
203
242
|
|