@tryinget/pi-activity-strip 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -157
- package/bin/pi-activity-strip-claude-hook.mjs +71 -0
- package/bin/pi-activity-strip.mjs +50 -60
- package/native/bin/linux-x64-gnu/artifact.json +21 -0
- package/native/bin/linux-x64-gnu/pi-activity-strip-panel +0 -0
- package/native/panel/Cargo.lock +1045 -0
- package/native/panel/Cargo.toml +23 -0
- package/native/panel/src/app.rs +606 -0
- package/native/panel/src/card_view.rs +602 -0
- package/native/panel/src/main.rs +31 -0
- package/native/panel/src/protocol.rs +297 -0
- package/native/panel/src/runtime.rs +151 -0
- package/native/panel/src/style.css +250 -0
- package/package.json +16 -4
- package/src/broker/server.mjs +43 -7
- package/src/broker/session-store.mjs +41 -13
- package/src/client/broker-client.mjs +10 -3
- package/src/client/session-telemetry.mjs +54 -14
- package/src/common/activity-order.mjs +15 -9
- package/src/common/agent-identity.mjs +163 -0
- package/src/common/ak-tasks.mjs +354 -0
- package/src/common/alignment-controller.mjs +24 -42
- package/src/common/card-display.mjs +5 -0
- package/src/common/claude-events.mjs +190 -0
- package/src/common/claude-hook-config.mjs +56 -0
- package/src/common/claude-transcript.mjs +163 -0
- package/src/common/codex-transcript.mjs +153 -0
- package/src/common/compatibility.mjs +28 -13
- package/src/common/constants.mjs +10 -28
- package/src/common/contracts.d.ts +68 -4
- package/src/common/contracts.ts +105 -4
- package/src/common/ghostty-present.mjs +100 -0
- package/src/common/ghostty-theme.mjs +149 -0
- package/src/common/niri-focus.mjs +209 -140
- package/src/common/protocol.mjs +48 -16
- package/src/common/session-cards.mjs +105 -0
- package/src/common/status-report.mjs +53 -1
- package/src/common/surface-bindings.mjs +234 -0
- package/src/common/telemetry.mjs +8 -1
- package/src/common/terminal-identity.mjs +212 -0
- package/src/common/window-placement.mjs +59 -0
- package/src/common/workspace-view.mjs +161 -0
- package/src/native/agent-discovery.mjs +294 -0
- package/src/native/ak-runtime.mjs +164 -0
- package/src/native/codex-discovery.mjs +128 -0
- package/src/native/ghostty-tab-inventory.py +99 -0
- package/src/native/height-repair.mjs +154 -0
- package/src/native/main.mjs +499 -0
- package/src/native/panel-projection.mjs +161 -0
- package/src/native/placement.mjs +225 -0
- package/src/native/tab-inventory.mjs +188 -0
- package/src/native/theme-runtime.mjs +72 -0
- package/src/native/theme-source.mjs +197 -0
- package/src/{electron/niri-workspace-events.mjs → native/workspace-events.mjs} +20 -2
- package/src/common/electron.mjs +0 -57
- package/src/electron/main.mjs +0 -499
- package/src/electron/niri-native-window-runtime.mjs +0 -178
- package/src/electron/preload.cjs +0 -50
- package/src/electron/renderer-visibility-runtime.mjs +0 -107
- package/src/electron/workspace-view-runtime.mjs +0 -360
- package/src/ui/strip-html.mjs +0 -500
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
use serde::Deserialize;
|
|
2
|
+
use serde_json::{Value, json};
|
|
3
|
+
use std::io::{self, Write};
|
|
4
|
+
use std::sync::{Mutex, OnceLock};
|
|
5
|
+
|
|
6
|
+
#[derive(Clone, Debug, Deserialize)]
|
|
7
|
+
#[serde(rename_all = "camelCase")]
|
|
8
|
+
pub struct AkTask {
|
|
9
|
+
#[serde(default)]
|
|
10
|
+
pub id: i64,
|
|
11
|
+
#[serde(default)]
|
|
12
|
+
pub title: String,
|
|
13
|
+
#[serde(default)]
|
|
14
|
+
pub state: String,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
impl AkTask {
|
|
18
|
+
/// Only an `active` chip is a button: the card's own session holds a live claim, so clicking
|
|
19
|
+
/// it focuses that session's Ghostty window. Every other state is a badge and never fires.
|
|
20
|
+
pub fn is_active(&self) -> bool {
|
|
21
|
+
self.state == "active"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#[derive(Clone, Debug, Deserialize)]
|
|
26
|
+
#[serde(rename_all = "camelCase")]
|
|
27
|
+
pub struct Card {
|
|
28
|
+
#[serde(default)]
|
|
29
|
+
pub card_id: String,
|
|
30
|
+
#[serde(default)]
|
|
31
|
+
pub session_id: String,
|
|
32
|
+
#[serde(default)]
|
|
33
|
+
pub repo_label: String,
|
|
34
|
+
#[serde(default)]
|
|
35
|
+
pub phase: String,
|
|
36
|
+
#[serde(default)]
|
|
37
|
+
pub state: String,
|
|
38
|
+
#[serde(default)]
|
|
39
|
+
pub tool_name: String,
|
|
40
|
+
#[serde(default)]
|
|
41
|
+
pub tool_target: String,
|
|
42
|
+
#[serde(default)]
|
|
43
|
+
pub detail: String,
|
|
44
|
+
#[serde(default)]
|
|
45
|
+
pub last_prompt_preview: String,
|
|
46
|
+
#[serde(default)]
|
|
47
|
+
pub assistant_preview: String,
|
|
48
|
+
#[serde(default)]
|
|
49
|
+
pub cwd: String,
|
|
50
|
+
#[serde(default)]
|
|
51
|
+
pub started_at: i64,
|
|
52
|
+
#[serde(default)]
|
|
53
|
+
pub agent_started_at: Option<i64>,
|
|
54
|
+
#[serde(default)]
|
|
55
|
+
pub last_event_at: i64,
|
|
56
|
+
#[serde(default)]
|
|
57
|
+
pub updated_at: i64,
|
|
58
|
+
#[serde(default)]
|
|
59
|
+
pub agent_active: bool,
|
|
60
|
+
#[serde(default, rename = "processId")]
|
|
61
|
+
pub pid: i64,
|
|
62
|
+
/// Set for tabs running an agent other than Pi, which publish no telemetry of their own.
|
|
63
|
+
#[serde(default)]
|
|
64
|
+
pub agent_label: String,
|
|
65
|
+
/// Read-only AK task references joined onto this card by the controller. `active` chips are
|
|
66
|
+
/// clickable and focus this card's terminal; the rest are inert badges.
|
|
67
|
+
#[serde(default)]
|
|
68
|
+
pub ak_tasks: Vec<AkTask>,
|
|
69
|
+
/// How many further AK references exist beyond the rendered chips.
|
|
70
|
+
#[serde(default)]
|
|
71
|
+
pub ak_task_overflow: u32,
|
|
72
|
+
/// `Some(false)` marks a tab hidden behind another tab of its Ghostty window; the controller
|
|
73
|
+
/// placed it through its host process, so activation presents the tab before focusing.
|
|
74
|
+
#[serde(default)]
|
|
75
|
+
pub surface_visible: Option<bool>,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
impl Card {
|
|
79
|
+
pub fn id(&self) -> &str {
|
|
80
|
+
if self.card_id.is_empty() {
|
|
81
|
+
&self.session_id
|
|
82
|
+
} else {
|
|
83
|
+
&self.card_id
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
pub fn hidden_tab(&self) -> bool {
|
|
88
|
+
self.surface_visible == Some(false)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
pub fn active(&self) -> bool {
|
|
92
|
+
self.agent_active || matches!(self.state.as_str(), "thinking" | "tool" | "waiting")
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
pub fn monitoring(&self) -> bool {
|
|
96
|
+
self.state == "success" && self.tool_name.is_empty() && self.tool_target.is_empty()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
pub fn stalled(&self, now_ms: i64) -> bool {
|
|
100
|
+
let real_event_at = if self.last_event_at > 0 {
|
|
101
|
+
self.last_event_at
|
|
102
|
+
} else {
|
|
103
|
+
self.updated_at
|
|
104
|
+
};
|
|
105
|
+
self.agent_active
|
|
106
|
+
&& matches!(self.state.as_str(), "thinking" | "tool" | "waiting")
|
|
107
|
+
&& real_event_at > 0
|
|
108
|
+
&& now_ms.saturating_sub(real_event_at) > 900_000
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#[derive(Clone, Debug, Deserialize)]
|
|
113
|
+
#[serde(rename_all = "camelCase")]
|
|
114
|
+
pub struct ViewMessage {
|
|
115
|
+
#[serde(default = "protocol_version")]
|
|
116
|
+
pub protocol: u8,
|
|
117
|
+
#[serde(rename = "type")]
|
|
118
|
+
pub message_type: String,
|
|
119
|
+
pub revision: u64,
|
|
120
|
+
#[serde(default)]
|
|
121
|
+
pub visible: bool,
|
|
122
|
+
#[serde(default)]
|
|
123
|
+
pub focused_card_id: Option<String>,
|
|
124
|
+
#[serde(default)]
|
|
125
|
+
pub sessions: Vec<Card>,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fn protocol_version() -> u8 {
|
|
129
|
+
1
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
static OUTPUT: OnceLock<Mutex<io::Stdout>> = OnceLock::new();
|
|
133
|
+
|
|
134
|
+
pub fn emit(event: Value) {
|
|
135
|
+
let stdout = OUTPUT.get_or_init(|| Mutex::new(io::stdout()));
|
|
136
|
+
if let Ok(mut output) = stdout.lock() {
|
|
137
|
+
let _ = serde_json::to_writer(&mut *output, &event);
|
|
138
|
+
let _ = output.write_all(b"\n");
|
|
139
|
+
let _ = output.flush();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
pub fn emit_ready() {
|
|
144
|
+
emit(json!({
|
|
145
|
+
"protocol": 1,
|
|
146
|
+
"type": "ready",
|
|
147
|
+
"pid": std::process::id(),
|
|
148
|
+
"namespace": "pi-activity-strip"
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
pub fn emit_error(message: impl Into<String>) {
|
|
153
|
+
emit(json!({ "protocol": 1, "type": "error", "message": message.into() }));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
pub fn demo_view() -> ViewMessage {
|
|
157
|
+
let now = std::time::SystemTime::now()
|
|
158
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
159
|
+
.unwrap_or_default()
|
|
160
|
+
.as_millis() as i64;
|
|
161
|
+
ViewMessage {
|
|
162
|
+
protocol: 1,
|
|
163
|
+
message_type: "view".into(),
|
|
164
|
+
revision: 1,
|
|
165
|
+
visible: true,
|
|
166
|
+
focused_card_id: Some("demo-tool".into()),
|
|
167
|
+
sessions: vec![
|
|
168
|
+
Card {
|
|
169
|
+
card_id: "demo-monitor".into(),
|
|
170
|
+
session_id: "demo-monitor".into(),
|
|
171
|
+
repo_label: "activity-strip".into(),
|
|
172
|
+
phase: "Monitoring".into(),
|
|
173
|
+
state: "success".into(),
|
|
174
|
+
tool_name: String::new(),
|
|
175
|
+
detail: "Native layer-shell panel online".into(),
|
|
176
|
+
cwd: "/home/tryinget/ai-society/softwareco/owned/pi-extensions".into(),
|
|
177
|
+
started_at: now - 135_000,
|
|
178
|
+
last_event_at: now - 4_000,
|
|
179
|
+
updated_at: now,
|
|
180
|
+
..empty_card()
|
|
181
|
+
},
|
|
182
|
+
Card {
|
|
183
|
+
card_id: "demo-tool".into(),
|
|
184
|
+
session_id: "demo-tool".into(),
|
|
185
|
+
repo_label: "native-dogfood".into(),
|
|
186
|
+
phase: "Rendering GTK cards".into(),
|
|
187
|
+
state: "tool".into(),
|
|
188
|
+
tool_name: "gtk4-layer-shell".into(),
|
|
189
|
+
tool_target: "top surface".into(),
|
|
190
|
+
detail: "Exclusive zone remains 84px while details expand".into(),
|
|
191
|
+
last_prompt_preview: "Replace Electron without losing the ribbon UX".into(),
|
|
192
|
+
assistant_preview: "Native panel fixture is active.".into(),
|
|
193
|
+
cwd: "/home/tryinget/ai-society/softwareco/owned/pi-extensions/packages/pi-activity-strip".into(),
|
|
194
|
+
started_at: now - 42_000,
|
|
195
|
+
agent_started_at: Some(now - 42_000),
|
|
196
|
+
last_event_at: now - 1_000,
|
|
197
|
+
updated_at: now,
|
|
198
|
+
agent_active: true,
|
|
199
|
+
ak_tasks: vec![AkTask {
|
|
200
|
+
id: 5701,
|
|
201
|
+
title: "Show clickable AK-task references on the activity ribbon".into(),
|
|
202
|
+
state: "active".into(),
|
|
203
|
+
}],
|
|
204
|
+
ak_task_overflow: 1,
|
|
205
|
+
..empty_card()
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
fn empty_card() -> Card {
|
|
212
|
+
Card {
|
|
213
|
+
card_id: String::new(),
|
|
214
|
+
session_id: String::new(),
|
|
215
|
+
repo_label: String::new(),
|
|
216
|
+
phase: String::new(),
|
|
217
|
+
state: String::new(),
|
|
218
|
+
tool_name: String::new(),
|
|
219
|
+
tool_target: String::new(),
|
|
220
|
+
detail: String::new(),
|
|
221
|
+
last_prompt_preview: String::new(),
|
|
222
|
+
assistant_preview: String::new(),
|
|
223
|
+
cwd: String::new(),
|
|
224
|
+
started_at: 0,
|
|
225
|
+
agent_started_at: None,
|
|
226
|
+
last_event_at: 0,
|
|
227
|
+
updated_at: 0,
|
|
228
|
+
agent_active: false,
|
|
229
|
+
pid: 0,
|
|
230
|
+
agent_label: String::new(),
|
|
231
|
+
ak_tasks: Vec::new(),
|
|
232
|
+
ak_task_overflow: 0,
|
|
233
|
+
surface_visible: None,
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
#[cfg(test)]
|
|
238
|
+
mod tests {
|
|
239
|
+
use super::*;
|
|
240
|
+
|
|
241
|
+
#[test]
|
|
242
|
+
fn view_messages_accept_current_node_projection_shape() {
|
|
243
|
+
let view: ViewMessage = serde_json::from_str(
|
|
244
|
+
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}]}"#,
|
|
245
|
+
)
|
|
246
|
+
.expect("view should parse");
|
|
247
|
+
assert_eq!(view.revision, 7);
|
|
248
|
+
assert_eq!(view.sessions[0].id(), "card-a");
|
|
249
|
+
assert!(view.sessions[0].active());
|
|
250
|
+
assert_eq!(view.sessions[0].agent_started_at, None);
|
|
251
|
+
assert_eq!(view.sessions[0].pid, 4242);
|
|
252
|
+
assert!(view.sessions[0].ak_tasks.is_empty());
|
|
253
|
+
assert_eq!(view.sessions[0].ak_task_overflow, 0);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
#[test]
|
|
257
|
+
fn ak_task_chips_parse_with_states() {
|
|
258
|
+
let view: ViewMessage = serde_json::from_str(
|
|
259
|
+
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}]}"#,
|
|
260
|
+
)
|
|
261
|
+
.expect("view should parse");
|
|
262
|
+
let card = &view.sessions[0];
|
|
263
|
+
assert_eq!(card.ak_tasks.len(), 3);
|
|
264
|
+
assert!(card.ak_tasks[0].is_active());
|
|
265
|
+
assert!(!card.ak_tasks[1].is_active());
|
|
266
|
+
assert!(!card.ak_tasks[2].is_active());
|
|
267
|
+
assert_eq!(card.ak_task_overflow, 3);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
#[test]
|
|
271
|
+
fn monitoring_cards_are_distinct_from_active_work() {
|
|
272
|
+
let mut card = empty_card();
|
|
273
|
+
card.state = "success".into();
|
|
274
|
+
assert!(card.monitoring());
|
|
275
|
+
card.tool_target = "old target".into();
|
|
276
|
+
assert!(!card.monitoring());
|
|
277
|
+
card.tool_target.clear();
|
|
278
|
+
assert!(!card.active());
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
#[test]
|
|
282
|
+
fn stalled_requires_agent_active_and_uses_real_event_time() {
|
|
283
|
+
let mut card = empty_card();
|
|
284
|
+
card.state = "tool".into();
|
|
285
|
+
card.last_event_at = 1_000;
|
|
286
|
+
card.updated_at = 1_000_000;
|
|
287
|
+
assert!(!card.stalled(1_000_001));
|
|
288
|
+
card.agent_active = true;
|
|
289
|
+
assert!(card.stalled(1_000_001));
|
|
290
|
+
card.state = "success".into();
|
|
291
|
+
assert!(!card.stalled(1_000_001));
|
|
292
|
+
card.state = "tool".into();
|
|
293
|
+
card.last_event_at = 0;
|
|
294
|
+
card.updated_at = 0;
|
|
295
|
+
assert!(!card.stalled(1_000_001));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
use crate::app::AppMsg;
|
|
2
|
+
use crate::protocol::{Card, ViewMessage};
|
|
3
|
+
use relm4::gtk;
|
|
4
|
+
use std::collections::{HashMap, HashSet};
|
|
5
|
+
use std::io::{BufRead, BufReader};
|
|
6
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
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.
|
|
26
|
+
#[allow(deprecated)]
|
|
27
|
+
pub fn install_css() {
|
|
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));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
pub fn start_input_reader(sender: relm4::Sender<AppMsg>) {
|
|
49
|
+
std::thread::spawn(move || {
|
|
50
|
+
for line in BufReader::new(std::io::stdin()).lines() {
|
|
51
|
+
match line {
|
|
52
|
+
Ok(line) if !line.trim().is_empty() => consume_line(&sender, &line),
|
|
53
|
+
Ok(_) => {}
|
|
54
|
+
Err(error) => {
|
|
55
|
+
let _ = sender.send(AppMsg::InputError(format!("panel input failed: {error}")));
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
let _ = sender.send(AppMsg::ParentGone);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fn consume_line(sender: &relm4::Sender<AppMsg>, line: &str) {
|
|
65
|
+
let value = match serde_json::from_str::<serde_json::Value>(line) {
|
|
66
|
+
Ok(value) => value,
|
|
67
|
+
Err(error) => {
|
|
68
|
+
let _ = sender.send(AppMsg::InputError(format!(
|
|
69
|
+
"invalid panel message: {error}"
|
|
70
|
+
)));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
match value.get("type").and_then(|kind| kind.as_str()) {
|
|
75
|
+
Some("view") => match serde_json::from_value::<ViewMessage>(value) {
|
|
76
|
+
Ok(view) => {
|
|
77
|
+
let _ = sender.send(AppMsg::View(view));
|
|
78
|
+
}
|
|
79
|
+
Err(error) => {
|
|
80
|
+
let _ = sender.send(AppMsg::InputError(format!("invalid view message: {error}")));
|
|
81
|
+
}
|
|
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
|
+
}
|
|
97
|
+
Some("focus-strip") => {
|
|
98
|
+
let _ = sender.send(AppMsg::FocusStrip);
|
|
99
|
+
}
|
|
100
|
+
Some("collapse") => {
|
|
101
|
+
let _ = sender.send(AppMsg::Collapse);
|
|
102
|
+
}
|
|
103
|
+
Some("activation-result") => {
|
|
104
|
+
let card_id = value
|
|
105
|
+
.get("cardId")
|
|
106
|
+
.and_then(|item| item.as_str())
|
|
107
|
+
.unwrap_or_default()
|
|
108
|
+
.to_owned();
|
|
109
|
+
let ok = value
|
|
110
|
+
.get("ok")
|
|
111
|
+
.and_then(|item| item.as_bool())
|
|
112
|
+
.unwrap_or(false);
|
|
113
|
+
let message = value
|
|
114
|
+
.get("message")
|
|
115
|
+
.and_then(|item| item.as_str())
|
|
116
|
+
.unwrap_or(if ok {
|
|
117
|
+
"Focused Ghostty window."
|
|
118
|
+
} else {
|
|
119
|
+
"Focus failed; nothing moved."
|
|
120
|
+
})
|
|
121
|
+
.to_owned();
|
|
122
|
+
let _ = sender.send(AppMsg::ActivationResult(card_id, ok, message));
|
|
123
|
+
}
|
|
124
|
+
Some(kind) => {
|
|
125
|
+
let _ = sender.send(AppMsg::InputError(format!(
|
|
126
|
+
"unsupported panel message: {kind}"
|
|
127
|
+
)));
|
|
128
|
+
}
|
|
129
|
+
None => {
|
|
130
|
+
let _ = sender.send(AppMsg::InputError("panel message type is required".into()));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
pub fn duplicate_labels<'a>(cards: impl Iterator<Item = &'a Card>) -> HashSet<String> {
|
|
136
|
+
let mut counts = HashMap::new();
|
|
137
|
+
for card in cards {
|
|
138
|
+
*counts.entry(card.repo_label.clone()).or_insert(0usize) += 1;
|
|
139
|
+
}
|
|
140
|
+
counts
|
|
141
|
+
.into_iter()
|
|
142
|
+
.filter_map(|(label, count)| (count > 1).then_some(label))
|
|
143
|
+
.collect()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
pub fn now_ms() -> i64 {
|
|
147
|
+
SystemTime::now()
|
|
148
|
+
.duration_since(UNIX_EPOCH)
|
|
149
|
+
.unwrap_or_default()
|
|
150
|
+
.as_millis() as i64
|
|
151
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
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
|
+
|
|
8
|
+
/* biome-ignore lint/correctness/noUnknownTypeSelector: GTK CSS uses widget node names. */
|
|
9
|
+
window {
|
|
10
|
+
background: transparent;
|
|
11
|
+
color: @pi_foreground;
|
|
12
|
+
font-family: "IBM Plex Sans", "Inter", "Segoe UI", sans-serif;
|
|
13
|
+
}
|
|
14
|
+
|
|
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;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.brand {
|
|
25
|
+
border-right: 1px solid alpha(@pi_foreground, 0.12);
|
|
26
|
+
margin-right: 12px;
|
|
27
|
+
padding: 11px 14px 11px 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.brand-eyebrow,
|
|
31
|
+
.meta,
|
|
32
|
+
.card-state,
|
|
33
|
+
.card-tool,
|
|
34
|
+
.card-elapsed,
|
|
35
|
+
.inspector-key {
|
|
36
|
+
font-family: "IBM Plex Mono", "JetBrains Mono", monospace;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.brand-eyebrow {
|
|
40
|
+
color: alpha(@pi_accent, 0.9);
|
|
41
|
+
font-size: 9px;
|
|
42
|
+
font-weight: 600;
|
|
43
|
+
letter-spacing: 1.4px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.brand-title {
|
|
47
|
+
color: @pi_foreground;
|
|
48
|
+
font-size: 17px;
|
|
49
|
+
font-weight: 650;
|
|
50
|
+
letter-spacing: -0.2px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.meta {
|
|
54
|
+
color: alpha(@pi_foreground, 0.56);
|
|
55
|
+
font-size: 9px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* biome-ignore lint/correctness/noUnknownTypeSelector: GTK CSS uses widget node names. */
|
|
59
|
+
.cards-panel,
|
|
60
|
+
.cards-panel > viewport {
|
|
61
|
+
background: transparent;
|
|
62
|
+
padding: 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* biome-ignore lint/correctness/noUnknownTypeSelector: GTK CSS uses widget node names. */
|
|
66
|
+
.cards-panel scrollbar {
|
|
67
|
+
opacity: 0;
|
|
68
|
+
min-width: 0;
|
|
69
|
+
min-height: 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.card {
|
|
73
|
+
color: @pi_foreground;
|
|
74
|
+
background: mix(@pi_background, @pi_foreground, 0.07);
|
|
75
|
+
border: 1px solid alpha(@pi_foreground, 0.1);
|
|
76
|
+
border-left: 3px solid alpha(@pi_foreground, 0.35);
|
|
77
|
+
border-radius: 12px;
|
|
78
|
+
padding: 7px 11px 7px 10px;
|
|
79
|
+
transition:
|
|
80
|
+
background 120ms ease,
|
|
81
|
+
border-color 120ms ease;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.card:hover,
|
|
85
|
+
.card:focus-visible,
|
|
86
|
+
.card.open {
|
|
87
|
+
background: mix(@pi_background, @pi_foreground, 0.14);
|
|
88
|
+
border-color: alpha(@pi_accent, 0.5);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* The tab the compositor is actually showing, marked by weight rather than another border. */
|
|
92
|
+
.card.current {
|
|
93
|
+
background: mix(@pi_background, @pi_foreground, 0.16);
|
|
94
|
+
border-color: alpha(@pi_accent, 0.42);
|
|
95
|
+
box-shadow: inset 0 0 0 1px alpha(@pi_accent, 0.22);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.card.hidden-tab {
|
|
99
|
+
background: alpha(@pi_background, 0.55);
|
|
100
|
+
border-style: dashed;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.card.hidden-tab .card-repo {
|
|
104
|
+
color: alpha(@pi_foreground, 0.78);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.card.stalled {
|
|
108
|
+
opacity: 0.62;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.card.stalled .card-phase {
|
|
112
|
+
text-decoration-line: line-through;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.card.state-thinking {
|
|
116
|
+
border-left-color: @pi_thinking;
|
|
117
|
+
}
|
|
118
|
+
.card.state-tool {
|
|
119
|
+
border-left-color: @pi_tool;
|
|
120
|
+
}
|
|
121
|
+
.card.state-waiting {
|
|
122
|
+
border-left-color: @pi_waiting;
|
|
123
|
+
}
|
|
124
|
+
.card.state-success {
|
|
125
|
+
border-left-color: @pi_success;
|
|
126
|
+
}
|
|
127
|
+
.card.state-error {
|
|
128
|
+
border-left-color: @pi_error;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.card-repo {
|
|
132
|
+
color: @pi_foreground;
|
|
133
|
+
font-size: 12px;
|
|
134
|
+
font-weight: 650;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.card-phase,
|
|
138
|
+
.inspector-value {
|
|
139
|
+
color: alpha(@pi_foreground, 0.74);
|
|
140
|
+
font-size: 10px;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* A quiet pill: the state is carried mostly by the card's left edge. */
|
|
144
|
+
.card-state {
|
|
145
|
+
background: alpha(@pi_foreground, 0.07);
|
|
146
|
+
border: 1px solid alpha(@pi_foreground, 0.16);
|
|
147
|
+
border-radius: 999px;
|
|
148
|
+
color: alpha(@pi_foreground, 0.68);
|
|
149
|
+
font-size: 8px;
|
|
150
|
+
letter-spacing: 0.4px;
|
|
151
|
+
padding: 3px 7px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.state-thinking .card-state {
|
|
155
|
+
background: alpha(@pi_thinking, 0.14);
|
|
156
|
+
border-color: alpha(@pi_thinking, 0.38);
|
|
157
|
+
color: @pi_thinking;
|
|
158
|
+
}
|
|
159
|
+
.state-tool .card-state {
|
|
160
|
+
background: alpha(@pi_tool, 0.14);
|
|
161
|
+
border-color: alpha(@pi_tool, 0.38);
|
|
162
|
+
color: @pi_tool;
|
|
163
|
+
}
|
|
164
|
+
.state-waiting .card-state {
|
|
165
|
+
background: alpha(@pi_waiting, 0.16);
|
|
166
|
+
border-color: alpha(@pi_waiting, 0.42);
|
|
167
|
+
color: @pi_waiting;
|
|
168
|
+
}
|
|
169
|
+
.state-success .card-state {
|
|
170
|
+
background: alpha(@pi_success, 0.14);
|
|
171
|
+
border-color: alpha(@pi_success, 0.38);
|
|
172
|
+
color: @pi_success;
|
|
173
|
+
}
|
|
174
|
+
.state-error .card-state {
|
|
175
|
+
background: alpha(@pi_error, 0.16);
|
|
176
|
+
border-color: alpha(@pi_error, 0.44);
|
|
177
|
+
color: @pi_error;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.card-tool,
|
|
181
|
+
.card-elapsed,
|
|
182
|
+
.inspector-key {
|
|
183
|
+
color: alpha(@pi_foreground, 0.55);
|
|
184
|
+
font-size: 8px;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.inspector {
|
|
188
|
+
border-top: 1px solid alpha(@pi_foreground, 0.12);
|
|
189
|
+
padding-top: 9px;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.inspector-value {
|
|
193
|
+
line-height: 1.3;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.activation {
|
|
197
|
+
color: alpha(@pi_foreground, 0.7);
|
|
198
|
+
font-size: 9px;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.activation.success {
|
|
202
|
+
color: @pi_success;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.activation.error {
|
|
206
|
+
color: @pi_error;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/*
|
|
210
|
+
* AK task references. An active chip is the one clickable element inside a card besides the card
|
|
211
|
+
* itself: it reuses the card's activation, so it carries the accent colour and a pointer cursor.
|
|
212
|
+
* Badge states (a claim that outlives its session, a deferred task) are deliberately inert pills.
|
|
213
|
+
*/
|
|
214
|
+
.ak-task {
|
|
215
|
+
background: alpha(@pi_foreground, 0.07);
|
|
216
|
+
border: 1px solid alpha(@pi_foreground, 0.16);
|
|
217
|
+
border-radius: 999px;
|
|
218
|
+
color: alpha(@pi_foreground, 0.68);
|
|
219
|
+
font-family: "IBM Plex Mono", "JetBrains Mono", monospace;
|
|
220
|
+
font-size: 8px;
|
|
221
|
+
padding: 3px 8px;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.ak-task.ak-active {
|
|
225
|
+
background: alpha(@pi_accent, 0.14);
|
|
226
|
+
border-color: alpha(@pi_accent, 0.42);
|
|
227
|
+
color: @pi_accent;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.ak-task.ak-active:hover {
|
|
231
|
+
background: alpha(@pi_accent, 0.26);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.ak-task.ak-orphaned {
|
|
235
|
+
background: alpha(@pi_error, 0.12);
|
|
236
|
+
border: 1px dashed alpha(@pi_error, 0.4);
|
|
237
|
+
color: alpha(@pi_error, 0.92);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.ak-task.ak-deferred {
|
|
241
|
+
background: alpha(@pi_tool, 0.12);
|
|
242
|
+
border: 1px dashed alpha(@pi_tool, 0.4);
|
|
243
|
+
color: alpha(@pi_tool, 0.92);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.ak-overflow {
|
|
247
|
+
color: alpha(@pi_foreground, 0.5);
|
|
248
|
+
font-family: "IBM Plex Mono", "JetBrains Mono", monospace;
|
|
249
|
+
font-size: 8px;
|
|
250
|
+
}
|