leapfrog-mcp 0.6.2 → 0.6.3
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/dist/adaptive-wait.d.ts +72 -0
- package/dist/adaptive-wait.js +695 -0
- package/dist/api-intelligence.d.ts +82 -0
- package/dist/api-intelligence.js +575 -0
- package/dist/captcha-solver.d.ts +26 -0
- package/dist/captcha-solver.js +547 -0
- package/dist/cdp-connector.d.ts +33 -0
- package/dist/cdp-connector.js +176 -0
- package/dist/consent-dismiss.d.ts +33 -0
- package/dist/consent-dismiss.js +358 -0
- package/dist/crash-recovery.d.ts +74 -0
- package/dist/crash-recovery.js +242 -0
- package/dist/domain-knowledge.d.ts +149 -0
- package/dist/domain-knowledge.js +449 -0
- package/dist/harness-intelligence.d.ts +65 -0
- package/dist/harness-intelligence.js +432 -0
- package/dist/humanize-fingerprint.d.ts +42 -0
- package/dist/humanize-fingerprint.js +161 -0
- package/dist/humanize-mouse.d.ts +95 -0
- package/dist/humanize-mouse.js +275 -0
- package/dist/humanize-pause.d.ts +48 -0
- package/dist/humanize-pause.js +111 -0
- package/dist/humanize-scroll.d.ts +67 -0
- package/dist/humanize-scroll.js +185 -0
- package/dist/humanize-typing.d.ts +60 -0
- package/dist/humanize-typing.js +258 -0
- package/dist/humanize-utils.d.ts +62 -0
- package/dist/humanize-utils.js +100 -0
- package/dist/intervention.d.ts +65 -0
- package/dist/intervention.js +591 -0
- package/dist/logger.d.ts +13 -0
- package/dist/logger.js +47 -0
- package/dist/network-intelligence.d.ts +70 -0
- package/dist/network-intelligence.js +424 -0
- package/dist/page-classifier.d.ts +33 -0
- package/dist/page-classifier.js +1000 -0
- package/dist/paginate.d.ts +42 -0
- package/dist/paginate.js +693 -0
- package/dist/recording.d.ts +72 -0
- package/dist/recording.js +934 -0
- package/dist/script-executor.d.ts +31 -0
- package/dist/script-executor.js +249 -0
- package/dist/session-hud.d.ts +20 -0
- package/dist/session-hud.js +134 -0
- package/dist/snapshot-differ.d.ts +26 -0
- package/dist/snapshot-differ.js +225 -0
- package/dist/ssrf.d.ts +28 -0
- package/dist/ssrf.js +290 -0
- package/dist/stealth-audit.d.ts +27 -0
- package/dist/stealth-audit.js +719 -0
- package/dist/stealth.d.ts +195 -0
- package/dist/stealth.js +1157 -0
- package/dist/tab-manager.d.ts +14 -0
- package/dist/tab-manager.js +306 -0
- package/dist/tiles-coordinator.d.ts +106 -0
- package/dist/tiles-coordinator.js +358 -0
- package/package.json +1 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { BrowserContext, Page } from "playwright-core";
|
|
2
|
+
import type { Session, TabInfo, WaitCondition } from "./types.js";
|
|
3
|
+
export declare class TabManager {
|
|
4
|
+
attachToContext(context: BrowserContext, session: Session): void;
|
|
5
|
+
getActivePage(session: Session): Page;
|
|
6
|
+
listTabs(session: Session): Promise<TabInfo[]>;
|
|
7
|
+
switchTab(session: Session, tabIndex: number): Page;
|
|
8
|
+
closeTab(session: Session, tabIndex?: number): Promise<Page>;
|
|
9
|
+
waitFor(page: Page, session: Session, condition: WaitCondition): Promise<void>;
|
|
10
|
+
private pruneClosedPages;
|
|
11
|
+
}
|
|
12
|
+
/** Singleton instance for use across the server */
|
|
13
|
+
export declare const tabManager: TabManager;
|
|
14
|
+
export default TabManager;
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { stealth } from "./stealth.js";
|
|
2
|
+
// ─── Tab Manager ──────────────────────────────────────────────────────────
|
|
3
|
+
//
|
|
4
|
+
// Manages multiple tabs (pages) within a single browser session.
|
|
5
|
+
// New tabs auto-become active (best UX for OAuth/popup flows).
|
|
6
|
+
// Closed pages are auto-cleaned from the array on list/switch operations.
|
|
7
|
+
// getActivePage() is the single source of truth for all tool operations.
|
|
8
|
+
//
|
|
9
|
+
const MAX_WAIT_TIMEOUT = 30_000;
|
|
10
|
+
const DEFAULT_WAIT_TIMEOUT = 10_000;
|
|
11
|
+
/**
|
|
12
|
+
* Ensure session.pages and session.activePageIndex are initialized.
|
|
13
|
+
* Safe to call multiple times — only initializes on first call or if
|
|
14
|
+
* the session was created before TabManager was wired up.
|
|
15
|
+
*/
|
|
16
|
+
function ensureInitialized(session) {
|
|
17
|
+
if (!session.pages) {
|
|
18
|
+
session.pages = [session.page];
|
|
19
|
+
}
|
|
20
|
+
if (session.activePageIndex === undefined || session.activePageIndex === null) {
|
|
21
|
+
session.activePageIndex = 0;
|
|
22
|
+
}
|
|
23
|
+
return { pages: session.pages, activePageIndex: session.activePageIndex };
|
|
24
|
+
}
|
|
25
|
+
export class TabManager {
|
|
26
|
+
// ── attachToContext ─────────────────────────────────────────────────
|
|
27
|
+
//
|
|
28
|
+
// Call once when a session is created. Initializes the pages array from
|
|
29
|
+
// the session's initial page and sets up event handlers to auto-track
|
|
30
|
+
// new tabs/popups opened by the browser context.
|
|
31
|
+
attachToContext(context, session) {
|
|
32
|
+
// Initialize pages array with the session's existing page
|
|
33
|
+
session.pages = [session.page];
|
|
34
|
+
session.activePageIndex = 0;
|
|
35
|
+
// Listen for new pages (popups, window.open, target=_blank links)
|
|
36
|
+
// BUG-002: Do NOT auto-switch activePageIndex to popup — keep the original
|
|
37
|
+
// page active. Popups are tracked but the caller must explicitly switch.
|
|
38
|
+
context.on("page", (newPage) => {
|
|
39
|
+
const { pages } = ensureInitialized(session);
|
|
40
|
+
// Add to pages array (but do NOT change activePageIndex)
|
|
41
|
+
pages.push(newPage);
|
|
42
|
+
// Auto-dismiss dialogs on the new page (same pattern as session-manager)
|
|
43
|
+
// P1 #6: Add 200-500ms random delay — instant dismiss (< 30ms) is a headless signal
|
|
44
|
+
newPage.on("dialog", (dialog) => {
|
|
45
|
+
const delay = stealth.isEnabled() ? stealth.getDialogDelay() : 0;
|
|
46
|
+
setTimeout(() => dialog.dismiss().catch(() => { }), delay);
|
|
47
|
+
});
|
|
48
|
+
// When this page closes, clean it from the array
|
|
49
|
+
newPage.on("close", () => {
|
|
50
|
+
this.pruneClosedPages(session);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
// ── getActivePage ──────────────────────────────────────────────────
|
|
55
|
+
//
|
|
56
|
+
// Single source of truth for the current active page. All tools should
|
|
57
|
+
// call this instead of accessing session.page directly.
|
|
58
|
+
// Prunes closed pages and falls back to the first open page if the
|
|
59
|
+
// active index has become invalid.
|
|
60
|
+
getActivePage(session) {
|
|
61
|
+
this.pruneClosedPages(session);
|
|
62
|
+
const { pages } = ensureInitialized(session);
|
|
63
|
+
let idx = session.activePageIndex;
|
|
64
|
+
// Validate index bounds after pruning
|
|
65
|
+
if (idx < 0 || idx >= pages.length) {
|
|
66
|
+
idx = 0;
|
|
67
|
+
session.activePageIndex = 0;
|
|
68
|
+
}
|
|
69
|
+
const page = pages[idx];
|
|
70
|
+
// Keep session.page in sync for backward compatibility
|
|
71
|
+
if (page && session.page !== page) {
|
|
72
|
+
session.page = page;
|
|
73
|
+
}
|
|
74
|
+
return page;
|
|
75
|
+
}
|
|
76
|
+
// ── listTabs ───────────────────────────────────────────────────────
|
|
77
|
+
//
|
|
78
|
+
// Returns info about all open tabs. Cleans up closed pages first.
|
|
79
|
+
// Each entry includes index, url, title, and active status.
|
|
80
|
+
async listTabs(session) {
|
|
81
|
+
this.pruneClosedPages(session);
|
|
82
|
+
const { pages, activePageIndex } = ensureInitialized(session);
|
|
83
|
+
const tabs = [];
|
|
84
|
+
for (let i = 0; i < pages.length; i++) {
|
|
85
|
+
const page = pages[i];
|
|
86
|
+
const isActive = i === activePageIndex;
|
|
87
|
+
let url = "";
|
|
88
|
+
let title = "";
|
|
89
|
+
try {
|
|
90
|
+
url = page.url();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Page may have crashed between prune and access
|
|
94
|
+
url = "(unavailable)";
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
title = await page.title();
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
title = "(unavailable)";
|
|
101
|
+
}
|
|
102
|
+
tabs.push({ index: i, url, title, isActive });
|
|
103
|
+
}
|
|
104
|
+
return tabs;
|
|
105
|
+
}
|
|
106
|
+
// ── switchTab ──────────────────────────────────────────────────────
|
|
107
|
+
//
|
|
108
|
+
// Switch the active page by index. Pass -1 to switch to the last
|
|
109
|
+
// (most recently opened) tab. Validates bounds and brings the
|
|
110
|
+
// target page to front.
|
|
111
|
+
switchTab(session, tabIndex) {
|
|
112
|
+
this.pruneClosedPages(session);
|
|
113
|
+
const { pages } = ensureInitialized(session);
|
|
114
|
+
const count = pages.length;
|
|
115
|
+
if (count === 0) {
|
|
116
|
+
throw new Error("No open tabs in session.");
|
|
117
|
+
}
|
|
118
|
+
// -1 means "last tab" (most recently opened)
|
|
119
|
+
const resolvedIndex = tabIndex === -1 ? count - 1 : tabIndex;
|
|
120
|
+
if (resolvedIndex < 0 || resolvedIndex >= count) {
|
|
121
|
+
throw new Error(`Tab index ${tabIndex} out of bounds. Valid range: 0-${count - 1} (or -1 for last).`);
|
|
122
|
+
}
|
|
123
|
+
const previousIndex = session.activePageIndex;
|
|
124
|
+
session.activePageIndex = resolvedIndex;
|
|
125
|
+
const page = pages[resolvedIndex];
|
|
126
|
+
// Keep session.page in sync for backward compatibility
|
|
127
|
+
session.page = page;
|
|
128
|
+
// BUG FIX: Invalidate snapshot refs when switching to a different tab.
|
|
129
|
+
// Refs from tab A's DOM are meaningless on tab B. Use the same stale-ref
|
|
130
|
+
// pattern as navigation: bump navGeneration and set staleRefThreshold so
|
|
131
|
+
// the act/extract handlers reject old refs and require a fresh snapshot.
|
|
132
|
+
if (resolvedIndex !== previousIndex) {
|
|
133
|
+
session.staleRefThreshold = session.refCounter;
|
|
134
|
+
session.navGeneration = (session.navGeneration ?? 0) + 1;
|
|
135
|
+
}
|
|
136
|
+
// Bring to front (non-blocking — fire and forget)
|
|
137
|
+
page.bringToFront().catch(() => { });
|
|
138
|
+
return page;
|
|
139
|
+
}
|
|
140
|
+
// ── closeTab ───────────────────────────────────────────────────────
|
|
141
|
+
//
|
|
142
|
+
// Close a tab by index (default: active tab). Cannot close the last
|
|
143
|
+
// remaining tab. If closing the active tab, switches to the previous
|
|
144
|
+
// tab or the first available one.
|
|
145
|
+
async closeTab(session, tabIndex) {
|
|
146
|
+
this.pruneClosedPages(session);
|
|
147
|
+
const { pages, activePageIndex } = ensureInitialized(session);
|
|
148
|
+
const count = pages.length;
|
|
149
|
+
if (count <= 1) {
|
|
150
|
+
throw new Error("Cannot close the last remaining tab.");
|
|
151
|
+
}
|
|
152
|
+
// Default to active tab if no index specified
|
|
153
|
+
const targetIndex = tabIndex ?? activePageIndex;
|
|
154
|
+
if (targetIndex < 0 || targetIndex >= count) {
|
|
155
|
+
throw new Error(`Tab index ${targetIndex} out of bounds. Valid range: 0-${count - 1}.`);
|
|
156
|
+
}
|
|
157
|
+
const targetPage = pages[targetIndex];
|
|
158
|
+
// Remove from array first (before closing, to handle race conditions)
|
|
159
|
+
pages.splice(targetIndex, 1);
|
|
160
|
+
// Determine new active index
|
|
161
|
+
if (targetIndex === activePageIndex) {
|
|
162
|
+
// Closing the active tab — prefer previous tab, clamp to valid range
|
|
163
|
+
session.activePageIndex = Math.min(Math.max(targetIndex - 1, 0), pages.length - 1);
|
|
164
|
+
}
|
|
165
|
+
else if (targetIndex < activePageIndex) {
|
|
166
|
+
// Closing a tab before the active one — adjust index to keep same page active
|
|
167
|
+
session.activePageIndex = activePageIndex - 1;
|
|
168
|
+
}
|
|
169
|
+
// If closing a tab after the active one, activePageIndex stays the same
|
|
170
|
+
// Close the page (async, may throw if already closed)
|
|
171
|
+
try {
|
|
172
|
+
await targetPage.close();
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Page may already be closed — safe to ignore
|
|
176
|
+
}
|
|
177
|
+
const newActivePage = pages[session.activePageIndex];
|
|
178
|
+
// Keep session.page in sync
|
|
179
|
+
session.page = newActivePage;
|
|
180
|
+
// Bring the new active page to front
|
|
181
|
+
newActivePage.bringToFront().catch(() => { });
|
|
182
|
+
return newActivePage;
|
|
183
|
+
}
|
|
184
|
+
// ── waitFor ────────────────────────────────────────────────────────
|
|
185
|
+
//
|
|
186
|
+
// Unified smart wait supporting multiple condition types:
|
|
187
|
+
// - element: wait for a CSS selector or @eN ref to be visible
|
|
188
|
+
// - text: wait for text content to appear (optionally scoped to target)
|
|
189
|
+
// - network_idle: wait for network to settle
|
|
190
|
+
// - navigation: wait for URL to match a pattern
|
|
191
|
+
// - js: wait for a JS expression to return truthy
|
|
192
|
+
async waitFor(page, session, condition) {
|
|
193
|
+
const timeout = Math.min(condition.timeout ?? DEFAULT_WAIT_TIMEOUT, MAX_WAIT_TIMEOUT);
|
|
194
|
+
// Resolve @eN refs to selectors (with stale-ref detection)
|
|
195
|
+
const resolveTarget = (ref) => {
|
|
196
|
+
if (ref.startsWith("@e")) {
|
|
197
|
+
if ((session.navGeneration ?? 0) > (session.refNavGeneration ?? 0)) {
|
|
198
|
+
throw new Error(`Ref ${ref} is stale — the page has navigated since the last snapshot. Take a new snapshot to get updated refs.`);
|
|
199
|
+
}
|
|
200
|
+
const selector = session.refMap.get(ref);
|
|
201
|
+
if (!selector) {
|
|
202
|
+
throw new Error(`Ref ${ref} not found. Take a fresh snapshot first.`);
|
|
203
|
+
}
|
|
204
|
+
return selector;
|
|
205
|
+
}
|
|
206
|
+
return ref; // CSS selectors don't go stale
|
|
207
|
+
};
|
|
208
|
+
switch (condition.type) {
|
|
209
|
+
case "element": {
|
|
210
|
+
if (!condition.target) {
|
|
211
|
+
throw new Error("'element' wait requires a target (CSS selector or @eN ref).");
|
|
212
|
+
}
|
|
213
|
+
const selector = resolveTarget(condition.target);
|
|
214
|
+
await page.locator(selector).waitFor({ state: "visible", timeout });
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
case "text": {
|
|
218
|
+
if (!condition.text) {
|
|
219
|
+
throw new Error("'text' wait requires a text string.");
|
|
220
|
+
}
|
|
221
|
+
if (condition.target) {
|
|
222
|
+
// Scoped to a specific element
|
|
223
|
+
const selector = resolveTarget(condition.target);
|
|
224
|
+
await page
|
|
225
|
+
.locator(selector)
|
|
226
|
+
.getByText(condition.text)
|
|
227
|
+
.waitFor({ timeout });
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
// Page-level text search
|
|
231
|
+
await page.getByText(condition.text).waitFor({ timeout });
|
|
232
|
+
}
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
case "network_idle": {
|
|
236
|
+
await page.waitForLoadState("networkidle", { timeout });
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
case "navigation": {
|
|
240
|
+
const urlPattern = condition.text || "**";
|
|
241
|
+
await page.waitForURL(urlPattern, { timeout });
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
case "js": {
|
|
245
|
+
if (!condition.js) {
|
|
246
|
+
throw new Error("'js' wait requires a js expression.");
|
|
247
|
+
}
|
|
248
|
+
await page.waitForFunction(condition.js, undefined, { timeout });
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
default: {
|
|
252
|
+
const _exhaustive = condition.type;
|
|
253
|
+
throw new Error(`Unknown wait condition type: ${_exhaustive}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// ── Internal: pruneClosedPages ─────────────────────────────────────
|
|
258
|
+
//
|
|
259
|
+
// Remove closed pages from session.pages and adjust activePageIndex.
|
|
260
|
+
// Called before any read/write operation on the pages array.
|
|
261
|
+
pruneClosedPages(session) {
|
|
262
|
+
const { pages, activePageIndex } = ensureInitialized(session);
|
|
263
|
+
const activePage = activePageIndex >= 0 && activePageIndex < pages.length
|
|
264
|
+
? pages[activePageIndex]
|
|
265
|
+
: null;
|
|
266
|
+
// Filter out closed pages
|
|
267
|
+
const openPages = pages.filter((p) => !p.isClosed());
|
|
268
|
+
if (openPages.length === pages.length) {
|
|
269
|
+
// Nothing was pruned
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
session.pages = openPages;
|
|
273
|
+
if (openPages.length === 0) {
|
|
274
|
+
// BUG-002: All pages closed — create a recovery page in the same context
|
|
275
|
+
// so the session stays usable instead of becoming a zombie.
|
|
276
|
+
session.activePageIndex = 0;
|
|
277
|
+
session.context.newPage().then((recoveryPage) => {
|
|
278
|
+
if (!session.pages)
|
|
279
|
+
session.pages = [];
|
|
280
|
+
session.pages.push(recoveryPage);
|
|
281
|
+
session.page = recoveryPage;
|
|
282
|
+
session.activePageIndex = 0;
|
|
283
|
+
// Auto-dismiss dialogs on recovery page
|
|
284
|
+
recoveryPage.on("dialog", (d) => d.dismiss().catch(() => { }));
|
|
285
|
+
}).catch(() => {
|
|
286
|
+
// Context may be dead — session will fail on next access
|
|
287
|
+
});
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
// Try to keep the same active page
|
|
291
|
+
if (activePage && !activePage.isClosed()) {
|
|
292
|
+
const newIndex = openPages.indexOf(activePage);
|
|
293
|
+
session.activePageIndex = newIndex >= 0 ? newIndex : 0;
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
// Active page was closed — fall back to last available page
|
|
297
|
+
session.activePageIndex = openPages.length - 1;
|
|
298
|
+
}
|
|
299
|
+
// Keep session.page in sync
|
|
300
|
+
session.page = openPages[session.activePageIndex];
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// ─── Exports ──────────────────────────────────────────────────────────────
|
|
304
|
+
/** Singleton instance for use across the server */
|
|
305
|
+
export const tabManager = new TabManager();
|
|
306
|
+
export default TabManager;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/** A single tile slot representing one MCP instance's screen region. */
|
|
2
|
+
export interface TileSlot {
|
|
3
|
+
sessionId: string;
|
|
4
|
+
instancePid: number;
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
}
|
|
10
|
+
/** Full grid state persisted to ~/.leapfrog/tiles.json. */
|
|
11
|
+
export interface TilesState {
|
|
12
|
+
slots: TileSlot[];
|
|
13
|
+
screenWidth: number;
|
|
14
|
+
screenHeight: number;
|
|
15
|
+
lastUpdated: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Coordinates screen real estate across multiple Leapfrog MCP instances
|
|
19
|
+
* running in separate terminals.
|
|
20
|
+
*
|
|
21
|
+
* All coordination happens through the filesystem — no sockets, no ports,
|
|
22
|
+
* no IPC. State is stored in `~/.leapfrog/tiles.json` and protected by
|
|
23
|
+
* a simple file lock (`~/.leapfrog/tiles.lock`).
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* const tiles = new TilesCoordinator(1920, 1080);
|
|
28
|
+
* const pos = await tiles.claimSlot("session-abc");
|
|
29
|
+
* // pos = { x: 0, y: 0, width: 960, height: 540 }
|
|
30
|
+
*
|
|
31
|
+
* // On shutdown:
|
|
32
|
+
* await tiles.releaseAll();
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare class TilesCoordinator {
|
|
36
|
+
private screenWidth;
|
|
37
|
+
private screenHeight;
|
|
38
|
+
private mySessions;
|
|
39
|
+
private watcher;
|
|
40
|
+
/**
|
|
41
|
+
* Create a new TilesCoordinator.
|
|
42
|
+
* Creates `~/.leapfrog/` if it doesn't already exist.
|
|
43
|
+
*
|
|
44
|
+
* @param screenWidth Total screen width in pixels.
|
|
45
|
+
* @param screenHeight Total screen height in pixels.
|
|
46
|
+
*/
|
|
47
|
+
constructor(screenWidth: number, screenHeight: number);
|
|
48
|
+
/**
|
|
49
|
+
* Update the screen dimensions used for grid calculations.
|
|
50
|
+
* Call this after detecting the real screen size (e.g., via CDP maximize).
|
|
51
|
+
* Triggers a recalculation of all slot positions in the shared state.
|
|
52
|
+
*/
|
|
53
|
+
updateScreenSize(width: number, height: number): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Claim a grid slot for the given session.
|
|
56
|
+
*
|
|
57
|
+
* Dead-PID slots are reaped first, then the new session is appended
|
|
58
|
+
* and all positions are recalculated to fill the grid evenly.
|
|
59
|
+
*
|
|
60
|
+
* @param sessionId Unique identifier for this session.
|
|
61
|
+
* @returns The assigned tile position and dimensions.
|
|
62
|
+
*/
|
|
63
|
+
claimSlot(sessionId: string): Promise<{
|
|
64
|
+
x: number;
|
|
65
|
+
y: number;
|
|
66
|
+
width: number;
|
|
67
|
+
height: number;
|
|
68
|
+
}>;
|
|
69
|
+
/**
|
|
70
|
+
* Release a slot, removing it from the shared grid.
|
|
71
|
+
* Remaining slots are repositioned to fill the gap.
|
|
72
|
+
*
|
|
73
|
+
* @param sessionId The session to release.
|
|
74
|
+
*/
|
|
75
|
+
releaseSlot(sessionId: string): Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* Get the current grid layout including all live instances.
|
|
78
|
+
*
|
|
79
|
+
* @returns A snapshot of the shared tiling state.
|
|
80
|
+
*/
|
|
81
|
+
getLayout(): Promise<TilesState>;
|
|
82
|
+
/**
|
|
83
|
+
* Remove slots whose owning process is no longer running.
|
|
84
|
+
* Called automatically on `claimSlot`, but can be invoked directly
|
|
85
|
+
* for periodic housekeeping.
|
|
86
|
+
*
|
|
87
|
+
* @returns Session IDs of the reaped (dead) slots.
|
|
88
|
+
*/
|
|
89
|
+
reapDeadSlots(): Promise<string[]>;
|
|
90
|
+
/**
|
|
91
|
+
* Watch `tiles.json` for changes made by other instances.
|
|
92
|
+
* The callback fires with the new state whenever the file changes.
|
|
93
|
+
*
|
|
94
|
+
* @param onChange Callback invoked with the updated tiling state.
|
|
95
|
+
*/
|
|
96
|
+
watch(onChange: (state: TilesState) => void): void;
|
|
97
|
+
/**
|
|
98
|
+
* Stop watching for tile changes from other instances.
|
|
99
|
+
*/
|
|
100
|
+
unwatch(): void;
|
|
101
|
+
/**
|
|
102
|
+
* Release all slots owned by this process.
|
|
103
|
+
* Call this on SIGINT / SIGTERM to clean up before exit.
|
|
104
|
+
*/
|
|
105
|
+
releaseAll(): Promise<void>;
|
|
106
|
+
}
|