Appearance
Client API Reference
The Extension Client API (ExtensionClientAPI) is the primary interface for extensions running in the Extension Host. All operations are sandboxed and permission-gated.
Overview
typescript
export async function activate(
context: ExtensionContext,
api: ExtensionClientAPI
): Promise<void> {
// api contains all available APIs
api.ui // UI operations
api.storage // Local storage
api.commands // Command registration
api.workspace // Workspace info
api.events // Event subscriptions
api.telemetry // Analytics
api.context // Read-only context
api.callOrgApi // Org API calls
api.callStoreApi // Store API calls
api.fetch // External HTTP
}Extension Context
The context object is passed to your activate function.
Properties
typescript
interface ExtensionContext {
extensionId: string // Your extension ID
extensionPath: string // Absolute path to extension files
storagePath: string // Path for extension data
subscriptions: Disposable[] // Auto-disposed on deactivate
log: ExtensionLogger // Scoped logger
manifest: ExtensionManifest // Your manifest
state: ExtensionState // Current state
}context.subscriptions
Push Disposable objects here for automatic cleanup when the extension deactivates:
typescript
export async function activate(context, api) {
// These are automatically disposed when extension deactivates
context.subscriptions.push(
api.commands.registerCommand('myext.cmd', handler),
api.workspace.onFileChanged(fileHandler),
api.events.on('vault:changed', vaultHandler)
)
}context.log
Scoped logger with extension prefix:
typescript
context.log.debug('Debug message') // Development only
context.log.info('Info message') // General info
context.log.warn('Warning message') // Warnings
context.log.error('Error message') // ErrorsUI API
Permission required: Various ui:* permissions
ui.showToast
Display a brief notification message.
typescript
api.ui.showToast(
message: string,
type?: 'success' | 'error' | 'info' | 'warning',
duration?: number
): voidPermission: ui:toast
Example:
typescript
api.ui.showToast('File saved!', 'success')
api.ui.showToast('Connection failed', 'error', 5000) // 5 secondsui.showDialog
Display a modal dialog and wait for user response.
typescript
api.ui.showDialog(options: DialogOptions): Promise<DialogResult>
interface DialogOptions {
title: string
message: string
type?: 'info' | 'warning' | 'error' | 'confirm'
confirmText?: string // Default: "OK"
cancelText?: string // Default: "Cancel"
dismissible?: boolean // Click outside to close
}
interface DialogResult {
confirmed: boolean
dismissed: boolean
}Permission: ui:dialog
Example:
typescript
const result = await api.ui.showDialog({
title: 'Confirm Delete',
message: 'Are you sure you want to delete this file?',
type: 'confirm',
confirmText: 'Delete',
cancelText: 'Keep'
})
if (result.confirmed) {
await deleteFile()
}ui.setStatus
Set the extension's status indicator in the UI.
typescript
api.ui.setStatus(status: 'online' | 'offline' | 'partial' | 'checking'): voidPermission: ui:status
Example:
typescript
api.ui.setStatus('checking')
const connected = await checkConnection()
api.ui.setStatus(connected ? 'online' : 'offline')ui.showProgress
Show a progress indicator while performing an operation.
typescript
api.ui.showProgress<T>(
options: ProgressOptions,
task: (progress: Progress, token: CancellationToken) => Promise<T>
): Promise<T>
interface ProgressOptions {
title: string
cancellable?: boolean
location?: 'notification' | 'statusbar'
}
interface Progress {
report(value: { message?: string; increment?: number }): void
}
interface CancellationToken {
readonly isCancellationRequested: boolean
onCancellationRequested: (callback: () => void) => Disposable
}Permission: ui:progress
Example:
typescript
const result = await api.ui.showProgress(
{ title: 'Syncing files...', cancellable: true },
async (progress, token) => {
const files = await getFiles()
for (let i = 0; i < files.length; i++) {
if (token.isCancellationRequested) {
return { cancelled: true, synced: i }
}
progress.report({
message: `Syncing ${files[i].name}...`,
increment: 100 / files.length
})
await syncFile(files[i])
}
return { cancelled: false, synced: files.length }
}
)ui.showQuickPick
Display a searchable selection list.
typescript
api.ui.showQuickPick(
items: QuickPickItem[],
options?: QuickPickOptions
): Promise<QuickPickItem | QuickPickItem[] | undefined>
interface QuickPickItem {
label: string
description?: string // Shown dimmer, to the right
detail?: string // Shown below label
picked?: boolean // Pre-selected
data?: unknown // Custom data
iconId?: string // Lucide icon name
}
interface QuickPickOptions {
title?: string
placeholder?: string
canPickMany?: boolean
matchOnDescription?: boolean
matchOnDetail?: boolean
}Permission: ui:dialog
Example:
typescript
const selected = await api.ui.showQuickPick([
{ label: 'Production', description: 'Main environment', data: 'prod' },
{ label: 'Staging', description: 'Test environment', data: 'staging' },
{ label: 'Development', description: 'Local dev', data: 'dev' }
], {
title: 'Select Environment',
placeholder: 'Choose deployment target...'
})
if (selected) {
console.log('Selected:', selected.data)
}Multi-select:
typescript
const selected = await api.ui.showQuickPick(items, { canPickMany: true })
if (selected && Array.isArray(selected)) {
console.log('Selected:', selected.map(s => s.label))
}ui.showInputBox
Display a text input dialog.
typescript
api.ui.showInputBox(options?: InputBoxOptions): Promise<string | undefined>
interface InputBoxOptions {
title?: string
placeholder?: string
value?: string // Initial value
valueSelection?: [number, number] // Selection range
password?: boolean // Mask input
prompt?: string // Text above input
validateInput?: (value: string) => string | undefined | Promise<string | undefined>
}Permission: ui:dialog
Example:
typescript
const projectName = await api.ui.showInputBox({
title: 'New Project',
placeholder: 'my-project',
prompt: 'Enter project name (lowercase, no spaces)',
validateInput: (value) => {
if (!value) return 'Name is required'
if (!/^[a-z0-9-]+$/.test(value)) {
return 'Only lowercase letters, numbers, and hyphens'
}
return undefined // Valid
}
})
if (projectName) {
await createProject(projectName)
}Password input:
typescript
const apiKey = await api.ui.showInputBox({
title: 'API Key',
password: true,
prompt: 'Enter your API key'
})Storage API
Permission required: storage:local
Extension-scoped persistent storage. Data is isolated per extension.
storage.get
typescript
api.storage.get<T>(key: string): Promise<T | undefined>Example:
typescript
const lastSync = await api.storage.get<number>('lastSyncTime')
const settings = await api.storage.get<Settings>('userSettings')storage.set
typescript
api.storage.set<T>(key: string, value: T): Promise<void>Values must be JSON-serializable.
Example:
typescript
await api.storage.set('lastSyncTime', Date.now())
await api.storage.set('userSettings', { theme: 'dark', autoSync: true })storage.delete
typescript
api.storage.delete(key: string): Promise<void>Example:
typescript
await api.storage.delete('cachedData')storage.keys
typescript
api.storage.keys(): Promise<string[]>Example:
typescript
const allKeys = await api.storage.keys()
// ['lastSyncTime', 'userSettings', 'cachedData']storage.has
typescript
api.storage.has(key: string): Promise<boolean>Example:
typescript
if (await api.storage.has('userSettings')) {
// Load existing settings
}storage.clear
typescript
api.storage.clear(): Promise<void>Warning: This deletes ALL extension data. Use with caution.
Commands API
commands.registerCommand
Register a command handler.
typescript
api.commands.registerCommand(
id: string,
handler: (...args: unknown[]) => unknown | Promise<unknown>,
options?: CommandOptions
): DisposablePermission: commands:register
Example:
typescript
context.subscriptions.push(
api.commands.registerCommand('myext.greet', (name: string) => {
api.ui.showToast(`Hello, ${name}!`, 'info')
})
)commands.executeCommand
Execute a registered command.
typescript
api.commands.executeCommand<T>(id: string, ...args: unknown[]): Promise<T>Permission: commands:execute
Example:
typescript
// Execute your own command
await api.commands.executeCommand('myext.greet', 'World')
// Execute another extension's command (if permitted)
const result = await api.commands.executeCommand<SyncResult>('other-ext.sync')commands.getCommands
Get all registered command IDs.
typescript
api.commands.getCommands(): Promise<string[]>Example:
typescript
const commands = await api.commands.getCommands()
// ['myext.greet', 'myext.sync', 'core.save', ...]Workspace API
workspace.onFileChanged
Subscribe to file change events.
typescript
api.workspace.onFileChanged(
callback: (events: FileChangeEvent[]) => void
): Disposable
interface FileChangeEvent {
type: 'created' | 'changed' | 'deleted'
path: string // Relative to vault root
vaultId: string
}Permission: workspace:files
Example:
typescript
context.subscriptions.push(
api.workspace.onFileChanged((events) => {
for (const event of events) {
console.log(`${event.type}: ${event.path}`)
if (event.type === 'changed' && event.path.endsWith('.sldprt')) {
// SolidWorks file modified
scheduleSync(event.vaultId, event.path)
}
}
})
)workspace.getOpenFiles
Get currently open files.
typescript
api.workspace.getOpenFiles(): Promise<OpenFile[]>
interface OpenFile {
path: string
vaultId: string
isDirty?: boolean // Has unsaved changes
extension?: string
}Permission: workspace:files
workspace.getCurrentVault
Get the currently active vault.
typescript
api.workspace.getCurrentVault(): Promise<VaultInfo | undefined>
interface VaultInfo {
id: string
name: string
localPath: string
orgId: string
}Example:
typescript
const vault = await api.workspace.getCurrentVault()
if (vault) {
console.log(`Current vault: ${vault.name} at ${vault.localPath}`)
}workspace.getVaults
Get all configured vaults.
typescript
api.workspace.getVaults(): Promise<VaultInfo[]>Permission: workspace:vaults
Events API
events.on
Subscribe to application events.
typescript
api.events.on(
event: ExtensionEvent,
callback: (...args: unknown[]) => void
): Disposable
type ExtensionEvent =
| 'extension:activated'
| 'extension:deactivating'
| 'config:changed'
| 'auth:changed'
| 'vault:changed'
| 'online:changed'Example:
typescript
context.subscriptions.push(
api.events.on('vault:changed', (newVaultId: string) => {
console.log('Vault changed to:', newVaultId)
refreshExtensionState()
}),
api.events.on('online:changed', (isOnline: boolean) => {
api.ui.setStatus(isOnline ? 'online' : 'offline')
}),
api.events.on('config:changed', () => {
// Reload configuration
loadSettings()
})
)events.emit
Emit custom events (prefixed with extension ID).
typescript
api.events.emit(event: string, ...args: unknown[]): voidExample:
typescript
// Only works for events prefixed with your extension ID
api.events.emit('myext.customEvent', { data: 'value' })Network API
callOrgApi
Call the organization's API server (authenticated).
typescript
api.callOrgApi<T>(
endpoint: string,
options?: FetchOptions
): Promise<FetchResponse<T>>
interface FetchOptions {
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
headers?: Record<string, string>
body?: unknown
timeout?: number
}
interface FetchResponse<T> {
status: number
statusText: string
headers: Record<string, string>
data: T
ok: boolean
}Permission: network:orgApi
Example:
typescript
// Call your extension's server handler
const response = await api.callOrgApi<SyncResult>('/extensions/myext/sync', {
method: 'POST',
body: { vaultId: 'xxx', force: true }
})
if (response.ok) {
console.log('Synced:', response.data.fileCount)
} else {
console.error('Error:', response.data)
}callStoreApi
Call the Extension Store API.
typescript
api.callStoreApi<T>(endpoint: string): Promise<FetchResponse<T>>Permission: network:storeApi
Example:
typescript
const response = await api.callStoreApi<Extension>('/store/extensions/blueplm.my-extension')
console.log('Latest version:', response.data.latestVersion)fetch
Make HTTP requests to external URLs.
typescript
api.fetch<T>(url: string, options?: FetchOptions): Promise<FetchResponse<T>>Permission: network:fetch AND declared domains in manifest
Manifest requirement:
json
"permissions": {
"client": ["network:fetch"],
"server": ["http:domain:api.example.com"]
}Example:
typescript
const response = await api.fetch<WeatherData>('https://api.example.com/weather', {
method: 'GET',
headers: { 'Authorization': 'Bearer xxx' }
})Note: Can only access domains declared in your manifest's permissions.
Telemetry API
Permission required: telemetry
Anonymous, privacy-respecting analytics.
telemetry.trackEvent
Track a named event.
typescript
api.telemetry.trackEvent(
name: string,
properties?: Record<string, string | number | boolean>
): voidExample:
typescript
api.telemetry.trackEvent('sync_completed', {
fileCount: 42,
duration: 1500,
usedCache: true
})telemetry.trackError
Track an error occurrence.
typescript
api.telemetry.trackError(
error: Error,
context?: Record<string, string>
): voidExample:
typescript
try {
await riskyOperation()
} catch (error) {
api.telemetry.trackError(error as Error, {
operation: 'sync',
vaultId: 'xxx'
})
}telemetry.trackTiming
Track a timing measurement.
typescript
api.telemetry.trackTiming(name: string, durationMs: number): voidExample:
typescript
const start = performance.now()
await longOperation()
api.telemetry.trackTiming('longOperation', performance.now() - start)Context API
Read-only context information about the current session.
typescript
interface ExtensionContextInfo {
extensionId: string
version: string
user: UserContext | null
organization: OrganizationContext | null
isOnline: boolean
appVersion: string
platform: 'win32' | 'darwin' | 'linux'
}
interface UserContext {
id: string
email: string
displayName?: string
}
interface OrganizationContext {
id: string
name: string
}Example:
typescript
const { user, organization, platform } = api.context
if (user) {
console.log(`User: ${user.email}`)
}
if (platform === 'win32') {
// Windows-specific logic
}Complete Example
typescript
import type { ExtensionContext, ExtensionClientAPI } from '@blueplm/extension-api'
export async function activate(
context: ExtensionContext,
api: ExtensionClientAPI
): Promise<void> {
context.log.info('Extension activating...')
// Register commands
context.subscriptions.push(
api.commands.registerCommand('myext.sync', async () => {
await syncWithProgress(api)
}),
api.commands.registerCommand('myext.configure', async () => {
const endpoint = await api.ui.showInputBox({
title: 'API Endpoint',
placeholder: 'https://api.example.com'
})
if (endpoint) {
await api.storage.set('apiEndpoint', endpoint)
api.ui.showToast('Configuration saved', 'success')
}
})
)
// Watch for file changes
context.subscriptions.push(
api.workspace.onFileChanged((events) => {
const modified = events.filter(e => e.type !== 'deleted')
if (modified.length > 0) {
context.log.debug(`${modified.length} files changed`)
}
})
)
// Subscribe to events
context.subscriptions.push(
api.events.on('vault:changed', () => {
context.log.info('Vault changed, refreshing...')
})
)
// Set initial status
api.ui.setStatus('online')
api.ui.showToast('Extension ready!', 'success')
}
async function syncWithProgress(api: ExtensionClientAPI): Promise<void> {
await api.ui.showProgress(
{ title: 'Syncing...', cancellable: true },
async (progress, token) => {
progress.report({ message: 'Connecting...' })
const vault = await api.workspace.getCurrentVault()
if (!vault) {
api.ui.showToast('No vault selected', 'error')
return
}
progress.report({ message: 'Syncing...', increment: 50 })
const response = await api.callOrgApi('/extensions/myext/sync', {
method: 'POST',
body: { vaultId: vault.id }
})
if (response.ok) {
api.ui.showToast('Sync complete!', 'success')
api.telemetry.trackEvent('sync_completed')
} else {
api.ui.showToast('Sync failed', 'error')
}
}
)
}
export function deactivate(): void {
// Cleanup (subscriptions are auto-disposed)
}