From 3d35d492ba18bc147c98c9d46eeaefd1eb9e0880 Mon Sep 17 00:00:00 2001 From: Makito Date: Tue, 14 Apr 2026 02:21:10 +0900 Subject: [PATCH] feat(stage-tamagotchi): support detached devtools window with params --- apps/stage-tamagotchi/src/main/index.ts | 4 +- .../src/main/windows/devtools/index.ts | 97 +++++++++++-------- .../src/main/windows/settings/index.ts | 4 +- .../windows/settings/rpc/index.electron.ts | 4 +- .../pages/settings/system/developer.vue | 2 +- apps/stage-tamagotchi/src/shared/eventa.ts | 11 ++- 6 files changed, 75 insertions(+), 47 deletions(-) diff --git a/apps/stage-tamagotchi/src/main/index.ts b/apps/stage-tamagotchi/src/main/index.ts index b7998c9d2..52b318457 100644 --- a/apps/stage-tamagotchi/src/main/index.ts +++ b/apps/stage-tamagotchi/src/main/index.ts @@ -139,7 +139,7 @@ app.whenReady().then(async () => { // BeatSync will create a background window to capture and process audio. const beatSync = injeca.provide('windows:beat-sync', () => setupBeatSync()) - const devtoolsMarkdownStressWindow = injeca.provide('windows:devtools:markdown-stress', () => setupDevtoolsWindow()) + const devtoolsWindow = injeca.provide('windows:devtools', () => setupDevtoolsWindow()) const onboardingWindowManager = injeca.provide('windows:onboarding', { dependsOn: { serverChannel, i18n, windowAuthManager }, @@ -167,7 +167,7 @@ app.whenReady().then(async () => { }) const settingsWindow = injeca.provide('windows:settings', { - dependsOn: { widgetsManager, beatSync, autoUpdater, devtoolsMarkdownStressWindow, serverChannel, mcpStdioManager, i18n, windowAuthManager }, + dependsOn: { widgetsManager, beatSync, autoUpdater, devtoolsWindow, serverChannel, mcpStdioManager, i18n, windowAuthManager }, build: async ({ dependsOn }) => setupSettingsWindowReusableFunc(dependsOn), }) diff --git a/apps/stage-tamagotchi/src/main/windows/devtools/index.ts b/apps/stage-tamagotchi/src/main/windows/devtools/index.ts index 4b9973477..67b851cdb 100644 --- a/apps/stage-tamagotchi/src/main/windows/devtools/index.ts +++ b/apps/stage-tamagotchi/src/main/windows/devtools/index.ts @@ -7,54 +7,74 @@ import icon from '../../../../resources/icon.png?asset' import { baseUrl, getElectronMainDirname, load, withHashRoute } from '../../libs/electron/location' import { createReusableWindow } from '../../libs/electron/window-manager' +export interface OpenDevtoolsWindowParams extends Partial { + key: string + route?: string +} + export interface DevtoolsWindowManager { - openWindow: (route?: string) => Promise - getWindow: () => Promise + openWindow: (params: OpenDevtoolsWindowParams) => Promise } export function setupDevtoolsWindow(): DevtoolsWindowManager { const rendererBase = baseUrl(resolve(getElectronMainDirname(), '..', 'renderer')) - const defaultRoute = '/devtools/markdown-stress' - let currentRoute = defaultRoute + const defaultRoute = '/devtools' + const reusableWindows = new Map>() - const reusable = createReusableWindow(async () => { - const window = new BrowserWindow({ - title: 'Devtools', - width: 1020, - height: 720, - minWidth: 640, - minHeight: 480, - show: false, - icon, - webPreferences: { - preload: join(getElectronMainDirname(), '../preload/index.mjs'), - // Preload exposes Electron APIs and needs Node access. - sandbox: false, - }, + function getReusableForKey(key: string, route: string) { + const existing = reusableWindows.get(key) + if (existing) + return existing + + const reusable = createReusableWindow(async () => { + const window = new BrowserWindow({ + title: 'Devtools', + width: 1020, + height: 720, + minWidth: 640, + minHeight: 480, + show: false, + icon, + webPreferences: { + preload: join(getElectronMainDirname(), '../preload/index.mjs'), + // Preload exposes Electron APIs and needs Node access. + sandbox: false, + }, + }) + + window.on('ready-to-show', () => window.show()) + window.on('closed', () => { + if (reusableWindows.get(key) === reusable) + reusableWindows.delete(key) + }) + window.webContents.setWindowOpenHandler((details) => { + shell.openExternal(details.url) + return { action: 'deny' } + }) + + await load(window, withHashRoute(rendererBase, route)) + return window }) - window.on('ready-to-show', () => window.show()) - window.webContents.setWindowOpenHandler((details) => { - shell.openExternal(details.url) - return { action: 'deny' } - }) + reusableWindows.set(key, reusable) + return reusable + } - await load(window, withHashRoute(rendererBase, currentRoute)) - return window - }) + async function openWindow(params: OpenDevtoolsWindowParams) { + const targetRoute = params.route ?? defaultRoute + const window = await getReusableForKey(params.key, targetRoute).getWindow() - async function openWindow(route?: string) { - if (route) - currentRoute = route - - const window = await reusable.getWindow() - const targetRoute = route ?? currentRoute - const url = withHashRoute(rendererBase, targetRoute) - - // If the route changes while the window is open, reload to that route. - const currentUrl = window.webContents.getURL() - if (!currentUrl.includes(`#${targetRoute}`)) { - await load(window, url) + if (params && (params.width !== undefined || params.height !== undefined || params.x !== undefined || params.y !== undefined)) { + const bounds: Partial = {} + if (params.width !== undefined) + bounds.width = params.width + if (params.height !== undefined) + bounds.height = params.height + if (params.x !== undefined) + bounds.x = params.x + if (params.y !== undefined) + bounds.y = params.y + window.setBounds(bounds) } return window @@ -62,6 +82,5 @@ export function setupDevtoolsWindow(): DevtoolsWindowManager { return { openWindow, - getWindow: reusable.getWindow, } } diff --git a/apps/stage-tamagotchi/src/main/windows/settings/index.ts b/apps/stage-tamagotchi/src/main/windows/settings/index.ts index 83ed986de..79fa9b6ed 100644 --- a/apps/stage-tamagotchi/src/main/windows/settings/index.ts +++ b/apps/stage-tamagotchi/src/main/windows/settings/index.ts @@ -27,7 +27,7 @@ export interface SettingsWindowManager { export function setupSettingsWindowReusableFunc(params: { widgetsManager: WidgetsWindowManager autoUpdater: AutoUpdater - devtoolsMarkdownStressWindow: DevtoolsWindowManager + devtoolsWindow: DevtoolsWindowManager onWindowCreated?: (window: BrowserWindow) => void serverChannel: ServerChannel mcpStdioManager: McpStdioManager @@ -67,7 +67,7 @@ export function setupSettingsWindowReusableFunc(params: { settingsWindow: window, widgetsManager: params.widgetsManager, autoUpdater: params.autoUpdater, - devtoolsMarkdownStressWindow: params.devtoolsMarkdownStressWindow, + devtoolsWindow: params.devtoolsWindow, serverChannel: params.serverChannel, mcpStdioManager: params.mcpStdioManager, i18n: params.i18n, diff --git a/apps/stage-tamagotchi/src/main/windows/settings/rpc/index.electron.ts b/apps/stage-tamagotchi/src/main/windows/settings/rpc/index.electron.ts index eb1c2e27f..5ed9ef109 100644 --- a/apps/stage-tamagotchi/src/main/windows/settings/rpc/index.electron.ts +++ b/apps/stage-tamagotchi/src/main/windows/settings/rpc/index.electron.ts @@ -23,7 +23,7 @@ export async function setupSettingsWindowInvokes(params: { settingsWindow: BrowserWindow widgetsManager: WidgetsWindowManager autoUpdater: AutoUpdater - devtoolsMarkdownStressWindow: DevtoolsWindowManager + devtoolsWindow: DevtoolsWindowManager serverChannel: ServerChannel mcpStdioManager: McpStdioManager i18n: I18n @@ -45,7 +45,7 @@ export async function setupSettingsWindowInvokes(params: { defineInvokeHandler(context, electronOpenSettingsDevtools, async () => params.settingsWindow.webContents.openDevTools({ mode: 'detach' })) defineInvokeHandler(context, electronOpenDevtoolsWindow, async (payload) => { - await params.devtoolsMarkdownStressWindow.openWindow(payload?.route) + await params.devtoolsWindow.openWindow(payload) }) return context diff --git a/apps/stage-tamagotchi/src/renderer/pages/settings/system/developer.vue b/apps/stage-tamagotchi/src/renderer/pages/settings/system/developer.vue index e7215ce11..302fb9c67 100644 --- a/apps/stage-tamagotchi/src/renderer/pages/settings/system/developer.vue +++ b/apps/stage-tamagotchi/src/renderer/pages/settings/system/developer.vue @@ -113,7 +113,7 @@ const openDevtoolsWindow = useElectronEventaInvoke(electronOpenDevtoolsWindow) icon="i-solar:code-bold-duotone" :text="t('tamagotchi.settings.devtools.pages.markdown-stress.title')" transition="all ease-in-out duration-250" - @click="() => openDevtoolsWindow({ route: '/devtools/markdown-stress' })" + @click="() => openDevtoolsWindow({ key: 'markdown-stress', route: '/devtools/markdown-stress' })" > {{ t('tamagotchi.settings.devtools.pages.markdown-stress.title') }} diff --git a/apps/stage-tamagotchi/src/shared/eventa.ts b/apps/stage-tamagotchi/src/shared/eventa.ts index 97a4668dd..3cd42c2f1 100644 --- a/apps/stage-tamagotchi/src/shared/eventa.ts +++ b/apps/stage-tamagotchi/src/shared/eventa.ts @@ -22,7 +22,16 @@ export const electronOpenSettings = defineInvokeEventa export const electronSettingsNavigate = defineEventa<{ route: string }>('eventa:event:electron:windows:settings:navigate') export const electronOpenChat = defineInvokeEventa('eventa:invoke:electron:windows:chat:open') export const electronOpenSettingsDevtools = defineInvokeEventa('eventa:invoke:electron:windows:settings:devtools:open') -export const electronOpenDevtoolsWindow = defineInvokeEventa('eventa:invoke:electron:windows:devtools:open') + +export interface OpenDevtoolsWindowParams { + key: string + route?: string + width?: number + height?: number + x?: number + y?: number +} +export const electronOpenDevtoolsWindow = defineInvokeEventa('eventa:invoke:electron:windows:devtools:open') export interface ElectronServerChannelConfig { tlsConfig?: ServerOptions['tlsConfig'] | null