@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.
Files changed (57) hide show
  1. package/README.md +59 -22
  2. package/bin/pi-activity-strip.mjs +32 -4
  3. package/native/bin/linux-x64-gnu/artifact.json +3 -2
  4. package/native/bin/linux-x64-gnu/pi-activity-strip-panel +0 -0
  5. package/native/panel/src/app.rs +572 -266
  6. package/native/panel/src/app_gui_tests.rs +730 -0
  7. package/native/panel/src/app_hover_gui_tests.rs +917 -0
  8. package/native/panel/src/card_view.rs +19 -4
  9. package/native/panel/src/directory_view.rs +79 -17
  10. package/native/panel/src/hints.rs +65 -0
  11. package/native/panel/src/hover-lifecycle-fixtures.json +30 -0
  12. package/native/panel/src/hover_keyboard.rs +237 -0
  13. package/native/panel/src/interaction.rs +156 -0
  14. package/native/panel/src/keyboard-observation-fixtures.json +241 -0
  15. package/native/panel/src/keyboard_observation.rs +70 -0
  16. package/native/panel/src/main.rs +4 -0
  17. package/native/panel/src/protocol.rs +22 -8
  18. package/native/panel/src/runtime.rs +43 -2
  19. package/package.json +5 -5
  20. package/src/broker/server.mjs +31 -14
  21. package/src/client/launcher.mjs +3 -7
  22. package/src/client/runtime-lock.mjs +2 -1
  23. package/src/common/agent-identity.mjs +6 -1
  24. package/src/common/contracts.d.ts +1 -0
  25. package/src/common/contracts.ts +38 -0
  26. package/src/common/keyboard-observation.mjs +35 -0
  27. package/src/common/overlay-readiness.mjs +11 -0
  28. package/src/common/presence-apps.mjs +57 -0
  29. package/src/common/recovery-identity.mjs +85 -0
  30. package/src/common/recovery-manifest.mjs +417 -0
  31. package/src/common/recovery-types.ts +75 -0
  32. package/src/common/ribbon-status.mjs +41 -0
  33. package/src/common/status-report.mjs +37 -2
  34. package/src/common/workspace-view.mjs +54 -0
  35. package/src/native/ak-runtime.mjs +50 -9
  36. package/src/native/focus-visibility.mjs +67 -0
  37. package/src/native/ghostty-tab-inventory.py +40 -23
  38. package/src/native/hover-keyboard.mjs +32 -0
  39. package/src/native/interaction-context.mjs +45 -0
  40. package/src/native/keyboard-entry.mjs +32 -0
  41. package/src/native/main.mjs +77 -71
  42. package/src/native/panel-binary.mjs +25 -8
  43. package/src/native/panel-neutral-source.mjs +53 -0
  44. package/src/native/panel-presentation.mjs +137 -0
  45. package/src/native/panel-projection.mjs +338 -24
  46. package/src/native/panel-protocol.mjs +23 -0
  47. package/src/native/panel-window-jump.mjs +90 -0
  48. package/src/native/placement.mjs +83 -20
  49. package/src/native/recovery-inventory.mjs +209 -0
  50. package/src/native/recovery-proc.mjs +178 -0
  51. package/src/native/recovery-sources.mjs +358 -0
  52. package/src/native/runtime-diagnostics.mjs +242 -0
  53. package/src/native/strip-hotkey-kdl.mjs +252 -0
  54. package/src/native/strip-hotkey.mjs +62 -0
  55. package/src/native/tab-inventory.mjs +62 -11
  56. package/src/native/window-jump.mjs +43 -0
  57. 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
+ }
@@ -1,6 +1,10 @@
1
1
  mod app;
2
2
  mod card_view;
3
3
  mod directory_view;
4
+ mod hints;
5
+ mod hover_keyboard;
6
+ mod interaction;
7
+ mod keyboard_observation;
4
8
  mod protocol;
5
9
  mod runtime;
6
10
 
@@ -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
- 1
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": 1,
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": 1, "type": "error", "message": message.into() }));
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: 1,
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":1,"type":"view","revision":7,"visible":true,"focusedCardId":"card-a","sessions":[{"cardId":"card-a","sessionId":"session-a","state":"tool","agentActive":true,"agentStartedAt":null,"processId":4242}]}"#,
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":1,"type":"view","revision":9,"sessions":[{"cardId":"card-a","windowId":43,"workspaceIdx":2},{"cardId":"card-b","windowId":null,"workspaceIdx":null},{"cardId":"card-c"}]}"#,
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":1,"type":"view","revision":10,"sessions":[{"cardId":"card-a","windowId":"43","workspaceIdx":2.5},{"cardId":"card-b","windowId":44,"workspaceIdx":3}]}"#,
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":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}]}"#,
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() => consume_line(&sender, &line),
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.9.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=stable cargo fmt --check && RUSTUP_TOOLCHAIN=stable cargo test",
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": "0.84.4",
92
- "@earendil-works/pi-ai": "0.84.4",
93
- "@earendil-works/pi-coding-agent": "0.84.4",
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"