Command Palette Commands¶
Register custom commands that appear in the Lumiverse command palette (Cmd/Ctrl+K). When users search and select your command, the extension receives the invocation with full UI context.
No permission is required. This is a free-tier API.
Quick Start¶
// Register commands
spindle.commands.register([
{
id: 'summarize-chat',
label: 'Summarize Chat',
description: 'Generate a summary of the current conversation',
keywords: ['summary', 'recap', 'tldr'],
scope: 'chat',
},
{
id: 'export-notes',
label: 'Export Notes',
description: 'Export conversation notes as markdown',
keywords: ['export', 'download', 'markdown', 'notes'],
scope: 'chat-idle',
},
])
// Handle invocations
spindle.commands.onInvoked((commandId, context) => {
switch (commandId) {
case 'summarize-chat':
summarizeChat(context.chatId)
break
case 'export-notes':
exportNotes(context.chatId)
break
}
})
Methods¶
spindle.commands.register(commands)¶
Register (or replace) all command palette entries for this extension. Each call replaces the full set — pass the complete list of commands you want visible.
spindle.commands.register([
{
id: 'my-command',
label: 'Do Something',
description: 'Performs a useful action',
keywords: ['do', 'action'],
scope: 'global',
},
])
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
Yes | Unique identifier within your extension (max 100 chars) |
label |
string |
Yes | Display name shown in the palette (max 80 chars) |
description |
string |
No | Help text shown below the label (max 200 chars) |
keywords |
string[] |
No | Search keywords for fuzzy matching (max 10, 30 chars each) |
scope |
string |
No | Visibility restriction (default: 'global') |
Limits: Maximum 20 commands per extension. Excess commands are silently truncated.
spindle.commands.unregister(commandIds?)¶
Remove specific commands by ID, or all commands if no IDs are given.
// Remove specific commands
spindle.commands.unregister(['export-notes'])
// Remove all commands
spindle.commands.unregister()
spindle.commands.onInvoked(handler)¶
Register a handler called when the user selects one of your commands. Returns an unsubscribe function.
const unsub = spindle.commands.onInvoked((commandId, context) => {
// commandId: the `id` from your registration
// context: snapshot of the frontend's current state
})
// Later, stop listening:
unsub()
Context object:
| Field | Type | Description |
|---|---|---|
route |
string |
Current URL path (e.g. "/chat/abc-123", "/") |
chatId |
string? |
Active chat ID, if in a chat view |
characterId |
string? |
Active character ID, if available |
isGroupChat |
boolean? |
Whether the active chat is a group chat |
Scopes¶
Scopes control when a command is visible in the palette, based on the user's current page:
| Scope | Visible When |
|---|---|
'global' |
Always (default) |
'chat' |
User is viewing any chat |
'chat-idle' |
User is in a chat and not currently streaming |
'landing' |
User is on the home page |
'character' |
User is on a character page |
If scope is omitted, the command defaults to 'global'.
Contextual Commands¶
Commands are designed to be contextual. You can re-register with different sets of commands based on the current state of the app. Subscribe to Lumiverse events and call register() again whenever your command set should change:
// Start with a base set
spindle.commands.register([
{ id: 'configure', label: 'Configure Extension', description: 'Open settings', scope: 'global' },
])
// Add chat-specific commands when a chat becomes active
spindle.on('CHAT_CHANGED', (payload) => {
const baseCommands = [
{ id: 'configure', label: 'Configure Extension', description: 'Open settings', scope: 'global' },
]
if (payload?.chatId) {
baseCommands.push({
id: 'analyze-chat',
label: 'Analyze Conversation',
description: 'Run sentiment analysis on the current chat',
scope: 'chat',
})
}
spindle.commands.register(baseCommands)
})
Presentation¶
Extension commands appear in the Extensions group in the command palette, alongside any drawer tabs your extension registers. They are searchable by label, description, keywords, and your extension name.
Your extension name (from spindle.json) is automatically included as a search keyword — users can always find your commands by searching for your extension name.
Example: Translation Extension¶
const LANGUAGES = ['Spanish', 'French', 'Japanese', 'German']
// Register a translate command for each language
spindle.commands.register(
LANGUAGES.map((lang) => ({
id: `translate-${lang.toLowerCase()}`,
label: `Translate to ${lang}`,
description: `Translate the last message to ${lang}`,
keywords: ['translate', 'language', lang.toLowerCase()],
scope: 'chat-idle',
}))
)
spindle.commands.onInvoked(async (commandId, context) => {
const lang = commandId.replace('translate-', '')
if (!context.chatId) return
const messages = await spindle.chat.getMessages(context.chatId)
const lastMessage = messages[messages.length - 1]
if (!lastMessage) return
const result = await spindle.generate.quiet({
type: 'quiet',
messages: [
{ role: 'system', content: `Translate the following text to ${lang}. Output only the translation.` },
{ role: 'user', content: lastMessage.content },
],
})
spindle.toast.success(`Translation complete`)
})
Cleanup¶
Commands are automatically unregistered when your extension is unloaded or disabled. You don't need to clean up manually — but you can call spindle.commands.unregister() at any time to remove your commands proactively.