diff --git a/apps/stage-tamagotchi/src/main/index.ts b/apps/stage-tamagotchi/src/main/index.ts index 5cb7a728c..3c4c9375c 100644 --- a/apps/stage-tamagotchi/src/main/index.ts +++ b/apps/stage-tamagotchi/src/main/index.ts @@ -109,7 +109,11 @@ app.whenReady().then(async () => { const devtoolsMarkdownStressWindow = injeca.provide('windows:devtools:markdown-stress', () => setupDevtoolsWindow()) - const onboardingWindow = injeca.provide('windows:onboarding', () => setupOnboardingWindowManager()) + const onboardingWindowManager = injeca.provide('windows:onboarding', { + dependsOn: { serverChannel, i18n }, + build: ({ dependsOn }) => setupOnboardingWindowManager(dependsOn), + }) + const noticeWindow = injeca.provide('windows:notice', { dependsOn: { i18n, serverChannel }, build: ({ dependsOn }) => setupNoticeWindowManager(dependsOn), @@ -136,7 +140,7 @@ app.whenReady().then(async () => { }) const mainWindow = injeca.provide('windows:main', { - dependsOn: { settingsWindow, chatWindow, widgetsManager, noticeWindow, beatSync, autoUpdater, serverChannel, mcpStdioManager, i18n }, + dependsOn: { settingsWindow, chatWindow, widgetsManager, noticeWindow, beatSync, autoUpdater, serverChannel, mcpStdioManager, i18n, onboardingWindowManager }, build: async ({ dependsOn }) => setupMainWindow(dependsOn), }) @@ -151,16 +155,10 @@ app.whenReady().then(async () => { }) injeca.invoke({ - dependsOn: { mainWindow, tray, serverChannel, pluginHost, mcpStdioManager }, + dependsOn: { mainWindow, tray, serverChannel, pluginHost, mcpStdioManager, onboardingWindow: onboardingWindowManager }, callback: noop, }) - // Show onboarding window if not completed - injeca.invoke({ - dependsOn: { onboardingWindow }, - callback: ({ onboardingWindow }) => { onboardingWindow.showIfNeeded() }, - }) - injeca.start().catch(err => console.error(err)) // Lifecycle diff --git a/apps/stage-tamagotchi/src/main/services/airi/onboarding/index.ts b/apps/stage-tamagotchi/src/main/services/airi/onboarding/index.ts new file mode 100644 index 000000000..95eeddcdf --- /dev/null +++ b/apps/stage-tamagotchi/src/main/services/airi/onboarding/index.ts @@ -0,0 +1,16 @@ +import type { createContext } from '@moeru/eventa/adapters/electron/main' + +import type { OnboardingWindowManager } from '../../../windows/onboarding' + +import { defineInvokeHandler } from '@moeru/eventa' + +import { electronOpenOnboarding } from '../../../../shared/eventa' + +export function createOnboardingService(params: { + context: ReturnType['context'] + onboardingWindowManager: OnboardingWindowManager +}) { + defineInvokeHandler(params.context, electronOpenOnboarding, async () => { + await params.onboardingWindowManager.getAndToggleWindow() + }) +} diff --git a/apps/stage-tamagotchi/src/main/services/electron/auto-updater.ts b/apps/stage-tamagotchi/src/main/services/electron/auto-updater.ts index 991264acc..5c1cd2e6b 100644 --- a/apps/stage-tamagotchi/src/main/services/electron/auto-updater.ts +++ b/apps/stage-tamagotchi/src/main/services/electron/auto-updater.ts @@ -9,7 +9,7 @@ import electronUpdater from 'electron-updater' import { is } from '@electron-toolkit/utils' import { useLogg } from '@guiiai/logg' import { defineInvokeHandler } from '@moeru/eventa' -import { errorMessageFrom } from '@moeru/std' +import { errorMessageFrom, tryCatch } from '@moeru/std' import { committerDate } from '~build/git' import { app } from 'electron' import { Semaphore } from 'es-toolkit' @@ -146,10 +146,7 @@ export function createAutoUpdaterService(params: { context: MainContext, window: if (window.isDestroyed()) return - try { - context.emit(electronAutoUpdaterStateChanged, state) - } - catch {} + tryCatch(() => context.emit(electronAutoUpdaterStateChanged, state)) }) const cleanups: Array<() => void> = [unsubscribe] diff --git a/apps/stage-tamagotchi/src/main/windows/main/index.ts b/apps/stage-tamagotchi/src/main/windows/main/index.ts index 0f9d9c6f3..f0e222e24 100644 --- a/apps/stage-tamagotchi/src/main/windows/main/index.ts +++ b/apps/stage-tamagotchi/src/main/windows/main/index.ts @@ -6,6 +6,7 @@ import type { ServerChannel } from '../../services/airi/channel-server' import type { McpStdioManager } from '../../services/airi/mcp-servers' import type { AutoUpdater } from '../../services/electron/auto-updater' import type { NoticeWindowManager } from '../notice' +import type { OnboardingWindowManager } from '../onboarding' import type { WidgetsWindowManager } from '../widgets' import { dirname, join, resolve } from 'node:path' @@ -54,6 +55,7 @@ export async function setupMainWindow(params: { serverChannel: ServerChannel mcpStdioManager: McpStdioManager i18n: I18n + onboardingWindowManager: OnboardingWindowManager }) { const { setup: setupConfig, @@ -167,6 +169,7 @@ export async function setupMainWindow(params: { serverChannel: params.serverChannel, mcpStdioManager: params.mcpStdioManager, i18n: params.i18n, + onboardingWindowManager: params.onboardingWindowManager, }) /** diff --git a/apps/stage-tamagotchi/src/main/windows/main/rpc/index.electron.ts b/apps/stage-tamagotchi/src/main/windows/main/rpc/index.electron.ts index f9eb06e00..bc970b523 100644 --- a/apps/stage-tamagotchi/src/main/windows/main/rpc/index.electron.ts +++ b/apps/stage-tamagotchi/src/main/windows/main/rpc/index.electron.ts @@ -5,6 +5,7 @@ import type { ServerChannel } from '../../../services/airi/channel-server' import type { McpStdioManager } from '../../../services/airi/mcp-servers' import type { AutoUpdater } from '../../../services/electron/auto-updater' import type { NoticeWindowManager } from '../../notice' +import type { OnboardingWindowManager } from '../../onboarding' import type { WidgetsWindowManager } from '../../widgets' import { defineInvokeHandler } from '@moeru/eventa' @@ -13,6 +14,7 @@ import { ipcMain } from 'electron' import { electronOpenChat, electronOpenMainDevtools, electronOpenSettings, noticeWindowEventa } from '../../../../shared/eventa' import { createMcpServersService } from '../../../services/airi/mcp-servers' +import { createOnboardingService } from '../../../services/airi/onboarding' import { createWidgetsService } from '../../../services/airi/widgets' import { createAutoUpdaterService } from '../../../services/electron' import { toggleWindowShow } from '../../shared' @@ -28,6 +30,7 @@ export async function setupMainWindowElectronInvokes(params: { serverChannel: ServerChannel mcpStdioManager: McpStdioManager i18n: I18n + onboardingWindowManager: OnboardingWindowManager }) { // TODO: once we refactored eventa to support window-namespaced contexts, // we can remove the setMaxListeners call below since eventa will be able to dispatch and @@ -40,6 +43,7 @@ export async function setupMainWindowElectronInvokes(params: { createWidgetsService({ context, widgetsManager: params.widgetsManager, window: params.window }) createAutoUpdaterService({ context, window: params.window, service: params.autoUpdater }) createMcpServersService({ context, manager: params.mcpStdioManager }) + createOnboardingService({ context, onboardingWindowManager: params.onboardingWindowManager }) defineInvokeHandler(context, electronOpenMainDevtools, () => params.window.webContents.openDevTools({ mode: 'detach' })) defineInvokeHandler(context, electronOpenSettings, async () => toggleWindowShow(await params.settingsWindow())) diff --git a/apps/stage-tamagotchi/src/main/windows/onboarding/index.ts b/apps/stage-tamagotchi/src/main/windows/onboarding/index.ts index 954e48f88..03f59f546 100644 --- a/apps/stage-tamagotchi/src/main/windows/onboarding/index.ts +++ b/apps/stage-tamagotchi/src/main/windows/onboarding/index.ts @@ -1,3 +1,6 @@ +import type { I18n } from '../../libs/i18n' +import type { ServerChannel } from '../../services/airi/channel-server' + import { join, resolve } from 'node:path' import { defineInvokeHandler } from '@moeru/eventa' @@ -6,43 +9,33 @@ import { BrowserWindow, ipcMain, shell } from 'electron' import icon from '../../../../resources/icon.png?asset' -import { electronOnboardingClose, electronOnboardingCompleted, electronOnboardingSkipped, electronOpenOnboarding } from '../../../shared/eventa' +import { electronOnboardingClose, electronOnboardingCompleted, electronOnboardingSkipped } from '../../../shared/eventa' import { baseUrl, getElectronMainDirname, load, withHashRoute } from '../../libs/electron/location' +import { createReusableWindow } from '../../libs/electron/window-manager' +import { toggleWindowShow } from '../shared' +import { setupBaseWindowElectronInvokes } from '../shared/window' export interface OnboardingWindowManager { - getWindow: () => BrowserWindow | null - showIfNeeded: () => Promise - close: () => void - markCompleted: () => void - markSkipped: () => void + getWindow: () => Promise + getAndToggleWindow: () => Promise } -export function setupOnboardingWindowManager(): OnboardingWindowManager { - let window: BrowserWindow | undefined +export function setupOnboardingWindowManager(params: { + serverChannel: ServerChannel + i18n: I18n +}): OnboardingWindowManager { + async function getOnboardingWindow(getWindow: () => Promise) { + const window = await getWindow() + await toggleWindowShow(window) - const close = () => { - if (window && !window.isDestroyed()) { - window.close() - } + return window } - const markCompleted = () => { - close() - } - - const markSkipped = () => { - close() - } - - const createWindow = async () => { - if (window && !window.isDestroyed()) { - return window - } - + const reusableWindow = createReusableWindow(async () => { const newWindow = new BrowserWindow({ title: 'Welcome to AIRI', - width: 500, - height: 700, + width: 1200, + height: 600, minWidth: 400, minHeight: 500, show: false, @@ -64,43 +57,26 @@ export function setupOnboardingWindowManager(): OnboardingWindowManager { return { action: 'deny' } }) - newWindow.on('closed', () => { - window = undefined - }) + // TODO: once we refactored eventa to support window-namespaced contexts, + // we can remove the setMaxListeners call below since eventa will be able to dispatch and + // manage events within eventa's context system. + ipcMain.setMaxListeners(0) + + const { context } = createContext(ipcMain, newWindow) + + defineInvokeHandler(context, electronOnboardingClose, async () => newWindow.close()) + defineInvokeHandler(context, electronOnboardingCompleted, async () => newWindow.close()) + defineInvokeHandler(context, electronOnboardingSkipped, async () => newWindow.close()) + + await setupBaseWindowElectronInvokes({ context, window: newWindow, i18n: params.i18n, serverChannel: params.serverChannel }) await load(newWindow, withHashRoute(baseUrl(resolve(getElectronMainDirname(), '..', 'renderer')), '/onboarding')) - // Setup IPC handlers for this window - const { context } = createContext(ipcMain, newWindow) - - defineInvokeHandler(context, electronOnboardingClose, () => close()) - defineInvokeHandler(context, electronOnboardingCompleted, () => markCompleted()) - defineInvokeHandler(context, electronOnboardingSkipped, () => markSkipped()) - defineInvokeHandler(context, electronOpenOnboarding, async () => { - await showIfNeeded() - return true - }) - - window = newWindow return newWindow - } - - async function showIfNeeded() { - if (window && !window.isDestroyed()) { - window.show() - window.focus() - return true - } - - await createWindow() - return true - } + }) return { - getWindow: () => window ?? null, - showIfNeeded, - close, - markCompleted, - markSkipped, + getWindow: async () => reusableWindow.getWindow(), + getAndToggleWindow: async () => await getOnboardingWindow(reusableWindow.getWindow), } } diff --git a/apps/stage-tamagotchi/src/renderer/App.vue b/apps/stage-tamagotchi/src/renderer/App.vue index 135656ee8..e5e74c060 100644 --- a/apps/stage-tamagotchi/src/renderer/App.vue +++ b/apps/stage-tamagotchi/src/renderer/App.vue @@ -29,6 +29,7 @@ import { electronGetServerChannelConfig, electronMcpCallTool, electronMcpListTools, + electronOpenOnboarding, electronOpenSettings, electronPluginInspect, electronPluginList, @@ -75,6 +76,7 @@ const reportPluginCapability = useElectronEventaInvoke(electronPluginUpdateCapab const listMcpTools = useElectronEventaInvoke(electronMcpListTools) const callMcpTool = useElectronEventaInvoke(electronMcpCallTool) const setLocale = useElectronEventaInvoke(i18nSetLocale) +const openOnboarding = useElectronEventaInvoke(electronOpenOnboarding) // NOTICE: register plugin host bridge during setup to avoid race with pages using it in immediate watchers. pluginHostInspectorStore.setBridge({ @@ -145,6 +147,12 @@ watch(themeColorsHueDynamic, () => { document.documentElement.classList.toggle('dynamic-hue', themeColorsHueDynamic.value) }, { immediate: true }) +watch(() => onboardingStore.shouldShowSetup, () => { + if (onboardingStore.shouldShowSetup) { + openOnboarding() + } +}, { immediate: true }) + onUnmounted(() => { contextBridgeStore.dispose() clearMcpToolBridge() diff --git a/apps/stage-tamagotchi/src/shared/eventa.ts b/apps/stage-tamagotchi/src/shared/eventa.ts index 5dac87938..549e5f01d 100644 --- a/apps/stage-tamagotchi/src/shared/eventa.ts +++ b/apps/stage-tamagotchi/src/shared/eventa.ts @@ -210,7 +210,7 @@ export const widgetsUpdateEvent = defineEventa<{ id: string, componentProps?: Re export const electronOnboardingClose = defineInvokeEventa('eventa:invoke:electron:windows:onboarding:close') export const electronOnboardingCompleted = defineInvokeEventa('eventa:invoke:electron:windows:onboarding:completed') export const electronOnboardingSkipped = defineInvokeEventa('eventa:invoke:electron:windows:onboarding:skipped') -export const electronOpenOnboarding = defineInvokeEventa('eventa:invoke:electron:windows:onboarding:open') +export const electronOpenOnboarding = defineInvokeEventa('eventa:invoke:electron:windows:onboarding:open') export const i18nSetLocale = defineInvokeEventa('eventa:invoke:electron:i18n:set-locale') export const i18nGetLocale = defineInvokeEventa('eventa:invoke:electron:i18n:get-locale')