@tryinget/pi-activity-strip 0.8.0 → 0.9.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.
@@ -123,6 +123,7 @@ export interface ActivityStripRuntimeStatus {
123
123
  clickThrough?: boolean;
124
124
  panelMoveCount?: number;
125
125
  panelActivationCount?: number;
126
+ panelJumpCount?: number;
126
127
  panelRestartCount?: number;
127
128
  lastMovedCardId?: string | null;
128
129
  warnings?: string[];
@@ -149,6 +149,70 @@ export function resolveFocusedWorkspaceView(windows, workspaces, sessions, optio
149
149
  return workspace ? resolveWorkspaceView(windows, workspace, sessions, options) : null;
150
150
  }
151
151
 
152
+ /**
153
+ * @typedef {{
154
+ * cardId: string;
155
+ * label: string;
156
+ * windowId: number;
157
+ * workspaceId: number;
158
+ * workspaceIdx: number | null;
159
+ * workspaceName: string | null;
160
+ * hiddenTab: boolean;
161
+ * current: boolean;
162
+ * }} WindowDirectoryEntry
163
+ */
164
+
165
+ /**
166
+ * Every agent window on every workspace, for the ribbon's window list. It reuses the per-workspace
167
+ * card projection, so an entry exists exactly when that workspace would show the card. Ordered
168
+ * alphabetically by project name, then by window number; each entry carries its workspace.
169
+ * @param {Array<Record<string, unknown>>} windows
170
+ * @param {Array<Record<string, unknown>>} workspaces
171
+ * @param {Array<Record<string, unknown>>} sessions
172
+ * @param {ResolveOptions} [options]
173
+ * @returns {{entries: WindowDirectoryEntry[]; cards: Map<string, Record<string, unknown>>}}
174
+ */
175
+ export function resolveWindowDirectory(windows, workspaces, sessions, options = {}) {
176
+ const ordered = workspaces
177
+ .filter((workspace) => Number.isInteger(workspace?.id))
178
+ .sort(
179
+ (a, b) =>
180
+ String(a.output ?? "").localeCompare(String(b.output ?? "")) ||
181
+ Number(a.idx ?? 0) - Number(b.idx ?? 0),
182
+ );
183
+ /** @type {WindowDirectoryEntry[]} */
184
+ const entries = [];
185
+ /** @type {Map<string, Record<string, unknown>>} */
186
+ const cards = new Map();
187
+ for (const workspace of ordered) {
188
+ const view = resolveWorkspaceView(windows, workspace, sessions, options);
189
+ if (!view) continue;
190
+ const placed = view.sessions
191
+ .filter((card) => Number.isInteger(card.windowId))
192
+ .sort((a, b) => Number(a.windowId) - Number(b.windowId));
193
+ for (const card of placed) {
194
+ const cardId = String(card.cardId ?? "");
195
+ if (!cardId || cards.has(cardId)) continue;
196
+ cards.set(cardId, card);
197
+ entries.push({
198
+ cardId,
199
+ label: String(card.repoLabel || "session"),
200
+ windowId: Number(card.windowId),
201
+ workspaceId: Number(workspace.id),
202
+ workspaceIdx: Number.isInteger(card.workspaceIdx) ? Number(card.workspaceIdx) : null,
203
+ workspaceName: typeof workspace.name === "string" ? workspace.name : null,
204
+ hiddenTab: card.surfaceVisible === false,
205
+ current: cardId === view.focusedCardId,
206
+ });
207
+ }
208
+ }
209
+ entries.sort(
210
+ (a, b) =>
211
+ a.label.localeCompare(b.label, undefined, { sensitivity: "base" }) || a.windowId - b.windowId,
212
+ );
213
+ return { entries, cards };
214
+ }
215
+
152
216
  /** @param {Array<Record<string, unknown>>} workspaces */
153
217
  export function resolveFocusedNiriWorkspace(workspaces) {
154
218
  const matches = workspaces.filter(
@@ -13,7 +13,7 @@ import { pruneClaudeEventRecords } from "../common/claude-events.mjs";
13
13
  import { ACTIVITY_STRIP_WORKSPACE_SYNC_MS } from "../common/constants.mjs";
14
14
  import { focusNiriSession, readNiriWindows, readNiriWorkspaces } from "../common/niri-focus.mjs";
15
15
  import { haveSameRecordMembership } from "../common/session-cards.mjs";
16
- import { resolveFocusedWorkspaceView } from "../common/workspace-view.mjs";
16
+ import { resolveFocusedWorkspaceView, resolveWindowDirectory } from "../common/workspace-view.mjs";
17
17
  import { discoverAgentTabs } from "./agent-discovery.mjs";
18
18
  import { startAkTaskProjection } from "./ak-runtime.mjs";
19
19
  import { createHeightRepair } from "./height-repair.mjs";
@@ -158,6 +158,37 @@ async function focusSession(targetId) {
158
158
  return result;
159
159
  }
160
160
 
161
+ /**
162
+ * Jump to a window by the number Niri gives it, as typed into the window list. Only a window that
163
+ * exists right now is focused; any other number does nothing.
164
+ * @param {number} windowId
165
+ * @returns {Promise<{ok: boolean; error?: string}>}
166
+ */
167
+ async function jumpToWindow(windowId) {
168
+ if (!isNiriSession()) return { ok: false, error: "Niri is not available; nothing moved." };
169
+ if (!Number.isInteger(windowId) || windowId < 0) {
170
+ return { ok: false, error: "Not a window number; nothing moved." };
171
+ }
172
+ let windows;
173
+ try {
174
+ windows = await getNiriWindows();
175
+ } catch {
176
+ return { ok: false, error: "Could not read Niri windows; nothing moved." };
177
+ }
178
+ if (!windows.some((window) => window?.id === windowId)) {
179
+ return { ok: false, error: `No window #${windowId}; nothing moved.` };
180
+ }
181
+ try {
182
+ await execFileAsync("niri", ["msg", "action", "focus-window", "--id", String(windowId)], {
183
+ env: process.env,
184
+ });
185
+ } catch {
186
+ return { ok: false, error: `Niri refused to focus #${windowId}; nothing moved.` };
187
+ }
188
+ reconcileRunner.request();
189
+ return { ok: true };
190
+ }
191
+
161
192
  async function focusStrip() {
162
193
  if (runtimeStatus.clickThrough) {
163
194
  return { ok: false, error: "Keyboard entry is disabled in click-through mode." };
@@ -231,6 +262,17 @@ function consumePanelEvents(child, chunk) {
231
262
  : result.error || "Focus failed; nothing moved.",
232
263
  }),
233
264
  );
265
+ } else if (event.type === "jump") {
266
+ runtimeStatus.panelJumpCount = (runtimeStatus.panelJumpCount ?? 0) + 1;
267
+ const windowId = Number(event.windowId);
268
+ void jumpToWindow(windowId).then((result) =>
269
+ writePanel({
270
+ type: "jump-result",
271
+ windowId,
272
+ ok: result.ok === true,
273
+ message: result.ok ? `Jumped to #${windowId}.` : result.error || "Nothing moved.",
274
+ }),
275
+ );
234
276
  } else if (event.type === "moved") {
235
277
  runtimeStatus.panelMoveCount = (runtimeStatus.panelMoveCount ?? 0) + 1;
236
278
  runtimeStatus.lastMovedCardId = String(event.cardId ?? "") || null;
@@ -362,6 +404,7 @@ const reconcileRunner = createLatestOnlyRunner(async ({ isCurrent }) => {
362
404
  runtimeStatus.unplacedSurfaceCount = placement.countUnplaced(windows, sessions);
363
405
  const view = resolveFocusedWorkspaceView(windows, workspaces, sessions, placement.options);
364
406
  if (view) placement.learnFromPlacements(view.sessions, windows);
407
+ projection.setDirectory(resolveWindowDirectory(windows, workspaces, sessions, placement.options));
365
408
  projection.publishWorkspaceView(
366
409
  view ?? { workspace: null, sessions: [], focusedSessionId: null, focusedCardId: null },
367
410
  );
@@ -36,6 +36,11 @@ export function createNativePanelProjection({ isNiriSession, publish }) {
36
36
  /** Read-only AK task claims joined onto cards; absent until the first successful AK read. */
37
37
  /** @type {{claims: import("../common/contracts.ts").AkTaskClaim[]; deferred: import("../common/contracts.ts").AkTaskDeferred[]} | null} */
38
38
  let akTasks = null;
39
+ /** Every agent window on every workspace, for the brand block's window list. */
40
+ /** @type {import("../common/workspace-view.mjs").WindowDirectoryEntry[]} */
41
+ let directory = [];
42
+ /** @type {Map<string, SessionRecord>} */
43
+ let directoryCards = new Map();
39
44
  let revision = 0;
40
45
 
41
46
  function allSessions() {
@@ -83,6 +88,7 @@ export function createNativePanelProjection({ isNiriSession, publish }) {
83
88
  focusedCardId,
84
89
  generatedAt: snapshot.generatedAt,
85
90
  sessions,
91
+ windows: directory,
86
92
  };
87
93
  }
88
94
 
@@ -117,8 +123,24 @@ export function createNativePanelProjection({ isNiriSession, publish }) {
117
123
  akTasks = state;
118
124
  send();
119
125
  },
120
- /** @param {string} targetId */
121
- resolveTarget: (targetId) => resolveSnapshotSession(getDisplaySessions(), targetId),
126
+ /**
127
+ * A card on this workspace, or else an entry of the window list, which spans every workspace.
128
+ * @param {string} targetId
129
+ */
130
+ resolveTarget: (targetId) =>
131
+ resolveSnapshotSession(getDisplaySessions(), targetId) ??
132
+ directoryCards.get(targetId) ??
133
+ null,
134
+ /** @param {number} windowId */
135
+ hasDirectoryWindow: (windowId) => directory.some((entry) => entry.windowId === windowId),
136
+ /**
137
+ * Replace the window list; it is published with the next view.
138
+ * @param {{entries: import("../common/workspace-view.mjs").WindowDirectoryEntry[]; cards: Map<string, SessionRecord>}} next
139
+ */
140
+ setDirectory(next) {
141
+ directory = next.entries;
142
+ directoryCards = next.cards;
143
+ },
122
144
  /** @param {WorkspaceView} view */
123
145
  publishWorkspaceView(view) {
124
146
  workspace = view.workspace ?? null;