diff --git a/apps/stage-tamagotchi/src/main/services/airi/plugins/index.test.ts b/apps/stage-tamagotchi/src/main/services/airi/plugins/index.test.ts index 26132c89e..e983f30e8 100644 --- a/apps/stage-tamagotchi/src/main/services/airi/plugins/index.test.ts +++ b/apps/stage-tamagotchi/src/main/services/airi/plugins/index.test.ts @@ -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') diff --git a/apps/stage-tamagotchi/src/main/services/airi/plugins/index.ts b/apps/stage-tamagotchi/src/main/services/airi/plugins/index.ts index 64234bd0b..f22423b0e 100644 --- a/apps/stage-tamagotchi/src/main/services/airi/plugins/index.ts +++ b/apps/stage-tamagotchi/src/main/services/airi/plugins/index.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 () => { diff --git a/apps/stage-tamagotchi/src/renderer/App.vue b/apps/stage-tamagotchi/src/renderer/App.vue index 08365e8a0..39accd11b 100644 --- a/apps/stage-tamagotchi/src/renderer/App.vue +++ b/apps/stage-tamagotchi/src/renderer/App.vue @@ -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() diff --git a/apps/stage-tamagotchi/src/shared/eventa/plugin/tools.ts b/apps/stage-tamagotchi/src/shared/eventa/plugin/tools.ts index 026815956..fdf83094a 100644 --- a/apps/stage-tamagotchi/src/shared/eventa/plugin/tools.ts +++ b/apps/stage-tamagotchi/src/shared/eventa/plugin/tools.ts @@ -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('eventa:invoke:electron:plugins:tools:list') export const electronPluginListXsaiTools = defineInvokeEventa('eventa:invoke:electron:plugins:tools:list-xsai') export const electronPluginInvokeTool = defineInvokeEventa('eventa:invoke:electron:plugins:tools:invoke') +export const electronPluginToolsChanged = defineEventa('eventa:event:electron:plugins:tools:changed')