@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.
- package/README.md +1 -0
- package/native/bin/linux-x64-gnu/artifact.json +3 -2
- package/native/bin/linux-x64-gnu/pi-activity-strip-panel +0 -0
- package/native/panel/src/app.rs +780 -29
- package/native/panel/src/directory_view.rs +409 -0
- package/native/panel/src/main.rs +1 -0
- package/native/panel/src/protocol.rs +71 -0
- package/native/panel/src/runtime.rs +43 -11
- package/native/panel/src/style.css +85 -0
- package/package.json +2 -3
- package/src/common/contracts.d.ts +1 -0
- package/src/common/contracts.ts +1 -0
- package/src/common/workspace-view.mjs +64 -0
- package/src/native/main.mjs +44 -1
- package/src/native/panel-projection.mjs +24 -2
package/src/common/contracts.ts
CHANGED
|
@@ -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(
|
package/src/native/main.mjs
CHANGED
|
@@ -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
|
-
/**
|
|
121
|
-
|
|
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;
|