···21632163 winOptions.y = parseInt(options.y);
21642164 }
2165216521662166+ // Off-screen bounds validation: if explicit x/y were provided, verify the
21672167+ // center point falls on an active display. If not (e.g. monitor disconnected),
21682168+ // drop x/y so the window centers on the current display instead.
21692169+ if (winOptions.x !== undefined && winOptions.y !== undefined) {
21702170+ const centerX = winOptions.x + Math.round(((winOptions.width as number) || 800) / 2);
21712171+ const centerY = winOptions.y + Math.round(((winOptions.height as number) || 600) / 2);
21722172+ const displays = screen.getAllDisplays();
21732173+ const onScreen = displays.some(d => {
21742174+ const { x: dx, y: dy, width: dw, height: dh } = d.bounds;
21752175+ return centerX >= dx && centerX < dx + dw && centerY >= dy && centerY < dy + dh;
21762176+ });
21772177+ if (!onScreen) {
21782178+ DEBUG && console.log(`[window-open] Dropping off-screen position (${winOptions.x},${winOptions.y}) center=(${centerX},${centerY})`);
21792179+ delete winOptions.x;
21802180+ delete winOptions.y;
21812181+ }
21822182+ }
21832183+21662184 // Determine opener window and whether it's a real content parent
21672185 // Background/infrastructure windows (background.html, extension-host.html) are
21682186 // not real parents — they're just the IPC sender. Only content windows count
···48324850 cancelled: false,
48334851 ...result,
48344852 };
48534853+ } catch (error) {
48544854+ const message = error instanceof Error ? error.message : String(error);
48554855+ return { success: false, error: message };
48564856+ }
48574857+ });
48584858+48594859+ // Save group workspace layouts on demand (called from groups extension)
48604860+ ipcMain.handle('save-group-workspaces', async () => {
48614861+ try {
48624862+ const { saveGroupWorkspaces } = await import('./session.js');
48634863+ saveGroupWorkspaces();
48644864+ return { success: true };
48354865 } catch (error) {
48364866 const message = error instanceof Error ? error.message : String(error);
48374867 return { success: false, error: message };
+87
backend/electron/session.ts
···271271 `).run('session-metadata', JSON.stringify(metadata), Date.now());
272272273273 DEBUG && console.log(`[session] Session snapshot saved successfully`);
274274+275275+ // Also save per-group workspace snapshots (for group layout restore)
276276+ saveGroupWorkspaces();
274277 } catch (error) {
275278 console.error('[session] Failed to save session snapshot:', error);
276279 }
···747750 DEBUG && console.log(`[session] Autosave interval changed: ${_autosaveIntervalMinutes} -> ${intervalMinutes}`);
748751 startAutosaveTimer(intervalMinutes);
749752 }
753753+}
754754+755755+/**
756756+ * Save per-group workspace snapshots to extension_settings.
757757+ *
758758+ * Groups windows by their group mode context and writes a layout snapshot
759759+ * for each group. Used by openGroup() to restore window positions.
760760+ * Synchronous (safe for before-quit handler).
761761+ */
762762+export function saveGroupWorkspaces(): void {
763763+ let db;
764764+ try {
765765+ db = getDb();
766766+ } catch {
767767+ return;
768768+ }
769769+770770+ const registeredWindows = getAllWindows();
771771+772772+ // Use BrowserWindow.getAllWindows() order for z-order approximation
773773+ const allBrowserWindows = BrowserWindow.getAllWindows();
774774+ const zOrderMap = new Map<number, number>();
775775+ allBrowserWindows.forEach((win, index) => {
776776+ zOrderMap.set(win.id, index);
777777+ });
778778+779779+ // Collect windows grouped by groupId
780780+ const groupWindows = new Map<string, { groupName: string; windows: Array<{ url: string; bounds: Electron.Rectangle; zOrder: number; focused: boolean }> }>();
781781+782782+ for (const [id] of registeredWindows) {
783783+ const win = BrowserWindow.fromId(id);
784784+ if (!win || win.isDestroyed() || !win.isVisible()) continue;
785785+786786+ const rawUrl = win.webContents.getURL();
787787+ if (rawUrl.includes('peek://app/background.html') || rawUrl.includes('peek://app/extension-host.html')) continue;
788788+789789+ // Check if this window is in group mode
790790+ let modeEntry;
791791+ try {
792792+ modeEntry = getContextEntry('mode', id);
793793+ } catch {
794794+ continue;
795795+ }
796796+ if (!modeEntry || modeEntry.value !== 'group') continue;
797797+798798+ const metadata = modeEntry.metadata as Record<string, unknown> | null;
799799+ const groupId = metadata?.groupId as string | undefined;
800800+ const groupName = metadata?.groupName as string | undefined;
801801+ if (!groupId) continue;
802802+803803+ const realUrl = extractRealUrl(rawUrl);
804804+ const canvasBounds = extractCanvasBounds(rawUrl);
805805+ const bounds = canvasBounds || win.getBounds();
806806+807807+ if (!groupWindows.has(groupId)) {
808808+ groupWindows.set(groupId, { groupName: groupName || '', windows: [] });
809809+ }
810810+ groupWindows.get(groupId)!.windows.push({
811811+ url: realUrl,
812812+ bounds,
813813+ zOrder: zOrderMap.get(id) ?? 0,
814814+ focused: win.isFocused(),
815815+ });
816816+ }
817817+818818+ // Write each group's workspace snapshot
819819+ const upsert = db.prepare(`
820820+ INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt)
821821+ VALUES (?, 'group-workspaces', ?, ?, ?)
822822+ `);
823823+824824+ for (const [groupId, group] of groupWindows) {
825825+ const snapshot = {
826826+ version: 1,
827827+ groupId,
828828+ groupName: group.groupName,
829829+ createdAt: Date.now(),
830830+ windows: group.windows,
831831+ };
832832+ const key = `workspace:${groupId}`;
833833+ upsert.run(`group-workspaces:${key}`, key, JSON.stringify(snapshot), Date.now());
834834+ }
835835+836836+ DEBUG && console.log(`[session] Saved workspace snapshots for ${groupWindows.size} group(s)`);
750837}
751838752839/**
+51-2
extensions/groups/background.js
···231231 }
232232233233 console.log(`[ext:groups] Saved ${savedCount} URLs to group "${groupName}"`);
234234+235235+ // Persist current window layouts for this group
236236+ try {
237237+ if (api.session?.saveGroupWorkspaces) {
238238+ await api.session.saveGroupWorkspaces();
239239+ debug && console.log('[ext:groups] Group workspace layouts saved');
240240+ }
241241+ } catch (err) {
242242+ debug && console.log('[ext:groups] Failed to save workspace layouts:', err);
243243+ }
244244+234245 return { success: true, count: savedCount, total: listResult.windows.length };
235246};
236247···290301 activeGroupId = tag.id;
291302 activeGroupName = tag.name;
292303304304+ // Load saved workspace snapshot for this group (if any)
305305+ let savedBoundsMap = null;
306306+ let sortedUrlItems = urlItems;
307307+ try {
308308+ const wsResult = await api.settings.getExtKey('group-workspaces', 'workspace:' + tag.id);
309309+ if (wsResult.success && wsResult.data) {
310310+ const snapshot = typeof wsResult.data === 'string' ? JSON.parse(wsResult.data) : wsResult.data;
311311+ if (snapshot.version === 1 && Array.isArray(snapshot.windows)) {
312312+ // Build URL → bounds map
313313+ savedBoundsMap = new Map();
314314+ for (const w of snapshot.windows) {
315315+ if (w.url && w.bounds) {
316316+ savedBoundsMap.set(normalizeUrlForCompare(w.url), { bounds: w.bounds, zOrder: w.zOrder ?? 0 });
317317+ }
318318+ }
319319+ // Sort items by saved z-order (highest zOrder = opened later = on top)
320320+ sortedUrlItems = [...urlItems].sort((a, b) => {
321321+ const aZ = savedBoundsMap.get(normalizeUrlForCompare(a._openUrl))?.zOrder ?? 0;
322322+ const bZ = savedBoundsMap.get(normalizeUrlForCompare(b._openUrl))?.zOrder ?? 0;
323323+ return bZ - aZ;
324324+ });
325325+ debug && console.log(`[ext:groups] Loaded workspace snapshot with ${snapshot.windows.length} saved positions`);
326326+ }
327327+ }
328328+ } catch (err) {
329329+ debug && console.log('[ext:groups] No saved workspace layout, using defaults:', err);
330330+ }
331331+293332 // Open windows and set group mode
294333 const openedWindows = [];
295295- for (const item of urlItems) {
334334+ for (const item of sortedUrlItems) {
335335+ // Look up saved bounds for this URL
336336+ const savedEntry = savedBoundsMap?.get(normalizeUrlForCompare(item._openUrl));
337337+ const boundsOpts = savedEntry?.bounds ? {
338338+ x: savedEntry.bounds.x,
339339+ y: savedEntry.bounds.y,
340340+ width: savedEntry.bounds.width,
341341+ height: savedEntry.bounds.height,
342342+ } : {};
343343+296344 const result = await api.window.open(item._openUrl, {
297345 role: 'content',
298346 trackingSource: 'cmd',
···302350 groupId: tag.id,
303351 groupName: tag.name,
304352 color: tag.color
305305- }
353353+ },
354354+ ...boundsOpts
306355 });
307356 if (result?.id) {
308357 openedWindows.push(result.id);
+11
preload.js
···17311731 }
17321732};
1733173317341734+// Session API — allows extensions to trigger session-level operations
17351735+api.session = {
17361736+ /**
17371737+ * Save group workspace layouts (window positions per group)
17381738+ * @returns {Promise<{success: boolean, error?: string}>}
17391739+ */
17401740+ saveGroupWorkspaces: () => {
17411741+ return ipcRenderer.invoke('save-group-workspaces');
17421742+ }
17431743+};
17441744+17341745// Net fetch API - proxies HTTP requests through main process to bypass CORS
17351746// Useful for extensions that need to fetch from external APIs
17361747api.net = {