refactor(stage-tamagotchi): reload tools when extension reloads

This commit is contained in:
Neko Ayaka
2026-06-12 16:11:59 +08:00
parent 8b98a2c138
commit 2eb05425c5
4 changed files with 93 additions and 21 deletions
@@ -28,11 +28,15 @@ import {
import {
electronPluginInspect,
electronPluginList,
electronPluginLoad,
electronPluginLoadEnabled,
electronPluginSetAutoReload,
electronPluginSetEnabled,
electronPluginUnload,
} from '../../../../shared/eventa/plugin/host'
import {
electronPluginToolsChanged,
} from '../../../../shared/eventa/plugin/tools'
import { setupExtensionHostServiceInternal } from './host'
import { loadManifestsFrom } from './host/registry'
import { setupExtensionHost as setupExtensionHostService } from './index'
@@ -544,6 +548,43 @@ describe('setupExtensionHost', () => {
expect(error).toEqual(expect.objectContaining({ enabled: true, loaded: false }))
})
it('emits a plugin tools changed event after loading an extension through IPC', async () => {
const pluginDir = join(pluginsDir, 'test-tools-changed')
await mkdir(pluginDir, { recursive: true })
await writeEntrypoint({
dir: pluginDir,
name: 'test-tools-changed.ts',
contents: createEmptyExtensionEntrypoint('test-tools-changed'),
})
await writeManifest({
dir: pluginDir,
name: 'test-tools-changed',
entrypoint: './test-tools-changed.ts',
})
await setupExtensionHost()
expect(contextState.lastContext).toBeDefined()
const toolsChangedEvents: Array<{ reason: string, name?: string }> = []
contextState.lastContext!.on(electronPluginToolsChanged, (event) => {
if (!event.body) {
throw new Error('Expected plugin tools changed event body.')
}
toolsChangedEvents.push(event.body)
})
const invokeLoad = defineInvoke(contextState.lastContext!, electronPluginLoad)
await invokeLoad({ name: 'test-tools-changed' })
expect(toolsChangedEvents).toEqual([
{
reason: 'loaded',
name: 'test-tools-changed',
},
])
})
it('loads the first matching manifest when duplicate plugin names exist', async () => {
const errorEntrypoint = join(testDataRoot, 'test-error-plugin.ts')
@@ -1,17 +1,8 @@
import type {
ExtensionHostService,
SetupExtensionHostOptions,
} from './types'
import type { ExtensionHostService, SetupExtensionHostOptions } from './types'
import {
defineInvoke,
defineInvokeHandler,
} from '@moeru/eventa'
import { defineInvoke, defineInvokeHandler } from '@moeru/eventa'
import { createContext } from '@moeru/eventa/adapters/electron/main'
import {
app,
ipcMain,
} from 'electron'
import { app, ipcMain } from 'electron'
import {
electronPluginGetAssetBaseUrl,
@@ -34,6 +25,7 @@ import {
electronPluginInvokeTool,
electronPluginListAgentTools,
electronPluginListXsaiTools,
electronPluginToolsChanged,
} from '../../../../shared/eventa/plugin/tools'
import { setupExtensionHostServiceInternal } from './host'
@@ -64,7 +56,12 @@ export async function setupExtensionHost(options: SetupExtensionHostOptions): Pr
})
defineInvokeHandler(context, electronPluginSetEnabled, async (payload) => {
return await hostService.setEnabled(payload)
const result = await hostService.setEnabled(payload)
context.emit(electronPluginToolsChanged, {
reason: 'enabled-state-changed',
name: payload.name,
})
return result
})
defineInvokeHandler(context, electronPluginSetAutoReload, async (payload) => {
@@ -72,15 +69,29 @@ export async function setupExtensionHost(options: SetupExtensionHostOptions): Pr
})
defineInvokeHandler(context, electronPluginLoadEnabled, async () => {
return await hostService.loadEnabled()
const result = await hostService.loadEnabled()
context.emit(electronPluginToolsChanged, {
reason: 'load-enabled',
})
return result
})
defineInvokeHandler(context, electronPluginLoad, async (payload) => {
return await hostService.load(payload.name)
const result = await hostService.load(payload.name)
context.emit(electronPluginToolsChanged, {
reason: 'loaded',
name: payload.name,
})
return result
})
defineInvokeHandler(context, electronPluginUnload, async (payload) => {
return await hostService.unload(payload.name)
const result = await hostService.unload(payload.name)
context.emit(electronPluginToolsChanged, {
reason: 'unloaded',
name: payload.name,
})
return result
})
defineInvokeHandler(context, electronPluginInspect, async () => {
+6 -4
View File
@@ -48,13 +48,11 @@ import {
electronPluginSetEnabled,
electronPluginUnload,
} from '../shared/eventa/plugin/host'
import { electronPluginToolsChanged } from '../shared/eventa/plugin/tools'
import { initializeElectronAuthCallbackBridge } from './bridges/electron-auth-callback'
import { initializeStageThreeRuntimeTraceBridge } from './bridges/stage-three-runtime-trace'
import { useLanguage } from './composables/use-language'
import {
createChatSyncWindowLifecycle,
resolveInitialChatSyncRoutePath,
} from './stores/chat-sync-lifecycle'
import { createChatSyncWindowLifecycle, resolveInitialChatSyncRoutePath } from './stores/chat-sync-lifecycle'
import { useTamagotchiMcpToolsStore } from './stores/mcp-tools'
import { useTamagotchiPluginToolsStore } from './stores/plugin-tools'
import { useServerChannelSettingsStore } from './stores/settings/server-channel'
@@ -188,6 +186,10 @@ function createFullStageRuntime() {
syncGodotStageRenderer(event.body)
})
context.value.on(electronPluginToolsChanged, () => {
void refreshPluginRuntimeTools()
})
return {
async initialize() {
analyticsStore.initialize()
@@ -1,4 +1,4 @@
import { defineInvokeEventa } from '@moeru/eventa'
import { defineEventa, defineInvokeEventa } from '@moeru/eventa'
/**
* Renderer-facing plugin tool descriptor used by agent tooling UIs.
@@ -80,6 +80,23 @@ export interface ElectronPluginXsaiToolsetDefinition {
prompts: ElectronPluginToolsetPromptDefinition[]
}
/**
* Describes why plugin-backed runtime tools should be refreshed.
*
* Use when:
* - The main process notifies renderers after plugin lifecycle changes
*
* Expects:
* - `name` is present when the change is scoped to one plugin
*
* Returns:
* - N/A
*/
export interface ElectronPluginToolsChangedPayload {
reason: 'loaded' | 'load-enabled' | 'unloaded' | 'enabled-state-changed'
name?: string
}
export const electronPluginListAgentTools = defineInvokeEventa<ElectronPluginToolDescriptor[]>('eventa:invoke:electron:plugins:tools:list')
export const electronPluginListXsaiTools = defineInvokeEventa<ElectronPluginXsaiToolsetDefinition>('eventa:invoke:electron:plugins:tools:list-xsai')
export const electronPluginInvokeTool = defineInvokeEventa<unknown, {
@@ -87,3 +104,4 @@ export const electronPluginInvokeTool = defineInvokeEventa<unknown, {
name: string
input: unknown
}>('eventa:invoke:electron:plugins:tools:invoke')
export const electronPluginToolsChanged = defineEventa<ElectronPluginToolsChangedPayload>('eventa:event:electron:plugins:tools:changed')