fix(stage-tamagotchi): shouldnt implement onboarding window like that

This commit is contained in:
Neko Ayaka
2026-03-10 01:08:17 +08:00
parent 34b12b0a9e
commit 8e6dd19669
8 changed files with 76 additions and 74 deletions
+7 -9
View File
@@ -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
@@ -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<typeof createContext>['context']
onboardingWindowManager: OnboardingWindowManager
}) {
defineInvokeHandler(params.context, electronOpenOnboarding, async () => {
await params.onboardingWindowManager.getAndToggleWindow()
})
}
@@ -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]
@@ -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,
})
/**
@@ -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()))
@@ -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<boolean>
close: () => void
markCompleted: () => void
markSkipped: () => void
getWindow: () => Promise<BrowserWindow>
getAndToggleWindow: () => Promise<BrowserWindow>
}
export function setupOnboardingWindowManager(): OnboardingWindowManager {
let window: BrowserWindow | undefined
export function setupOnboardingWindowManager(params: {
serverChannel: ServerChannel
i18n: I18n
}): OnboardingWindowManager {
async function getOnboardingWindow(getWindow: () => Promise<BrowserWindow>) {
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),
}
}
@@ -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()
+1 -1
View File
@@ -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<boolean>('eventa:invoke:electron:windows:onboarding:open')
export const electronOpenOnboarding = defineInvokeEventa('eventa:invoke:electron:windows:onboarding:open')
export const i18nSetLocale = defineInvokeEventa<void, Locale>('eventa:invoke:electron:i18n:set-locale')
export const i18nGetLocale = defineInvokeEventa<Locale>('eventa:invoke:electron:i18n:get-locale')