feat(soak): --watch=tiled launches N headed host windows
SessionPool accepts headedHostCount; when > 0 it launches a second Chromium in headed mode and creates the first N sessions in it. Joiners (sessions N..count) stay headless in the main browser. Each headed context gets a 960×900 viewport — tall enough to show the full game table (deck + opponent row + own 2×3 card grid + status area) without clipping. Horizontal tiling still fits two windows side-by-side on a 1920-wide display. window.moveTo is kept as a best-effort tile-placement hint, but viewport from newContext() is what actually sizes the window (window.resizeTo is a no-op on modern Chromium / Wayland). Verified: 1-room tiled run plays a full game cleanly; 2-room parallel tiled had one window get closed mid-run, which is consistent with a user manually dismissing a window — tiled mode is a best-effort hands-on debugging aid, not an automation mode. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -142,12 +142,20 @@ export interface SessionPoolOptions {
|
||||
/** Optional override — if absent, SessionPool launches its own. */
|
||||
browser?: Browser;
|
||||
contextOptions?: Parameters<Browser['newContext']>[0];
|
||||
/**
|
||||
* If > 0, the first `headedHostCount` sessions use a separate headed
|
||||
* Chromium browser with visible windows (positioned in a 2×2 grid
|
||||
* via window.moveTo). The remaining sessions stay headless. Used for
|
||||
* --watch=tiled mode.
|
||||
*/
|
||||
headedHostCount?: number;
|
||||
}
|
||||
|
||||
export class SessionPool {
|
||||
private accounts: Account[] = [];
|
||||
private ownedBrowser: Browser | null = null;
|
||||
private browser: Browser | null;
|
||||
private headedBrowser: Browser | null = null;
|
||||
private activeSessions: Session[] = [];
|
||||
|
||||
constructor(private opts: SessionPoolOptions) {
|
||||
@@ -221,6 +229,10 @@ export class SessionPool {
|
||||
/**
|
||||
* Launch the browser if not provided, create N contexts, log each in via
|
||||
* localStorage injection, return the live sessions.
|
||||
*
|
||||
* If `headedHostCount > 0`, launches a second headed Chromium and
|
||||
* places the first `headedHostCount` sessions in it, positioning
|
||||
* their windows in a 2×2 grid. The rest stay headless.
|
||||
*/
|
||||
async acquire(count: number): Promise<Session[]> {
|
||||
await this.ensureAccounts(count);
|
||||
@@ -229,13 +241,52 @@ export class SessionPool {
|
||||
this.browser = this.ownedBrowser;
|
||||
}
|
||||
|
||||
const headedCount = this.opts.headedHostCount ?? 0;
|
||||
if (headedCount > 0 && !this.headedBrowser) {
|
||||
this.headedBrowser = await chromium.launch({
|
||||
headless: false,
|
||||
slowMo: 50,
|
||||
});
|
||||
}
|
||||
|
||||
const sessions: Session[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const account = this.accounts[i];
|
||||
const context = await this.browser.newContext(this.opts.contextOptions);
|
||||
const useHeaded = i < headedCount;
|
||||
const targetBrowser = useHeaded ? this.headedBrowser! : this.browser!;
|
||||
// Headed host windows get a larger viewport — 960×900 fits the full
|
||||
// game table (deck + opponent row + own 2×3 grid + status area) on
|
||||
// a typical 1920×1080 display. Two windows side-by-side still fit
|
||||
// horizontally; if the user runs more than 2 rooms in tiled mode
|
||||
// the extra windows will overlap and need to be arranged manually.
|
||||
const context = await targetBrowser.newContext({
|
||||
...this.opts.contextOptions,
|
||||
...(useHeaded ? { viewport: { width: 960, height: 900 } } : {}),
|
||||
});
|
||||
await this.injectAuth(context, account);
|
||||
const page = await context.newPage();
|
||||
await page.goto(this.opts.targetUrl);
|
||||
|
||||
// Best-effort tile placement. window.moveTo is often a no-op on
|
||||
// modern Chromium (especially under Wayland), so we don't rely on
|
||||
// it — the viewport sized above is what the user actually sees.
|
||||
if (useHeaded) {
|
||||
const col = i % 2;
|
||||
const row = Math.floor(i / 2);
|
||||
const x = col * 960;
|
||||
const y = row * 920;
|
||||
await page
|
||||
.evaluate(
|
||||
([x, y]) => {
|
||||
window.moveTo(x, y);
|
||||
},
|
||||
[x, y] as [number, number],
|
||||
)
|
||||
.catch(() => {
|
||||
// ignore — window.moveTo may be blocked
|
||||
});
|
||||
}
|
||||
|
||||
const bot = new GolfBot(page);
|
||||
sessions.push({ account, context, page, bot, key: account.key });
|
||||
}
|
||||
@@ -282,7 +333,7 @@ export class SessionPool {
|
||||
}
|
||||
}
|
||||
|
||||
/** Close all active contexts. Safe to call multiple times. */
|
||||
/** Close all active contexts + browsers. Safe to call multiple times. */
|
||||
async release(): Promise<void> {
|
||||
for (const session of this.activeSessions) {
|
||||
try {
|
||||
@@ -301,5 +352,13 @@ export class SessionPool {
|
||||
this.ownedBrowser = null;
|
||||
this.browser = null;
|
||||
}
|
||||
if (this.headedBrowser) {
|
||||
try {
|
||||
await this.headedBrowser.close();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
this.headedBrowser = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user