@tryinget/pi-activity-strip 0.5.0 → 0.7.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 (46) hide show
  1. package/README.md +59 -7
  2. package/bin/pi-activity-strip-claude-hook.mjs +71 -0
  3. package/bin/pi-activity-strip.mjs +98 -64
  4. package/extensions/activity-strip.js +25 -21
  5. package/native/bin/linux-x64-gnu/artifact.json +2 -2
  6. package/native/bin/linux-x64-gnu/pi-activity-strip-panel +0 -0
  7. package/native/panel/src/app.rs +20 -20
  8. package/native/panel/src/card_view.rs +396 -9
  9. package/native/panel/src/main.rs +64 -1
  10. package/native/panel/src/protocol.rs +142 -0
  11. package/native/panel/src/runtime.rs +50 -9
  12. package/native/panel/src/style.css +147 -60
  13. package/package.json +4 -4
  14. package/src/client/runtime-lock.mjs +140 -0
  15. package/src/common/agent-identity.mjs +163 -0
  16. package/src/common/ak-tasks.mjs +354 -0
  17. package/src/common/claude-events.mjs +190 -0
  18. package/src/common/claude-hook-config.mjs +56 -0
  19. package/src/common/claude-transcript.mjs +163 -0
  20. package/src/common/codex-transcript.mjs +153 -0
  21. package/src/common/compatibility.mjs +13 -0
  22. package/src/common/constants.mjs +2 -0
  23. package/src/common/contracts.d.ts +35 -0
  24. package/src/common/contracts.ts +67 -1
  25. package/src/common/ghostty-present.mjs +100 -0
  26. package/src/common/ghostty-theme.mjs +149 -0
  27. package/src/common/niri-focus.mjs +179 -111
  28. package/src/common/session-cards.mjs +5 -2
  29. package/src/common/status-report.mjs +51 -0
  30. package/src/common/surface-bindings.mjs +234 -0
  31. package/src/common/terminal-identity.mjs +75 -12
  32. package/src/common/window-placement.mjs +59 -0
  33. package/src/common/workspace-view.mjs +161 -0
  34. package/src/native/agent-discovery.mjs +294 -0
  35. package/src/native/ak-runtime.mjs +164 -0
  36. package/src/native/codex-discovery.mjs +128 -0
  37. package/src/native/ghostty-tab-inventory.py +99 -0
  38. package/src/native/height-repair.mjs +154 -0
  39. package/src/native/main.mjs +122 -64
  40. package/src/native/panel-binary.mjs +56 -0
  41. package/src/native/panel-projection.mjs +70 -5
  42. package/src/native/placement.mjs +225 -0
  43. package/src/native/tab-inventory.mjs +188 -0
  44. package/src/native/theme-runtime.mjs +72 -0
  45. package/src/native/theme-source.mjs +197 -0
  46. package/src/native/workspace-events.mjs +22 -1
@@ -1,8 +1,28 @@
1
1
  use serde::Deserialize;
2
2
  use serde_json::{Value, json};
3
3
  use std::io::{self, Write};
4
+ use std::sync::atomic::{AtomicBool, Ordering};
4
5
  use std::sync::{Mutex, OnceLock};
5
6
 
7
+ #[derive(Clone, Debug, Deserialize)]
8
+ #[serde(rename_all = "camelCase")]
9
+ pub struct AkTask {
10
+ #[serde(default)]
11
+ pub id: i64,
12
+ #[serde(default)]
13
+ pub title: String,
14
+ #[serde(default)]
15
+ pub state: String,
16
+ }
17
+
18
+ impl AkTask {
19
+ /// Only an `active` chip is a button: the card's own session holds a live claim, so clicking
20
+ /// it focuses that session's Ghostty window. Every other state is a badge and never fires.
21
+ pub fn is_active(&self) -> bool {
22
+ self.state == "active"
23
+ }
24
+ }
25
+
6
26
  #[derive(Clone, Debug, Deserialize)]
7
27
  #[serde(rename_all = "camelCase")]
8
28
  pub struct Card {
@@ -40,6 +60,36 @@ pub struct Card {
40
60
  pub agent_active: bool,
41
61
  #[serde(default, rename = "processId")]
42
62
  pub pid: i64,
63
+ /// Set for tabs running an agent other than Pi, which publish no telemetry of their own.
64
+ #[serde(default)]
65
+ pub agent_label: String,
66
+ /// Read-only AK task references joined onto this card by the controller. `active` chips are
67
+ /// clickable and focus this card's terminal; the rest are inert badges.
68
+ #[serde(default)]
69
+ pub ak_tasks: Vec<AkTask>,
70
+ /// How many further AK references exist beyond the rendered chips.
71
+ #[serde(default)]
72
+ pub ak_task_overflow: u32,
73
+ /// `Some(false)` marks a tab hidden behind another tab of its Ghostty window; the controller
74
+ /// placed it through its host process, so activation presents the tab before focusing.
75
+ #[serde(default)]
76
+ pub surface_visible: Option<bool>,
77
+ /// Niri window id, so a window an agent or tool names by id can be found on the ribbon. A
78
+ /// hidden tab has no window of its own and carries the id of the window hosting it.
79
+ #[serde(default, deserialize_with = "lenient_integer")]
80
+ pub window_id: Option<i64>,
81
+ /// Niri workspace `idx`: the number the operator sees, not the internal workspace id.
82
+ #[serde(default, deserialize_with = "lenient_integer")]
83
+ pub workspace_idx: Option<i64>,
84
+ }
85
+
86
+ /// An optional label is not worth a view: a value that is not an integer drops the field rather
87
+ /// than failing the whole message and every card in it.
88
+ fn lenient_integer<'de, D>(deserializer: D) -> Result<Option<i64>, D::Error>
89
+ where
90
+ D: serde::Deserializer<'de>,
91
+ {
92
+ Ok(Value::deserialize(deserializer)?.as_i64())
43
93
  }
44
94
 
45
95
  impl Card {
@@ -51,6 +101,10 @@ impl Card {
51
101
  }
52
102
  }
53
103
 
104
+ pub fn hidden_tab(&self) -> bool {
105
+ self.surface_visible == Some(false)
106
+ }
107
+
54
108
  pub fn active(&self) -> bool {
55
109
  self.agent_active || matches!(self.state.as_str(), "thinking" | "tool" | "waiting")
56
110
  }
@@ -93,6 +147,7 @@ fn protocol_version() -> u8 {
93
147
  }
94
148
 
95
149
  static OUTPUT: OnceLock<Mutex<io::Stdout>> = OnceLock::new();
150
+ static READY: AtomicBool = AtomicBool::new(false);
96
151
 
97
152
  pub fn emit(event: Value) {
98
153
  let stdout = OUTPUT.get_or_init(|| Mutex::new(io::stdout()));
@@ -103,7 +158,14 @@ pub fn emit(event: Value) {
103
158
  }
104
159
  }
105
160
 
161
+ /// Whether this panel ever came up. GTK returns from `run` without building anything when it hands
162
+ /// the activation to another panel on the same display.
163
+ pub fn was_ready() -> bool {
164
+ READY.load(Ordering::SeqCst)
165
+ }
166
+
106
167
  pub fn emit_ready() {
168
+ READY.store(true, Ordering::SeqCst);
107
169
  emit(json!({
108
170
  "protocol": 1,
109
171
  "type": "ready",
@@ -140,6 +202,8 @@ pub fn demo_view() -> ViewMessage {
140
202
  started_at: now - 135_000,
141
203
  last_event_at: now - 4_000,
142
204
  updated_at: now,
205
+ window_id: Some(36),
206
+ workspace_idx: Some(2),
143
207
  ..empty_card()
144
208
  },
145
209
  Card {
@@ -159,6 +223,32 @@ pub fn demo_view() -> ViewMessage {
159
223
  last_event_at: now - 1_000,
160
224
  updated_at: now,
161
225
  agent_active: true,
226
+ ak_tasks: vec![AkTask {
227
+ id: 5701,
228
+ title: "Show clickable AK-task references on the activity ribbon".into(),
229
+ state: "active".into(),
230
+ }],
231
+ ak_task_overflow: 1,
232
+ window_id: Some(43),
233
+ workspace_idx: Some(2),
234
+ ..empty_card()
235
+ },
236
+ Card {
237
+ card_id: "demo-hidden".into(),
238
+ session_id: "demo-hidden".into(),
239
+ repo_label: "pi-extensions".into(),
240
+ phase: "Waiting behind another tab".into(),
241
+ state: "idle".into(),
242
+ detail: "Hidden tab placed through its Ghostty host window".into(),
243
+ cwd: "/home/tryinget/ai-society/softwareco/owned/pi-extensions".into(),
244
+ started_at: now - 610_000,
245
+ last_event_at: now - 95_000,
246
+ updated_at: now,
247
+ pid: 48213,
248
+ agent_label: "Claude Code".into(),
249
+ surface_visible: Some(false),
250
+ window_id: Some(43),
251
+ workspace_idx: Some(2),
162
252
  ..empty_card()
163
253
  },
164
254
  ],
@@ -184,6 +274,12 @@ fn empty_card() -> Card {
184
274
  updated_at: 0,
185
275
  agent_active: false,
186
276
  pid: 0,
277
+ agent_label: String::new(),
278
+ ak_tasks: Vec::new(),
279
+ ak_task_overflow: 0,
280
+ surface_visible: None,
281
+ window_id: None,
282
+ workspace_idx: None,
187
283
  }
188
284
  }
189
285
 
@@ -202,6 +298,52 @@ mod tests {
202
298
  assert!(view.sessions[0].active());
203
299
  assert_eq!(view.sessions[0].agent_started_at, None);
204
300
  assert_eq!(view.sessions[0].pid, 4242);
301
+ assert!(view.sessions[0].ak_tasks.is_empty());
302
+ assert_eq!(view.sessions[0].ak_task_overflow, 0);
303
+ }
304
+
305
+ #[test]
306
+ fn window_and_workspace_numbers_are_optional_card_fields() {
307
+ let view: ViewMessage = serde_json::from_str(
308
+ 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"}]}"#,
309
+ )
310
+ .expect("view should parse");
311
+ assert_eq!(view.sessions[0].window_id, Some(43));
312
+ assert_eq!(view.sessions[0].workspace_idx, Some(2));
313
+ assert_eq!(view.sessions[1].window_id, None);
314
+ assert_eq!(view.sessions[1].workspace_idx, None);
315
+ assert_eq!(
316
+ view.sessions[2].window_id, None,
317
+ "controllers that send no window id still parse"
318
+ );
319
+ assert_eq!(view.sessions[2].workspace_idx, None);
320
+ }
321
+
322
+ #[test]
323
+ fn a_malformed_window_or_workspace_number_drops_only_that_field() {
324
+ let view: ViewMessage = serde_json::from_str(
325
+ r#"{"protocol":1,"type":"view","revision":10,"sessions":[{"cardId":"card-a","windowId":"43","workspaceIdx":2.5},{"cardId":"card-b","windowId":44,"workspaceIdx":3}]}"#,
326
+ )
327
+ .expect("a bad optional field must not reject the whole view");
328
+ assert_eq!(view.sessions.len(), 2);
329
+ assert_eq!(view.sessions[0].window_id, None);
330
+ assert_eq!(view.sessions[0].workspace_idx, None);
331
+ assert_eq!(view.sessions[1].window_id, Some(44));
332
+ assert_eq!(view.sessions[1].workspace_idx, Some(3));
333
+ }
334
+
335
+ #[test]
336
+ fn ak_task_chips_parse_with_states() {
337
+ let view: ViewMessage = serde_json::from_str(
338
+ 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}]}"#,
339
+ )
340
+ .expect("view should parse");
341
+ let card = &view.sessions[0];
342
+ assert_eq!(card.ak_tasks.len(), 3);
343
+ assert!(card.ak_tasks[0].is_active());
344
+ assert!(!card.ak_tasks[1].is_active());
345
+ assert!(!card.ak_tasks[2].is_active());
346
+ assert_eq!(card.ak_task_overflow, 3);
205
347
  }
206
348
 
207
349
  #[test]
@@ -5,17 +5,44 @@ use std::collections::{HashMap, HashSet};
5
5
  use std::io::{BufRead, BufReader};
6
6
  use std::time::{SystemTime, UNIX_EPOCH};
7
7
 
8
+ /// Colour definitions used until the controller reports what Ghostty is showing.
9
+ const FALLBACK_DEFINITIONS: &str = concat!(
10
+ "@define-color pi_background #1e2326;\n",
11
+ "@define-color pi_foreground #d3c6aa;\n",
12
+ "@define-color pi_accent #7fbbb3;\n",
13
+ "@define-color pi_thinking #7fbbb3;\n",
14
+ "@define-color pi_tool #dbbc7f;\n",
15
+ "@define-color pi_waiting #e69875;\n",
16
+ "@define-color pi_success #a7c080;\n",
17
+ "@define-color pi_error #e67e80;\n",
18
+ );
19
+
20
+ thread_local! {
21
+ static THEME_PROVIDER: gtk::CssProvider = gtk::CssProvider::new();
22
+ }
23
+
24
+ /// The stylesheet derives every shade from a handful of named colours, so following the terminal's
25
+ /// theme is a matter of redefining those names and reloading one provider.
8
26
  #[allow(deprecated)]
9
27
  pub fn install_css() {
10
- let provider = gtk::CssProvider::new();
11
- provider.load_from_data(include_str!("style.css"));
12
- if let Some(display) = gtk::gdk::Display::default() {
13
- gtk::style_context_add_provider_for_display(
14
- &display,
15
- &provider,
16
- gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
17
- );
18
- }
28
+ apply_theme(FALLBACK_DEFINITIONS);
29
+ THEME_PROVIDER.with(|provider| {
30
+ if let Some(display) = gtk::gdk::Display::default() {
31
+ gtk::style_context_add_provider_for_display(
32
+ &display,
33
+ provider,
34
+ gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
35
+ );
36
+ }
37
+ });
38
+ }
39
+
40
+ /// Replace the active colour definitions. Definitions must sit in the same provider as the rules
41
+ /// that reference them, so both are loaded together.
42
+ #[allow(deprecated)]
43
+ pub fn apply_theme(definitions: &str) {
44
+ let css = format!("{definitions}\n{}", include_str!("style.css"));
45
+ THEME_PROVIDER.with(|provider| provider.load_from_data(&css));
19
46
  }
20
47
 
21
48
  pub fn start_input_reader(sender: relm4::Sender<AppMsg>) {
@@ -53,6 +80,20 @@ fn consume_line(sender: &relm4::Sender<AppMsg>, line: &str) {
53
80
  let _ = sender.send(AppMsg::InputError(format!("invalid view message: {error}")));
54
81
  }
55
82
  },
83
+ Some("theme") => {
84
+ let definitions = value
85
+ .get("definitions")
86
+ .and_then(|item| item.as_str())
87
+ .unwrap_or_default()
88
+ .to_owned();
89
+ if definitions.is_empty() {
90
+ let _ = sender.send(AppMsg::InputError(
91
+ "theme message carried no colours".into(),
92
+ ));
93
+ } else {
94
+ let _ = sender.send(AppMsg::Theme(definitions));
95
+ }
96
+ }
56
97
  Some("focus-strip") => {
57
98
  let _ = sender.send(AppMsg::FocusStrip);
58
99
  }
@@ -1,56 +1,66 @@
1
+ /*
2
+ * Every colour here derives from the handful of names the controller defines from Ghostty's own
3
+ * theme, so the ribbon follows the terminal into light and dark without a second palette to keep
4
+ * in sync. mix() against the background and alpha() against the foreground both invert correctly
5
+ * between schemes: a raised surface is lighter on a dark theme and darker on a light one.
6
+ */
7
+
1
8
  /* biome-ignore lint/correctness/noUnknownTypeSelector: GTK CSS uses widget node names. */
2
9
  window {
3
10
  background: transparent;
4
- color: rgba(247, 250, 255, 0.96);
5
- font-family: "IBM Plex Sans", "Avenir Next", "Segoe UI", sans-serif;
11
+ color: @pi_foreground;
12
+ font-family: "IBM Plex Sans", "Inter", "Segoe UI", sans-serif;
6
13
  }
7
14
 
8
- .brand,
9
- .cards-panel {
10
- background: linear-gradient(to bottom, rgba(12, 19, 33, 0.96), rgba(8, 13, 22, 0.92));
11
- border: 1px solid rgba(255, 255, 255, 0.08);
12
- border-radius: 20px;
15
+ /* One surface on the same rhythm and radius as tiled windows, so it reads as a peer of them. */
16
+ .band {
17
+ /* Solid: the ribbon is the one surface that never dims, so it reads as the fixed ground. */
18
+ background: @pi_background;
19
+ border: 1px solid alpha(@pi_foreground, 0.16);
20
+ border-radius: 12px;
21
+ padding: 0 14px 0 16px;
13
22
  }
14
23
 
15
24
  .brand {
16
- padding: 10px 12px;
25
+ border-right: 1px solid alpha(@pi_foreground, 0.12);
26
+ margin-right: 12px;
27
+ padding: 11px 14px 11px 0;
17
28
  }
18
29
 
19
30
  .brand-eyebrow,
20
31
  .meta,
21
32
  .card-state,
33
+ .card-window,
22
34
  .card-tool,
35
+ .card-elapsed,
23
36
  .inspector-key {
24
37
  font-family: "IBM Plex Mono", "JetBrains Mono", monospace;
25
38
  }
26
39
 
27
40
  .brand-eyebrow {
28
- color: #79b8ff;
41
+ color: alpha(@pi_accent, 0.9);
29
42
  font-size: 9px;
30
- letter-spacing: 1px;
43
+ font-weight: 600;
44
+ letter-spacing: 1.4px;
31
45
  }
32
46
 
33
47
  .brand-title {
34
- color: rgba(247, 250, 255, 0.96);
35
- font-size: 18px;
48
+ color: @pi_foreground;
49
+ font-size: 17px;
36
50
  font-weight: 650;
37
- }
38
-
39
- .brand-subtitle {
40
- color: rgba(194, 205, 224, 0.74);
41
- font-size: 10px;
51
+ letter-spacing: -0.2px;
42
52
  }
43
53
 
44
54
  .meta {
45
- color: rgba(167, 180, 203, 0.58);
55
+ color: alpha(@pi_foreground, 0.56);
46
56
  font-size: 9px;
47
57
  }
48
58
 
49
59
  /* biome-ignore lint/correctness/noUnknownTypeSelector: GTK CSS uses widget node names. */
60
+ .cards-panel,
50
61
  .cards-panel > viewport {
51
- background: linear-gradient(to bottom, rgba(10, 16, 28, 0.94), rgba(6, 10, 18, 0.9));
52
- border-radius: 20px;
53
- padding: 6px 8px 8px;
62
+ background: transparent;
63
+ padding: 0;
54
64
  }
55
65
 
56
66
  /* biome-ignore lint/correctness/noUnknownTypeSelector: GTK CSS uses widget node names. */
@@ -61,32 +71,42 @@ window {
61
71
  }
62
72
 
63
73
  .card {
64
- color: rgba(247, 250, 255, 0.96);
65
- background: linear-gradient(to bottom, rgba(13, 20, 35, 0.98), rgba(9, 14, 24, 0.96));
66
- border: 1px solid rgba(255, 255, 255, 0.14);
67
- border-left: 3px solid #79b8ff;
68
- border-radius: 16px;
69
- padding: 8px 10px 8px 12px;
70
- box-shadow: none;
71
- transition: 120ms ease;
74
+ color: @pi_foreground;
75
+ background: mix(@pi_background, @pi_foreground, 0.07);
76
+ border: 1px solid alpha(@pi_foreground, 0.1);
77
+ border-left: 3px solid alpha(@pi_foreground, 0.35);
78
+ border-radius: 12px;
79
+ padding: 7px 11px 7px 10px;
80
+ transition:
81
+ background 120ms ease,
82
+ border-color 120ms ease;
72
83
  }
73
84
 
74
85
  .card:hover,
75
86
  .card:focus-visible,
76
87
  .card.open {
77
- background: linear-gradient(to bottom, rgba(18, 28, 47, 0.99), rgba(10, 16, 28, 0.97));
78
- border-color: rgba(121, 184, 255, 0.72);
88
+ background: mix(@pi_background, @pi_foreground, 0.14);
89
+ border-color: alpha(@pi_accent, 0.5);
79
90
  }
80
91
 
92
+ /* The tab the compositor is actually showing, marked by weight rather than another border. */
81
93
  .card.current {
82
- border-left-width: 5px;
83
- border-color: rgba(150, 202, 255, 0.92);
84
- outline: 1px solid rgba(121, 184, 255, 0.38);
85
- outline-offset: -3px;
94
+ background: mix(@pi_background, @pi_foreground, 0.16);
95
+ border-color: alpha(@pi_accent, 0.42);
96
+ box-shadow: inset 0 0 0 1px alpha(@pi_accent, 0.22);
97
+ }
98
+
99
+ .card.hidden-tab {
100
+ background: alpha(@pi_background, 0.55);
101
+ border-style: dashed;
102
+ }
103
+
104
+ .card.hidden-tab .card-repo {
105
+ color: alpha(@pi_foreground, 0.78);
86
106
  }
87
107
 
88
108
  .card.stalled {
89
- opacity: 0.72;
109
+ opacity: 0.62;
90
110
  }
91
111
 
92
112
  .card.stalled .card-phase {
@@ -94,83 +114,150 @@ window {
94
114
  }
95
115
 
96
116
  .card.state-thinking {
97
- border-left-color: #8ab4ff;
117
+ border-left-color: @pi_thinking;
98
118
  }
99
119
  .card.state-tool {
100
- border-left-color: #ffd166;
120
+ border-left-color: @pi_tool;
101
121
  }
102
122
  .card.state-waiting {
103
- border-left-color: #ff9f7a;
123
+ border-left-color: @pi_waiting;
104
124
  }
105
125
  .card.state-success {
106
- border-left-color: #57d9a3;
126
+ border-left-color: @pi_success;
107
127
  }
108
128
  .card.state-error {
109
- border-left-color: #ff7d7d;
129
+ border-left-color: @pi_error;
110
130
  }
111
131
 
112
132
  .card-repo {
113
- color: rgba(247, 250, 255, 0.96);
133
+ color: @pi_foreground;
114
134
  font-size: 12px;
115
135
  font-weight: 650;
116
136
  }
117
137
 
118
138
  .card-phase,
119
139
  .inspector-value {
120
- color: rgba(194, 205, 224, 0.76);
140
+ color: alpha(@pi_foreground, 0.74);
121
141
  font-size: 10px;
122
142
  }
123
143
 
144
+ /* A quiet pill: the state is carried mostly by the card's left edge. */
124
145
  .card-state {
125
- border: 1px solid rgba(121, 184, 255, 0.36);
146
+ background: alpha(@pi_foreground, 0.07);
147
+ border: 1px solid alpha(@pi_foreground, 0.16);
126
148
  border-radius: 999px;
127
- color: #79b8ff;
149
+ color: alpha(@pi_foreground, 0.68);
128
150
  font-size: 8px;
129
- padding: 4px 6px;
151
+ letter-spacing: 0.4px;
152
+ padding: 3px 7px;
130
153
  }
131
154
 
155
+ .state-thinking .card-state {
156
+ background: alpha(@pi_thinking, 0.14);
157
+ border-color: alpha(@pi_thinking, 0.38);
158
+ color: @pi_thinking;
159
+ }
132
160
  .state-tool .card-state {
133
- color: #ffd166;
134
- border-color: rgba(255, 209, 102, 0.42);
161
+ background: alpha(@pi_tool, 0.14);
162
+ border-color: alpha(@pi_tool, 0.38);
163
+ color: @pi_tool;
135
164
  }
136
165
  .state-waiting .card-state {
137
- color: #ff9f7a;
138
- border-color: rgba(255, 159, 122, 0.42);
166
+ background: alpha(@pi_waiting, 0.16);
167
+ border-color: alpha(@pi_waiting, 0.42);
168
+ color: @pi_waiting;
139
169
  }
140
170
  .state-success .card-state {
141
- color: #57d9a3;
142
- border-color: rgba(87, 217, 163, 0.42);
171
+ background: alpha(@pi_success, 0.14);
172
+ border-color: alpha(@pi_success, 0.38);
173
+ color: @pi_success;
143
174
  }
144
175
  .state-error .card-state {
145
- color: #ff7d7d;
146
- border-color: rgba(255, 125, 125, 0.42);
176
+ background: alpha(@pi_error, 0.16);
177
+ border-color: alpha(@pi_error, 0.44);
178
+ color: @pi_error;
147
179
  }
148
180
 
149
181
  .card-tool,
150
182
  .card-elapsed,
151
183
  .inspector-key {
152
- color: rgba(167, 180, 203, 0.58);
184
+ color: alpha(@pi_foreground, 0.55);
185
+ font-size: 8px;
186
+ }
187
+
188
+ /*
189
+ * The Niri window id, for finding a window an agent or tool names by id. A tint rather than a
190
+ * border and no vertical padding, so it reads as a chip without growing the footer line.
191
+ */
192
+ .card-window {
193
+ background: alpha(@pi_foreground, 0.09);
194
+ border-radius: 999px;
195
+ color: alpha(@pi_foreground, 0.7);
153
196
  font-size: 8px;
197
+ padding: 0 5px;
154
198
  }
155
199
 
156
200
  .inspector {
157
- border-top: 1px solid rgba(255, 255, 255, 0.08);
201
+ border-top: 1px solid alpha(@pi_foreground, 0.12);
158
202
  padding-top: 9px;
159
203
  }
160
204
 
161
205
  .inspector-value {
162
- line-height: 1.25;
206
+ line-height: 1.3;
163
207
  }
164
208
 
165
209
  .activation {
166
- color: rgba(167, 180, 203, 0.72);
210
+ color: alpha(@pi_foreground, 0.7);
167
211
  font-size: 9px;
168
212
  }
169
213
 
170
214
  .activation.success {
171
- color: #57d9a3;
215
+ color: @pi_success;
172
216
  }
173
217
 
174
218
  .activation.error {
175
- color: #ff7d7d;
219
+ color: @pi_error;
220
+ }
221
+
222
+ /*
223
+ * AK task references. An active chip is the one clickable element inside a card besides the card
224
+ * itself: it reuses the card's activation, so it carries the accent colour and a pointer cursor.
225
+ * Badge states (a claim that outlives its session, a deferred task) are deliberately inert pills.
226
+ */
227
+ .ak-task {
228
+ background: alpha(@pi_foreground, 0.07);
229
+ border: 1px solid alpha(@pi_foreground, 0.16);
230
+ border-radius: 999px;
231
+ color: alpha(@pi_foreground, 0.68);
232
+ font-family: "IBM Plex Mono", "JetBrains Mono", monospace;
233
+ font-size: 8px;
234
+ padding: 3px 8px;
235
+ }
236
+
237
+ .ak-task.ak-active {
238
+ background: alpha(@pi_accent, 0.14);
239
+ border-color: alpha(@pi_accent, 0.42);
240
+ color: @pi_accent;
241
+ }
242
+
243
+ .ak-task.ak-active:hover {
244
+ background: alpha(@pi_accent, 0.26);
245
+ }
246
+
247
+ .ak-task.ak-orphaned {
248
+ background: alpha(@pi_error, 0.12);
249
+ border: 1px dashed alpha(@pi_error, 0.4);
250
+ color: alpha(@pi_error, 0.92);
251
+ }
252
+
253
+ .ak-task.ak-deferred {
254
+ background: alpha(@pi_tool, 0.12);
255
+ border: 1px dashed alpha(@pi_tool, 0.4);
256
+ color: alpha(@pi_tool, 0.92);
257
+ }
258
+
259
+ .ak-overflow {
260
+ color: alpha(@pi_foreground, 0.5);
261
+ font-family: "IBM Plex Mono", "JetBrains Mono", monospace;
262
+ font-size: 8px;
176
263
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tryinget/pi-activity-strip",
3
- "version": "0.5.0",
3
+ "version": "0.7.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",
@@ -88,9 +88,9 @@
88
88
  },
89
89
  "devDependencies": {
90
90
  "@biomejs/biome": "2.4.4",
91
- "@earendil-works/pi-agent-core": "0.84.3",
92
- "@earendil-works/pi-ai": "0.84.3",
93
- "@earendil-works/pi-coding-agent": "0.84.3",
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",
94
94
  "@types/node": "22.14.0",
95
95
  "@typescript/native-preview": "7.0.0-dev.20260417.1",
96
96
  "typebox": "1.3.7",