refactor(stage-tamagotchi,plugin-*): unified to extension id

This commit is contained in:
Neko Ayaka
2026-06-12 19:27:23 +08:00
parent eb98845b11
commit 0f975a4f73
29 changed files with 346 additions and 342 deletions
@@ -37,7 +37,7 @@ describe('createStaticAssetService', () => {
const sessionStore = createStaticAssetSessionStore() const sessionStore = createStaticAssetSessionStore()
const validateInputs: string[] = [] const validateInputs: string[] = []
const server = createStaticAssetService({ const server = createStaticAssetService({
getManifestEntryByName: () => new Map([ getManifestEntryByExtensionId: () => new Map([
[extensionId, { rootDir, version }], [extensionId, { rootDir, version }],
]), ]),
sessionStore: { sessionStore: {
@@ -206,7 +206,7 @@ describe('createStaticAssetService', () => {
] ]
let manifestReadCount = 0 let manifestReadCount = 0
const server = createStaticAssetService({ const server = createStaticAssetService({
getManifestEntryByName: () => manifestEntries[Math.min(manifestReadCount++, manifestEntries.length - 1)], getManifestEntryByExtensionId: () => manifestEntries[Math.min(manifestReadCount++, manifestEntries.length - 1)],
}) })
servers.push(server) servers.push(server)
await server.start() await server.start()
@@ -242,7 +242,7 @@ describe('createStaticAssetService', () => {
const version = '1.0.0' const version = '1.0.0'
const sessionStore = createStaticAssetSessionStore() const sessionStore = createStaticAssetSessionStore()
const server = createStaticAssetService({ const server = createStaticAssetService({
getManifestEntryByName: () => new Map([ getManifestEntryByExtensionId: () => new Map([
[extensionId, { rootDir, version }], [extensionId, { rootDir, version }],
]), ]),
sessionStore: { sessionStore: {
@@ -39,13 +39,13 @@ export interface StaticAssetService extends ServerManager {
* - A higher-level plugin asset service needs an HTTP transport adapter * - A higher-level plugin asset service needs an HTTP transport adapter
* *
* Expects: * Expects:
* - `getManifestEntryByName` returns up-to-date plugin root/version map * - `getManifestEntryByExtensionId` returns up-to-date extension root/version map
* *
* Returns: * Returns:
* - Lifecycle service with session create/revoke APIs and local base URL getter * - Lifecycle service with session create/revoke APIs and local base URL getter
*/ */
export function createStaticAssetService(options: { export function createStaticAssetService(options: {
getManifestEntryByName: () => Map<string, StaticAssetManifestEntry> getManifestEntryByExtensionId: () => Map<string, StaticAssetManifestEntry>
host?: string host?: string
sessionStore?: StaticAssetSessionStore sessionStore?: StaticAssetSessionStore
getType?: (ext: string) => string | undefined getType?: (ext: string) => string | undefined
@@ -60,11 +60,11 @@ export function createStaticAssetService(options: {
const getManifestEntryForRequest = (extensionId: string) => { const getManifestEntryForRequest = (extensionId: string) => {
const cache = manifestEntryRequestCache.getStore() const cache = manifestEntryRequestCache.getStore()
if (!cache) { if (!cache) {
return options.getManifestEntryByName().get(extensionId) return options.getManifestEntryByExtensionId().get(extensionId)
} }
if (!cache.has(extensionId)) { if (!cache.has(extensionId)) {
cache.set(extensionId, options.getManifestEntryByName().get(extensionId)) cache.set(extensionId, options.getManifestEntryByExtensionId().get(extensionId))
} }
return cache.get(extensionId) return cache.get(extensionId)
@@ -27,9 +27,9 @@ export interface ExtensionAutoReloadFeatureOptions {
log: ReturnType<typeof useLogg> log: ReturnType<typeof useLogg>
getConfig: () => ExtensionConfig getConfig: () => ExtensionConfig
listEntries: () => ManifestEntry[] listEntries: () => ManifestEntry[]
isLoaded: (name: string) => boolean isLoaded: (extensionId: string) => boolean
resolveWatchPaths: (name: string) => string[] resolveWatchPaths: (extensionId: string) => string[]
reload: (name: string, changedPath: string) => Promise<void> reload: (extensionId: string, changedPath: string) => Promise<void>
} }
/** /**
@@ -41,7 +41,7 @@ export interface ExtensionAutoReloadFeatureOptions {
* *
* Expects: * Expects:
* - Call `sync()` after registry/config/load-state changes * - Call `sync()` after registry/config/load-state changes
* - Call `clearExtension(name)` before unloading or disabling a plugin * - Call `clearExtension(extensionId)` before unloading or disabling an extension
* - Call `dispose()` during host shutdown * - Call `dispose()` during host shutdown
* *
* Returns: * Returns:
@@ -52,18 +52,18 @@ export function createExtensionAutoReloadFeature(options: ExtensionAutoReloadFea
const autoReloadTimers = new Map<string, ReturnType<typeof setTimeout>>() const autoReloadTimers = new Map<string, ReturnType<typeof setTimeout>>()
const autoReloadWatchers = new Map<string, FSWatcher[]>() const autoReloadWatchers = new Map<string, FSWatcher[]>()
const clearTimer = (name: string) => { const clearTimer = (extensionId: string) => {
const timer = autoReloadTimers.get(name) const timer = autoReloadTimers.get(extensionId)
if (!timer) { if (!timer) {
return return
} }
clearTimeout(timer) clearTimeout(timer)
autoReloadTimers.delete(name) autoReloadTimers.delete(extensionId)
} }
const closeWatchers = (name: string) => { const closeWatchers = (extensionId: string) => {
const watchers = autoReloadWatchers.get(name) const watchers = autoReloadWatchers.get(extensionId)
if (!watchers) { if (!watchers) {
return return
} }
@@ -72,55 +72,55 @@ export function createExtensionAutoReloadFeature(options: ExtensionAutoReloadFea
watcher.close() watcher.close()
} }
autoReloadWatchers.delete(name) autoReloadWatchers.delete(extensionId)
} }
const reloadExtensionById = async (name: string, changedPath: string) => { const reloadExtensionById = async (extensionId: string, changedPath: string) => {
if (autoReloadInFlight.has(name)) { if (autoReloadInFlight.has(extensionId)) {
return return
} }
autoReloadInFlight.add(name) autoReloadInFlight.add(extensionId)
try { try {
await options.reload(name, changedPath) await options.reload(extensionId, changedPath)
options.log.log('extension auto-reloaded after file change', { extension: name, path: changedPath }) options.log.log('extension auto-reloaded after file change', { extensionId, path: changedPath })
} }
catch (error) { catch (error) {
options.log.withError(error).withFields({ extension: name, path: changedPath }).error('extension auto-reload failed') options.log.withError(error).withFields({ extensionId, path: changedPath }).error('extension auto-reload failed')
} }
finally { finally {
autoReloadInFlight.delete(name) autoReloadInFlight.delete(extensionId)
} }
} }
const scheduleReload = (name: string, changedPath: string) => { const scheduleReload = (extensionId: string, changedPath: string) => {
clearTimer(name) clearTimer(extensionId)
autoReloadTimers.set(name, setTimeout(() => { autoReloadTimers.set(extensionId, setTimeout(() => {
autoReloadTimers.delete(name) autoReloadTimers.delete(extensionId)
void reloadExtensionById(name, changedPath) void reloadExtensionById(extensionId, changedPath)
}, 180)) }, 180))
} }
return { return {
sync() { sync() {
const enabledNames = new Set(options.getConfig().autoReload) const enabledExtensionIds = new Set(options.getConfig().autoReload)
const desiredNames = new Set(options.listEntries() const desiredExtensionIds = new Set(options.listEntries()
.map(entry => manifestIdOf(entry.manifest)) .map(entry => manifestIdOf(entry.manifest))
.filter(name => enabledNames.has(name) && options.isLoaded(name))) .filter(extensionId => enabledExtensionIds.has(extensionId) && options.isLoaded(extensionId)))
for (const name of autoReloadWatchers.keys()) { for (const extensionId of autoReloadWatchers.keys()) {
if (!desiredNames.has(name)) { if (!desiredExtensionIds.has(extensionId)) {
clearTimer(name) clearTimer(extensionId)
closeWatchers(name) closeWatchers(extensionId)
} }
} }
for (const name of desiredNames) { for (const extensionId of desiredExtensionIds) {
if (autoReloadWatchers.has(name)) { if (autoReloadWatchers.has(extensionId)) {
continue continue
} }
const watchPaths = options.resolveWatchPaths(name) const watchPaths = options.resolveWatchPaths(extensionId)
if (watchPaths.length === 0) { if (watchPaths.length === 0) {
continue continue
} }
@@ -128,25 +128,25 @@ export function createExtensionAutoReloadFeature(options: ExtensionAutoReloadFea
const watchers: FSWatcher[] = [] const watchers: FSWatcher[] = []
for (const watchPath of watchPaths) { for (const watchPath of watchPaths) {
try { try {
const watcher = watchFile(watchPath, { persistent: false }, () => scheduleReload(name, watchPath)) const watcher = watchFile(watchPath, { persistent: false }, () => scheduleReload(extensionId, watchPath))
watcher.on('error', (error) => { watcher.on('error', (error) => {
options.log.withError(error).withFields({ extension: name, path: watchPath }).warn('extension auto-reload watcher error') options.log.withError(error).withFields({ extensionId, path: watchPath }).warn('extension auto-reload watcher error')
}) })
watchers.push(watcher) watchers.push(watcher)
} }
catch (error) { catch (error) {
options.log.withError(error).withFields({ extension: name, path: watchPath }).warn('failed to watch extension file for auto-reload') options.log.withError(error).withFields({ extensionId, path: watchPath }).warn('failed to watch extension file for auto-reload')
} }
} }
if (watchers.length > 0) { if (watchers.length > 0) {
autoReloadWatchers.set(name, watchers) autoReloadWatchers.set(extensionId, watchers)
} }
} }
}, },
clearExtension(name: string) { clearExtension(extensionId: string) {
clearTimer(name) clearTimer(extensionId)
closeWatchers(name) closeWatchers(extensionId)
}, },
dispose() { dispose() {
const managedNames = new Set([ const managedNames = new Set([
@@ -154,9 +154,9 @@ export function createExtensionAutoReloadFeature(options: ExtensionAutoReloadFea
...autoReloadWatchers.keys(), ...autoReloadWatchers.keys(),
]) ])
for (const name of managedNames) { for (const extensionId of managedNames) {
clearTimer(name) clearTimer(extensionId)
closeWatchers(name) closeWatchers(extensionId)
} }
}, },
} }
@@ -76,12 +76,12 @@ describe('createExtensionAssetService', () => {
mockState.createStaticAssetService.mockReturnValue(server) mockState.createStaticAssetService.mockReturnValue(server)
const service = createExtensionAssetService({ const service = createExtensionAssetService({
getManifestEntryByName: () => new Map(), getManifestEntryByExtensionId: () => new Map(),
cookieAdapter: adapter, cookieAdapter: adapter,
}) })
const result = await service.createAssetSession({ const result = await service.createAssetSession({
pluginId: 'airi-plugin-game-chess', extensionId: 'airi-plugin-game-chess',
version: '1.0.0', version: '1.0.0',
ownerSessionId: 'owner-session-1', ownerSessionId: 'owner-session-1',
routeAssetPath: 'assets/app.js', routeAssetPath: 'assets/app.js',
@@ -123,12 +123,12 @@ describe('createExtensionAssetService', () => {
mockState.createStaticAssetService.mockReturnValue(server) mockState.createStaticAssetService.mockReturnValue(server)
const service = createExtensionAssetService({ const service = createExtensionAssetService({
getManifestEntryByName: () => new Map(), getManifestEntryByExtensionId: () => new Map(),
cookieAdapter: adapter, cookieAdapter: adapter,
}) })
await expect(service.createAssetSession({ await expect(service.createAssetSession({
pluginId: 'airi-plugin-game-chess', extensionId: 'airi-plugin-game-chess',
version: '1.0.0', version: '1.0.0',
ownerSessionId: 'owner-session-1', ownerSessionId: 'owner-session-1',
routeAssetPath: 'assets/app.js', routeAssetPath: 'assets/app.js',
@@ -149,12 +149,12 @@ describe('createExtensionAssetService', () => {
const { adapter } = createFakeCookieAdapter() const { adapter } = createFakeCookieAdapter()
mockState.createStaticAssetService.mockReturnValue(server) mockState.createStaticAssetService.mockReturnValue(server)
const service = createExtensionAssetService({ const service = createExtensionAssetService({
getManifestEntryByName: () => new Map(), getManifestEntryByExtensionId: () => new Map(),
cookieAdapter: adapter, cookieAdapter: adapter,
}) })
await expect(service.createAssetSession({ await expect(service.createAssetSession({
pluginId: 'airi-plugin-game-chess', extensionId: 'airi-plugin-game-chess',
version: '1.0.0', version: '1.0.0',
ownerSessionId: 'owner-session-1', ownerSessionId: 'owner-session-1',
routeAssetPath: '../secret.txt', routeAssetPath: '../secret.txt',
@@ -169,7 +169,7 @@ describe('createExtensionAssetService', () => {
adapter.setCookie.mockRejectedValueOnce(new Error('cookie jar unavailable')) adapter.setCookie.mockRejectedValueOnce(new Error('cookie jar unavailable'))
await expect(service.createAssetSession({ await expect(service.createAssetSession({
pluginId: 'airi-plugin-game-chess', extensionId: 'airi-plugin-game-chess',
version: '1.0.0', version: '1.0.0',
ownerSessionId: 'owner-session-1', ownerSessionId: 'owner-session-1',
routeAssetPath: 'assets/app.js', routeAssetPath: 'assets/app.js',
@@ -196,7 +196,7 @@ describe('createExtensionAssetService', () => {
mockState.createStaticAssetService.mockReturnValue(server) mockState.createStaticAssetService.mockReturnValue(server)
const service = createExtensionAssetService({ const service = createExtensionAssetService({
getManifestEntryByName: () => new Map(), getManifestEntryByExtensionId: () => new Map(),
cookieAdapter: adapter, cookieAdapter: adapter,
}) })
@@ -252,7 +252,7 @@ describe('createExtensionAssetService', () => {
mockState.createStaticAssetService.mockReturnValue(server) mockState.createStaticAssetService.mockReturnValue(server)
const service = createExtensionAssetService({ const service = createExtensionAssetService({
getManifestEntryByName: () => new Map(), getManifestEntryByExtensionId: () => new Map(),
cookieAdapter: adapter, cookieAdapter: adapter,
}) })
@@ -13,7 +13,7 @@ import { buildMountedStaticAssetPath } from '../../../http-server/static-assets/
* - Snapshot builders need a transport-agnostic way to authorize one extension asset route before iframe load * - Snapshot builders need a transport-agnostic way to authorize one extension asset route before iframe load
* *
* Expects: * Expects:
* - `pluginId` matches a manifest entry registered in the asset host * - `extensionId` matches a manifest entry registered in the asset host
* - `routeAssetPath` identifies the iframe entry asset relative to the mounted `/ui` route * - `routeAssetPath` identifies the iframe entry asset relative to the mounted `/ui` route
* - `pathPrefix` is scoped to the mounted route prefix accepted by the session store * - `pathPrefix` is scoped to the mounted route prefix accepted by the session store
* *
@@ -21,8 +21,8 @@ import { buildMountedStaticAssetPath } from '../../../http-server/static-assets/
* - N/A * - N/A
*/ */
export interface ExtensionAssetSessionInput { export interface ExtensionAssetSessionInput {
/** Extension/plugin manifest id that owns the static asset root. */ /** Extension manifest id that owns the static asset root. */
pluginId: string extensionId: string
/** Extension/plugin version expected by the server-side session validator. */ /** Extension/plugin version expected by the server-side session validator. */
version: string version: string
/** Parent extension session id used for owner-scoped revocation. */ /** Parent extension session id used for owner-scoped revocation. */
@@ -164,17 +164,17 @@ function createExtensionAssetCookie(baseUrl: string, session: StaticAssetSession
* - Asset session lifecycle should stay inside the extension domain instead of the HTTP server layer * - Asset session lifecycle should stay inside the extension domain instead of the HTTP server layer
* *
* Expects: * Expects:
* - `getManifestEntryByName` returns the latest extension root/version map * - `getManifestEntryByExtensionId` returns the latest extension root/version map
* - `cookieAdapter` writes and removes cookies in the Electron host session used by plugin iframes * - `cookieAdapter` writes and removes cookies in the Electron host session used by plugin iframes
* *
* Returns: * Returns:
* - An extension-facing asset host service with generic extension asset methods * - An extension-facing asset host service with generic extension asset methods
*/ */
export function createExtensionAssetService(options: { export function createExtensionAssetService(options: {
getManifestEntryByName: () => Map<string, StaticAssetManifestEntry> getManifestEntryByExtensionId: () => Map<string, StaticAssetManifestEntry>
cookieAdapter: ExtensionAssetCookieAdapter cookieAdapter: ExtensionAssetCookieAdapter
}): ExtensionAssetService { }): ExtensionAssetService {
const server = createStaticAssetService({ getManifestEntryByName: options.getManifestEntryByName }) const server = createStaticAssetService({ getManifestEntryByExtensionId: options.getManifestEntryByExtensionId })
let lastBaseUrl: string | undefined let lastBaseUrl: string | undefined
const readBaseUrl = () => { const readBaseUrl = () => {
@@ -208,7 +208,7 @@ export function createExtensionAssetService(options: {
}, },
async createAssetSession(input) { async createAssetSession(input) {
const session = server.createSession({ const session = server.createSession({
extensionId: input.pluginId, extensionId: input.extensionId,
version: input.version, version: input.version,
ownerSessionId: input.ownerSessionId, ownerSessionId: input.ownerSessionId,
pathPrefix: input.pathPrefix, pathPrefix: input.pathPrefix,
@@ -222,7 +222,7 @@ export function createExtensionAssetService(options: {
} }
const mountedPath = buildMountedStaticAssetPath({ const mountedPath = buildMountedStaticAssetPath({
extensionId: input.pluginId, extensionId: input.extensionId,
assetSessionId: session.assetSessionId, assetSessionId: session.assetSessionId,
assetPath: input.routeAssetPath, assetPath: input.routeAssetPath,
}) })
@@ -2,7 +2,6 @@ import type { ExtensionHost } from '@proj-airi/plugin-sdk/plugin-host'
import type { import type {
PluginHostDebugSnapshot, PluginHostDebugSnapshot,
PluginHostModuleSummary,
} from '../../../../../shared/eventa/plugin/host' } from '../../../../../shared/eventa/plugin/host'
import type { ExtensionAssetSnapshotService } from '../features/static-assets' import type { ExtensionAssetSnapshotService } from '../features/static-assets'
import type { ExtensionConfig, ManifestEntry } from '../types' import type { ExtensionConfig, ManifestEntry } from '../types'
@@ -19,7 +18,7 @@ import { buildPluginRegistrySnapshot } from './registry'
* *
* Expects: * Expects:
* - `host` is the initialized extension host instance * - `host` is the initialized extension host instance
* - `manifestEntryByName` contains entries for any extension-owned modules being inspected * - `manifestEntryByExtensionId` contains entries for any extension-owned modules being inspected
* - `extensionAssetService` owns extension asset URL/session lifecycle when mounted asset URLs are needed * - `extensionAssetService` owns extension asset URL/session lifecycle when mounted asset URLs are needed
* *
* Returns: * Returns:
@@ -31,7 +30,7 @@ export function buildPluginHostDebugSnapshot(options: {
entries: ManifestEntry[] entries: ManifestEntry[]
config: ExtensionConfig config: ExtensionConfig
loaded: Set<string> loaded: Set<string>
manifestEntryByName: Map<string, ManifestEntry> manifestEntryByExtensionId: Map<string, ManifestEntry>
extensionAssetService?: ExtensionAssetSnapshotService extensionAssetService?: ExtensionAssetSnapshotService
}): Promise<PluginHostDebugSnapshot> { }): Promise<PluginHostDebugSnapshot> {
const extensionAssetService = options.extensionAssetService const extensionAssetService = options.extensionAssetService
@@ -39,10 +38,10 @@ export function buildPluginHostDebugSnapshot(options: {
.listBindings() .listBindings()
.map(module => .map(module =>
rewriteWidgetModuleAssetUrl( rewriteWidgetModuleAssetUrl(
module as PluginHostModuleSummary, module,
options.manifestEntryByName, options.manifestEntryByExtensionId,
{ {
pluginAssetBaseUrl: extensionAssetService?.getBaseUrl(), extensionAssetBaseUrl: extensionAssetService?.getBaseUrl(),
...(extensionAssetService ...(extensionAssetService
? { ? {
createAssetSession: ({ extensionId, version, sessionId, routeAssetPath, sessionPathPrefix }: { createAssetSession: ({ extensionId, version, sessionId, routeAssetPath, sessionPathPrefix }: {
@@ -52,7 +51,7 @@ export function buildPluginHostDebugSnapshot(options: {
routeAssetPath: string routeAssetPath: string
sessionPathPrefix: string sessionPathPrefix: string
}) => extensionAssetService.createAssetSession({ }) => extensionAssetService.createAssetSession({
pluginId: extensionId, extensionId,
version, version,
ownerSessionId: sessionId, ownerSessionId: sessionId,
routeAssetPath, routeAssetPath,
@@ -62,7 +61,7 @@ export function buildPluginHostDebugSnapshot(options: {
: {}), : {}),
}, },
), ),
) as Array<PluginHostModuleSummary | Promise<PluginHostModuleSummary>>) ))
return modules.then(resolvedModules => ({ return modules.then(resolvedModules => ({
registry: buildPluginRegistrySnapshot({ registry: buildPluginRegistrySnapshot({
@@ -73,13 +72,13 @@ export function buildPluginHostDebugSnapshot(options: {
}), }),
sessions: options.host.listSessions().map(session => ({ sessions: options.host.listSessions().map(session => ({
id: session.id, id: session.id,
manifestName: session.manifest.id, extensionId: session.manifest.id,
phase: session.phase, phase: session.phase,
runtime: session.runtime ?? 'electron', runtime: session.runtime ?? 'electron',
moduleId: session.extension.id, moduleId: session.extension.id,
})), })),
kits: options.host.listKits(), kits: options.host.listKits(),
modules: resolvedModules as PluginHostDebugSnapshot['modules'], modules: resolvedModules,
capabilities: options.host.listCapabilities(), capabilities: options.host.listCapabilities(),
refreshedAt: Date.now(), refreshedAt: Date.now(),
})) }))
@@ -93,13 +93,13 @@ export interface ExtensionHostServiceInternal extends ExtensionHostService {
* - Host state must remember a known manifest path for a plugin name * - Host state must remember a known manifest path for a plugin name
* *
* Expects: * Expects:
* - `payload.name` matches a discovered or previously known plugin * - `payload.extensionId` matches a discovered or previously known extension
* - `payload.path` is only needed when the manifest is not currently discoverable * - `payload.path` is only needed when the manifest is not currently discoverable
* *
* Returns: * Returns:
* - The updated extension registry snapshot after persistence * - The updated extension registry snapshot after persistence
*/ */
setEnabled: (payload: { name: string, enabled: boolean, path?: string }) => Promise<PluginRegistrySnapshot> setEnabled: (payload: { extensionId: string, enabled: boolean, path?: string }) => Promise<PluginRegistrySnapshot>
/** /**
* Persists whether one loaded plugin should use auto-reload. * Persists whether one loaded plugin should use auto-reload.
@@ -109,12 +109,12 @@ export interface ExtensionHostServiceInternal extends ExtensionHostService {
* - Host features need to resync optional watcher state after config changes * - Host features need to resync optional watcher state after config changes
* *
* Expects: * Expects:
* - `payload.name` matches one plugin entry in config or discovery state * - `payload.extensionId` matches one extension entry in config or discovery state
* *
* Returns: * Returns:
* - The updated extension registry snapshot after persistence * - The updated extension registry snapshot after persistence
*/ */
setAutoReload: (payload: { name: string, enabled: boolean }) => Promise<PluginRegistrySnapshot> setAutoReload: (payload: { extensionId: string, enabled: boolean }) => Promise<PluginRegistrySnapshot>
/** /**
* Loads every plugin currently marked as enabled. * Loads every plugin currently marked as enabled.
@@ -132,34 +132,34 @@ export interface ExtensionHostServiceInternal extends ExtensionHostService {
loadEnabled: () => Promise<PluginRegistrySnapshot> loadEnabled: () => Promise<PluginRegistrySnapshot>
/** /**
* Loads one plugin by manifest name. * Loads one extension by manifest id.
* *
* Use when: * Use when:
* - Renderer explicitly requests one plugin to start * - Renderer explicitly requests one plugin to start
* - Host features need to restart a plugin after manifest or entrypoint changes * - Host features need to restart a plugin after manifest or entrypoint changes
* *
* Expects: * Expects:
* - `name` resolves to a manifest entry in the current registry * - `extensionId` resolves to a manifest entry in the current registry
* *
* Returns: * Returns:
* - The extension registry snapshot after the load completes * - The extension registry snapshot after the load completes
*/ */
load: (name: string) => Promise<PluginRegistrySnapshot> load: (extensionId: string) => Promise<PluginRegistrySnapshot>
/** /**
* Stops one loaded plugin by manifest name. * Stops one loaded extension by manifest id.
* *
* Use when: * Use when:
* - Renderer explicitly requests one plugin to stop * - Renderer explicitly requests one plugin to stop
* - Host features need to stop a plugin before reload or disposal * - Host features need to stop a plugin before reload or disposal
* *
* Expects: * Expects:
* - `name` identifies a plugin that may or may not currently be loaded * - `extensionId` identifies an extension that may or may not currently be loaded
* *
* Returns: * Returns:
* - The extension registry snapshot after unload bookkeeping completes * - The extension registry snapshot after unload bookkeeping completes
*/ */
unload: (name: string) => Promise<PluginRegistrySnapshot> unload: (extensionId: string) => Promise<PluginRegistrySnapshot>
/** /**
* Builds the full extension host debug snapshot. * Builds the full extension host debug snapshot.
@@ -248,7 +248,7 @@ export async function setupExtensionHostServiceInternal(
// Extension feature: Static Assets serving // Extension feature: Static Assets serving
const extensionAssetService = createExtensionAssetService({ const extensionAssetService = createExtensionAssetService({
getManifestEntryByName: () => extensionRegistry.getManifestEntryByName(), getManifestEntryByExtensionId: () => extensionRegistry.getManifestEntryByExtensionId(),
cookieAdapter: createElectronExtensionAssetCookieAdapter(), cookieAdapter: createElectronExtensionAssetCookieAdapter(),
}) })
await extensionAssetService.start() await extensionAssetService.start()
@@ -290,21 +290,21 @@ export async function setupExtensionHostServiceInternal(
} }
const createModuleAssetSession = async (input: { const createModuleAssetSession = async (input: {
pluginId: string extensionId: string
version: string version: string
ownerSessionId: string ownerSessionId: string
routeAssetPath: string routeAssetPath: string
pathPrefix: string pathPrefix: string
}) => { }) => {
const { pluginId, version, ownerSessionId, routeAssetPath, pathPrefix } = input const { extensionId, version, ownerSessionId, routeAssetPath, pathPrefix } = input
const cacheKey = `${pluginId}:${version}:${ownerSessionId}:${routeAssetPath}:${pathPrefix}` const cacheKey = `${extensionId}:${version}:${ownerSessionId}:${routeAssetPath}:${pathPrefix}`
const cachedSession = moduleAssetSessionCache.get(cacheKey) const cachedSession = moduleAssetSessionCache.get(cacheKey)
if (cachedSession) { if (cachedSession) {
return cachedSession return cachedSession
} }
const session = await extensionAssetService.createAssetSession({ const session = await extensionAssetService.createAssetSession({
pluginId, extensionId,
version, version,
ownerSessionId, ownerSessionId,
routeAssetPath, routeAssetPath,
@@ -317,9 +317,9 @@ export async function setupExtensionHostServiceInternal(
const extensionAssetSnapshotService: ExtensionAssetSnapshotService = { const extensionAssetSnapshotService: ExtensionAssetSnapshotService = {
getBaseUrl: extensionAssetService.getBaseUrl, getBaseUrl: extensionAssetService.getBaseUrl,
createAssetSession: ({ pluginId, version, ownerSessionId, routeAssetPath, pathPrefix }) => { createAssetSession: ({ extensionId, version, ownerSessionId, routeAssetPath, pathPrefix }) => {
return createModuleAssetSession({ return createModuleAssetSession({
pluginId, extensionId,
version, version,
ownerSessionId, ownerSessionId,
routeAssetPath, routeAssetPath,
@@ -335,50 +335,50 @@ export async function setupExtensionHostServiceInternal(
entries: extensionRegistry.listEntries(), entries: extensionRegistry.listEntries(),
config: getConfig(), config: getConfig(),
loaded, loaded,
manifestEntryByName: extensionRegistry.getManifestEntryByName(), manifestEntryByExtensionId: extensionRegistry.getManifestEntryByExtensionId(),
extensionAssetService: extensionAssetSnapshotService, extensionAssetService: extensionAssetSnapshotService,
}) })
} }
const loadExtensionById = async ( const loadExtensionById = async (
name: string, extensionId: string,
loadOptions: { cacheBustKey?: string } = {}, loadOptions: { cacheBustKey?: string } = {},
) => { ) => {
if (loaded.has(name)) { if (loaded.has(extensionId)) {
return return
} }
const entry = extensionRegistry.findManifestEntry(name) const entry = extensionRegistry.findManifestEntry(extensionId)
if (!entry) { if (!entry) {
throw new Error(`Extension manifest not found: ${name}`) throw new Error(`Extension manifest not found: ${extensionId}`)
} }
const manifestForLoad = createManifestForLoad(entry, loadOptions) const manifestForLoad = createManifestForLoad(entry, loadOptions)
const session = await host.start(manifestForLoad, { cwd: dirname(entry.path) }) const session = await host.start(manifestForLoad, { cwd: dirname(entry.path) })
loaded.add(name) loaded.add(extensionId)
loadedSessionIds.set(name, session.id) loadedSessionIds.set(extensionId, session.id)
log.log('extension loaded', { extension: name, sessionId: session.id }) log.withFields({ extensionId, sessionId: session.id }).log('extension loaded')
} }
const stopLoadedExtensionById = async (name: string) => { const stopLoadedExtensionById = async (extensionId: string) => {
const sessionId = loadedSessionIds.get(name) const sessionId = loadedSessionIds.get(extensionId)
if (!sessionId) { if (!sessionId) {
loaded.delete(name) loaded.delete(extensionId)
return return
} }
await host.stop(sessionId) await host.stop(sessionId)
loadedSessionIds.delete(name) loadedSessionIds.delete(extensionId)
loaded.delete(name) loaded.delete(extensionId)
clearModuleAssetSessionCacheByOwnerSessionId(sessionId) clearModuleAssetSessionCacheByOwnerSessionId(sessionId)
await extensionAssetService.revokeByOwnerSessionId(sessionId) await extensionAssetService.revokeByOwnerSessionId(sessionId)
log.log('extension unloaded', { extension: name, sessionId }) log.withFields({ extensionId, sessionId }).log('extension unloaded')
} }
const resolveAutoReloadWatchPaths = (name: string) => { const resolveAutoReloadWatchPaths = (extensionId: string) => {
const entry = extensionRegistry.findManifestEntry(name) const entry = extensionRegistry.findManifestEntry(extensionId)
if (!entry) { if (!entry) {
return [] return []
} }
@@ -392,36 +392,36 @@ export async function setupExtensionHostServiceInternal(
log, log,
getConfig, getConfig,
listEntries: () => extensionRegistry.listEntries(), listEntries: () => extensionRegistry.listEntries(),
isLoaded: name => loaded.has(name), isLoaded: extensionId => loaded.has(extensionId),
resolveWatchPaths: resolveAutoReloadWatchPaths, resolveWatchPaths: resolveAutoReloadWatchPaths,
reload: async (name) => { reload: async (extensionId) => {
await stopLoadedExtensionById(name) await stopLoadedExtensionById(extensionId)
await refreshManifests() await refreshManifests()
await loadExtensionById(name, { cacheBustKey: `auto-reload-${Date.now()}` }) await loadExtensionById(extensionId, { cacheBustKey: `auto-reload-${Date.now()}` })
}, },
}) })
const unloadExtensionById = async (name: string) => { const unloadExtensionById = async (extensionId: string) => {
autoReloadFeature.clearExtension(name) autoReloadFeature.clearExtension(extensionId)
await stopLoadedExtensionById(name) await stopLoadedExtensionById(extensionId)
} }
const loadEnabledExtensions = async () => { const loadEnabledExtensions = async () => {
const config = getConfig() const config = getConfig()
for (const entry of extensionRegistry.listEntries()) { for (const entry of extensionRegistry.listEntries()) {
const name = manifestIdOf(entry.manifest) const extensionId = manifestIdOf(entry.manifest)
if (!config.enabled.includes(name)) { if (!config.enabled.includes(extensionId)) {
continue continue
} }
if (loaded.has(name)) { if (loaded.has(extensionId)) {
continue continue
} }
try { try {
await loadExtensionById(name) await loadExtensionById(extensionId)
} }
catch (error) { catch (error) {
log.withError(error).withFields({ extension: name }).error('extension failed to start') log.withError(error).withFields({ extensionId }).error('extension failed to start')
} }
} }
@@ -450,22 +450,22 @@ export async function setupExtensionHostServiceInternal(
const config = getConfig() const config = getConfig()
const enabled = new Set(config.enabled) const enabled = new Set(config.enabled)
if (payload.enabled) { if (payload.enabled) {
enabled.add(payload.name) enabled.add(payload.extensionId)
} }
else { else {
enabled.delete(payload.name) enabled.delete(payload.extensionId)
clearModuleAssetSessionCacheByExtensionId(payload.name) clearModuleAssetSessionCacheByExtensionId(payload.extensionId)
await extensionAssetService.revokeByExtensionId(payload.name) await extensionAssetService.revokeByExtensionId(payload.extensionId)
} }
const entry = extensionRegistry.findManifestEntry(payload.name) const entry = extensionRegistry.findManifestEntry(payload.extensionId)
const manifestPath = entry?.path ?? payload.path ?? '' const manifestPath = entry?.path ?? payload.path ?? ''
extensionConfig.update({ extensionConfig.update({
enabled: [...enabled], enabled: [...enabled],
autoReload: config.autoReload, autoReload: config.autoReload,
known: { known: {
...config.known, ...config.known,
[payload.name]: { path: manifestPath }, [payload.extensionId]: { path: manifestPath },
}, },
}) })
@@ -478,10 +478,10 @@ export async function setupExtensionHostServiceInternal(
const config = getConfig() const config = getConfig()
const autoReload = new Set(config.autoReload) const autoReload = new Set(config.autoReload)
if (payload.enabled) { if (payload.enabled) {
autoReload.add(payload.name) autoReload.add(payload.extensionId)
} }
else { else {
autoReload.delete(payload.name) autoReload.delete(payload.extensionId)
} }
extensionConfig.update({ extensionConfig.update({
@@ -498,14 +498,14 @@ export async function setupExtensionHostServiceInternal(
autoReloadFeature.sync() autoReloadFeature.sync()
return listSnapshot() return listSnapshot()
}, },
async load(name) { async load(extensionId) {
await refreshManifests() await refreshManifests()
await loadExtensionById(name) await loadExtensionById(extensionId)
autoReloadFeature.sync() autoReloadFeature.sync()
return listSnapshot() return listSnapshot()
}, },
async unload(name) { async unload(extensionId) {
await unloadExtensionById(name) await unloadExtensionById(extensionId)
autoReloadFeature.sync() autoReloadFeature.sync()
return listSnapshot() return listSnapshot()
}, },
@@ -164,7 +164,7 @@ export async function loadManifestsFrom(
} }
/** /**
* Builds a renderer-facing plugin summary from manifest, config, and runtime state. * Builds a renderer-facing extension summary from manifest, config, and runtime state.
* *
* Use when: * Use when:
* - Registry snapshots need one UI-friendly entry per discovered plugin * - Registry snapshots need one UI-friendly entry per discovered plugin
@@ -182,15 +182,15 @@ export function createPluginSummary(
config: ExtensionConfig, config: ExtensionConfig,
loaded: Set<string>, loaded: Set<string>,
): PluginManifestSummary { ): PluginManifestSummary {
const name = manifestIdOf(entry.manifest) const extensionId = manifestIdOf(entry.manifest)
return { return {
name, extensionId,
entrypoints: entry.manifest.entrypoints, entrypoints: entry.manifest.entrypoints,
path: entry.path, path: entry.path,
enabled: config.enabled.includes(name), enabled: config.enabled.includes(extensionId),
autoReload: config.autoReload.includes(name), autoReload: config.autoReload.includes(extensionId),
loaded: loaded.has(name), loaded: loaded.has(extensionId),
isNew: !config.known[name], isNew: !config.known[extensionId],
} }
} }
@@ -285,7 +285,7 @@ export function createManifestForLoad(
* *
* Use when: * Use when:
* - Refreshing extension manifests from disk * - Refreshing extension manifests from disk
* - Looking up manifests by plugin name during load or inspect operations * - Looking up manifests by extension id during load or inspect operations
* *
* Expects: * Expects:
* - `refresh()` is called before consumers read entries or manifests * - `refresh()` is called before consumers read entries or manifests
@@ -299,8 +299,8 @@ export interface ExtensionHostRegistry {
refresh: () => Promise<ManifestEntry[]> refresh: () => Promise<ManifestEntry[]>
listEntries: () => ManifestEntry[] listEntries: () => ManifestEntry[]
listManifests: () => ExtensionManifestV1[] listManifests: () => ExtensionManifestV1[]
findManifestEntry: (name: string) => ManifestEntry | undefined findManifestEntry: (extensionId: string) => ManifestEntry | undefined
getManifestEntryByName: () => Map<string, ManifestEntry> getManifestEntryByExtensionId: () => Map<string, ManifestEntry>
} }
/** /**
@@ -321,7 +321,7 @@ export function createExtensionHostRegistry(options: {
}): ExtensionHostRegistry { }): ExtensionHostRegistry {
let entries: ManifestEntry[] = [] let entries: ManifestEntry[] = []
let manifests: ExtensionManifestV1[] = [] let manifests: ExtensionManifestV1[] = []
let manifestEntryByName = new Map<string, ManifestEntry>() let manifestEntryByExtensionId = new Map<string, ManifestEntry>()
return { return {
getRoot() { getRoot() {
@@ -329,11 +329,11 @@ export function createExtensionHostRegistry(options: {
}, },
async refresh() { async refresh() {
entries = await loadManifestsFrom(options.extensionsRoot, options.log) entries = await loadManifestsFrom(options.extensionsRoot, options.log)
manifestEntryByName = new Map() manifestEntryByExtensionId = new Map()
for (const entry of entries) { for (const entry of entries) {
const id = manifestIdOf(entry.manifest) const id = manifestIdOf(entry.manifest)
if (!manifestEntryByName.has(id)) { if (!manifestEntryByExtensionId.has(id)) {
manifestEntryByName.set(id, entry) manifestEntryByExtensionId.set(id, entry)
} }
} }
manifests = entries.map(entry => entry.manifest) manifests = entries.map(entry => entry.manifest)
@@ -345,11 +345,11 @@ export function createExtensionHostRegistry(options: {
listManifests() { listManifests() {
return manifests return manifests
}, },
findManifestEntry(name) { findManifestEntry(extensionId) {
return manifestEntryByName.get(name) return manifestEntryByExtensionId.get(extensionId)
}, },
getManifestEntryByName() { getManifestEntryByExtensionId() {
return manifestEntryByName return manifestEntryByExtensionId
}, },
} }
} }
@@ -426,8 +426,8 @@ describe('setupExtensionHost', () => {
expect(snapshot.root).toBe(pluginsDir) expect(snapshot.root).toBe(pluginsDir)
expect(snapshot.plugins).toHaveLength(2) expect(snapshot.plugins).toHaveLength(2)
expect(snapshot.plugins).toEqual(expect.arrayContaining([ expect(snapshot.plugins).toEqual(expect.arrayContaining([
expect.objectContaining({ name: 'test-normal', path: normalPath, enabled: false, loaded: false, isNew: true }), expect.objectContaining({ extensionId: 'test-normal', path: normalPath, enabled: false, loaded: false, isNew: true }),
expect.objectContaining({ name: 'test-error', path: errorPath, enabled: false, loaded: false, isNew: true }), expect.objectContaining({ extensionId: 'test-error', path: errorPath, enabled: false, loaded: false, isNew: true }),
])) ]))
}) })
@@ -491,7 +491,7 @@ describe('setupExtensionHost', () => {
expect(snapshot.plugins).toEqual([ expect(snapshot.plugins).toEqual([
expect.objectContaining({ expect.objectContaining({
name: 'devtools-sample-plugin', extensionId: 'devtools-sample-plugin',
path: manifestPath, path: manifestPath,
enabled: false, enabled: false,
loaded: false, loaded: false,
@@ -528,13 +528,13 @@ describe('setupExtensionHost', () => {
const invokeSetEnabled = defineInvoke(contextState.lastContext!, electronPluginSetEnabled) const invokeSetEnabled = defineInvoke(contextState.lastContext!, electronPluginSetEnabled)
const invokeLoadEnabled = defineInvoke(contextState.lastContext!, electronPluginLoadEnabled) const invokeLoadEnabled = defineInvoke(contextState.lastContext!, electronPluginLoadEnabled)
await invokeSetEnabled({ name: 'test-normal', enabled: true }) await invokeSetEnabled({ extensionId: 'test-normal', enabled: true })
await invokeSetEnabled({ name: 'test-error', enabled: true }) await invokeSetEnabled({ extensionId: 'test-error', enabled: true })
const snapshot = await invokeLoadEnabled() const snapshot = await invokeLoadEnabled()
const normal = snapshot.plugins.find(plugin => plugin.name === 'test-normal') const normal = snapshot.plugins.find(plugin => plugin.extensionId === 'test-normal')
const error = snapshot.plugins.find(plugin => plugin.name === 'test-error') const error = snapshot.plugins.find(plugin => plugin.extensionId === 'test-error')
expect(normal).toEqual(expect.objectContaining({ enabled: true, loaded: true })) expect(normal).toEqual(expect.objectContaining({ enabled: true, loaded: true }))
expect(error).toEqual(expect.objectContaining({ enabled: true, loaded: false })) expect(error).toEqual(expect.objectContaining({ enabled: true, loaded: false }))
@@ -557,7 +557,7 @@ describe('setupExtensionHost', () => {
await setupExtensionHost() await setupExtensionHost()
expect(contextState.lastContext).toBeDefined() expect(contextState.lastContext).toBeDefined()
const toolsChangedEvents: Array<{ reason: string, name?: string }> = [] const toolsChangedEvents: Array<{ reason: string, extensionId?: string }> = []
contextState.lastContext!.on(electronPluginToolsChanged, (event) => { contextState.lastContext!.on(electronPluginToolsChanged, (event) => {
if (!event.body) { if (!event.body) {
throw new Error('Expected plugin tools changed event body.') throw new Error('Expected plugin tools changed event body.')
@@ -567,12 +567,12 @@ describe('setupExtensionHost', () => {
const invokeLoad = defineInvoke(contextState.lastContext!, electronPluginLoad) const invokeLoad = defineInvoke(contextState.lastContext!, electronPluginLoad)
await invokeLoad({ name: 'test-tools-changed' }) await invokeLoad({ extensionId: 'test-tools-changed' })
expect(toolsChangedEvents).toEqual([ expect(toolsChangedEvents).toEqual([
{ {
reason: 'loaded', reason: 'loaded',
name: 'test-tools-changed', extensionId: 'test-tools-changed',
}, },
]) ])
}) })
@@ -605,7 +605,7 @@ describe('setupExtensionHost', () => {
const invokeSetEnabled = defineInvoke(contextState.lastContext!, electronPluginSetEnabled) const invokeSetEnabled = defineInvoke(contextState.lastContext!, electronPluginSetEnabled)
const invokeLoadEnabled = defineInvoke(contextState.lastContext!, electronPluginLoadEnabled) const invokeLoadEnabled = defineInvoke(contextState.lastContext!, electronPluginLoadEnabled)
await invokeSetEnabled({ name: 'duplicate-plugin', enabled: true }) await invokeSetEnabled({ extensionId: 'duplicate-plugin', enabled: true })
await invokeLoadEnabled() await invokeLoadEnabled()
const duplicateSession = service.host const duplicateSession = service.host
@@ -631,16 +631,16 @@ describe('setupExtensionHost', () => {
const invokeSetAutoReload = defineInvoke(contextState.lastContext!, electronPluginSetAutoReload) const invokeSetAutoReload = defineInvoke(contextState.lastContext!, electronPluginSetAutoReload)
const invokeList = defineInvoke(contextState.lastContext!, electronPluginList) const invokeList = defineInvoke(contextState.lastContext!, electronPluginList)
await invokeSetAutoReload({ name: 'test-auto-reload', enabled: true }) await invokeSetAutoReload({ extensionId: 'test-auto-reload', enabled: true })
let snapshot = await invokeList() let snapshot = await invokeList()
expect(snapshot.plugins).toEqual(expect.arrayContaining([ expect(snapshot.plugins).toEqual(expect.arrayContaining([
expect.objectContaining({ name: 'test-auto-reload', autoReload: true }), expect.objectContaining({ extensionId: 'test-auto-reload', autoReload: true }),
])) ]))
await invokeSetAutoReload({ name: 'test-auto-reload', enabled: false }) await invokeSetAutoReload({ extensionId: 'test-auto-reload', enabled: false })
snapshot = await invokeList() snapshot = await invokeList()
expect(snapshot.plugins).toEqual(expect.arrayContaining([ expect(snapshot.plugins).toEqual(expect.arrayContaining([
expect.objectContaining({ name: 'test-auto-reload', autoReload: false }), expect.objectContaining({ extensionId: 'test-auto-reload', autoReload: false }),
])) ]))
}) })
@@ -667,12 +667,12 @@ describe('setupExtensionHost', () => {
const invokeInspect = defineInvoke(contextState.lastContext!, electronPluginInspect) const invokeInspect = defineInvoke(contextState.lastContext!, electronPluginInspect)
const invokeUnload = defineInvoke(contextState.lastContext!, electronPluginUnload) const invokeUnload = defineInvoke(contextState.lastContext!, electronPluginUnload)
await invokeSetEnabled({ name: 'test-auto-reload-reload', enabled: true }) await invokeSetEnabled({ extensionId: 'test-auto-reload-reload', enabled: true })
await invokeLoadEnabled() await invokeLoadEnabled()
await invokeSetAutoReload({ name: 'test-auto-reload-reload', enabled: true }) await invokeSetAutoReload({ extensionId: 'test-auto-reload-reload', enabled: true })
const before = await invokeInspect() const before = await invokeInspect()
const beforeSession = before.sessions.find(session => session.manifestName === 'test-auto-reload-reload') const beforeSession = before.sessions.find(session => session.extensionId === 'test-auto-reload-reload')
expect(beforeSession).toBeDefined() expect(beforeSession).toBeDefined()
const pluginSdkUrl = pathToFileURL(resolve(repoRoot, 'packages/plugin-sdk/src/index.ts')).href const pluginSdkUrl = pathToFileURL(resolve(repoRoot, 'packages/plugin-sdk/src/index.ts')).href
@@ -692,14 +692,14 @@ describe('setupExtensionHost', () => {
while (Date.now() < deadline && afterSessionId === beforeSession?.id) { while (Date.now() < deadline && afterSessionId === beforeSession?.id) {
await new Promise(resolve => setTimeout(resolve, 100)) await new Promise(resolve => setTimeout(resolve, 100))
const snapshot = await invokeInspect() const snapshot = await invokeInspect()
afterSessionId = snapshot.sessions.find(session => session.manifestName === 'test-auto-reload-reload')?.id afterSessionId = snapshot.sessions.find(session => session.extensionId === 'test-auto-reload-reload')?.id
} }
expect(afterSessionId).toBeDefined() expect(afterSessionId).toBeDefined()
expect(afterSessionId).not.toEqual(beforeSession?.id) expect(afterSessionId).not.toEqual(beforeSession?.id)
await invokeSetAutoReload({ name: 'test-auto-reload-reload', enabled: false }) await invokeSetAutoReload({ extensionId: 'test-auto-reload-reload', enabled: false })
await invokeUnload({ name: 'test-auto-reload-reload' }) await invokeUnload({ extensionId: 'test-auto-reload-reload' })
}) })
it('loads enabled plugins with absolute manifest entrypoints outside the plugin directory', async () => { it('loads enabled plugins with absolute manifest entrypoints outside the plugin directory', async () => {
@@ -725,10 +725,10 @@ describe('setupExtensionHost', () => {
const invokeSetEnabled = defineInvoke(contextState.lastContext!, electronPluginSetEnabled) const invokeSetEnabled = defineInvoke(contextState.lastContext!, electronPluginSetEnabled)
const invokeLoadEnabled = defineInvoke(contextState.lastContext!, electronPluginLoadEnabled) const invokeLoadEnabled = defineInvoke(contextState.lastContext!, electronPluginLoadEnabled)
await invokeSetEnabled({ name: 'test-absolute-entrypoint', enabled: true }) await invokeSetEnabled({ extensionId: 'test-absolute-entrypoint', enabled: true })
const snapshot = await invokeLoadEnabled() const snapshot = await invokeLoadEnabled()
const plugin = snapshot.plugins.find(item => item.name === 'test-absolute-entrypoint') const plugin = snapshot.plugins.find(item => item.extensionId === 'test-absolute-entrypoint')
expect(plugin).toEqual(expect.objectContaining({ enabled: true, loaded: true })) expect(plugin).toEqual(expect.objectContaining({ enabled: true, loaded: true }))
} }
@@ -759,10 +759,10 @@ describe('setupExtensionHost', () => {
const invokeSetEnabled = defineInvoke(contextState.lastContext!, electronPluginSetEnabled) const invokeSetEnabled = defineInvoke(contextState.lastContext!, electronPluginSetEnabled)
const invokeLoadEnabled = defineInvoke(contextState.lastContext!, electronPluginLoadEnabled) const invokeLoadEnabled = defineInvoke(contextState.lastContext!, electronPluginLoadEnabled)
await invokeSetEnabled({ name: 'devtools-sample-plugin', enabled: true }) await invokeSetEnabled({ extensionId: 'devtools-sample-plugin', enabled: true })
const snapshot = await invokeLoadEnabled() const snapshot = await invokeLoadEnabled()
const plugin = snapshot.plugins.find(item => item.name === 'devtools-sample-plugin') const plugin = snapshot.plugins.find(item => item.extensionId === 'devtools-sample-plugin')
expect(plugin).toEqual(expect.objectContaining({ enabled: true, loaded: true })) expect(plugin).toEqual(expect.objectContaining({ enabled: true, loaded: true }))
}) })
@@ -825,10 +825,10 @@ describe('setupExtensionHost', () => {
const invokeLoadEnabled = defineInvoke(contextState.lastContext!, electronPluginLoadEnabled) const invokeLoadEnabled = defineInvoke(contextState.lastContext!, electronPluginLoadEnabled)
const invokeInspect = defineInvoke(contextState.lastContext!, electronPluginInspect) const invokeInspect = defineInvoke(contextState.lastContext!, electronPluginInspect)
await invokeSetEnabled({ name: 'airi-plugin-game-chess', enabled: true }) await invokeSetEnabled({ extensionId: 'airi-plugin-game-chess', enabled: true })
const registry = await invokeLoadEnabled() const registry = await invokeLoadEnabled()
const plugin = registry.plugins.find(item => item.name === 'airi-plugin-game-chess') const plugin = registry.plugins.find(item => item.extensionId === 'airi-plugin-game-chess')
expect(plugin).toEqual(expect.objectContaining({ enabled: true, loaded: true })) expect(plugin).toEqual(expect.objectContaining({ enabled: true, loaded: true }))
const snapshot = await invokeInspect() const snapshot = await invokeInspect()
@@ -837,7 +837,7 @@ describe('setupExtensionHost', () => {
expect(snapshot.modules).toEqual(expect.arrayContaining([ expect(snapshot.modules).toEqual(expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
moduleId: 'chess-like-main:gamelet', moduleId: 'chess-like-main:gamelet',
ownerPluginId: 'airi-plugin-game-chess', ownerExtensionId: 'airi-plugin-game-chess',
kitId: 'kit.gamelet', kitId: 'kit.gamelet',
kitModuleType: 'gamelet', kitModuleType: 'gamelet',
runtime: 'electron', runtime: 'electron',
@@ -912,7 +912,7 @@ describe('setupExtensionHost', () => {
const invokeLoadEnabled = defineInvoke(contextState.lastContext!, electronPluginLoadEnabled) const invokeLoadEnabled = defineInvoke(contextState.lastContext!, electronPluginLoadEnabled)
const invokeInspect = defineInvoke(contextState.lastContext!, electronPluginInspect) const invokeInspect = defineInvoke(contextState.lastContext!, electronPluginInspect)
await invokeSetEnabled({ name: 'test-plugin-widget-asset-url', enabled: true }) await invokeSetEnabled({ extensionId: 'test-plugin-widget-asset-url', enabled: true })
await invokeLoadEnabled() await invokeLoadEnabled()
const session = service.host const session = service.host
.listSessions() .listSessions()
@@ -947,7 +947,7 @@ describe('setupExtensionHost', () => {
expect(snapshot.modules).toEqual(expect.arrayContaining([ expect(snapshot.modules).toEqual(expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
moduleId: 'widget-shell-under-test', moduleId: 'widget-shell-under-test',
ownerPluginId: 'test-plugin-widget-asset-url', ownerExtensionId: 'test-plugin-widget-asset-url',
kitId: 'kit.widget', kitId: 'kit.widget',
kitModuleType: 'window', kitModuleType: 'window',
runtime: 'electron', runtime: 'electron',
@@ -1084,7 +1084,7 @@ describe('setupExtensionHost', () => {
expect.objectContaining({ expect.objectContaining({
moduleId: 'widget-shell', moduleId: 'widget-shell',
ownerSessionId: session.id, ownerSessionId: session.id,
ownerPluginId: 'test-dynamic-module', ownerExtensionId: 'test-dynamic-module',
kitId: 'kit.widget', kitId: 'kit.widget',
kitModuleType: 'window', kitModuleType: 'window',
runtime: 'electron', runtime: 'electron',
@@ -1183,7 +1183,7 @@ describe('setupExtensionHost', () => {
expect(binding).toEqual(expect.objectContaining({ expect(binding).toEqual(expect.objectContaining({
moduleId: 'kit-module:gamelet', moduleId: 'kit-module:gamelet',
ownerPluginId: 'test-extension-gamelet-kit', ownerExtensionId: 'test-extension-gamelet-kit',
ownerSessionId: session.id, ownerSessionId: session.id,
kitId: 'kit.gamelet', kitId: 'kit.gamelet',
kitModuleType: 'gamelet', kitModuleType: 'gamelet',
@@ -57,7 +57,7 @@ export async function setupExtensionHost(options: SetupExtensionHostOptions): Pr
const result = await hostService.setEnabled(payload) const result = await hostService.setEnabled(payload)
context.emit(electronPluginToolsChanged, { context.emit(electronPluginToolsChanged, {
reason: 'enabled-state-changed', reason: 'enabled-state-changed',
name: payload.name, extensionId: payload.extensionId,
}) })
return result return result
}) })
@@ -75,19 +75,19 @@ export async function setupExtensionHost(options: SetupExtensionHostOptions): Pr
}) })
defineInvokeHandler(context, electronPluginLoad, async (payload) => { defineInvokeHandler(context, electronPluginLoad, async (payload) => {
const result = await hostService.load(payload.name) const result = await hostService.load(payload.extensionId)
context.emit(electronPluginToolsChanged, { context.emit(electronPluginToolsChanged, {
reason: 'loaded', reason: 'loaded',
name: payload.name, extensionId: payload.extensionId,
}) })
return result return result
}) })
defineInvokeHandler(context, electronPluginUnload, async (payload) => { defineInvokeHandler(context, electronPluginUnload, async (payload) => {
const result = await hostService.unload(payload.name) const result = await hostService.unload(payload.extensionId)
context.emit(electronPluginToolsChanged, { context.emit(electronPluginToolsChanged, {
reason: 'unloaded', reason: 'unloaded',
name: payload.name, extensionId: payload.extensionId,
}) })
return result return result
}) })
@@ -109,7 +109,7 @@ export async function setupExtensionHost(options: SetupExtensionHostOptions): Pr
}) })
defineInvokeHandler(context, electronPluginInvokeTool, async (payload) => { defineInvokeHandler(context, electronPluginInvokeTool, async (payload) => {
return await hostService.tools.invoke(payload.ownerPluginId, payload.name, payload.input) return await hostService.tools.invoke(payload.ownerExtensionId, payload.name, payload.input)
}) })
defineInvokeHandler(context, electronPluginUpdateCapability, async (payload) => { defineInvokeHandler(context, electronPluginUpdateCapability, async (payload) => {
@@ -57,7 +57,7 @@ function createHostToolKit(options: { tools: TamagotchiToolRegistry }): KitRef<T
ensureCleanup() ensureCleanup()
options.tools.register({ options.tools.register({
ownerSessionId: runtime.sessionId, ownerSessionId: runtime.sessionId,
ownerPluginId: runtime.extensionId, ownerExtensionId: runtime.extensionId,
ownerModuleId: runtime.moduleId, ownerModuleId: runtime.moduleId,
...input, ...input,
}) })
@@ -66,7 +66,7 @@ function createHostToolKit(options: { tools: TamagotchiToolRegistry }): KitRef<T
ensureCleanup() ensureCleanup()
options.tools.registerToolsetPrompt({ options.tools.registerToolsetPrompt({
ownerSessionId: runtime.sessionId, ownerSessionId: runtime.sessionId,
ownerPluginId: runtime.extensionId, ownerExtensionId: runtime.extensionId,
ownerModuleId: runtime.moduleId, ownerModuleId: runtime.moduleId,
toolset: input, toolset: input,
}) })
@@ -87,7 +87,7 @@ export function resolveWidgetAssetRoute(assetPath: string): WidgetAssetRoute | u
* *
* Expects: * Expects:
* - Module config may contain widget iframe `src` or `assetPath` fields * - Module config may contain widget iframe `src` or `assetPath` fields
* - Mapping includes a manifest entry for `module.ownerPluginId` * - Mapping includes a manifest entry for `module.ownerExtensionId`
* *
* Returns: * Returns:
* - Original module when rewrite is not applicable * - Original module when rewrite is not applicable
@@ -95,9 +95,9 @@ export function resolveWidgetAssetRoute(assetPath: string): WidgetAssetRoute | u
*/ */
export function rewriteWidgetModuleAssetUrl( export function rewriteWidgetModuleAssetUrl(
module: PluginHostModuleSummary, module: PluginHostModuleSummary,
manifestEntryByName: Map<string, ManifestEntry>, manifestEntryByExtensionId: Map<string, ManifestEntry>,
options?: { options?: {
pluginAssetBaseUrl?: string extensionAssetBaseUrl?: string
createAssetSession?: (input: { createAssetSession?: (input: {
extensionId: string extensionId: string
version: string version: string
@@ -107,7 +107,7 @@ export function rewriteWidgetModuleAssetUrl(
}) => Promise<{ assetSessionId: string, url?: string }> }) => Promise<{ assetSessionId: string, url?: string }>
}, },
): Promise<PluginHostModuleSummary> | PluginHostModuleSummary { ): Promise<PluginHostModuleSummary> | PluginHostModuleSummary {
const entry = manifestEntryByName.get(module.ownerPluginId) const entry = manifestEntryByExtensionId.get(module.ownerExtensionId)
if (!entry) { if (!entry) {
return module return module
} }
@@ -138,23 +138,23 @@ export function rewriteWidgetModuleAssetUrl(
return module return module
} }
if (!options?.pluginAssetBaseUrl || !options.createAssetSession) { if (!options?.extensionAssetBaseUrl || !options.createAssetSession) {
return module return module
} }
return options.createAssetSession({ return options.createAssetSession({
extensionId: module.ownerPluginId, extensionId: module.ownerExtensionId,
version: entry.version, version: entry.version,
sessionId: module.ownerSessionId, sessionId: module.ownerSessionId,
routeAssetPath: widgetAssetRoute.routeAssetPath, routeAssetPath: widgetAssetRoute.routeAssetPath,
sessionPathPrefix: widgetAssetRoute.sessionPathPrefix, sessionPathPrefix: widgetAssetRoute.sessionPathPrefix,
}).then((session) => { }).then((session) => {
const mountedPath = buildMountedStaticAssetPath({ const mountedPath = buildMountedStaticAssetPath({
extensionId: module.ownerPluginId, extensionId: module.ownerExtensionId,
assetSessionId: session.assetSessionId, assetSessionId: session.assetSessionId,
assetPath: widgetAssetRoute.routeAssetPath, assetPath: widgetAssetRoute.routeAssetPath,
}) })
const iframeUrl = session.url ?? (mountedPath ? new URL(mountedPath, options.pluginAssetBaseUrl).toString() : '') const iframeUrl = session.url ?? (mountedPath ? new URL(mountedPath, options.extensionAssetBaseUrl).toString() : '')
if (!iframeUrl) { if (!iframeUrl) {
return module return module
} }
@@ -6,6 +6,11 @@ import type {
WidgetsUpdatePayload, WidgetsUpdatePayload,
} from '../../../../shared/eventa' } from '../../../../shared/eventa'
/**
* Stable manifest id used as the runtime identity for one extension.
*/
export type ExtensionId = string
/** /**
* Runtime-facing extension host service bundle returned by setup. * Runtime-facing extension host service bundle returned by setup.
* *
@@ -108,20 +113,20 @@ export interface ExtensionHostBindingListOptions {
* Persisted extension configuration snapshot. * Persisted extension configuration snapshot.
* *
* Use when: * Use when:
* - Reading/writing enabled and auto-reload plugin state * - Reading/writing enabled and auto-reload extension state
* - Keeping known extension manifest path metadata * - Keeping known extension manifest path metadata
* *
* Expects: * Expects:
* - Arrays contain extension manifest names * - Arrays contain extension manifest ids
* - `known` maps plugin names to canonical manifest paths * - `known` maps extension manifest ids to canonical manifest paths
* *
* Returns: * Returns:
* - N/A * - N/A
*/ */
export interface ExtensionConfig { export interface ExtensionConfig {
enabled: string[] enabled: ExtensionId[]
autoReload: string[] autoReload: ExtensionId[]
known: Record<string, { path: string }> known: Record<ExtensionId, { path: string }>
} }
/** /**
@@ -10,7 +10,7 @@ const invokeMocks = vi.hoisted(() => ({
listPluginXsaiTools: vi.fn(async () => ({ listPluginXsaiTools: vi.fn(async () => ({
tools: [ tools: [
{ {
ownerPluginId: 'plugin-chess', ownerExtensionId: 'plugin-chess',
name: 'play_chess', name: 'play_chess',
description: 'Play a chess move.', description: 'Play a chess move.',
parameters: { parameters: {
@@ -21,7 +21,7 @@ const invokeMocks = vi.hoisted(() => ({
], ],
prompts: [ prompts: [
{ {
ownerPluginId: 'plugin-chess', ownerExtensionId: 'plugin-chess',
id: 'chess-tools', id: 'chess-tools',
prompt: { prompt: {
id: 'airi-plugin-game-chess.prompt', id: 'airi-plugin-game-chess.prompt',
@@ -84,14 +84,14 @@ describe('useTamagotchiPluginToolsStore', async () => {
}, toolOptions) }, toolOptions)
expect(invokeMocks.invokePluginTool).toHaveBeenCalledWith({ expect(invokeMocks.invokePluginTool).toHaveBeenCalledWith({
ownerPluginId: 'plugin-chess', ownerExtensionId: 'plugin-chess',
name: 'play_chess', name: 'play_chess',
input: { input: {
move: 'e2e4', move: 'e2e4',
}, },
}) })
expect(executionResult).toEqual({ expect(executionResult).toEqual({
ownerPluginId: 'plugin-chess', ownerExtensionId: 'plugin-chess',
name: 'play_chess', name: 'play_chess',
input: { input: {
move: 'e2e4', move: 'e2e4',
@@ -43,7 +43,7 @@ export const useTamagotchiPluginToolsStore = defineStore('tamagotchi-plugin-tool
llmToolsetPromptsStore.registerToolsetPrompts( llmToolsetPromptsStore.registerToolsetPrompts(
'plugin-tools', 'plugin-tools',
definitions.prompts.map(definition => ({ definitions.prompts.map(definition => ({
id: `${definition.ownerPluginId}:${definition.id}`, id: `${definition.ownerExtensionId}:${definition.id}`,
title: definition.prompt.title, title: definition.prompt.title,
content: definition.prompt.content, content: definition.prompt.content,
})), })),
@@ -55,7 +55,7 @@ export const useTamagotchiPluginToolsStore = defineStore('tamagotchi-plugin-tool
description: definition.description, description: definition.description,
parameters: definition.parameters, parameters: definition.parameters,
execute: async input => invokePluginTool({ execute: async input => invokePluginTool({
ownerPluginId: definition.ownerPluginId, ownerExtensionId: definition.ownerExtensionId,
name: definition.name, name: definition.name,
input, input,
}), }),
@@ -571,7 +571,7 @@ describe('widgets tool helpers', () => {
moduleSnapshot: { moduleSnapshot: {
moduleId: 'module-1', moduleId: 'module-1',
ownerSessionId: 'session-1', ownerSessionId: 'session-1',
ownerPluginId: 'plugin-1', ownerExtensionId: 'plugin-1',
kitId: 'kit.widget', kitId: 'kit.widget',
kitModuleType: 'window', kitModuleType: 'window',
state: 'active', state: 'active',
@@ -142,7 +142,7 @@ export interface WidgetSnapshot {
} }
export interface PluginManifestSummary { export interface PluginManifestSummary {
name: string extensionId: string
entrypoints: Record<string, string | undefined> entrypoints: Record<string, string | undefined>
path: string path: string
enabled: boolean enabled: boolean
@@ -173,7 +173,7 @@ export interface PluginCapabilityState {
export interface PluginHostSessionSummary { export interface PluginHostSessionSummary {
id: string id: string
manifestName: string extensionId: string
phase: string phase: string
runtime: 'electron' | 'node' | 'web' runtime: 'electron' | 'node' | 'web'
moduleId: string moduleId: string
@@ -58,7 +58,7 @@ export interface PluginModuleWidgetPayload {
* - N/A * - N/A
*/ */
export interface PluginManifestSummary { export interface PluginManifestSummary {
name: string extensionId: string
entrypoints: Record<string, string | undefined> entrypoints: Record<string, string | undefined>
path: string path: string
enabled: boolean enabled: boolean
@@ -98,7 +98,7 @@ export interface PluginRegistrySnapshot {
*/ */
export interface PluginHostSessionSummary { export interface PluginHostSessionSummary {
id: string id: string
manifestName: string extensionId: string
phase: string phase: string
runtime: 'electron' | 'node' | 'web' runtime: 'electron' | 'node' | 'web'
moduleId: string moduleId: string
@@ -155,7 +155,7 @@ export interface PluginHostKitSummary {
export interface PluginHostModuleSummary { export interface PluginHostModuleSummary {
moduleId: string moduleId: string
ownerSessionId: string ownerSessionId: string
ownerPluginId: string ownerExtensionId: string
kitId: string kitId: string
kitModuleType: string kitModuleType: string
state: 'announced' | 'active' | 'degraded' | 'withdrawn' state: 'announced' | 'active' | 'degraded' | 'withdrawn'
@@ -187,9 +187,9 @@ export interface PluginHostDebugSnapshot {
} }
export const electronPluginList = defineInvokeEventa<PluginRegistrySnapshot>('eventa:invoke:electron:plugins:list') export const electronPluginList = defineInvokeEventa<PluginRegistrySnapshot>('eventa:invoke:electron:plugins:list')
export const electronPluginSetEnabled = defineInvokeEventa<PluginRegistrySnapshot, { name: string, enabled: boolean, path?: string }>('eventa:invoke:electron:plugins:set-enabled') export const electronPluginSetEnabled = defineInvokeEventa<PluginRegistrySnapshot, { extensionId: string, enabled: boolean, path?: string }>('eventa:invoke:electron:plugins:set-enabled')
export const electronPluginSetAutoReload = defineInvokeEventa<PluginRegistrySnapshot, { name: string, enabled: boolean }>('eventa:invoke:electron:plugins:set-auto-reload') export const electronPluginSetAutoReload = defineInvokeEventa<PluginRegistrySnapshot, { extensionId: string, enabled: boolean }>('eventa:invoke:electron:plugins:set-auto-reload')
export const electronPluginLoadEnabled = defineInvokeEventa<PluginRegistrySnapshot>('eventa:invoke:electron:plugins:load-enabled') export const electronPluginLoadEnabled = defineInvokeEventa<PluginRegistrySnapshot>('eventa:invoke:electron:plugins:load-enabled')
export const electronPluginLoad = defineInvokeEventa<PluginRegistrySnapshot, { name: string }>('eventa:invoke:electron:plugins:load') export const electronPluginLoad = defineInvokeEventa<PluginRegistrySnapshot, { extensionId: string }>('eventa:invoke:electron:plugins:load')
export const electronPluginUnload = defineInvokeEventa<PluginRegistrySnapshot, { name: string }>('eventa:invoke:electron:plugins:unload') export const electronPluginUnload = defineInvokeEventa<PluginRegistrySnapshot, { extensionId: string }>('eventa:invoke:electron:plugins:unload')
export const electronPluginInspect = defineInvokeEventa<PluginHostDebugSnapshot>('eventa:invoke:electron:plugins:inspect') export const electronPluginInspect = defineInvokeEventa<PluginHostDebugSnapshot>('eventa:invoke:electron:plugins:inspect')
@@ -35,7 +35,7 @@ export interface ElectronPluginToolDescriptor {
* - N/A * - N/A
*/ */
export interface ElectronPluginXsaiToolDefinition { export interface ElectronPluginXsaiToolDefinition {
ownerPluginId: string ownerExtensionId: string
name: string name: string
description: string description: string
parameters: Record<string, unknown> parameters: Record<string, unknown>
@@ -54,7 +54,7 @@ export interface ElectronPluginXsaiToolDefinition {
* - N/A * - N/A
*/ */
export interface ElectronPluginToolsetPromptDefinition { export interface ElectronPluginToolsetPromptDefinition {
ownerPluginId: string ownerExtensionId: string
id: string id: string
prompt: { prompt: {
id: string id: string
@@ -87,20 +87,20 @@ export interface ElectronPluginXsaiToolsetDefinition {
* - The main process notifies renderers after plugin lifecycle changes * - The main process notifies renderers after plugin lifecycle changes
* *
* Expects: * Expects:
* - `name` is present when the change is scoped to one plugin * - `extensionId` is present when the change is scoped to one extension
* *
* Returns: * Returns:
* - N/A * - N/A
*/ */
export interface ElectronPluginToolsChangedPayload { export interface ElectronPluginToolsChangedPayload {
reason: 'loaded' | 'load-enabled' | 'unloaded' | 'enabled-state-changed' reason: 'loaded' | 'load-enabled' | 'unloaded' | 'enabled-state-changed'
name?: string extensionId?: string
} }
export const electronPluginListAgentTools = defineInvokeEventa<ElectronPluginToolDescriptor[]>('eventa:invoke:electron:plugins:tools:list') 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 electronPluginListXsaiTools = defineInvokeEventa<ElectronPluginXsaiToolsetDefinition>('eventa:invoke:electron:plugins:tools:list-xsai')
export const electronPluginInvokeTool = defineInvokeEventa<unknown, { export const electronPluginInvokeTool = defineInvokeEventa<unknown, {
ownerPluginId: string ownerExtensionId: string
name: string name: string
input: unknown input: unknown
}>('eventa:invoke:electron:plugins:tools:invoke') }>('eventa:invoke:electron:plugins:tools:invoke')
@@ -467,7 +467,7 @@ describe('plugin-sdk-tamagotchi', () => {
registry.register({ registry.register({
ownerSessionId: 'session-1', ownerSessionId: 'session-1',
ownerPluginId: 'airi-extension-chess', ownerExtensionId: 'airi-extension-chess',
ownerModuleId: 'chess', ownerModuleId: 'chess',
tool: { tool: {
id: 'play_chess', id: 'play_chess',
@@ -486,7 +486,7 @@ describe('plugin-sdk-tamagotchi', () => {
}) })
registry.registerToolsetPrompt({ registry.registerToolsetPrompt({
ownerSessionId: 'session-1', ownerSessionId: 'session-1',
ownerPluginId: 'airi-extension-chess', ownerExtensionId: 'airi-extension-chess',
ownerModuleId: 'chess', ownerModuleId: 'chess',
toolset: { toolset: {
id: 'chess-tools', id: 'chess-tools',
@@ -508,7 +508,7 @@ describe('plugin-sdk-tamagotchi', () => {
}]) }])
await expect(registry.listSerializedXsaiTools()).resolves.toEqual({ await expect(registry.listSerializedXsaiTools()).resolves.toEqual({
prompts: [{ prompts: [{
ownerPluginId: 'airi-extension-chess', ownerExtensionId: 'airi-extension-chess',
id: 'chess-tools', id: 'chess-tools',
prompt: { prompt: {
id: 'airi-plugin-game-chess.prompt', id: 'airi-plugin-game-chess.prompt',
@@ -516,7 +516,7 @@ describe('plugin-sdk-tamagotchi', () => {
}, },
}], }],
tools: [{ tools: [{
ownerPluginId: 'airi-extension-chess', ownerExtensionId: 'airi-extension-chess',
name: 'play_chess', name: 'play_chess',
description: 'Open chess.', description: 'Open chess.',
parameters: { parameters: {
@@ -17,7 +17,7 @@ export interface RegisteredPluginToolDescriptor {
* Describes the JSON-schema side of an xsai-compatible Tamagotchi extension tool. * Describes the JSON-schema side of an xsai-compatible Tamagotchi extension tool.
*/ */
export interface SerializedXsaiToolDefinition { export interface SerializedXsaiToolDefinition {
ownerPluginId: string ownerExtensionId: string
name: string name: string
description: string description: string
parameters: HostDataRecord parameters: HostDataRecord
@@ -36,7 +36,7 @@ export interface ToolsetPromptManifest {
* Captures one registered toolset prompt with extension ownership metadata. * Captures one registered toolset prompt with extension ownership metadata.
*/ */
export interface SerializedToolsetPromptDefinition { export interface SerializedToolsetPromptDefinition {
ownerPluginId: string ownerExtensionId: string
id: string id: string
prompt: ToolsetPromptManifest prompt: ToolsetPromptManifest
} }
@@ -76,7 +76,7 @@ export interface PluginToolsetPromptDefinitionRecord {
*/ */
export interface ToolRegistryRecord { export interface ToolRegistryRecord {
ownerSessionId: string ownerSessionId: string
ownerPluginId: string ownerExtensionId: string
ownerModuleId?: string ownerModuleId?: string
tool: PluginToolDefinitionRecord tool: PluginToolDefinitionRecord
availability?: () => Promise<boolean> | boolean availability?: () => Promise<boolean> | boolean
@@ -88,7 +88,7 @@ export interface ToolRegistryRecord {
*/ */
export interface ToolsetPromptRegistryRecord { export interface ToolsetPromptRegistryRecord {
ownerSessionId: string ownerSessionId: string
ownerPluginId: string ownerExtensionId: string
ownerModuleId?: string ownerModuleId?: string
toolset: PluginToolsetPromptDefinitionRecord toolset: PluginToolsetPromptDefinitionRecord
availability?: () => Promise<boolean> | boolean availability?: () => Promise<boolean> | boolean
@@ -112,23 +112,23 @@ export class TamagotchiToolRegistry {
private readonly toolsetPrompts = new Map<string, ToolsetPromptRegistryRecord>() private readonly toolsetPrompts = new Map<string, ToolsetPromptRegistryRecord>()
register(record: ToolRegistryRecord) { register(record: ToolRegistryRecord) {
const key = `${record.ownerPluginId}:${record.tool.id}` const key = `${record.ownerExtensionId}:${record.tool.id}`
this.tools.set(key, record) this.tools.set(key, record)
return record return record
} }
registerToolsetPrompt(record: ToolsetPromptRegistryRecord) { registerToolsetPrompt(record: ToolsetPromptRegistryRecord) {
const key = `${record.ownerPluginId}:${record.toolset.id}` const key = `${record.ownerExtensionId}:${record.toolset.id}`
this.toolsetPrompts.set(key, record) this.toolsetPrompts.set(key, record)
return record return record
} }
unregister(ownerPluginId: string, toolId: string) { unregister(ownerExtensionId: string, toolId: string) {
return this.tools.delete(`${ownerPluginId}:${toolId}`) return this.tools.delete(`${ownerExtensionId}:${toolId}`)
} }
unregisterToolsetPrompt(ownerPluginId: string, toolsetId: string) { unregisterToolsetPrompt(ownerExtensionId: string, toolsetId: string) {
return this.toolsetPrompts.delete(`${ownerPluginId}:${toolsetId}`) return this.toolsetPrompts.delete(`${ownerExtensionId}:${toolsetId}`)
} }
unregisterOwnerSession(ownerSessionId: string) { unregisterOwnerSession(ownerSessionId: string) {
@@ -195,7 +195,7 @@ export class TamagotchiToolRegistry {
} }
prompts.push({ prompts.push({
ownerPluginId: record.ownerPluginId, ownerExtensionId: record.ownerExtensionId,
id: record.toolset.id, id: record.toolset.id,
prompt: structuredClone(record.toolset.prompt), prompt: structuredClone(record.toolset.prompt),
}) })
@@ -213,7 +213,7 @@ export class TamagotchiToolRegistry {
} }
items.push({ items.push({
ownerPluginId: record.ownerPluginId, ownerExtensionId: record.ownerExtensionId,
name: record.tool.id, name: record.tool.id,
description: record.tool.description, description: record.tool.description,
parameters: structuredClone(record.tool.parameters), parameters: structuredClone(record.tool.parameters),
@@ -226,8 +226,8 @@ export class TamagotchiToolRegistry {
} }
} }
async invoke(ownerPluginId: string, toolId: string, input: unknown) { async invoke(ownerExtensionId: string, toolId: string, input: unknown) {
const key = `${ownerPluginId}:${toolId}` const key = `${ownerExtensionId}:${toolId}`
const record = this.tools.get(key) const record = this.tools.get(key)
if (!record) { if (!record) {
throw new Error(`Tamagotchi extension tool not found: ${key}`) throw new Error(`Tamagotchi extension tool not found: ${key}`)
+2 -2
View File
@@ -665,7 +665,7 @@ export class ExtensionHost {
return cloneBindingRecord(this.modules.bind({ return cloneBindingRecord(this.modules.bind({
...input, ...input,
ownerSessionId: session.id, ownerSessionId: session.id,
ownerPluginId: session.extension.id, ownerExtensionId: session.extension.id,
runtime: session.runtime ?? this.runtime, runtime: session.runtime ?? this.runtime,
}) as BindingRecord<C>) }) as BindingRecord<C>)
} }
@@ -763,7 +763,7 @@ export class ExtensionHost {
const binding = cloneBindingRecord(this.modules.bind({ const binding = cloneBindingRecord(this.modules.bind({
...input, ...input,
ownerSessionId: session.id, ownerSessionId: session.id,
ownerPluginId: session.extension.id, ownerExtensionId: session.extension.id,
runtime: this.runtime, runtime: this.runtime,
}) as BindingRecord<C>) }) as BindingRecord<C>)
@@ -9,7 +9,7 @@ describe('kitApiBindingRegistryService', () => {
const binding = service.bind({ const binding = service.bind({
moduleId: 'chess-gamelet', moduleId: 'chess-gamelet',
ownerSessionId: 'session-1', ownerSessionId: 'session-1',
ownerPluginId: 'airi-extension-chess', ownerExtensionId: 'airi-extension-chess',
kitId: 'kit.gamelet', kitId: 'kit.gamelet',
kitModuleType: 'gamelet', kitModuleType: 'gamelet',
config: { title: 'Chess' }, config: { title: 'Chess' },
@@ -26,7 +26,7 @@ describe('kitApiBindingRegistryService', () => {
service.bind({ service.bind({
moduleId: 'm1', moduleId: 'm1',
ownerSessionId: 'session-a', ownerSessionId: 'session-a',
ownerPluginId: 'plugin-a', ownerExtensionId: 'plugin-a',
kitId: 'kit.widget', kitId: 'kit.widget',
kitModuleType: 'panel', kitModuleType: 'panel',
config: {}, config: {},
@@ -42,7 +42,7 @@ describe('kitApiBindingRegistryService', () => {
const announced = service.bind({ const announced = service.bind({
moduleId: 'm2', moduleId: 'm2',
ownerSessionId: 'session-a', ownerSessionId: 'session-a',
ownerPluginId: 'plugin-a', ownerExtensionId: 'plugin-a',
kitId: 'kit.widget', kitId: 'kit.widget',
kitModuleType: 'panel', kitModuleType: 'panel',
config: { mountPoint: 'widgets' }, config: { mountPoint: 'widgets' },
@@ -67,7 +67,7 @@ describe('kitApiBindingRegistryService', () => {
service.bind({ service.bind({
moduleId: 'm3', moduleId: 'm3',
ownerSessionId: 'session-a', ownerSessionId: 'session-a',
ownerPluginId: 'plugin-a', ownerExtensionId: 'plugin-a',
kitId: 'kit.widget', kitId: 'kit.widget',
kitModuleType: 'panel', kitModuleType: 'panel',
config: {}, config: {},
@@ -85,7 +85,7 @@ describe('kitApiBindingRegistryService', () => {
service.bind({ service.bind({
moduleId: 'm4', moduleId: 'm4',
ownerSessionId: 'session-a', ownerSessionId: 'session-a',
ownerPluginId: 'plugin-a', ownerExtensionId: 'plugin-a',
kitId: 'kit.widget', kitId: 'kit.widget',
kitModuleType: 'panel', kitModuleType: 'panel',
config: {}, config: {},
@@ -96,7 +96,7 @@ describe('kitApiBindingRegistryService', () => {
service.bind({ service.bind({
moduleId: 'm4', moduleId: 'm4',
ownerSessionId: 'session-b', ownerSessionId: 'session-b',
ownerPluginId: 'plugin-b', ownerExtensionId: 'plugin-b',
kitId: 'kit.widget', kitId: 'kit.widget',
kitModuleType: 'panel', kitModuleType: 'panel',
config: {}, config: {},
@@ -111,7 +111,7 @@ describe('kitApiBindingRegistryService', () => {
const original = service.bind({ const original = service.bind({
moduleId: 'm5', moduleId: 'm5',
ownerSessionId: 'session-a', ownerSessionId: 'session-a',
ownerPluginId: 'plugin-a', ownerExtensionId: 'plugin-a',
kitId: 'kit.widget', kitId: 'kit.widget',
kitModuleType: 'panel', kitModuleType: 'panel',
config: { mountPoint: 'widgets' }, config: { mountPoint: 'widgets' },
@@ -121,7 +121,7 @@ describe('kitApiBindingRegistryService', () => {
const duplicate = service.bind({ const duplicate = service.bind({
moduleId: 'm5', moduleId: 'm5',
ownerSessionId: 'session-a', ownerSessionId: 'session-a',
ownerPluginId: 'plugin-a', ownerExtensionId: 'plugin-a',
kitId: 'kit.widget', kitId: 'kit.widget',
kitModuleType: 'dialog', kitModuleType: 'dialog',
config: { mountPoint: 'mutated', width: 480 }, config: { mountPoint: 'mutated', width: 480 },
@@ -140,7 +140,7 @@ describe('kitApiBindingRegistryService', () => {
service.bind({ service.bind({
moduleId: 'm6', moduleId: 'm6',
ownerSessionId: 'session-a', ownerSessionId: 'session-a',
ownerPluginId: 'plugin-a', ownerExtensionId: 'plugin-a',
kitId: 'kit.widget', kitId: 'kit.widget',
kitModuleType: 'panel', kitModuleType: 'panel',
config: {}, config: {},
@@ -151,7 +151,7 @@ describe('kitApiBindingRegistryService', () => {
service.bind({ service.bind({
moduleId: 'm6', moduleId: 'm6',
ownerSessionId: 'session-a', ownerSessionId: 'session-a',
ownerPluginId: 'plugin-b', ownerExtensionId: 'plugin-b',
kitId: 'kit.widget', kitId: 'kit.widget',
kitModuleType: 'panel', kitModuleType: 'panel',
config: {}, config: {},
@@ -166,7 +166,7 @@ describe('kitApiBindingRegistryService', () => {
service.bind({ service.bind({
moduleId: 'm7', moduleId: 'm7',
ownerSessionId: 'session-a', ownerSessionId: 'session-a',
ownerPluginId: 'plugin-a', ownerExtensionId: 'plugin-a',
kitId: 'kit.widget', kitId: 'kit.widget',
kitModuleType: 'panel', kitModuleType: 'panel',
config: {}, config: {},
@@ -20,7 +20,7 @@ import type { HostDataRecord, PluginRuntime } from '../../../shared/types'
export interface BindingInput<C extends HostDataRecord = HostDataRecord> { export interface BindingInput<C extends HostDataRecord = HostDataRecord> {
moduleId: string moduleId: string
ownerSessionId: string ownerSessionId: string
ownerPluginId: string ownerExtensionId: string
kitId: string kitId: string
kitModuleType: string kitModuleType: string
runtime: PluginRuntime runtime: PluginRuntime
@@ -55,14 +55,14 @@ export interface BindingUpdatePatch<C extends HostDataRecord = HostDataRecord> {
* *
* Expects: * Expects:
* - `ownerSessionId` is the ephemeral runtime session id * - `ownerSessionId` is the ephemeral runtime session id
* - `ownerPluginId` is the stable plugin identity across sessions * - `ownerExtensionId` is the stable extension identity across sessions
* *
* Returns: * Returns:
* - A compact identity tuple used in collision and ownership checks * - A compact identity tuple used in collision and ownership checks
*/ */
export interface BindingOwnerIdentity { export interface BindingOwnerIdentity {
ownerSessionId: string ownerSessionId: string
ownerPluginId: string ownerExtensionId: string
} }
const allowedBindingTransitions: Record<BindingState, readonly BindingState[]> = { const allowedBindingTransitions: Record<BindingState, readonly BindingState[]> = {
@@ -78,7 +78,7 @@ function createOwnershipError(
actual: BindingOwnerIdentity, actual: BindingOwnerIdentity,
) { ) {
return new Error( return new Error(
`Ownership violation for module \`${moduleId}\`: owned by \`${expected.ownerSessionId}/${expected.ownerPluginId}\`, not \`${actual.ownerSessionId}/${actual.ownerPluginId}\`.`, `Ownership violation for module \`${moduleId}\`: owned by \`${expected.ownerSessionId}/${expected.ownerExtensionId}\`, not \`${actual.ownerSessionId}/${actual.ownerExtensionId}\`.`,
) )
} }
@@ -88,7 +88,7 @@ function createModuleCollisionError(
actual: BindingOwnerIdentity, actual: BindingOwnerIdentity,
) { ) {
return new Error( return new Error(
`Module id collision for \`${moduleId}\`: owned by \`${expected.ownerSessionId}/${expected.ownerPluginId}\`, not \`${actual.ownerSessionId}/${actual.ownerPluginId}\`.`, `Module id collision for \`${moduleId}\`: owned by \`${expected.ownerSessionId}/${expected.ownerExtensionId}\`, not \`${actual.ownerSessionId}/${actual.ownerExtensionId}\`.`,
) )
} }
@@ -192,17 +192,17 @@ export class KitApiBindingRegistryService<C extends HostDataRecord = HostDataRec
if (current) { if (current) {
if ( if (
current.ownerSessionId !== input.ownerSessionId current.ownerSessionId !== input.ownerSessionId
|| current.ownerPluginId !== input.ownerPluginId || current.ownerExtensionId !== input.ownerExtensionId
) { ) {
throw createModuleCollisionError( throw createModuleCollisionError(
input.moduleId, input.moduleId,
{ {
ownerSessionId: current.ownerSessionId, ownerSessionId: current.ownerSessionId,
ownerPluginId: current.ownerPluginId, ownerExtensionId: current.ownerExtensionId,
}, },
{ {
ownerSessionId: input.ownerSessionId, ownerSessionId: input.ownerSessionId,
ownerPluginId: input.ownerPluginId, ownerExtensionId: input.ownerExtensionId,
}, },
) )
} }
@@ -213,7 +213,7 @@ export class KitApiBindingRegistryService<C extends HostDataRecord = HostDataRec
const record: BindingRecord<C> = { const record: BindingRecord<C> = {
moduleId: input.moduleId, moduleId: input.moduleId,
ownerSessionId: input.ownerSessionId, ownerSessionId: input.ownerSessionId,
ownerPluginId: input.ownerPluginId, ownerExtensionId: input.ownerExtensionId,
kitId: input.kitId, kitId: input.kitId,
kitModuleType: input.kitModuleType, kitModuleType: input.kitModuleType,
state: 'announced', state: 'announced',
@@ -343,8 +343,8 @@ export class KitApiBindingRegistryService<C extends HostDataRecord = HostDataRec
* Returns: * Returns:
* - The updated binding record with incremented revision and timestamp * - The updated binding record with incremented revision and timestamp
*/ */
update(ownerSessionId: string, ownerPluginId: string, moduleId: string, patch: BindingUpdatePatch<C>) { update(ownerSessionId: string, ownerExtensionId: string, moduleId: string, patch: BindingUpdatePatch<C>) {
return this.transition({ ownerSessionId, ownerPluginId }, moduleId, patch.state, patch) return this.transition({ ownerSessionId, ownerExtensionId }, moduleId, patch.state, patch)
} }
/** /**
@@ -359,8 +359,8 @@ export class KitApiBindingRegistryService<C extends HostDataRecord = HostDataRec
* Returns: * Returns:
* - The updated active binding record * - The updated active binding record
*/ */
activate(ownerSessionId: string, ownerPluginId: string, moduleId: string) { activate(ownerSessionId: string, ownerExtensionId: string, moduleId: string) {
return this.transition({ ownerSessionId, ownerPluginId }, moduleId, 'active') return this.transition({ ownerSessionId, ownerExtensionId }, moduleId, 'active')
} }
/** /**
@@ -375,8 +375,8 @@ export class KitApiBindingRegistryService<C extends HostDataRecord = HostDataRec
* Returns: * Returns:
* - The updated degraded binding record * - The updated degraded binding record
*/ */
degrade(ownerSessionId: string, ownerPluginId: string, moduleId: string) { degrade(ownerSessionId: string, ownerExtensionId: string, moduleId: string) {
return this.transition({ ownerSessionId, ownerPluginId }, moduleId, 'degraded') return this.transition({ ownerSessionId, ownerExtensionId }, moduleId, 'degraded')
} }
/** /**
@@ -391,8 +391,8 @@ export class KitApiBindingRegistryService<C extends HostDataRecord = HostDataRec
* Returns: * Returns:
* - The updated withdrawn binding record * - The updated withdrawn binding record
*/ */
withdraw(ownerSessionId: string, ownerPluginId: string, moduleId: string) { withdraw(ownerSessionId: string, ownerExtensionId: string, moduleId: string) {
return this.transition({ ownerSessionId, ownerPluginId }, moduleId, 'withdrawn') return this.transition({ ownerSessionId, ownerExtensionId }, moduleId, 'withdrawn')
} }
/** /**
@@ -421,13 +421,13 @@ export class KitApiBindingRegistryService<C extends HostDataRecord = HostDataRec
if ( if (
current.ownerSessionId !== owner.ownerSessionId current.ownerSessionId !== owner.ownerSessionId
|| current.ownerPluginId !== owner.ownerPluginId || current.ownerExtensionId !== owner.ownerExtensionId
) { ) {
throw createOwnershipError( throw createOwnershipError(
moduleId, moduleId,
{ {
ownerSessionId: current.ownerSessionId, ownerSessionId: current.ownerSessionId,
ownerPluginId: current.ownerPluginId, ownerExtensionId: current.ownerExtensionId,
}, },
owner, owner,
) )
@@ -464,7 +464,7 @@ export class KitApiBindingRegistryService<C extends HostDataRecord = HostDataRec
* Returns: * Returns:
* - The removed binding record, or `undefined` when nothing existed * - The removed binding record, or `undefined` when nothing existed
*/ */
unbind(ownerSessionId: string, ownerPluginId: string, moduleId: string) { unbind(ownerSessionId: string, ownerExtensionId: string, moduleId: string) {
const current = this.bindings.get(moduleId) const current = this.bindings.get(moduleId)
if (!current) { if (!current) {
return undefined return undefined
@@ -472,17 +472,17 @@ export class KitApiBindingRegistryService<C extends HostDataRecord = HostDataRec
if ( if (
current.ownerSessionId !== ownerSessionId current.ownerSessionId !== ownerSessionId
|| current.ownerPluginId !== ownerPluginId || current.ownerExtensionId !== ownerExtensionId
) { ) {
throw createOwnershipError( throw createOwnershipError(
moduleId, moduleId,
{ {
ownerSessionId: current.ownerSessionId, ownerSessionId: current.ownerSessionId,
ownerPluginId: current.ownerPluginId, ownerExtensionId: current.ownerExtensionId,
}, },
{ {
ownerSessionId, ownerSessionId,
ownerPluginId, ownerExtensionId,
}, },
) )
} }
@@ -293,7 +293,7 @@ export class PermissionService {
} }
initialize( initialize(
pluginId: string, extensionId: string,
requestedDeclaration: ModulePermissionDeclaration, requestedDeclaration: ModulePermissionDeclaration,
options?: { options?: {
grant?: ModulePermissionGrant grant?: ModulePermissionGrant
@@ -305,21 +305,21 @@ export class PermissionService {
const explicitGrant = options?.grant ?? requested const explicitGrant = options?.grant ?? requested
const mergedGrant = mergePermissions(persisted, explicitGrant) const mergedGrant = mergePermissions(persisted, explicitGrant)
const granted = intersectPermissions(requested, mergedGrant) const granted = intersectPermissions(requested, mergedGrant)
const previousRevision = this.store.get(pluginId)?.revision ?? 0 const previousRevision = this.store.get(extensionId)?.revision ?? 0
const snapshot: PermissionSnapshot = { const snapshot: PermissionSnapshot = {
requested, requested,
granted, granted,
revision: previousRevision + 1, revision: previousRevision + 1,
} }
this.store.set(pluginId, snapshot) this.store.set(extensionId, snapshot)
return snapshot return snapshot
} }
declare(pluginId: string, requestedDeclaration: ModulePermissionDeclaration) { declare(extensionId: string, requestedDeclaration: ModulePermissionDeclaration) {
const existing = this.store.get(pluginId) const existing = this.store.get(extensionId)
if (!existing) { if (!existing) {
throw new Error(`Cannot declare permissions for unknown plugin "${pluginId}".`) throw new Error(`Cannot declare permissions for unknown plugin "${extensionId}".`)
} }
const requested = normalizeDeclaration(requestedDeclaration) const requested = normalizeDeclaration(requestedDeclaration)
@@ -329,14 +329,14 @@ export class PermissionService {
revision: existing.revision + 1, revision: existing.revision + 1,
} }
this.store.set(pluginId, snapshot) this.store.set(extensionId, snapshot)
return snapshot return snapshot
} }
grant(pluginId: string, grant: ModulePermissionGrant) { grant(extensionId: string, grant: ModulePermissionGrant) {
const existing = this.store.get(pluginId) const existing = this.store.get(extensionId)
if (!existing) { if (!existing) {
throw new Error(`Cannot grant permissions to unknown plugin "${pluginId}".`) throw new Error(`Cannot grant permissions to unknown plugin "${extensionId}".`)
} }
const mergedGranted = mergePermissions(existing.granted, grant) const mergedGranted = mergePermissions(existing.granted, grant)
@@ -345,16 +345,16 @@ export class PermissionService {
granted: intersectPermissions(existing.requested, mergedGranted), granted: intersectPermissions(existing.requested, mergedGranted),
revision: existing.revision + 1, revision: existing.revision + 1,
} }
this.store.set(pluginId, snapshot) this.store.set(extensionId, snapshot)
return snapshot return snapshot
} }
get(pluginId: string) { get(extensionId: string) {
return this.store.get(pluginId) return this.store.get(extensionId)
} }
isAllowed(pluginId: string, area: ModulePermissionArea, action: string, key: string) { isAllowed(extensionId: string, area: ModulePermissionArea, action: string, key: string) {
const snapshot = this.store.get(pluginId) const snapshot = this.store.get(extensionId)
if (!snapshot) { if (!snapshot) {
return false return false
} }
@@ -8,7 +8,7 @@ describe('bindingRecordSchema', () => {
const parsed = parse(bindingRecordSchema, { const parsed = parse(bindingRecordSchema, {
moduleId: 'board-main', moduleId: 'board-main',
ownerSessionId: 'extension-session-1', ownerSessionId: 'extension-session-1',
ownerPluginId: 'demo-plugin', ownerExtensionId: 'demo-plugin',
kitId: 'kit.widget', kitId: 'kit.widget',
kitModuleType: 'panel', kitModuleType: 'panel',
state: 'announced', state: 'announced',
@@ -27,7 +27,7 @@ describe('bindingRecordSchema', () => {
parse(bindingRecordSchema, { parse(bindingRecordSchema, {
moduleId: 'board-main', moduleId: 'board-main',
ownerSessionId: 'extension-session-1', ownerSessionId: 'extension-session-1',
ownerPluginId: 'demo-plugin', ownerExtensionId: 'demo-plugin',
kitId: 'kit.widget', kitId: 'kit.widget',
kitModuleType: 'panel', kitModuleType: 'panel',
state: 'booting', state: 'booting',
@@ -44,7 +44,7 @@ describe('bindingRecordSchema', () => {
parse(bindingRecordSchema, { parse(bindingRecordSchema, {
moduleId: 'board-main', moduleId: 'board-main',
ownerSessionId: 'extension-session-1', ownerSessionId: 'extension-session-1',
ownerPluginId: 'demo-plugin', ownerExtensionId: 'demo-plugin',
kitId: 'kit.widget', kitId: 'kit.widget',
kitModuleType: 'panel', kitModuleType: 'panel',
state: 'announced', state: 'announced',
@@ -65,7 +65,7 @@ describe('bindingRecordSchema', () => {
parse(bindingRecordSchema, { parse(bindingRecordSchema, {
moduleId: 'board-main', moduleId: 'board-main',
ownerSessionId: 'extension-session-1', ownerSessionId: 'extension-session-1',
ownerPluginId: 'demo-plugin', ownerExtensionId: 'demo-plugin',
kitId: 'kit.widget', kitId: 'kit.widget',
kitModuleType: 'panel', kitModuleType: 'panel',
state: 'announced', state: 'announced',
@@ -36,7 +36,7 @@ export const bindingStateValues = ['announced', 'active', 'degraded', 'withdrawn
export const bindingRecordSchema = object({ export const bindingRecordSchema = object({
moduleId: string(), moduleId: string(),
ownerSessionId: string(), ownerSessionId: string(),
ownerPluginId: string(), ownerExtensionId: string(),
kitId: string(), kitId: string(),
kitModuleType: string(), kitModuleType: string(),
state: picklist(bindingStateValues), state: picklist(bindingStateValues),
@@ -76,7 +76,7 @@ export type BindingState = typeof bindingStateValues[number]
export interface BindingRecord<C extends HostDataRecord = HostDataRecord> { export interface BindingRecord<C extends HostDataRecord = HostDataRecord> {
moduleId: string moduleId: string
ownerSessionId: string ownerSessionId: string
ownerPluginId: string ownerExtensionId: string
kitId: string kitId: string
kitModuleType: string kitModuleType: string
state: BindingState state: BindingState
@@ -13,15 +13,15 @@ import { toast } from 'vue-sonner'
const store = usePluginHostInspectorStore() const store = usePluginHostInspectorStore()
const filter = ref('') const filter = ref('')
const selectedPluginName = ref('') const selectedExtensionId = ref('')
const discoveredPlugins = computed(() => { const discoveredPlugins = computed(() => {
const query = filter.value.trim().toLowerCase() const query = filter.value.trim().toLowerCase()
const plugins = store.discoveredPlugins.slice().sort((left, right) => left.name.localeCompare(right.name)) const plugins = store.discoveredPlugins.slice().sort((left, right) => left.extensionId.localeCompare(right.extensionId))
if (!query) if (!query)
return plugins return plugins
return plugins.filter(plugin => return plugins.filter(plugin =>
plugin.name.toLowerCase().includes(query) plugin.extensionId.toLowerCase().includes(query)
|| plugin.path.toLowerCase().includes(query), || plugin.path.toLowerCase().includes(query),
) )
}) })
@@ -34,10 +34,10 @@ const loadedPlugins = computed(() => {
return discoveredPlugins.value.filter(plugin => plugin.loaded) return discoveredPlugins.value.filter(plugin => plugin.loaded)
}) })
const sessionByPluginName = computed(() => { const sessionByExtensionId = computed(() => {
const map = new Map<string, PluginHostSessionSummary>() const map = new Map<string, PluginHostSessionSummary>()
for (const session of store.sessions) { for (const session of store.sessions) {
map.set(session.manifestName, session) map.set(session.extensionId, session)
} }
return map return map
}) })
@@ -110,58 +110,58 @@ async function loadEnabled() {
async function setAutoReload(plugin: PluginManifestSummary, enabled: boolean) { async function setAutoReload(plugin: PluginManifestSummary, enabled: boolean) {
try { try {
await store.setAutoReload({ await store.setAutoReload({
name: plugin.name, extensionId: plugin.extensionId,
enabled, enabled,
}) })
} }
catch (error) { catch (error) {
toast.error(errorMessageFrom(error) ?? `Failed to update auto-reload state for ${plugin.name}.`) toast.error(errorMessageFrom(error) ?? `Failed to update auto-reload state for ${plugin.extensionId}.`)
} }
} }
async function setEnabled(plugin: PluginManifestSummary, enabled: boolean) { async function setEnabled(plugin: PluginManifestSummary, enabled: boolean) {
try { try {
await store.setEnabled({ await store.setEnabled({
name: plugin.name, extensionId: plugin.extensionId,
enabled, enabled,
path: plugin.path, path: plugin.path,
}) })
} }
catch (error) { catch (error) {
toast.error(errorMessageFrom(error) ?? `Failed to update enabled state for ${plugin.name}.`) toast.error(errorMessageFrom(error) ?? `Failed to update enabled state for ${plugin.extensionId}.`)
} }
} }
async function loadPlugin(plugin: PluginManifestSummary) { async function loadPlugin(plugin: PluginManifestSummary) {
try { try {
await store.load({ name: plugin.name }) await store.load({ extensionId: plugin.extensionId })
} }
catch (error) { catch (error) {
toast.error(errorMessageFrom(error) ?? `Failed to load plugin ${plugin.name}.`) toast.error(errorMessageFrom(error) ?? `Failed to load plugin ${plugin.extensionId}.`)
} }
} }
async function unloadPlugin(plugin: PluginManifestSummary) { async function unloadPlugin(plugin: PluginManifestSummary) {
try { try {
await store.unload({ name: plugin.name }) await store.unload({ extensionId: plugin.extensionId })
} }
catch (error) { catch (error) {
toast.error(errorMessageFrom(error) ?? `Failed to unload plugin ${plugin.name}.`) toast.error(errorMessageFrom(error) ?? `Failed to unload plugin ${plugin.extensionId}.`)
} }
} }
async function loadSelectedPlugin() { async function loadSelectedPlugin() {
const name = selectedPluginName.value.trim() const extensionId = selectedExtensionId.value.trim()
if (!name) { if (!extensionId) {
toast.error('Enter a plugin name to load.') toast.error('Enter an extension id to load.')
return return
} }
try { try {
await store.load({ name }) await store.load({ extensionId })
} }
catch (error) { catch (error) {
toast.error(errorMessageFrom(error) ?? `Failed to load plugin ${name}.`) toast.error(errorMessageFrom(error) ?? `Failed to load plugin ${extensionId}.`)
} }
} }
@@ -253,15 +253,15 @@ onMounted(async () => {
<div :class="['flex', 'flex-wrap', 'items-center', 'gap-2']"> <div :class="['flex', 'flex-wrap', 'items-center', 'gap-2']">
<Input <Input
v-model="selectedPluginName" v-model="selectedExtensionId"
placeholder="Load discovered plugin by exact name..." placeholder="Load discovered extension by exact id..."
class="max-w-[520px] min-w-[320px]" class="max-w-[520px] min-w-[320px]"
/> />
<Button <Button
label="Load Plugin" label="Load Plugin"
icon="i-solar:download-minimalistic-bold-duotone" icon="i-solar:download-minimalistic-bold-duotone"
size="sm" size="sm"
:disabled="!selectedPluginName.trim()" :disabled="!selectedExtensionId.trim()"
:loading="store.loading" :loading="store.loading"
@click="loadSelectedPlugin" @click="loadSelectedPlugin"
/> />
@@ -288,7 +288,7 @@ onMounted(async () => {
<div :class="['flex', 'flex-wrap', 'items-center', 'justify-between', 'gap-2']"> <div :class="['flex', 'flex-wrap', 'items-center', 'justify-between', 'gap-2']">
<div :class="['flex', 'flex-wrap', 'items-center', 'gap-2']"> <div :class="['flex', 'flex-wrap', 'items-center', 'gap-2']">
<div :class="['font-semibold']"> <div :class="['font-semibold']">
{{ plugin.name }} {{ plugin.extensionId }}
</div> </div>
<span :class="['rounded-full', 'border', 'px-2', 'py-0.5', 'text-xs', ...chipClasses(plugin.enabled ? 'emerald' : 'neutral')]"> <span :class="['rounded-full', 'border', 'px-2', 'py-0.5', 'text-xs', ...chipClasses(plugin.enabled ? 'emerald' : 'neutral')]">
{{ plugin.enabled ? 'enabled' : 'disabled' }} {{ plugin.enabled ? 'enabled' : 'disabled' }}
@@ -348,14 +348,14 @@ onMounted(async () => {
entrypoints: {{ JSON.stringify(plugin.entrypoints) }} entrypoints: {{ JSON.stringify(plugin.entrypoints) }}
</div> </div>
<div <div
v-if="sessionByPluginName.get(plugin.name)" v-if="sessionByExtensionId.get(plugin.extensionId)"
:class="['mt-2', 'flex', 'items-center', 'gap-2', 'text-sm']" :class="['mt-2', 'flex', 'items-center', 'gap-2', 'text-sm']"
> >
<span>phase:</span> <span>phase:</span>
<span :class="['rounded-full', 'border', 'px-2', 'py-0.5', 'text-xs', ...chipClasses(phaseChipTheme(sessionByPluginName.get(plugin.name)!.phase))]"> <span :class="['rounded-full', 'border', 'px-2', 'py-0.5', 'text-xs', ...chipClasses(phaseChipTheme(sessionByExtensionId.get(plugin.extensionId)!.phase))]">
{{ sessionByPluginName.get(plugin.name)!.phase }} {{ sessionByExtensionId.get(plugin.extensionId)!.phase }}
</span> </span>
<span :class="['opacity-70', 'font-mono']">{{ sessionByPluginName.get(plugin.name)!.moduleId }}</span> <span :class="['opacity-70', 'font-mono']">{{ sessionByExtensionId.get(plugin.extensionId)!.moduleId }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -375,7 +375,7 @@ onMounted(async () => {
:key="`enabled-${plugin.path}`" :key="`enabled-${plugin.path}`"
:class="['rounded-full', 'border', 'px-2', 'py-0.5', 'text-xs', ...chipClasses('emerald')]" :class="['rounded-full', 'border', 'px-2', 'py-0.5', 'text-xs', ...chipClasses('emerald')]"
> >
{{ plugin.name }} {{ plugin.extensionId }}
</span> </span>
</div> </div>
</Section> </Section>
@@ -395,9 +395,9 @@ onMounted(async () => {
:class="['rounded-lg', 'bg-neutral-100', 'p-2', 'dark:bg-neutral-900/70']" :class="['rounded-lg', 'bg-neutral-100', 'p-2', 'dark:bg-neutral-900/70']"
> >
<div :class="['flex', 'items-center', 'justify-between', 'gap-2']"> <div :class="['flex', 'items-center', 'justify-between', 'gap-2']">
<span :class="['font-semibold']">{{ plugin.name }}</span> <span :class="['font-semibold']">{{ plugin.extensionId }}</span>
<span :class="['rounded-full', 'border', 'px-2', 'py-0.5', 'text-xs', ...chipClasses(phaseChipTheme(sessionByPluginName.get(plugin.name)?.phase ?? 'unknown'))]"> <span :class="['rounded-full', 'border', 'px-2', 'py-0.5', 'text-xs', ...chipClasses(phaseChipTheme(sessionByExtensionId.get(plugin.extensionId)?.phase ?? 'unknown'))]">
{{ sessionByPluginName.get(plugin.name)?.phase ?? 'unknown' }} {{ sessionByExtensionId.get(plugin.extensionId)?.phase ?? 'unknown' }}
</span> </span>
</div> </div>
</div> </div>
@@ -3,7 +3,7 @@ import { defineStore } from 'pinia'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
export interface PluginManifestSummary { export interface PluginManifestSummary {
name: string extensionId: string
entrypoints: Record<string, string | undefined> entrypoints: Record<string, string | undefined>
path: string path: string
enabled: boolean enabled: boolean
@@ -28,7 +28,7 @@ export interface PluginCapabilityState {
export interface PluginHostSessionSummary { export interface PluginHostSessionSummary {
id: string id: string
manifestName: string extensionId: string
phase: string phase: string
runtime: 'electron' | 'node' | 'web' runtime: 'electron' | 'node' | 'web'
moduleId: string moduleId: string
@@ -49,7 +49,7 @@ export interface PluginHostKitSummary {
export interface PluginHostModuleSummary { export interface PluginHostModuleSummary {
moduleId: string moduleId: string
ownerSessionId: string ownerSessionId: string
ownerPluginId: string ownerExtensionId: string
kitId: string kitId: string
kitModuleType: string kitModuleType: string
state: 'announced' | 'active' | 'degraded' | 'withdrawn' state: 'announced' | 'active' | 'degraded' | 'withdrawn'
@@ -70,11 +70,11 @@ export interface PluginHostDebugSnapshot {
interface PluginHostDebugBridge { interface PluginHostDebugBridge {
list: () => Promise<PluginRegistrySnapshot> list: () => Promise<PluginRegistrySnapshot>
setEnabled: (payload: { name: string, enabled: boolean, path?: string }) => Promise<PluginRegistrySnapshot> setEnabled: (payload: { extensionId: string, enabled: boolean, path?: string }) => Promise<PluginRegistrySnapshot>
setAutoReload: (payload: { name: string, enabled: boolean }) => Promise<PluginRegistrySnapshot> setAutoReload: (payload: { extensionId: string, enabled: boolean }) => Promise<PluginRegistrySnapshot>
loadEnabled: () => Promise<PluginRegistrySnapshot> loadEnabled: () => Promise<PluginRegistrySnapshot>
load: (payload: { name: string }) => Promise<PluginRegistrySnapshot> load: (payload: { extensionId: string }) => Promise<PluginRegistrySnapshot>
unload: (payload: { name: string }) => Promise<PluginRegistrySnapshot> unload: (payload: { extensionId: string }) => Promise<PluginRegistrySnapshot>
inspect: () => Promise<PluginHostDebugSnapshot> inspect: () => Promise<PluginHostDebugSnapshot>
} }
@@ -171,14 +171,14 @@ export const usePluginHostInspectorStore = defineStore('devtools:plugin-host-deb
return refreshInspection() return refreshInspection()
} }
async function setEnabled(payload: { name: string, enabled: boolean, path?: string }) { async function setEnabled(payload: { extensionId: string, enabled: boolean, path?: string }) {
const nextRegistry = await withBridge(activeBridge => activeBridge.setEnabled(payload)) const nextRegistry = await withBridge(activeBridge => activeBridge.setEnabled(payload))
assignRegistry(nextRegistry) assignRegistry(nextRegistry)
await refreshInspection() await refreshInspection()
return nextRegistry return nextRegistry
} }
async function setAutoReload(payload: { name: string, enabled: boolean }) { async function setAutoReload(payload: { extensionId: string, enabled: boolean }) {
const nextRegistry = await withBridge(activeBridge => activeBridge.setAutoReload(payload)) const nextRegistry = await withBridge(activeBridge => activeBridge.setAutoReload(payload))
assignRegistry(nextRegistry) assignRegistry(nextRegistry)
await refreshInspection() await refreshInspection()
@@ -192,14 +192,14 @@ export const usePluginHostInspectorStore = defineStore('devtools:plugin-host-deb
return nextRegistry return nextRegistry
} }
async function load(payload: { name: string }) { async function load(payload: { extensionId: string }) {
const nextRegistry = await withBridge(activeBridge => activeBridge.load(payload)) const nextRegistry = await withBridge(activeBridge => activeBridge.load(payload))
assignRegistry(nextRegistry) assignRegistry(nextRegistry)
await refreshInspection() await refreshInspection()
return nextRegistry return nextRegistry
} }
async function unload(payload: { name: string }) { async function unload(payload: { extensionId: string }) {
const nextRegistry = await withBridge(activeBridge => activeBridge.unload(payload)) const nextRegistry = await withBridge(activeBridge => activeBridge.unload(payload))
assignRegistry(nextRegistry) assignRegistry(nextRegistry)
await refreshInspection() await refreshInspection()