@tryinget/pi-activity-strip 0.9.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 -22
- package/bin/pi-activity-strip.mjs +32 -4
- package/native/bin/linux-x64-gnu/artifact.json +3 -2
- package/native/bin/linux-x64-gnu/pi-activity-strip-panel +0 -0
- package/native/panel/src/app.rs +572 -266
- 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 +79 -17
- 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 +4 -0
- package/native/panel/src/protocol.rs +22 -8
- package/native/panel/src/runtime.rs +43 -2
- package/package.json +5 -5
- 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 +1 -0
- package/src/common/contracts.ts +38 -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 +54 -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 +77 -71
- 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 +338 -24
- 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
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
use std::cell::Cell;
|
|
2
|
+
use std::rc::Rc;
|
|
3
|
+
|
|
4
|
+
/// Shared with delayed GTK callbacks. Revocation survives a hide/show cycle, unlike `is_mapped`.
|
|
5
|
+
#[derive(Clone, Default)]
|
|
6
|
+
pub struct InteractionLifetime(Rc<Cell<u64>>);
|
|
7
|
+
|
|
8
|
+
impl InteractionLifetime {
|
|
9
|
+
pub fn invalidate(&self) {
|
|
10
|
+
self.0.set(self.0.get().wrapping_add(1));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
pub fn ticket(&self) -> u64 {
|
|
14
|
+
self.0.get()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
pub fn accepts(&self, ticket: u64, visible: bool, interactive: bool) -> bool {
|
|
18
|
+
visible && interactive && ticket == self.ticket()
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// A real mapped pointer is necessary for hover intent, but controller permission is ALSO required.
|
|
23
|
+
pub fn accepts_pointer(visible: bool, interactive: bool, mapped: bool, contains: bool) -> bool {
|
|
24
|
+
visible && interactive && mapped && contains
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Where a pointer that was inside the body sat when the panel hid. A Wayland pointer enter carries
|
|
28
|
+
/// no cause, so the compositor re-entering that unmoved pointer (remap, workspace return) looks like
|
|
29
|
+
/// a genuine return, except that it reproduces this exact surface position.
|
|
30
|
+
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
31
|
+
pub enum PointerAnchor {
|
|
32
|
+
At(f64, f64),
|
|
33
|
+
/// GDK had no position at hide: fail safe, the next entry is not taken as intent.
|
|
34
|
+
Unknown,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// Whether an entry is the anchored pointer coming back rather than a new arrival. An entry whose
|
|
38
|
+
/// position GDK cannot report is not evidence of movement either.
|
|
39
|
+
pub fn is_stationary_reentry(
|
|
40
|
+
anchor: Option<PointerAnchor>,
|
|
41
|
+
entered_at: Option<(f64, f64)>,
|
|
42
|
+
) -> bool {
|
|
43
|
+
match anchor {
|
|
44
|
+
Some(PointerAnchor::At(x, y)) => entered_at.is_none_or(|at| at == (x, y)),
|
|
45
|
+
Some(PointerAnchor::Unknown) => true,
|
|
46
|
+
None => false,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// A pointer anchor plus the mapped absences it has outlived. Only the first mapped/body-absent
|
|
51
|
+
/// Tick after a remap can fall before the compositor re-enters a stationary pointer; a second one,
|
|
52
|
+
/// a full Tick later, means the pointer is elsewhere, and the anchor stops applying.
|
|
53
|
+
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
54
|
+
pub struct HideAnchor {
|
|
55
|
+
pub at: PointerAnchor,
|
|
56
|
+
mapped_absences: u8,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
impl HideAnchor {
|
|
60
|
+
pub fn new(at: PointerAnchor) -> Self {
|
|
61
|
+
Self {
|
|
62
|
+
at,
|
|
63
|
+
mapped_absences: 0,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
pub fn after_mapped_absence(self) -> Option<Self> {
|
|
68
|
+
(self.mapped_absences == 0).then_some(Self {
|
|
69
|
+
mapped_absences: 1,
|
|
70
|
+
..self
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
pub fn surface_state(visible: bool, expanded: bool) -> (i32, i32) {
|
|
76
|
+
(
|
|
77
|
+
if visible && expanded { 276 } else { 84 },
|
|
78
|
+
if visible { 84 } else { -1 },
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#[cfg(test)]
|
|
83
|
+
mod tests {
|
|
84
|
+
use super::*;
|
|
85
|
+
|
|
86
|
+
#[test]
|
|
87
|
+
fn hide_and_return_revoke_old_scroll_and_timer_tickets() {
|
|
88
|
+
let lifetime = InteractionLifetime::default();
|
|
89
|
+
let callback = lifetime.clone();
|
|
90
|
+
let ticket = lifetime.ticket();
|
|
91
|
+
assert!(callback.accepts(ticket, true, true));
|
|
92
|
+
lifetime.invalidate();
|
|
93
|
+
assert!(!callback.accepts(ticket, false, true));
|
|
94
|
+
assert!(!callback.accepts(ticket, true, true));
|
|
95
|
+
assert!(!callback.accepts(lifetime.ticket(), true, false));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#[test]
|
|
99
|
+
fn invisible_surface_cannot_expand_or_reclaim_zone() {
|
|
100
|
+
assert_eq!(surface_state(false, true), (84, -1));
|
|
101
|
+
assert_eq!(surface_state(false, false), (84, -1));
|
|
102
|
+
assert_eq!(surface_state(true, false), (84, 84));
|
|
103
|
+
assert_eq!(surface_state(true, true), (276, 84));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#[test]
|
|
107
|
+
fn pending_hover_requires_actual_pointer_on_an_interactive_mapped_surface() {
|
|
108
|
+
for facts in [
|
|
109
|
+
(false, true, true, true),
|
|
110
|
+
(true, false, true, true),
|
|
111
|
+
(true, true, false, true),
|
|
112
|
+
(true, true, true, false),
|
|
113
|
+
] {
|
|
114
|
+
assert!(!accepts_pointer(facts.0, facts.1, facts.2, facts.3));
|
|
115
|
+
}
|
|
116
|
+
assert!(accepts_pointer(true, true, true, true));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#[test]
|
|
120
|
+
fn given_a_pointer_anchored_at_hide_when_an_entry_arrives_then_only_movement_is_intent() {
|
|
121
|
+
let anchor = Some(PointerAnchor::At(52.0, 32.0));
|
|
122
|
+
// Then: the exact pre-hide position is the stationary pointer, not a new arrival.
|
|
123
|
+
assert!(is_stationary_reentry(anchor, Some((52.0, 32.0))));
|
|
124
|
+
assert!(
|
|
125
|
+
is_stationary_reentry(anchor, None),
|
|
126
|
+
"unknown entry position is not movement"
|
|
127
|
+
);
|
|
128
|
+
assert!(is_stationary_reentry(
|
|
129
|
+
Some(PointerAnchor::Unknown),
|
|
130
|
+
Some((1.0, 1.0))
|
|
131
|
+
));
|
|
132
|
+
// Then: any other position, or no anchor at all, is a genuine entry.
|
|
133
|
+
assert!(!is_stationary_reentry(anchor, Some((52.0, 32.5))));
|
|
134
|
+
assert!(!is_stationary_reentry(anchor, Some((53.0, 32.0))));
|
|
135
|
+
assert!(!is_stationary_reentry(None, Some((52.0, 32.0))));
|
|
136
|
+
assert!(!is_stationary_reentry(None, None));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#[test]
|
|
140
|
+
fn given_a_hide_anchor_when_mapped_absences_pass_then_only_the_first_is_survived() {
|
|
141
|
+
let anchor = HideAnchor::new(PointerAnchor::At(52.0, 32.0));
|
|
142
|
+
// Then: the remap-phase Tick (F02/F03) keeps the anchor.
|
|
143
|
+
let survived = anchor.after_mapped_absence().expect("first mapped absence");
|
|
144
|
+
assert_eq!(survived.at, PointerAnchor::At(52.0, 32.0));
|
|
145
|
+
// Then: a second mapped absence a full Tick later ends it, Unknown included.
|
|
146
|
+
assert_eq!(survived.after_mapped_absence(), None);
|
|
147
|
+
let unknown = HideAnchor::new(PointerAnchor::Unknown);
|
|
148
|
+
assert_eq!(
|
|
149
|
+
unknown
|
|
150
|
+
.after_mapped_absence()
|
|
151
|
+
.unwrap()
|
|
152
|
+
.after_mapped_absence(),
|
|
153
|
+
None
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "hover actual acquisition before native deadline",
|
|
4
|
+
"valid": true,
|
|
5
|
+
"event": {
|
|
6
|
+
"protocol": 2,
|
|
7
|
+
"type": "keyboard-observation",
|
|
8
|
+
"request": 1,
|
|
9
|
+
"origin": "hover",
|
|
10
|
+
"reason": "acquired",
|
|
11
|
+
"nativeMs": 1000,
|
|
12
|
+
"deadlineMs": 4000,
|
|
13
|
+
"captured": true
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "typing activity renews observed hover deadline",
|
|
18
|
+
"valid": true,
|
|
19
|
+
"event": {
|
|
20
|
+
"protocol": 2,
|
|
21
|
+
"type": "keyboard-observation",
|
|
22
|
+
"request": 1,
|
|
23
|
+
"origin": "hover",
|
|
24
|
+
"reason": "activity",
|
|
25
|
+
"nativeMs": 1200,
|
|
26
|
+
"deadlineMs": 4200,
|
|
27
|
+
"captured": true
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "source revocation before expiry",
|
|
32
|
+
"valid": true,
|
|
33
|
+
"event": {
|
|
34
|
+
"protocol": 2,
|
|
35
|
+
"type": "keyboard-observation",
|
|
36
|
+
"request": 1,
|
|
37
|
+
"origin": "hover",
|
|
38
|
+
"reason": "source-revoked",
|
|
39
|
+
"nativeMs": 1200,
|
|
40
|
+
"deadlineMs": 4000,
|
|
41
|
+
"captured": true
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "native idle expiry at boundary",
|
|
46
|
+
"valid": true,
|
|
47
|
+
"event": {
|
|
48
|
+
"protocol": 2,
|
|
49
|
+
"type": "keyboard-observation",
|
|
50
|
+
"request": 2,
|
|
51
|
+
"origin": "hover",
|
|
52
|
+
"reason": "idle-expired",
|
|
53
|
+
"nativeMs": 4000,
|
|
54
|
+
"deadlineMs": 4000,
|
|
55
|
+
"captured": true
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"name": "explicit capture has no hover deadline",
|
|
60
|
+
"valid": true,
|
|
61
|
+
"event": {
|
|
62
|
+
"protocol": 2,
|
|
63
|
+
"type": "keyboard-observation",
|
|
64
|
+
"request": 3,
|
|
65
|
+
"origin": "explicit",
|
|
66
|
+
"reason": "acquired",
|
|
67
|
+
"nativeMs": 5000,
|
|
68
|
+
"deadlineMs": null,
|
|
69
|
+
"captured": true
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"name": "explicit source revocation",
|
|
74
|
+
"valid": true,
|
|
75
|
+
"event": {
|
|
76
|
+
"protocol": 2,
|
|
77
|
+
"type": "keyboard-observation",
|
|
78
|
+
"request": 3,
|
|
79
|
+
"origin": "explicit",
|
|
80
|
+
"reason": "source-revoked",
|
|
81
|
+
"nativeMs": 5100,
|
|
82
|
+
"deadlineMs": null,
|
|
83
|
+
"captured": true
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"name": "unknown text is never projected",
|
|
88
|
+
"valid": true,
|
|
89
|
+
"event": {
|
|
90
|
+
"protocol": 2,
|
|
91
|
+
"type": "keyboard-observation",
|
|
92
|
+
"request": 4,
|
|
93
|
+
"origin": "hover",
|
|
94
|
+
"reason": "acquired",
|
|
95
|
+
"nativeMs": 6000,
|
|
96
|
+
"deadlineMs": 9000,
|
|
97
|
+
"captured": true,
|
|
98
|
+
"query": "SECRET_QUERY",
|
|
99
|
+
"credential": "SECRET_CREDENTIAL"
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"name": "grant is not capture",
|
|
104
|
+
"valid": false,
|
|
105
|
+
"event": {
|
|
106
|
+
"protocol": 2,
|
|
107
|
+
"type": "keyboard-observation",
|
|
108
|
+
"request": 4,
|
|
109
|
+
"origin": "hover",
|
|
110
|
+
"reason": "acquired",
|
|
111
|
+
"nativeMs": 6000,
|
|
112
|
+
"deadlineMs": 9000,
|
|
113
|
+
"captured": false
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"name": "early idle cause is rejected",
|
|
118
|
+
"valid": false,
|
|
119
|
+
"event": {
|
|
120
|
+
"protocol": 2,
|
|
121
|
+
"type": "keyboard-observation",
|
|
122
|
+
"request": 4,
|
|
123
|
+
"origin": "hover",
|
|
124
|
+
"reason": "idle-expired",
|
|
125
|
+
"nativeMs": 8999,
|
|
126
|
+
"deadlineMs": 9000,
|
|
127
|
+
"captured": true
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"name": "expired acquisition is rejected",
|
|
132
|
+
"valid": false,
|
|
133
|
+
"event": {
|
|
134
|
+
"protocol": 2,
|
|
135
|
+
"type": "keyboard-observation",
|
|
136
|
+
"request": 4,
|
|
137
|
+
"origin": "hover",
|
|
138
|
+
"reason": "acquired",
|
|
139
|
+
"nativeMs": 9000,
|
|
140
|
+
"deadlineMs": 9000,
|
|
141
|
+
"captured": true
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"name": "explicit never expires by hover idle",
|
|
146
|
+
"valid": false,
|
|
147
|
+
"event": {
|
|
148
|
+
"protocol": 2,
|
|
149
|
+
"type": "keyboard-observation",
|
|
150
|
+
"request": 4,
|
|
151
|
+
"origin": "explicit",
|
|
152
|
+
"reason": "idle-expired",
|
|
153
|
+
"nativeMs": 9000,
|
|
154
|
+
"deadlineMs": null,
|
|
155
|
+
"captured": true
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"name": "no missing native deadline",
|
|
160
|
+
"valid": false,
|
|
161
|
+
"event": {
|
|
162
|
+
"protocol": 2,
|
|
163
|
+
"type": "keyboard-observation",
|
|
164
|
+
"request": 4,
|
|
165
|
+
"origin": "hover",
|
|
166
|
+
"reason": "acquired",
|
|
167
|
+
"nativeMs": 6000,
|
|
168
|
+
"captured": true
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"name": "no untyped origin",
|
|
173
|
+
"valid": false,
|
|
174
|
+
"event": {
|
|
175
|
+
"protocol": 2,
|
|
176
|
+
"type": "keyboard-observation",
|
|
177
|
+
"request": 4,
|
|
178
|
+
"origin": "SECRET",
|
|
179
|
+
"reason": "acquired",
|
|
180
|
+
"nativeMs": 6000,
|
|
181
|
+
"deadlineMs": 9000,
|
|
182
|
+
"captured": true
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"name": "no negative clock",
|
|
187
|
+
"valid": false,
|
|
188
|
+
"event": {
|
|
189
|
+
"protocol": 2,
|
|
190
|
+
"type": "keyboard-observation",
|
|
191
|
+
"request": 4,
|
|
192
|
+
"origin": "hover",
|
|
193
|
+
"reason": "acquired",
|
|
194
|
+
"nativeMs": -1,
|
|
195
|
+
"deadlineMs": 9000,
|
|
196
|
+
"captured": true
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"name": "no string clock coercion",
|
|
201
|
+
"valid": false,
|
|
202
|
+
"event": {
|
|
203
|
+
"protocol": 2,
|
|
204
|
+
"type": "keyboard-observation",
|
|
205
|
+
"request": 4,
|
|
206
|
+
"origin": "hover",
|
|
207
|
+
"reason": "acquired",
|
|
208
|
+
"nativeMs": "6000",
|
|
209
|
+
"deadlineMs": 9000,
|
|
210
|
+
"captured": true
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"name": "no unsafe request integer",
|
|
215
|
+
"valid": false,
|
|
216
|
+
"event": {
|
|
217
|
+
"protocol": 2,
|
|
218
|
+
"type": "keyboard-observation",
|
|
219
|
+
"request": 9007199254740992,
|
|
220
|
+
"origin": "hover",
|
|
221
|
+
"reason": "acquired",
|
|
222
|
+
"nativeMs": 6000,
|
|
223
|
+
"deadlineMs": 9000,
|
|
224
|
+
"captured": true
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
"name": "legacy ABI still rejected",
|
|
229
|
+
"valid": false,
|
|
230
|
+
"event": {
|
|
231
|
+
"protocol": 1,
|
|
232
|
+
"type": "keyboard-observation",
|
|
233
|
+
"request": 4,
|
|
234
|
+
"origin": "hover",
|
|
235
|
+
"reason": "acquired",
|
|
236
|
+
"nativeMs": 6000,
|
|
237
|
+
"deadlineMs": 9000,
|
|
238
|
+
"captured": true
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
//! Optional private-v2 observation, never authorization or a cross-clock countdown.
|
|
2
|
+
use serde_json::{Value, json};
|
|
3
|
+
|
|
4
|
+
const MAX_SAFE: u64 = 9_007_199_254_740_991;
|
|
5
|
+
|
|
6
|
+
/// Validate/allowlist the exact same executable protocol fixtures as the Node observer.
|
|
7
|
+
/// Unknown fields are discarded, including all application/query text.
|
|
8
|
+
pub(crate) fn normalize(value: &Value) -> Option<Value> {
|
|
9
|
+
if value["protocol"] != 2 || value["type"] != "keyboard-observation" {
|
|
10
|
+
return None;
|
|
11
|
+
}
|
|
12
|
+
let request = value["request"]
|
|
13
|
+
.as_u64()
|
|
14
|
+
.filter(|v| *v > 0 && *v <= MAX_SAFE)?;
|
|
15
|
+
let native_ms = value["nativeMs"].as_u64().filter(|v| *v <= MAX_SAFE)?;
|
|
16
|
+
let captured = value["captured"].as_bool()?;
|
|
17
|
+
let origin = value["origin"].as_str()?;
|
|
18
|
+
let reason = value["reason"].as_str()?;
|
|
19
|
+
if !captured
|
|
20
|
+
|| !matches!(
|
|
21
|
+
reason,
|
|
22
|
+
"acquired" | "activity" | "idle-expired" | "source-revoked"
|
|
23
|
+
)
|
|
24
|
+
{
|
|
25
|
+
return None;
|
|
26
|
+
}
|
|
27
|
+
let deadline = value.get("deadlineMs")?;
|
|
28
|
+
match origin {
|
|
29
|
+
"hover" => {
|
|
30
|
+
let limit = deadline.as_u64().filter(|v| *v <= MAX_SAFE)?;
|
|
31
|
+
if (reason == "idle-expired" && native_ms < limit)
|
|
32
|
+
|| (matches!(reason, "acquired" | "activity") && native_ms >= limit)
|
|
33
|
+
{
|
|
34
|
+
return None;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
"explicit" if deadline.is_null() && reason != "idle-expired" => {}
|
|
38
|
+
_ => return None,
|
|
39
|
+
}
|
|
40
|
+
Some(
|
|
41
|
+
json!({"protocol":2, "type":"keyboard-observation", "request":request,
|
|
42
|
+
"origin":origin, "reason":reason, "nativeMs":native_ms,
|
|
43
|
+
"deadlineMs":deadline, "captured":captured}),
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#[cfg(test)]
|
|
48
|
+
mod tests {
|
|
49
|
+
use super::*;
|
|
50
|
+
|
|
51
|
+
#[test]
|
|
52
|
+
fn shared_node_rust_protocol_transitions_are_observations_only() {
|
|
53
|
+
let cases: Value =
|
|
54
|
+
serde_json::from_str(include_str!("keyboard-observation-fixtures.json")).unwrap();
|
|
55
|
+
for case in cases.as_array().unwrap() {
|
|
56
|
+
let got = normalize(&case["event"]);
|
|
57
|
+
assert_eq!(
|
|
58
|
+
got.is_some(),
|
|
59
|
+
case["valid"].as_bool().unwrap(),
|
|
60
|
+
"{}",
|
|
61
|
+
case["name"]
|
|
62
|
+
);
|
|
63
|
+
if let Some(got) = got {
|
|
64
|
+
assert!(got.get("query").is_none());
|
|
65
|
+
assert!(got.get("credential").is_none());
|
|
66
|
+
assert_eq!(got.as_object().unwrap().len(), 8);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
package/native/panel/src/main.rs
CHANGED
|
@@ -134,6 +134,9 @@ 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)]
|
|
@@ -143,6 +146,9 @@ pub struct ViewMessage {
|
|
|
143
146
|
/// Every agent window on every workspace, for the brand block's window list.
|
|
144
147
|
#[serde(default)]
|
|
145
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>,
|
|
146
152
|
}
|
|
147
153
|
|
|
148
154
|
/// One entry of the window list: the card's first line and the Niri window number to jump to.
|
|
@@ -168,11 +174,17 @@ pub struct WindowEntry {
|
|
|
168
174
|
}
|
|
169
175
|
|
|
170
176
|
fn protocol_version() -> u8 {
|
|
171
|
-
|
|
177
|
+
2
|
|
172
178
|
}
|
|
173
179
|
|
|
174
180
|
static OUTPUT: OnceLock<Mutex<io::Stdout>> = OnceLock::new();
|
|
175
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
|
+
}
|
|
176
188
|
|
|
177
189
|
pub fn emit(event: Value) {
|
|
178
190
|
let stdout = OUTPUT.get_or_init(|| Mutex::new(io::stdout()));
|
|
@@ -192,7 +204,7 @@ pub fn was_ready() -> bool {
|
|
|
192
204
|
pub fn emit_ready() {
|
|
193
205
|
READY.store(true, Ordering::SeqCst);
|
|
194
206
|
emit(json!({
|
|
195
|
-
"protocol":
|
|
207
|
+
"protocol": 2,
|
|
196
208
|
"type": "ready",
|
|
197
209
|
"pid": std::process::id(),
|
|
198
210
|
"namespace": "pi-activity-strip"
|
|
@@ -200,7 +212,7 @@ pub fn emit_ready() {
|
|
|
200
212
|
}
|
|
201
213
|
|
|
202
214
|
pub fn emit_error(message: impl Into<String>) {
|
|
203
|
-
emit(json!({ "protocol":
|
|
215
|
+
emit(json!({ "protocol": 2, "type": "error", "message": message.into() }));
|
|
204
216
|
}
|
|
205
217
|
|
|
206
218
|
pub fn demo_view() -> ViewMessage {
|
|
@@ -209,11 +221,13 @@ pub fn demo_view() -> ViewMessage {
|
|
|
209
221
|
.unwrap_or_default()
|
|
210
222
|
.as_millis() as i64;
|
|
211
223
|
ViewMessage {
|
|
212
|
-
protocol:
|
|
224
|
+
protocol: 2,
|
|
213
225
|
message_type: "view".into(),
|
|
214
226
|
revision: 1,
|
|
227
|
+
interaction_epoch: 1,
|
|
215
228
|
visible: true,
|
|
216
229
|
focused_card_id: Some("demo-tool".into()),
|
|
230
|
+
hotkey: Some("Super+Shift+A".into()),
|
|
217
231
|
sessions: vec![
|
|
218
232
|
Card {
|
|
219
233
|
card_id: "demo-monitor".into(),
|
|
@@ -342,7 +356,7 @@ mod tests {
|
|
|
342
356
|
#[test]
|
|
343
357
|
fn view_messages_accept_current_node_projection_shape() {
|
|
344
358
|
let view: ViewMessage = serde_json::from_str(
|
|
345
|
-
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}]}"#,
|
|
346
360
|
)
|
|
347
361
|
.expect("view should parse");
|
|
348
362
|
assert_eq!(view.revision, 7);
|
|
@@ -376,7 +390,7 @@ mod tests {
|
|
|
376
390
|
#[test]
|
|
377
391
|
fn window_and_workspace_numbers_are_optional_card_fields() {
|
|
378
392
|
let view: ViewMessage = serde_json::from_str(
|
|
379
|
-
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"}]}"#,
|
|
380
394
|
)
|
|
381
395
|
.expect("view should parse");
|
|
382
396
|
assert_eq!(view.sessions[0].window_id, Some(43));
|
|
@@ -393,7 +407,7 @@ mod tests {
|
|
|
393
407
|
#[test]
|
|
394
408
|
fn a_malformed_window_or_workspace_number_drops_only_that_field() {
|
|
395
409
|
let view: ViewMessage = serde_json::from_str(
|
|
396
|
-
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}]}"#,
|
|
397
411
|
)
|
|
398
412
|
.expect("a bad optional field must not reject the whole view");
|
|
399
413
|
assert_eq!(view.sessions.len(), 2);
|
|
@@ -406,7 +420,7 @@ mod tests {
|
|
|
406
420
|
#[test]
|
|
407
421
|
fn ak_task_chips_parse_with_states() {
|
|
408
422
|
let view: ViewMessage = serde_json::from_str(
|
|
409
|
-
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}]}"#,
|
|
410
424
|
)
|
|
411
425
|
.expect("view should parse");
|
|
412
426
|
let card = &view.sessions[0];
|
|
@@ -66,7 +66,9 @@ pub fn start_input_reader(sender: relm4::Sender<AppMsg>) {
|
|
|
66
66
|
fn read_input(sender: relm4::Sender<AppMsg>) {
|
|
67
67
|
for line in BufReader::new(std::io::stdin()).lines() {
|
|
68
68
|
match line {
|
|
69
|
-
Ok(line) if !line.trim().is_empty() =>
|
|
69
|
+
Ok(line) if !line.trim().is_empty() => {
|
|
70
|
+
consume_line(&sender, &line);
|
|
71
|
+
}
|
|
70
72
|
Ok(_) => {}
|
|
71
73
|
Err(error) => {
|
|
72
74
|
let _ = sender.send(AppMsg::InputError(format!("panel input failed: {error}")));
|
|
@@ -87,6 +89,12 @@ fn consume_line(sender: &relm4::Sender<AppMsg>, line: &str) {
|
|
|
87
89
|
return;
|
|
88
90
|
}
|
|
89
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
|
+
}
|
|
90
98
|
match value.get("type").and_then(|kind| kind.as_str()) {
|
|
91
99
|
Some("view") => match serde_json::from_value::<ViewMessage>(value) {
|
|
92
100
|
Ok(view) => {
|
|
@@ -111,7 +119,40 @@ fn consume_line(sender: &relm4::Sender<AppMsg>, line: &str) {
|
|
|
111
119
|
}
|
|
112
120
|
}
|
|
113
121
|
Some("focus-strip") => {
|
|
114
|
-
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
|
+
));
|
|
115
156
|
}
|
|
116
157
|
Some("collapse") => {
|
|
117
158
|
let _ = sender.send(AppMsg::Collapse);
|
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,9 +88,9 @@
|
|
|
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
95
|
"typebox": "1.3.7",
|
|
96
96
|
"typescript": "7.0.2"
|