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