Execution
Selection triggers dispatch. Dispatch reads the result and acts. There is no decision logic in between.
Two Dispatch Paths
When a user selects a result from the command palette, exactly one of two things happens:

- Route dispatch — the result has a
routefield. The executor callsnavigate(route). - Command dispatch — the result has a
commandIdfield. The executor looks up the handler in theCommandRegistryand calls it.
If neither field is present, nothing happens. There is no fallback, no default route, no error. This is intentional — an IndexItem with no execution target is a data problem, not a runtime problem.
CommandExecutor
The executor is a class, not a singleton. It receives its dependencies at construction:
type ExecutorDeps = {
navigate: (path: string) => void;
};
class CommandExecutor {
constructor(private deps: ExecutorDeps) {}
execute(item: CommandResult) {
if (item.route) {
this.deps.navigate(item.route);
return;
}
if (item.commandId) {
commandRegistry.execute(item.commandId);
}
}
}
Why dependency injection
The executor runs in a Next.js app where router.push is only available inside React components. Rather than importing the router directly (which would couple the executor to Next.js internals), the consumer injects navigate at construction time.
This means:
- The executor can be tested without a router
- The executor does not import from
next/navigation - Swapping navigation strategies (e.g., hard navigation vs. client-side) requires changing the injection site, not the executor
Why route takes priority
The if (item.route) return check means route dispatch always wins when both route and commandId are present on a result. This is intentional — navigation is the primary use case, and dual-target items should not exist in practice. If they do, navigation is the safer default.
CommandRegistry
The registry is a Map<string, Command> that holds action handlers:
type Command = {
id: string;
handler: (context?: unknown) => void;
};
:::tip Register a command
commandRegistry.register({
id: 'theme.toggle',
handler: () => toggleTheme(),
});
:::
:::tip Execute by ID
commandRegistry.execute('theme.toggle');
:::
If the ID is not found, execute silently returns. There is no error, no warning. Missing commands are treated as unregistered, not as failures.
Separation from IndexRegistry
The CommandRegistry and IndexRegistry are separate systems with different purposes:
| Registry | Stores | Used by | Purpose |
|---|---|---|---|
IndexRegistry |
IndexProvider functions |
Indexer | Build the search index |
CommandRegistry |
Command handlers |
CommandExecutor | Execute actions by ID |
An IndexItem with type: 'nav' needs no entry in the CommandRegistry — its route field is sufficient. Only items with commandId need a registered handler.
Execution Is Not Search
The executor receives a CommandResult, not a query. By the time execution runs:
- Search is complete
- Scoring is complete
- The user has explicitly selected one result
The executor does not filter, rank, or validate. It reads route or commandId and dispatches. This keeps execution deterministic — the same result always triggers the same action regardless of the query that surfaced it.