@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
@@ -6,19 +6,33 @@
6
6
 
7
7
  import fs from "node:fs";
8
8
  import path from "node:path";
9
- import { projectSessionCards, sessionCardId } from "./session-cards.mjs";
9
+ import { presentGhosttySurface } from "./ghostty-present.mjs";
10
+ import { sessionCardId } from "./session-cards.mjs";
10
11
  import {
11
12
  appIdForGhosttyFamily,
12
13
  canonicalGhosttyTerminalKey,
13
14
  normalizeGhosttySurfaceId,
14
15
  terminalTitleSegment,
15
16
  } from "./terminal-identity.mjs";
17
+ import { resolveHiddenSurfaceWindow } from "./window-placement.mjs";
16
18
 
17
19
  const GHOSTTY_APP_IDS = new Set(["com.mitchellh.ghostty", "com.tryinget.ghosttysidequest"]);
18
20
  const PI_SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
19
21
  const SESSION_PRESENCE_SOURCE = "@tryinget/pi-little-helpers/session-presence";
20
22
  const SESSION_TITLE_TOKEN_HEX_LENGTH = 32;
21
23
  const LEGACY_SESSION_TITLE_TOKEN_HEX_LENGTH = 8;
24
+ const PRESENT_VERIFY_ATTEMPTS = 4;
25
+ const PRESENT_VERIFY_DELAY_MS = 120;
26
+
27
+ /** @typedef {import("./window-placement.mjs").PlacementOptions} PlacementOptions */
28
+ /** @typedef {import("./window-placement.mjs").WindowPlacement} WindowPlacement */
29
+ /**
30
+ * @typedef {PlacementOptions & {
31
+ * env?: NodeJS.ProcessEnv;
32
+ * readFileSync?: typeof fs.readFileSync;
33
+ * existsSync?: typeof fs.existsSync;
34
+ * }} ResolveOptions
35
+ */
22
36
 
23
37
  /**
24
38
  * Read one bounded Niri JSON list. Polling callers treat command, timeout,
@@ -167,156 +181,210 @@ export function resolveExactGhosttyWindow(windows, sessionId, session = {}) {
167
181
  }
168
182
 
169
183
  /**
170
- * Resolve the one snapshot session whose exact Ghostty window currently owns
171
- * compositor focus. Missing, ambiguous, non-Ghostty, and stale identities all
172
- * fail closed so the renderer never highlights a guessed session.
184
+ * Place a session on its window. A visible title is exact proof; a bound surface whose tab is
185
+ * hidden behind another tab is placed through its Ghostty host process and remembered window.
173
186
  * @param {Array<Record<string, unknown>>} windows
174
- * @param {Array<Record<string, unknown>>} sessions
175
- * @param {{env?: NodeJS.ProcessEnv; readFileSync?: typeof fs.readFileSync; existsSync?: typeof fs.existsSync}} [options]
187
+ * @param {string} sessionId
188
+ * @param {Record<string, unknown>} [session]
189
+ * @param {PlacementOptions} [options]
190
+ * @returns {WindowPlacement | null}
176
191
  */
177
- export function resolveFocusedSnapshotSessionId(windows, sessions, options = {}) {
178
- const focusedWindows = windows.filter((window) => window?.is_focused === true);
179
- if (focusedWindows.length !== 1) return null;
180
- const focusedWindow = focusedWindows[0];
181
- const matches = sessions.filter((session) => {
182
- const sessionId = resolvePiSessionIdentity(session, options);
183
- if (!sessionId) return false;
184
- return resolveExactGhosttyWindow(windows, sessionId, session)?.id === focusedWindow.id;
192
+ export function resolveSessionWindow(windows, sessionId, session = {}, options = {}) {
193
+ const exact = resolveExactGhosttyWindow(windows, sessionId, session);
194
+ if (exact) return { window: exact, placement: "title" };
195
+ return resolveHiddenSurfaceWindow(windows, session, {
196
+ ...options,
197
+ sessionToken: shortSessionId(sessionId),
185
198
  });
186
- return matches.length === 1 ? String(matches[0]?.sessionId ?? "") || null : null;
187
199
  }
188
200
 
189
201
  /**
190
- * Project the global broker snapshot onto one exact Niri workspace. Membership
191
- * requires an exact Ghostty window. Multiple publisher streams bound to the same terminal are
192
- * aggregated into one stable card; ambiguity between distinct windows still fails closed.
202
+ * A tab occupied by a coding agent other than Pi carries no Pi identity, so it is placed by the
203
+ * exact title the agent put on its own terminal, and otherwise by host containment and window
204
+ * memory. A title claimed by more than one window is ambiguous and places nothing.
193
205
  * @param {Array<Record<string, unknown>>} windows
194
- * @param {Record<string, unknown>} workspace
195
- * @param {Array<Record<string, unknown>>} sessions
196
- * @param {{env?: NodeJS.ProcessEnv; readFileSync?: typeof fs.readFileSync; existsSync?: typeof fs.existsSync}} [options]
206
+ * @param {Record<string, unknown>} session
207
+ * @param {PlacementOptions} [options]
208
+ * @returns {WindowPlacement | null}
197
209
  */
198
- export function resolveWorkspaceView(windows, workspace, sessions, options = {}) {
199
- if (!Number.isInteger(workspace?.id)) return null;
200
- /** @type {Array<{cardId: string; session: Record<string, unknown>; window: Record<string, unknown>}>} */
201
- const candidates = [];
202
- for (const session of sessions) {
203
- const sessionIdentity = resolvePiSessionIdentity(session, options);
204
- if (!sessionIdentity) continue;
205
- const window = resolveExactGhosttyWindow(windows, sessionIdentity, session);
206
- if (!window || window.workspace_id !== workspace.id) continue;
207
- const cardId = sessionCardId(session);
208
- if (!cardId) continue;
209
- candidates.push({ cardId, session: { ...session, cardId }, window });
210
- }
211
-
212
- const cardIdsByWindow = new Map();
213
- for (const candidate of candidates) {
214
- const ids = cardIdsByWindow.get(candidate.window.id) ?? new Set();
215
- ids.add(candidate.cardId);
216
- cardIdsByWindow.set(candidate.window.id, ids);
217
- }
218
- const allowedCardByWindow = new Map();
219
- for (const [windowId, ids] of cardIdsByWindow) {
220
- const terminalIds = [...ids].filter((cardId) => cardId.startsWith("terminal:"));
221
- if (terminalIds.length === 1) allowedCardByWindow.set(windowId, terminalIds[0]);
222
- else if (ids.size === 1) allowedCardByWindow.set(windowId, [...ids][0]);
210
+ export function resolveAgentSessionWindow(windows, session, options = {}) {
211
+ const titleSuffix = String(session.titleSuffix ?? "").trim();
212
+ if (titleSuffix) {
213
+ const expectedAppId = appIdForGhosttyFamily(String(session.terminalFamily ?? ""));
214
+ const matches = windows.filter(
215
+ (window) =>
216
+ Number.isInteger(window?.id) &&
217
+ String(window?.app_id ?? "") === expectedAppId &&
218
+ String(window?.title ?? "").endsWith(titleSuffix),
219
+ );
220
+ if (matches.length === 1) return { window: matches[0], placement: "title" };
221
+ if (matches.length > 1) return null;
223
222
  }
223
+ return resolveHiddenSurfaceWindow(windows, session, options);
224
+ }
224
225
 
225
- /** @type {Map<string, Array<{session: Record<string, unknown>; window: Record<string, unknown>}>>} */
226
- const groups = new Map();
227
- for (const candidate of candidates) {
228
- if (allowedCardByWindow.get(candidate.window.id) !== candidate.cardId) continue;
229
- const group = groups.get(candidate.cardId) ?? [];
230
- group.push(candidate);
231
- groups.set(candidate.cardId, group);
232
- }
226
+ /**
227
+ * Whether a session could ever be placed by a title. Activation can only confirm that a presented
228
+ * tab became visible for these; for anything else the compositor offers no readable proof.
229
+ * @param {Record<string, unknown>} session
230
+ * @param {ResolveOptions} [options]
231
+ */
232
+ export function canVerifyTabVisibility(session, options = {}) {
233
+ if (resolvePiSessionIdentity(session, options)) return true;
234
+ return String(session?.titleSuffix ?? "").trim().length > 0;
235
+ }
233
236
 
234
- const projectedSessions = [];
235
- for (const [cardId, group] of groups) {
236
- const windowIds = new Set(group.map((candidate) => candidate.window.id));
237
- if (windowIds.size !== 1) continue;
238
- const [card] = projectSessionCards(group.map((candidate) => candidate.session));
239
- if (!card) continue;
240
- const window = group[0]?.window;
241
- projectedSessions.push({ ...card, cardId, windowId: window?.id });
242
- }
243
- const focusedCard = projectedSessions.find((session) =>
244
- windows.some((window) => window?.id === session.windowId && window?.is_focused === true),
245
- );
246
- return {
247
- workspace,
248
- sessions: projectedSessions,
249
- focusedSessionId: focusedCard ? String(focusedCard.sessionId ?? "") || null : null,
250
- focusedCardId: focusedCard ? String(focusedCard.cardId ?? "") || null : null,
251
- };
237
+ /**
238
+ * Place any tracked session: a Pi session by its exact Pi identity, an agent tab by its own title.
239
+ * @param {Array<Record<string, unknown>>} windows
240
+ * @param {Record<string, unknown>} session
241
+ * @param {ResolveOptions} [options]
242
+ * @returns {WindowPlacement | null}
243
+ */
244
+ export function placeSessionWindow(windows, session, options = {}) {
245
+ const piSessionId = resolvePiSessionIdentity(session, options);
246
+ if (piSessionId) return resolveSessionWindow(windows, piSessionId, session, options);
247
+ return canonicalGhosttyTerminalKey(session)
248
+ ? resolveAgentSessionWindow(windows, session, options)
249
+ : null;
252
250
  }
253
251
 
254
252
  /**
255
- * Project the global broker snapshot onto the one focused Niri workspace.
253
+ * Resolve the one snapshot session whose exact Ghostty window currently owns
254
+ * compositor focus. Missing, ambiguous, non-Ghostty, and stale identities all
255
+ * fail closed so the renderer never highlights a guessed session.
256
256
  * @param {Array<Record<string, unknown>>} windows
257
- * @param {Array<Record<string, unknown>>} workspaces
258
257
  * @param {Array<Record<string, unknown>>} sessions
259
258
  * @param {{env?: NodeJS.ProcessEnv; readFileSync?: typeof fs.readFileSync; existsSync?: typeof fs.existsSync}} [options]
260
259
  */
261
- export function resolveFocusedWorkspaceView(windows, workspaces, sessions, options = {}) {
262
- const workspace = resolveFocusedNiriWorkspace(workspaces);
263
- return workspace ? resolveWorkspaceView(windows, workspace, sessions, options) : null;
264
- }
265
-
266
- /** @param {Array<Record<string, unknown>>} workspaces */
267
- export function resolveFocusedNiriWorkspace(workspaces) {
268
- const matches = workspaces.filter(
269
- (workspace) =>
270
- workspace?.is_focused === true &&
271
- Number.isInteger(workspace?.id) &&
272
- (typeof workspace?.name === "string" || Number.isInteger(workspace?.idx)),
273
- );
274
- return matches.length === 1 ? matches[0] : null;
260
+ export function resolveFocusedSnapshotSessionId(windows, sessions, options = {}) {
261
+ const focusedWindows = windows.filter((window) => window?.is_focused === true);
262
+ if (focusedWindows.length !== 1) return null;
263
+ const focusedWindow = focusedWindows[0];
264
+ const matches = sessions.filter((session) => {
265
+ const sessionId = resolvePiSessionIdentity(session, options);
266
+ if (!sessionId) return false;
267
+ return resolveExactGhosttyWindow(windows, sessionId, session)?.id === focusedWindow.id;
268
+ });
269
+ return matches.length === 1 ? String(matches[0]?.sessionId ?? "") || null : null;
275
270
  }
276
271
 
277
272
  /**
273
+ * Focus the exact terminal behind a card. A visible tab needs only compositor focus. A hidden tab
274
+ * is first presented inside its proven Ghostty host process, then its window is focused, and the
275
+ * result counts as success only once the window title proves the tab became visible.
278
276
  * @param {string | Record<string, unknown>} session
279
277
  * @param {(file: string, args: string[], options: object) => Promise<{stdout?: string}>} execFileAsync
280
278
  * @param {NodeJS.ProcessEnv} [env]
281
- * @param {{readFileSync?: typeof fs.readFileSync; existsSync?: typeof fs.existsSync}} [options]
279
+ * @param {PlacementOptions & {
280
+ * readFileSync?: typeof fs.readFileSync;
281
+ * existsSync?: typeof fs.existsSync;
282
+ * presentSurface?: typeof presentGhosttySurface;
283
+ * sleep?: (milliseconds: number) => Promise<void>;
284
+ * }} [options]
285
+ * @returns {Promise<{ok: boolean; error?: string; windowId?: number; presented?: boolean; verified?: boolean}>}
282
286
  */
283
287
  export async function focusNiriSession(session, execFileAsync, env = process.env, options = {}) {
284
288
  if (!env.NIRI_SOCKET) return { ok: false, error: "Niri is not available; focus did nothing." };
285
- const sessionId = resolvePiSessionIdentity(session, {
286
- env,
287
- readFileSync: options.readFileSync,
288
- existsSync: options.existsSync,
289
- });
290
- if (!sessionId) {
289
+ const record =
290
+ session && typeof session === "object" ? session : { sessionId: String(session ?? "") };
291
+ const placementOptions = { ...options, env };
292
+ if (!resolvePiSessionIdentity(record, placementOptions) && !canonicalGhosttyTerminalKey(record)) {
291
293
  return {
292
294
  ok: false,
293
295
  error: "Exact Pi identity is unavailable; reload that Pi tab and try again.",
294
296
  };
295
297
  }
298
+ const readWindows = async () => {
299
+ const result = await execFileAsync("niri", ["msg", "-j", "windows"], { env });
300
+ const windows = JSON.parse(String(result.stdout ?? "[]"));
301
+ if (!Array.isArray(windows)) throw new Error("Unexpected Niri window data");
302
+ return windows;
303
+ };
296
304
  let windows;
297
305
  try {
298
- const result = await execFileAsync("niri", ["msg", "-j", "windows"], { env });
299
- windows = JSON.parse(String(result.stdout ?? "[]"));
300
- } catch {
301
- return { ok: false, error: "Could not inspect Niri windows; focus did nothing." };
302
- }
303
- if (!Array.isArray(windows)) {
304
- return { ok: false, error: "Unexpected Niri window data; focus did nothing." };
306
+ windows = await readWindows();
307
+ } catch (error) {
308
+ return {
309
+ ok: false,
310
+ error:
311
+ error instanceof Error && error.message === "Unexpected Niri window data"
312
+ ? "Unexpected Niri window data; focus did nothing."
313
+ : "Could not inspect Niri windows; focus did nothing.",
314
+ };
305
315
  }
306
- const record = session && typeof session === "object" ? session : {};
307
- const target = resolveExactGhosttyWindow(windows, sessionId, record);
308
- if (!target) {
316
+ const placed = placeSessionWindow(windows, record, placementOptions);
317
+ if (!placed) {
309
318
  return {
310
319
  ok: false,
311
- error: "Pi identity did not resolve to exactly one Ghostty window; focus did nothing.",
320
+ error: "That session did not resolve to exactly one Ghostty window; focus did nothing.",
312
321
  };
313
322
  }
323
+ const target = placed.window;
324
+ const targetId = Number(target.id);
325
+ const focusWindow = () =>
326
+ execFileAsync("niri", ["msg", "action", "focus-window", "--id", String(targetId)], { env });
327
+ if (placed.placement === "title") {
328
+ try {
329
+ await focusWindow();
330
+ return { ok: true, windowId: targetId };
331
+ } catch {
332
+ return { ok: false, error: "Niri rejected the exact focus request; focus did nothing." };
333
+ }
334
+ }
335
+
336
+ const presented = await (options.presentSurface ?? presentGhosttySurface)({
337
+ execFileAsync,
338
+ env,
339
+ hostPid: Number(target.pid),
340
+ terminalFamily: record.terminalFamily,
341
+ surfaceId: record.terminalSurfaceId,
342
+ });
314
343
  try {
315
- await execFileAsync("niri", ["msg", "action", "focus-window", "--id", String(target.id)], {
316
- env,
317
- });
318
- return { ok: true, windowId: target.id };
344
+ await focusWindow();
319
345
  } catch {
320
- return { ok: false, error: "Niri rejected the exact focus request; focus did nothing." };
346
+ return {
347
+ ok: false,
348
+ windowId: targetId,
349
+ presented: false,
350
+ error: presented.ok
351
+ ? "Presented the hidden tab, but Niri rejected the window focus request."
352
+ : "Niri rejected the exact focus request; focus did nothing.",
353
+ };
354
+ }
355
+ if (!presented.ok) {
356
+ return {
357
+ ok: false,
358
+ windowId: targetId,
359
+ presented: false,
360
+ error: `Focused the Ghostty window, but could not present the hidden tab (${presented.error}); select it manually.`,
361
+ };
362
+ }
363
+ if (!canVerifyTabVisibility(record, placementOptions)) {
364
+ // Nothing this session writes is readable from the compositor, so the presented tab cannot be
365
+ // confirmed. The action succeeded; the outcome is reported without claiming proof.
366
+ return { ok: true, windowId: targetId, presented: true, verified: false };
367
+ }
368
+ const sleep =
369
+ options.sleep ??
370
+ ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
371
+ for (let attempt = 0; attempt < PRESENT_VERIFY_ATTEMPTS; attempt += 1) {
372
+ if (attempt > 0) await sleep(PRESENT_VERIFY_DELAY_MS);
373
+ try {
374
+ const current = await readWindows();
375
+ const confirmed = placeSessionWindow(current, record, placementOptions);
376
+ if (confirmed?.placement === "title" && Number(confirmed.window.id) === targetId) {
377
+ return { ok: true, windowId: targetId, presented: true, verified: true };
378
+ }
379
+ } catch {
380
+ break;
381
+ }
321
382
  }
383
+ return {
384
+ ok: false,
385
+ windowId: targetId,
386
+ presented: false,
387
+ error:
388
+ "Focused the Ghostty window, but the hidden tab did not become visible; select it manually.",
389
+ };
322
390
  }
@@ -54,7 +54,7 @@ export function comparePublisherRepresentatives(left, right) {
54
54
  /**
55
55
  * @param {SessionLike[]} [sessions]
56
56
  * @param {Set<string> | null} [allowedCardIds]
57
- * @returns {Array<SessionLike & {cardId: string; publisherCount: number; publisherIds: string[]; publisherRecordKeys: string[]}>}
57
+ * @returns {Array<SessionLike & {cardId: string; publisherCount: number; publisherIds: string[]; publisherRecordKeys: string[]; publisherSessionIds: string[]}>}
58
58
  */
59
59
  export function projectSessionCards(sessions = [], allowedCardIds = null) {
60
60
  /** @type {Map<string, SessionLike[]>} */
@@ -67,7 +67,7 @@ export function projectSessionCards(sessions = [], allowedCardIds = null) {
67
67
  groups.set(cardId, group);
68
68
  }
69
69
 
70
- /** @type {Array<SessionLike & {cardId: string; publisherCount: number; publisherIds: string[]; publisherRecordKeys: string[]}>} */
70
+ /** @type {Array<SessionLike & {cardId: string; publisherCount: number; publisherIds: string[]; publisherRecordKeys: string[]; publisherSessionIds: string[]}>} */
71
71
  const cards = [];
72
72
  for (const [cardId, group] of groups) {
73
73
  const ordered = [...group].sort(comparePublisherRepresentatives);
@@ -79,6 +79,9 @@ export function projectSessionCards(sessions = [], allowedCardIds = null) {
79
79
  publisherCount: group.length,
80
80
  publisherIds: group.map((session) => String(session.publisherId ?? "")).filter(Boolean),
81
81
  publisherRecordKeys: group.map(sessionRecordKey),
82
+ publisherSessionIds: [
83
+ ...new Set(group.map((session) => String(session.sessionId ?? "")).filter(Boolean)),
84
+ ],
82
85
  });
83
86
  }
84
87
  return cards;
@@ -26,6 +26,57 @@ export function formatBrokerRuntimeStatus(result) {
26
26
  if (typeof runtime.windowVisible === "boolean") {
27
27
  lines.push(`surface visible: ${runtime.windowVisible ? "yes" : "no"}`);
28
28
  }
29
+ if (typeof runtime.rendererCardCount === "number") {
30
+ const hidden =
31
+ typeof runtime.rendererHiddenTabCardCount === "number"
32
+ ? ` (${runtime.rendererHiddenTabCardCount} hidden tabs)`
33
+ : "";
34
+ lines.push(`cards on focused workspace: ${runtime.rendererCardCount}${hidden}`);
35
+ }
36
+ if (typeof runtime.surfaceBindingCount === "number") {
37
+ lines.push(`remembered tab windows: ${runtime.surfaceBindingCount}`);
38
+ }
39
+ if (typeof runtime.agentTabCount === "number") {
40
+ lines.push(`agent tabs discovered: ${runtime.agentTabCount}`);
41
+ }
42
+ if (runtime.themeName) {
43
+ lines.push(`theme: ${runtime.themeName} (${runtime.themeScheme ?? "unknown"})`);
44
+ }
45
+ if (runtime.agentScanError) {
46
+ lines.push(`agent discovery error: ${runtime.agentScanError}`);
47
+ }
48
+ if (typeof runtime.heightRepairState === "string") {
49
+ const repaired = Number(runtime.heightRepairCount ?? 0);
50
+ lines.push(
51
+ `window height repair: ${runtime.heightRepairState}${repaired > 0 ? ` (${repaired} restored to automatic)` : ""}`,
52
+ );
53
+ }
54
+ if (typeof runtime.tabInventoryState === "string") {
55
+ const detail =
56
+ runtime.tabInventoryState === "ready"
57
+ ? ` (${runtime.tabInventoryFrameCount ?? 0} Ghostty windows, ${runtime.tabInventoryTabCount ?? 0} tabs)`
58
+ : runtime.tabInventoryDetail
59
+ ? ` (${runtime.tabInventoryDetail})`
60
+ : "";
61
+ lines.push(`tab inventory: ${runtime.tabInventoryState}${detail}`);
62
+ }
63
+ if (typeof runtime.akTaskState === "string") {
64
+ if (runtime.akTaskState === "ok") {
65
+ const orphaned = Number(runtime.akTaskOrphanCount ?? 0);
66
+ lines.push(
67
+ `AK task refs: ok (${runtime.akTaskClaimCount ?? 0} live claims, ${runtime.akTaskDeferredCount ?? 0} deferred${orphaned > 0 ? `, ${orphaned} orphaned claims` : ""})`,
68
+ );
69
+ } else if (runtime.akTaskState !== "disabled") {
70
+ lines.push(
71
+ `AK task refs: ${runtime.akTaskState}${runtime.akTaskError ? ` (${runtime.akTaskError})` : ""}`,
72
+ );
73
+ }
74
+ }
75
+ if (typeof runtime.unplacedSurfaceCount === "number" && runtime.unplacedSurfaceCount > 0) {
76
+ lines.push(
77
+ `unplaced hidden tabs: ${runtime.unplacedSurfaceCount} (placed once the tab inventory or a visible title reveals their window)`,
78
+ );
79
+ }
29
80
  if (runtime.error) {
30
81
  lines.push(`error: ${runtime.error}`);
31
82
  }