@tryinget/pi-activity-strip 0.5.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 +41 -6
- package/bin/pi-activity-strip-claude-hook.mjs +71 -0
- package/bin/pi-activity-strip.mjs +22 -1
- package/native/bin/linux-x64-gnu/artifact.json +2 -2
- package/native/bin/linux-x64-gnu/pi-activity-strip-panel +0 -0
- package/native/panel/src/app.rs +20 -20
- package/native/panel/src/card_view.rs +308 -9
- package/native/panel/src/protocol.rs +63 -0
- package/native/panel/src/runtime.rs +50 -9
- package/native/panel/src/style.css +134 -60
- package/package.json +4 -4
- package/src/common/agent-identity.mjs +163 -0
- package/src/common/ak-tasks.mjs +354 -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 +13 -0
- package/src/common/contracts.d.ts +26 -0
- package/src/common/contracts.ts +64 -1
- package/src/common/ghostty-present.mjs +100 -0
- package/src/common/ghostty-theme.mjs +149 -0
- package/src/common/niri-focus.mjs +179 -110
- package/src/common/session-cards.mjs +5 -2
- package/src/common/status-report.mjs +51 -0
- package/src/common/surface-bindings.mjs +234 -0
- package/src/common/terminal-identity.mjs +75 -12
- 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 +120 -14
- package/src/native/panel-projection.mjs +67 -5
- 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/native/workspace-events.mjs +16 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// ---
|
|
2
|
+
// summary: "derives live Claude Code session state from the tail of its transcript"
|
|
3
|
+
// read_when:
|
|
4
|
+
// - "changing Claude Code card telemetry or the transcript records it depends on"
|
|
5
|
+
// ---
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Claude Code appends newline-delimited records to a per-session transcript. The format is internal
|
|
9
|
+
* to Claude Code and carries no stability guarantee, so every field is optional here and a record
|
|
10
|
+
* that does not parse is skipped. When nothing usable is found the caller keeps the process-only
|
|
11
|
+
* card rather than inventing activity.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const PREVIEW_LIMIT = 100;
|
|
15
|
+
const DETAIL_LIMIT = 100;
|
|
16
|
+
|
|
17
|
+
/** @typedef {"idle" | "thinking" | "tool"} ClaudeState */
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {{
|
|
20
|
+
* title: string; lastPrompt: string; assistantPreview: string; state: ClaudeState;
|
|
21
|
+
* toolName: string; toolTarget: string; turnIndex: number; lastEventAt: number;
|
|
22
|
+
* mode: string; permissionMode: string; sessionId: string; cwd: string; gitBranch: string;
|
|
23
|
+
* }} ClaudeTelemetry
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** @param {unknown} value @param {number} limit */
|
|
27
|
+
function preview(value, limit) {
|
|
28
|
+
const text = String(value ?? "")
|
|
29
|
+
.replace(/\s+/g, " ")
|
|
30
|
+
.trim();
|
|
31
|
+
return text.length > limit ? `${text.slice(0, limit - 1)}…` : text;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** @param {unknown} value */
|
|
35
|
+
function timestampMs(value) {
|
|
36
|
+
const parsed = Date.parse(String(value ?? ""));
|
|
37
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @param {any} record */
|
|
41
|
+
function contentBlocks(record) {
|
|
42
|
+
const content = record?.message?.content;
|
|
43
|
+
return Array.isArray(content) ? content : [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Name the thing a tool call is acting on, preferring the fields that read well on a card.
|
|
48
|
+
* @param {any} block
|
|
49
|
+
*/
|
|
50
|
+
export function describeToolTarget(block) {
|
|
51
|
+
const input = block?.input ?? {};
|
|
52
|
+
for (const key of ["description", "command", "file_path", "path", "pattern", "query", "url"]) {
|
|
53
|
+
const value = input?.[key];
|
|
54
|
+
if (typeof value === "string" && value.trim()) return preview(value, DETAIL_LIMIT);
|
|
55
|
+
}
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** @returns {ClaudeTelemetry} */
|
|
60
|
+
function emptyTelemetry() {
|
|
61
|
+
return {
|
|
62
|
+
title: "",
|
|
63
|
+
lastPrompt: "",
|
|
64
|
+
assistantPreview: "",
|
|
65
|
+
state: "idle",
|
|
66
|
+
toolName: "",
|
|
67
|
+
toolTarget: "",
|
|
68
|
+
turnIndex: 0,
|
|
69
|
+
lastEventAt: 0,
|
|
70
|
+
mode: "",
|
|
71
|
+
permissionMode: "",
|
|
72
|
+
sessionId: "",
|
|
73
|
+
cwd: "",
|
|
74
|
+
gitBranch: "",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Read the tail of a transcript. The first line is usually a fragment and is skipped by the parse
|
|
80
|
+
* failure path. State comes from the newest record that carries a timestamp, because the latched
|
|
81
|
+
* records Claude Code rewrites after every turn have none and would otherwise mask real activity.
|
|
82
|
+
* @param {string} tail
|
|
83
|
+
* @returns {ClaudeTelemetry}
|
|
84
|
+
*/
|
|
85
|
+
export function parseClaudeTranscriptTail(tail) {
|
|
86
|
+
const telemetry = emptyTelemetry();
|
|
87
|
+
if (!tail) return telemetry;
|
|
88
|
+
|
|
89
|
+
/** @type {any[]} */
|
|
90
|
+
const records = [];
|
|
91
|
+
for (const line of String(tail).split("\n")) {
|
|
92
|
+
const trimmed = line.trim();
|
|
93
|
+
if (!trimmed.startsWith("{")) continue;
|
|
94
|
+
try {
|
|
95
|
+
records.push(JSON.parse(trimmed));
|
|
96
|
+
} catch {
|
|
97
|
+
// A truncated first line or a partially written last line is not evidence.
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (records.length === 0) return telemetry;
|
|
101
|
+
|
|
102
|
+
for (const record of records) {
|
|
103
|
+
switch (record?.type) {
|
|
104
|
+
case "ai-title":
|
|
105
|
+
if (typeof record.aiTitle === "string") telemetry.title = record.aiTitle;
|
|
106
|
+
break;
|
|
107
|
+
case "last-prompt":
|
|
108
|
+
telemetry.lastPrompt = preview(record.lastPrompt, PREVIEW_LIMIT);
|
|
109
|
+
break;
|
|
110
|
+
case "mode":
|
|
111
|
+
telemetry.mode = String(record.mode ?? "");
|
|
112
|
+
break;
|
|
113
|
+
case "permission-mode":
|
|
114
|
+
telemetry.permissionMode = String(record.permissionMode ?? "");
|
|
115
|
+
break;
|
|
116
|
+
case "system":
|
|
117
|
+
if (record.subtype === "turn_duration" && Number.isFinite(Number(record.messageCount))) {
|
|
118
|
+
telemetry.turnIndex = Number(record.messageCount);
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
default:
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
if (typeof record?.sessionId === "string") telemetry.sessionId = record.sessionId;
|
|
125
|
+
if (typeof record?.cwd === "string") telemetry.cwd = record.cwd;
|
|
126
|
+
if (typeof record?.gitBranch === "string") telemetry.gitBranch = record.gitBranch;
|
|
127
|
+
if (record?.type === "assistant") {
|
|
128
|
+
const text = contentBlocks(record)
|
|
129
|
+
.filter((block) => block?.type === "text" && typeof block.text === "string")
|
|
130
|
+
.map((block) => block.text)
|
|
131
|
+
.join(" ");
|
|
132
|
+
if (text.trim()) telemetry.assistantPreview = preview(text, PREVIEW_LIMIT);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const timed = records.filter(
|
|
137
|
+
(record) => record?.type !== "attachment" && timestampMs(record?.timestamp) > 0,
|
|
138
|
+
);
|
|
139
|
+
const latest = timed.at(-1);
|
|
140
|
+
if (!latest) return telemetry;
|
|
141
|
+
telemetry.lastEventAt = timestampMs(latest.timestamp);
|
|
142
|
+
|
|
143
|
+
if (latest.type === "system" && latest.subtype === "turn_duration") {
|
|
144
|
+
telemetry.state = "idle";
|
|
145
|
+
return telemetry;
|
|
146
|
+
}
|
|
147
|
+
if (latest.type === "assistant") {
|
|
148
|
+
const toolBlock = contentBlocks(latest).find((block) => block?.type === "tool_use");
|
|
149
|
+
if (toolBlock) {
|
|
150
|
+
telemetry.state = "tool";
|
|
151
|
+
telemetry.toolName = String(toolBlock.name ?? "");
|
|
152
|
+
telemetry.toolTarget = describeToolTarget(toolBlock);
|
|
153
|
+
return telemetry;
|
|
154
|
+
}
|
|
155
|
+
telemetry.state = latest.message?.stop_reason === "end_turn" ? "idle" : "thinking";
|
|
156
|
+
return telemetry;
|
|
157
|
+
}
|
|
158
|
+
if (latest.type === "user") {
|
|
159
|
+
telemetry.state = "thinking";
|
|
160
|
+
return telemetry;
|
|
161
|
+
}
|
|
162
|
+
return telemetry;
|
|
163
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// ---
|
|
2
|
+
// summary: "derives live Codex session state from the tail of its rollout file"
|
|
3
|
+
// read_when:
|
|
4
|
+
// - "changing Codex card telemetry or the rollout records it depends on"
|
|
5
|
+
// ---
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Codex appends newline-delimited records to a per-session rollout. Every record carries a
|
|
9
|
+
* top-level `timestamp`, a `type` of `session_meta`, `turn_context`, `event_msg` or
|
|
10
|
+
* `response_item`, and a `payload` whose own `type` names the event. The format belongs to Codex
|
|
11
|
+
* and carries no stability guarantee, so every field is optional here and an unparseable record is
|
|
12
|
+
* skipped. When nothing usable is found the caller keeps the process-only card.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const PREVIEW_LIMIT = 100;
|
|
16
|
+
const TOOL_ARGUMENT_KEYS = ["cmd", "command", "query", "path", "file_path", "pattern", "workdir"];
|
|
17
|
+
|
|
18
|
+
/** @typedef {import("./claude-transcript.mjs").ClaudeTelemetry} AgentTelemetry */
|
|
19
|
+
|
|
20
|
+
/** @param {unknown} value */
|
|
21
|
+
function preview(value) {
|
|
22
|
+
const text = String(value ?? "")
|
|
23
|
+
.replace(/\s+/g, " ")
|
|
24
|
+
.trim();
|
|
25
|
+
return text.length > PREVIEW_LIMIT ? `${text.slice(0, PREVIEW_LIMIT - 1)}…` : text;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** @param {unknown} value */
|
|
29
|
+
function timestampMs(value) {
|
|
30
|
+
const parsed = Date.parse(String(value ?? ""));
|
|
31
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Name what a Codex tool call is acting on. Arguments arrive as a JSON string, and a call whose
|
|
36
|
+
* arguments do not parse still reports its tool name.
|
|
37
|
+
* @param {any} payload
|
|
38
|
+
*/
|
|
39
|
+
export function describeCodexToolTarget(payload) {
|
|
40
|
+
let args = payload?.arguments;
|
|
41
|
+
if (typeof args === "string") {
|
|
42
|
+
try {
|
|
43
|
+
args = JSON.parse(args);
|
|
44
|
+
} catch {
|
|
45
|
+
return preview(args);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (!args || typeof args !== "object") return "";
|
|
49
|
+
for (const key of TOOL_ARGUMENT_KEYS) {
|
|
50
|
+
const value = args[key];
|
|
51
|
+
if (typeof value === "string" && value.trim()) return preview(value);
|
|
52
|
+
if (Array.isArray(value) && value.length > 0) return preview(value.join(" "));
|
|
53
|
+
}
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** @param {any} payload */
|
|
58
|
+
function toolNameFor(payload) {
|
|
59
|
+
const type = String(payload?.type ?? "");
|
|
60
|
+
if (type === "function_call" || type === "custom_tool_call") {
|
|
61
|
+
return String(payload?.name ?? "") || "tool";
|
|
62
|
+
}
|
|
63
|
+
if (type === "local_shell_call") return "shell";
|
|
64
|
+
if (type === "web_search_call") return "web_search";
|
|
65
|
+
return "";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** @returns {AgentTelemetry} */
|
|
69
|
+
function emptyTelemetry() {
|
|
70
|
+
return {
|
|
71
|
+
title: "",
|
|
72
|
+
lastPrompt: "",
|
|
73
|
+
assistantPreview: "",
|
|
74
|
+
state: "idle",
|
|
75
|
+
toolName: "",
|
|
76
|
+
toolTarget: "",
|
|
77
|
+
turnIndex: 0,
|
|
78
|
+
lastEventAt: 0,
|
|
79
|
+
mode: "",
|
|
80
|
+
permissionMode: "",
|
|
81
|
+
sessionId: "",
|
|
82
|
+
cwd: "",
|
|
83
|
+
gitBranch: "",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Read the tail of a rollout. The first line is usually a fragment and is skipped by the parse
|
|
89
|
+
* failure path. State comes from the newest record that carries a timestamp, so a tool call is
|
|
90
|
+
* reported as running only while it is still the newest record; its output lands after it.
|
|
91
|
+
* @param {string} tail
|
|
92
|
+
* @returns {AgentTelemetry}
|
|
93
|
+
*/
|
|
94
|
+
export function parseCodexRolloutTail(tail) {
|
|
95
|
+
const telemetry = emptyTelemetry();
|
|
96
|
+
if (!tail) return telemetry;
|
|
97
|
+
|
|
98
|
+
/** @type {any[]} */
|
|
99
|
+
const records = [];
|
|
100
|
+
for (const line of String(tail).split("\n")) {
|
|
101
|
+
const trimmed = line.trim();
|
|
102
|
+
if (!trimmed.startsWith("{")) continue;
|
|
103
|
+
try {
|
|
104
|
+
records.push(JSON.parse(trimmed));
|
|
105
|
+
} catch {
|
|
106
|
+
// A truncated first or last line is not evidence.
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (records.length === 0) return telemetry;
|
|
110
|
+
|
|
111
|
+
for (const record of records) {
|
|
112
|
+
const payload = record?.payload;
|
|
113
|
+
const payloadType = String(payload?.type ?? "");
|
|
114
|
+
if (record?.type === "session_meta") {
|
|
115
|
+
telemetry.sessionId = String(payload?.id ?? telemetry.sessionId);
|
|
116
|
+
telemetry.cwd = String(payload?.cwd ?? telemetry.cwd);
|
|
117
|
+
const branch = payload?.git?.branch;
|
|
118
|
+
if (typeof branch === "string") telemetry.gitBranch = branch;
|
|
119
|
+
} else if (record?.type === "turn_context") {
|
|
120
|
+
telemetry.cwd = String(payload?.cwd ?? telemetry.cwd);
|
|
121
|
+
telemetry.mode = String(payload?.approval_policy ?? telemetry.mode);
|
|
122
|
+
const sandbox = payload?.sandbox_policy?.type;
|
|
123
|
+
if (typeof sandbox === "string") telemetry.permissionMode = sandbox;
|
|
124
|
+
} else if (payloadType === "user_message") {
|
|
125
|
+
telemetry.lastPrompt = preview(payload?.message);
|
|
126
|
+
} else if (payloadType === "agent_message") {
|
|
127
|
+
telemetry.assistantPreview = preview(payload?.message);
|
|
128
|
+
} else if (payloadType === "task_complete") {
|
|
129
|
+
telemetry.turnIndex += 1;
|
|
130
|
+
if (payload?.last_agent_message) {
|
|
131
|
+
telemetry.assistantPreview = preview(payload.last_agent_message);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const timed = records.filter((record) => timestampMs(record?.timestamp) > 0);
|
|
137
|
+
const latest = timed.at(-1);
|
|
138
|
+
if (!latest) return telemetry;
|
|
139
|
+
telemetry.lastEventAt = timestampMs(latest.timestamp);
|
|
140
|
+
|
|
141
|
+
const payload = latest.payload;
|
|
142
|
+
const payloadType = String(payload?.type ?? "");
|
|
143
|
+
// A tool call that is the newest record is still running: its output would appear after it.
|
|
144
|
+
const toolName = toolNameFor(payload);
|
|
145
|
+
if (toolName) {
|
|
146
|
+
telemetry.state = "tool";
|
|
147
|
+
telemetry.toolName = toolName;
|
|
148
|
+
telemetry.toolTarget = describeCodexToolTarget(payload);
|
|
149
|
+
return telemetry;
|
|
150
|
+
}
|
|
151
|
+
telemetry.state = payloadType === "task_complete" ? "idle" : "thinking";
|
|
152
|
+
return telemetry;
|
|
153
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { execFile } from "node:child_process";
|
|
8
8
|
import { promisify } from "node:util";
|
|
9
|
+
import { detectTabInventorySupport } from "../native/tab-inventory.mjs";
|
|
9
10
|
|
|
10
11
|
const execFileAsync = promisify(execFile);
|
|
11
12
|
|
|
@@ -83,6 +84,10 @@ export async function assessActivityStripCompatibility(options = {}) {
|
|
|
83
84
|
env,
|
|
84
85
|
execFileAsyncImpl: options.execFileAsyncImpl,
|
|
85
86
|
});
|
|
87
|
+
const tabInventory = await detectTabInventorySupport({
|
|
88
|
+
execFileAsync: options.execFileAsyncImpl ?? execFileAsync,
|
|
89
|
+
env,
|
|
90
|
+
});
|
|
86
91
|
const blockers = [];
|
|
87
92
|
const warnings = [];
|
|
88
93
|
|
|
@@ -117,6 +122,12 @@ export async function assessActivityStripCompatibility(options = {}) {
|
|
|
117
122
|
);
|
|
118
123
|
}
|
|
119
124
|
|
|
125
|
+
if (windowManager === "niri" && !tabInventory.available) {
|
|
126
|
+
warnings.push(
|
|
127
|
+
`Hidden Ghostty tabs that were never shown cannot be placed: ${tabInventory.detail}. Tabs shown while the strip runs are still remembered.`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
120
131
|
return {
|
|
121
132
|
ok: blockers.length === 0,
|
|
122
133
|
backend,
|
|
@@ -126,6 +137,7 @@ export async function assessActivityStripCompatibility(options = {}) {
|
|
|
126
137
|
alignmentMode: "layer-shell",
|
|
127
138
|
primaryDisplayOnly: true,
|
|
128
139
|
clickThroughDefault: env.PI_ACTIVITY_STRIP_CLICK_THROUGH === "1",
|
|
140
|
+
tabInventory,
|
|
129
141
|
blockers,
|
|
130
142
|
warnings,
|
|
131
143
|
};
|
|
@@ -143,6 +155,7 @@ export function formatCompatibilityReport(report) {
|
|
|
143
155
|
`Alignment mode: ${report.alignmentMode}`,
|
|
144
156
|
`Primary-display only: ${report.primaryDisplayOnly ? "yes" : "no"}`,
|
|
145
157
|
`Click-through mode: ${report.clickThroughDefault ? "enabled by environment" : "disabled (interactive default)"}`,
|
|
158
|
+
`Hidden-tab inventory: ${report.tabInventory?.available ? "available" : "unavailable"} (${report.tabInventory?.detail ?? "unknown"})`,
|
|
146
159
|
];
|
|
147
160
|
|
|
148
161
|
if (typeof report.displayCount === "number") {
|
|
@@ -38,6 +38,32 @@ export interface SessionSnapshot {
|
|
|
38
38
|
errorMessage: string;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
export type AkTaskChipState = "active" | "deferred" | "orphaned";
|
|
42
|
+
|
|
43
|
+
/** One read-only AK task reference joined onto a terminal card by the native projection. */
|
|
44
|
+
export interface AkTaskChip {
|
|
45
|
+
id: number;
|
|
46
|
+
title: string;
|
|
47
|
+
state: AkTaskChipState;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A live-lease task claim parsed from read-only `ak task list` output. */
|
|
51
|
+
export interface AkTaskClaim {
|
|
52
|
+
id: number;
|
|
53
|
+
title: string;
|
|
54
|
+
repo: string;
|
|
55
|
+
sessionId: string;
|
|
56
|
+
leaseExpiresAt: number;
|
|
57
|
+
claimedAt: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A task with an active deferral parsed from read-only `ak task deferred` output. */
|
|
61
|
+
export interface AkTaskDeferred {
|
|
62
|
+
id: number;
|
|
63
|
+
title: string;
|
|
64
|
+
repo: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
41
67
|
export interface BrokerSnapshot {
|
|
42
68
|
generatedAt: number;
|
|
43
69
|
sessions: SessionSnapshot[];
|
package/src/common/contracts.ts
CHANGED
|
@@ -19,6 +19,12 @@ export interface SessionSnapshot {
|
|
|
19
19
|
publisherCount?: number;
|
|
20
20
|
publisherIds?: string[];
|
|
21
21
|
publisherRecordKeys?: string[];
|
|
22
|
+
publisherSessionIds?: string[];
|
|
23
|
+
akTasks?: AkTaskChip[];
|
|
24
|
+
akTaskOverflow?: number;
|
|
25
|
+
windowId?: number | null;
|
|
26
|
+
placement?: "title" | "host" | "binding";
|
|
27
|
+
surfaceVisible?: boolean;
|
|
22
28
|
cwd: string;
|
|
23
29
|
repoLabel: string;
|
|
24
30
|
sessionName: string;
|
|
@@ -38,6 +44,32 @@ export interface SessionSnapshot {
|
|
|
38
44
|
errorMessage: string;
|
|
39
45
|
}
|
|
40
46
|
|
|
47
|
+
export type AkTaskChipState = "active" | "deferred" | "orphaned";
|
|
48
|
+
|
|
49
|
+
/** One read-only AK task reference joined onto a terminal card by the native projection. */
|
|
50
|
+
export interface AkTaskChip {
|
|
51
|
+
id: number;
|
|
52
|
+
title: string;
|
|
53
|
+
state: AkTaskChipState;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A live-lease task claim parsed from read-only `ak task list` output. */
|
|
57
|
+
export interface AkTaskClaim {
|
|
58
|
+
id: number;
|
|
59
|
+
title: string;
|
|
60
|
+
repo: string;
|
|
61
|
+
sessionId: string;
|
|
62
|
+
leaseExpiresAt: number;
|
|
63
|
+
claimedAt: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A task with an active deferral parsed from read-only `ak task deferred` output. */
|
|
67
|
+
export interface AkTaskDeferred {
|
|
68
|
+
id: number;
|
|
69
|
+
title: string;
|
|
70
|
+
repo: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
41
73
|
export interface BrokerSnapshot {
|
|
42
74
|
generatedAt: number;
|
|
43
75
|
sessions: SessionSnapshot[];
|
|
@@ -57,6 +89,30 @@ export interface ActivityStripRuntimeStatus {
|
|
|
57
89
|
alignmentMode?: "layer-shell";
|
|
58
90
|
rendererCardCount?: number;
|
|
59
91
|
rendererCardIds?: string[];
|
|
92
|
+
rendererHiddenTabCardCount?: number;
|
|
93
|
+
surfaceBindingCount?: number;
|
|
94
|
+
unplacedSurfaceCount?: number;
|
|
95
|
+
agentTabCount?: number;
|
|
96
|
+
themeScheme?: "dark" | "light" | "unknown";
|
|
97
|
+
themeName?: string | null;
|
|
98
|
+
themeSource?: string | null;
|
|
99
|
+
themeFingerprint?: string;
|
|
100
|
+
themeError?: string;
|
|
101
|
+
agentScanError?: string | null;
|
|
102
|
+
heightRepairState?: "enabled" | "disabled";
|
|
103
|
+
heightRepairCount?: number;
|
|
104
|
+
lastHeightRepairAt?: number;
|
|
105
|
+
tabInventoryState?: "pending" | "ready" | "failed" | "unavailable" | "disabled";
|
|
106
|
+
tabInventoryDetail?: string | null;
|
|
107
|
+
tabInventoryFrameCount?: number;
|
|
108
|
+
tabInventoryTabCount?: number;
|
|
109
|
+
tabInventoryProbedAt?: number | null;
|
|
110
|
+
akTaskState?: "pending" | "ok" | "unavailable" | "disabled";
|
|
111
|
+
akTaskError?: string | null;
|
|
112
|
+
akTaskClaimCount?: number;
|
|
113
|
+
akTaskDeferredCount?: number;
|
|
114
|
+
akTaskOrphanCount?: number;
|
|
115
|
+
akTaskRenderedCount?: number;
|
|
60
116
|
rendererVisibilityTransitionCount?: number;
|
|
61
117
|
controllerPid?: number;
|
|
62
118
|
panelPid?: number | null;
|
|
@@ -98,7 +154,13 @@ export interface ActivityStripBrokerOptions {
|
|
|
98
154
|
remove(sessionId: string, publisherId?: string): boolean;
|
|
99
155
|
};
|
|
100
156
|
getRuntimeStatus?: () => ActivityStripRuntimeStatus | undefined;
|
|
101
|
-
focusSession?: (targetId: string) => Promise<{
|
|
157
|
+
focusSession?: (targetId: string) => Promise<{
|
|
158
|
+
ok: boolean;
|
|
159
|
+
error?: string;
|
|
160
|
+
windowId?: number;
|
|
161
|
+
presented?: boolean;
|
|
162
|
+
verified?: boolean;
|
|
163
|
+
}>;
|
|
102
164
|
focusStrip?: () => Promise<{ ok: boolean; error?: string }>;
|
|
103
165
|
}
|
|
104
166
|
|
|
@@ -196,6 +258,7 @@ export interface ActivityStripCompatibilityReport {
|
|
|
196
258
|
alignmentMode: "layer-shell";
|
|
197
259
|
primaryDisplayOnly: boolean;
|
|
198
260
|
clickThroughDefault: boolean;
|
|
261
|
+
tabInventory?: { available: boolean; detail: string };
|
|
199
262
|
blockers: string[];
|
|
200
263
|
warnings: string[];
|
|
201
264
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// ---
|
|
2
|
+
// summary: "presents one Ghostty surface through the running process's session-bus present-surface action"
|
|
3
|
+
// read_when:
|
|
4
|
+
// - "changing hidden-tab activation, Ghostty D-Bus targeting, or its fail-closed conditions"
|
|
5
|
+
// ---
|
|
6
|
+
|
|
7
|
+
import { normalizeGhosttySurfaceId } from "./terminal-identity.mjs";
|
|
8
|
+
|
|
9
|
+
const GHOSTTY_OBJECT_PATHS = new Map([
|
|
10
|
+
["main", "/com/mitchellh/ghostty"],
|
|
11
|
+
["legacy", "/com/tryinget/ghosttysidequest"],
|
|
12
|
+
]);
|
|
13
|
+
export const GHOSTTY_PRESENT_SURFACE_TIMEOUT_MS = 2000;
|
|
14
|
+
|
|
15
|
+
/** @typedef {(file: string, args: string[], options: object) => Promise<{stdout?: string}>} ExecFileAsync */
|
|
16
|
+
/** @typedef {{ok: true; busName: string} | {ok: false; error: string}} PresentSurfaceResult */
|
|
17
|
+
|
|
18
|
+
/** @param {unknown} family */
|
|
19
|
+
export function ghosttyObjectPathForFamily(family) {
|
|
20
|
+
return GHOSTTY_OBJECT_PATHS.get(String(family ?? "")) ?? "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Every running Ghostty process, single-instance server or standalone window, exports
|
|
25
|
+
* `org.gtk.Actions` on its unique bus name. `present-surface` selects the tab owning the surface
|
|
26
|
+
* and presents its window; a surface id the process does not own is ignored by Ghostty itself.
|
|
27
|
+
* The target is the exact host process already proven to contain the surface, never a
|
|
28
|
+
* well-known name that another build could claim.
|
|
29
|
+
* @param {{execFileAsync: ExecFileAsync; env?: NodeJS.ProcessEnv; hostPid: number; terminalFamily: unknown; surfaceId: unknown; timeout?: number}} options
|
|
30
|
+
* @returns {Promise<PresentSurfaceResult>}
|
|
31
|
+
*/
|
|
32
|
+
export async function presentGhosttySurface({
|
|
33
|
+
execFileAsync,
|
|
34
|
+
env = process.env,
|
|
35
|
+
hostPid,
|
|
36
|
+
terminalFamily,
|
|
37
|
+
surfaceId,
|
|
38
|
+
timeout = GHOSTTY_PRESENT_SURFACE_TIMEOUT_MS,
|
|
39
|
+
}) {
|
|
40
|
+
const objectPath = ghosttyObjectPathForFamily(terminalFamily);
|
|
41
|
+
const normalizedSurfaceId = normalizeGhosttySurfaceId(surfaceId);
|
|
42
|
+
if (
|
|
43
|
+
!objectPath ||
|
|
44
|
+
!normalizedSurfaceId ||
|
|
45
|
+
normalizedSurfaceId === "0" ||
|
|
46
|
+
!Number.isInteger(hostPid) ||
|
|
47
|
+
hostPid <= 0
|
|
48
|
+
) {
|
|
49
|
+
return { ok: false, error: "the surface target is incomplete" };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let rows;
|
|
53
|
+
try {
|
|
54
|
+
const { stdout } = await execFileAsync(
|
|
55
|
+
"busctl",
|
|
56
|
+
["--user", "list", "--no-pager", "--no-legend"],
|
|
57
|
+
{ env, timeout },
|
|
58
|
+
);
|
|
59
|
+
rows = String(stdout ?? "")
|
|
60
|
+
.split("\n")
|
|
61
|
+
.map((line) => line.trim().split(/\s+/));
|
|
62
|
+
} catch {
|
|
63
|
+
return { ok: false, error: "the session bus is unavailable" };
|
|
64
|
+
}
|
|
65
|
+
const uniqueNames = rows
|
|
66
|
+
.filter(
|
|
67
|
+
(fields) => fields[0]?.startsWith(":") && Number.parseInt(fields[1] ?? "", 10) === hostPid,
|
|
68
|
+
)
|
|
69
|
+
.map((fields) => String(fields[0]));
|
|
70
|
+
if (uniqueNames.length === 0) {
|
|
71
|
+
return { ok: false, error: "the Ghostty process is not on the session bus" };
|
|
72
|
+
}
|
|
73
|
+
if (uniqueNames.length > 1) {
|
|
74
|
+
return { ok: false, error: "the Ghostty process owns several bus connections" };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
await execFileAsync(
|
|
79
|
+
"busctl",
|
|
80
|
+
[
|
|
81
|
+
"--user",
|
|
82
|
+
"call",
|
|
83
|
+
uniqueNames[0],
|
|
84
|
+
objectPath,
|
|
85
|
+
"org.gtk.Actions",
|
|
86
|
+
"Activate",
|
|
87
|
+
"sava{sv}",
|
|
88
|
+
"present-surface",
|
|
89
|
+
"1",
|
|
90
|
+
"t",
|
|
91
|
+
normalizedSurfaceId,
|
|
92
|
+
"0",
|
|
93
|
+
],
|
|
94
|
+
{ env, timeout },
|
|
95
|
+
);
|
|
96
|
+
return { ok: true, busName: uniqueNames[0] };
|
|
97
|
+
} catch {
|
|
98
|
+
return { ok: false, error: "Ghostty rejected present-surface" };
|
|
99
|
+
}
|
|
100
|
+
}
|