mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-01 16:09:46 +02:00
Compare commits
1 Commits
main
...
dev/mjolle
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ca1516940 |
248
src/modules/cmdpal/doc/json-rpc-spec/01-architecture.md
Normal file
248
src/modules/cmdpal/doc/json-rpc-spec/01-architecture.md
Normal file
@@ -0,0 +1,248 @@
|
||||
# 01 - Architecture Overview
|
||||
|
||||
## Process Model
|
||||
|
||||
Each JavaScript/TypeScript extension runs as an **isolated Node.js process**. The CmdPal host manages these processes through `JsonRpcExtensionService`, which implements the same `IExtensionService` interface used by WinRT and built-in extensions.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Host["Host Process (CmdPal)"]
|
||||
JSSVC["JsonRpcExtensionService"]
|
||||
WRAP["JSExtensionWrapper"]
|
||||
RPC["JsonRpcConnection"]
|
||||
PROXY["JSCommandProviderProxy"]
|
||||
ONE["(one per extension)"]
|
||||
|
||||
JSSVC --> WRAP --> RPC --> PROXY
|
||||
JSSVC --> ONE
|
||||
end
|
||||
|
||||
subgraph Ext["Extension Process (Node.js)"]
|
||||
SDK["@microsoft/cmdpal-sdk"]
|
||||
STDIO["stdio-server.ts\n(JSON-RPC 2.0)"]
|
||||
CODE["Extension code\nindex.ts"]
|
||||
|
||||
SDK --> STDIO
|
||||
CODE --> STDIO
|
||||
end
|
||||
|
||||
RPC -->|"stdin"| STDIO
|
||||
STDIO -->|"stdout"| RPC
|
||||
```
|
||||
|
||||
### Why Per-Process?
|
||||
|
||||
1. **Crash isolation** — A runaway extension cannot crash CmdPal. If an extension process dies, only that extension stops working.
|
||||
2. **Resource isolation** — Each extension has independent memory, CPU, and event loop.
|
||||
3. **Independent debugging** — Attach a debugger to a specific extension's Node.js process via `--inspect`.
|
||||
4. **Clean lifecycle** — Stop/restart an extension by killing and re-spawning its process.
|
||||
5. **Security boundary** — Extensions cannot access host memory or other extensions' state.
|
||||
|
||||
### Crash Recovery
|
||||
|
||||
The host tracks consecutive crashes per extension:
|
||||
|
||||
| Crash Count | Behavior |
|
||||
|-------------|----------|
|
||||
| 1–3 | Extension marked as disconnected, available for restart |
|
||||
| > 3 | Extension marked as **unhealthy**, disabled until manual re-enable |
|
||||
|
||||
## Extension Discovery
|
||||
|
||||
Extensions are discovered from a well-known directory:
|
||||
|
||||
```
|
||||
%LOCALAPPDATA%\Microsoft\PowerToys\CommandPalette\JSExtensions\
|
||||
```
|
||||
|
||||
Each subdirectory containing a valid `package.json` with a `cmdpal` section is treated as an extension:
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
ROOT["JSExtensions/"] --> MY["my-extension/"]
|
||||
ROOT --> ANOTHER["another-extension/"]
|
||||
|
||||
MY --> PKG["package.json\nmanifest with cmdpal section (required)"]
|
||||
MY --> DIST["dist/"]
|
||||
DIST --> JSENTRY["index.js\ncompiled entry point"]
|
||||
MY --> NM["node_modules/"]
|
||||
NM --> MS["@microsoft/cmdpal-sdk"]
|
||||
MY --> SRC["src/"]
|
||||
SRC --> TSENTRY["index.ts\nTypeScript source"]
|
||||
|
||||
ANOTHER --> APKG["package.json"]
|
||||
ANOTHER --> AETC["..."]
|
||||
```
|
||||
|
||||
### Directory Watching
|
||||
|
||||
The service watches the `JSExtensions` directory for:
|
||||
|
||||
| Event | Behavior |
|
||||
|-------|----------|
|
||||
| **New subdirectory created** | Scans for `package.json` with `cmdpal` section, loads extension if valid |
|
||||
| **Subdirectory deleted** | Stops the extension process, removes from provider list |
|
||||
| **`*.js` file changed** (within an extension) | Hot-reloads the extension (debounced 500ms) |
|
||||
|
||||
Source file watchers ignore `node_modules/` changes.
|
||||
|
||||
## Extension Lifecycle
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
DISC["Discover\n(find package.json with cmdpal)"] --> PARSE["Parse Manifest\n(validate manifest)"]
|
||||
PARSE --> START["Start Process\n(spawn node process)"]
|
||||
START --> INIT["Initialize\n(JSON-RPC initialize)"]
|
||||
INIT --> READY["Ready\n(host queries commands)"]
|
||||
READY -->|"on shutdown/hot-reload"| STOP["Stop\n(send dispose, kill process)"]
|
||||
READY -->|"on crash or error"| STOP
|
||||
```
|
||||
|
||||
### Startup Sequence
|
||||
|
||||
1. `JsonRpcExtensionService.LoadProvidersAsync()` scans `JSExtensions/`
|
||||
2. For each valid `package.json` containing a `cmdpal` section:
|
||||
- Creates `JSExtensionWrapper`
|
||||
- Spawns `node <entrypoint>` with stdio redirection
|
||||
- Creates `JsonRpcConnection` over the process's stdin/stdout
|
||||
- Sends `initialize` request and waits for response
|
||||
- Creates `JSCommandProviderProxy` as the `ICommandProvider` implementation
|
||||
- Wraps in `CommandProviderWrapper` and returns to `TopLevelCommandManager`
|
||||
|
||||
### Hot-Reload (Development)
|
||||
|
||||
When a `*.js` file changes in an extension directory:
|
||||
|
||||
1. Change detected by `FileSystemWatcher`
|
||||
2. Debounced 500ms to coalesce rapid saves
|
||||
3. Current process receives `dispose` notification
|
||||
4. Process is killed after 2s grace period
|
||||
5. New process is spawned and initialized
|
||||
6. Crash counter is reset on successful initialization
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Set `"debug": true` in the `cmdpal` section of `package.json` to start the Node.js process with `--inspect`:
|
||||
|
||||
```json
|
||||
{
|
||||
"cmdpal": {
|
||||
"debug": true,
|
||||
"debugPort": 9230
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The host logs a Chrome DevTools URL for attaching:
|
||||
|
||||
```
|
||||
chrome-devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=127.0.0.1:9230
|
||||
```
|
||||
|
||||
If `debugPort` is not specified, ports are auto-assigned starting at 9229.
|
||||
|
||||
## Transport Layer
|
||||
|
||||
### Framing
|
||||
|
||||
All messages use **LSP-style framing** (Language Server Protocol):
|
||||
|
||||
```http
|
||||
Content-Length: <byte-count>\r\n
|
||||
\r\n
|
||||
<UTF-8 JSON body>
|
||||
```
|
||||
|
||||
This is the same framing used by VS Code's Language Server Protocol, making it compatible with existing JSON-RPC tooling.
|
||||
|
||||
### Connection Details
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Transport | Process stdio (stdin for writes, stdout for reads) |
|
||||
| Encoding | UTF-8 |
|
||||
| Framing | `Content-Length` header (LSP-style) |
|
||||
| Protocol | JSON-RPC 2.0 |
|
||||
| Request timeout | 10 seconds |
|
||||
| Concurrency | Serialized writes (lock-protected), async reads |
|
||||
| Error channel | stderr (logged by host, not part of protocol) |
|
||||
|
||||
### Message Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Host
|
||||
participant Extension
|
||||
|
||||
Host->>Extension: Content-Length: N {"jsonrpc":"2.0","id":1,...}
|
||||
Extension-->>Host: Content-Length: M {"jsonrpc":"2.0","id":1,...}
|
||||
Extension-->>Host: Content-Length: K {"jsonrpc":"2.0","method":...} (no id)
|
||||
```
|
||||
|
||||
## C# Host-Side Architecture
|
||||
|
||||
### Key Classes
|
||||
|
||||
| Class | Role |
|
||||
|-------|------|
|
||||
| `JsonRpcExtensionService` | Discovers, loads, and manages JS extension processes |
|
||||
| `JSExtensionManifest` | Parses and validates `package.json` with `cmdpal` section |
|
||||
| `JSExtensionWrapper` | Manages a single Node.js process lifecycle (implements `IExtensionWrapper`) |
|
||||
| `JsonRpcConnection` | Low-level JSON-RPC 2.0 transport over stdio |
|
||||
| `JSCommandProviderProxy` | Translates `ICommandProvider` interface calls to JSON-RPC requests |
|
||||
| `JSCommandAdapter` | Adapts JSON command data to `ICommand` interface |
|
||||
| `JSListPageProxy` | Adapts JSON list page data to `IListPage` interface |
|
||||
| `JSContentPageProxy` | Adapts JSON content page data to `IContentPage` interface |
|
||||
| `JSIconInfoAdapter` | Adapts JSON icon data (glyphs, base64, data URIs) to `IIconInfo` |
|
||||
| `JSSeparatorAdapter` | Adapts separator markers to `ISeparatorFilterItem` |
|
||||
|
||||
### Adapter Pattern
|
||||
|
||||
The host-side uses an **adapter/proxy pattern** to present JSON-RPC responses as native `ICommand`, `IListPage`, `IContentPage`, etc. interfaces. This allows the existing CmdPal UI (built for WinRT extensions) to consume JS extensions without any changes to the UI layer.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
UI["UI Layer (unchanged)"] --> CPP["JSCommandProviderProxy: ICommandProvider"] --> CONN["JsonRpcConnection"] --> NODE["Node.js"]
|
||||
|
||||
LPP["JSListPageProxy: IListPage"]
|
||||
CPPG["JSContentPageProxy: IContentPage"]
|
||||
CAD["JSCommandAdapter: ICommand"]
|
||||
IAD["JSIconInfoAdapter: IIconInfo"]
|
||||
|
||||
CPP --> LPP
|
||||
CPP --> CPPG
|
||||
CPP --> CAD
|
||||
CPP --> IAD
|
||||
```
|
||||
|
||||
### Icon Data Pipeline
|
||||
|
||||
JS extensions can provide icons in three formats:
|
||||
|
||||
| Format | `IconData` field | C# handling |
|
||||
|--------|------------------|-------------|
|
||||
| **Font glyph** | `icon: "\uE91B"` | `IconPathConverter.IconSourceMUX` → `FontIconSource` |
|
||||
| **File/URI path** | `icon: "C:\\path\\icon.png"` | `IconPathConverter.IconSourceMUX` → `BitmapImage` |
|
||||
| **Base64 data** | `data: "iVBOR..."` | `JSIconDataAdapter.Data` → `InMemoryRandomAccessStream` → `BitmapImage` |
|
||||
| **Data URI** | `data: "data:image/png;base64,..."` | `JSIconDataAdapter.Data` → parse data URI → decode → stream |
|
||||
|
||||
For base64 images, the TS extension fetches/encodes the image data at runtime. The SDK provides helper functions (`iconFromUrl`, `iconFromFile`, `iconFromBase64`) to simplify this.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Current State (v1)
|
||||
|
||||
JS extensions run as **unsandboxed Node.js processes** with the same permissions as the user running CmdPal. This is equivalent to running any Node.js application.
|
||||
|
||||
### Mitigations
|
||||
|
||||
- **Process isolation** — Extensions cannot access host process memory
|
||||
- **No elevated privileges** — Extensions run at the user's permission level, never elevated
|
||||
- **Crash containment** — Runaway extensions are auto-disabled after 3 consecutive crashes
|
||||
- **No network exposure** — Communication is via stdio, not network sockets
|
||||
|
||||
### Future Considerations
|
||||
|
||||
- Permission model for filesystem, network, and clipboard access
|
||||
- Extension signing and trust verification
|
||||
- Sandboxed execution environments (e.g., V8 isolates)
|
||||
603
src/modules/cmdpal/doc/json-rpc-spec/02-typescript-sdk.md
Normal file
603
src/modules/cmdpal/doc/json-rpc-spec/02-typescript-sdk.md
Normal file
@@ -0,0 +1,603 @@
|
||||
# 02 - TypeScript SDK Reference
|
||||
|
||||
> **Package:** `@microsoft/cmdpal-sdk`
|
||||
> **Node.js:** ≥ 22.0.0
|
||||
> **TypeScript:** ≥ 5.8
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @microsoft/cmdpal-sdk
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Core Types
|
||||
|
||||
### Icon Types
|
||||
|
||||
```typescript
|
||||
interface IconData {
|
||||
icon?: string; // Font glyph character or file/URI path
|
||||
data?: string | null; // Base64-encoded image data or data URI
|
||||
}
|
||||
|
||||
interface IconInfo {
|
||||
light?: IconData; // Icon for light theme
|
||||
dark?: IconData; // Icon for dark theme
|
||||
}
|
||||
```
|
||||
|
||||
Icons can be provided as:
|
||||
- **Font glyphs:** `{ icon: '\uE91B' }` — Segoe Fluent Icons / MDL2 Assets
|
||||
- **File paths:** `{ icon: 'C:\\path\\to\\icon.png' }`
|
||||
- **Base64 data:** `{ data: 'iVBORw0KGgo...' }` — raw base64-encoded image bytes
|
||||
- **Data URIs:** `{ data: 'data:image/png;base64,iVBOR...' }`
|
||||
|
||||
### Color Types
|
||||
|
||||
```typescript
|
||||
interface Color {
|
||||
r: number; // 0-255
|
||||
g: number;
|
||||
b: number;
|
||||
a: number; // 0-255 (default: 255)
|
||||
}
|
||||
|
||||
interface OptionalColor {
|
||||
hasValue: boolean;
|
||||
color?: Color;
|
||||
}
|
||||
```
|
||||
|
||||
### Tags
|
||||
|
||||
```typescript
|
||||
interface Tag {
|
||||
icon?: IconInfo | null;
|
||||
text: string;
|
||||
foreground?: OptionalColor | null;
|
||||
background?: OptionalColor | null;
|
||||
toolTip?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Key Chords
|
||||
|
||||
```typescript
|
||||
interface KeyChord {
|
||||
modifiers: number; // Bitmask: Ctrl=1, Alt=2, Shift=4, Win=8
|
||||
vkey: number; // Virtual key code
|
||||
scanCode: number;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Command Types
|
||||
|
||||
### ICommand
|
||||
|
||||
The base contract for all commands and pages.
|
||||
|
||||
```typescript
|
||||
interface ICommand {
|
||||
id: string; // Unique identifier
|
||||
name: string; // Display name
|
||||
icon?: IconInfo | null; // Optional icon
|
||||
}
|
||||
```
|
||||
|
||||
### IInvokableCommand
|
||||
|
||||
A command that can be executed.
|
||||
|
||||
```typescript
|
||||
interface IInvokableCommand extends ICommand {
|
||||
invoke(): Promise<CommandResult> | CommandResult;
|
||||
}
|
||||
```
|
||||
|
||||
### CommandResult
|
||||
|
||||
Returned from `invoke()` to tell the host what to do next.
|
||||
|
||||
```typescript
|
||||
type CommandResultKind =
|
||||
| 'dismiss' // Close CmdPal
|
||||
| 'goHome' // Navigate to home
|
||||
| 'goBack' // Navigate back
|
||||
| 'hide' // Hide CmdPal (keep state)
|
||||
| 'keepOpen' // Stay on current page
|
||||
| 'goToPage' // Navigate to a page
|
||||
| 'showToast' // Show toast notification
|
||||
| 'confirm'; // Show confirmation dialog
|
||||
|
||||
interface CommandResult {
|
||||
kind: CommandResultKind;
|
||||
args?: CommandResultArgs;
|
||||
}
|
||||
```
|
||||
|
||||
### Result Args
|
||||
|
||||
```typescript
|
||||
interface GoToPageArgs {
|
||||
pageId: string;
|
||||
navigationMode?: 'push' | 'goBack' | 'goHome';
|
||||
}
|
||||
|
||||
interface ToastArgs {
|
||||
message: string;
|
||||
result?: CommandResult; // What to do after toast is dismissed
|
||||
}
|
||||
|
||||
interface ConfirmationArgs {
|
||||
title: string;
|
||||
description: string;
|
||||
primaryCommand?: ICommand;
|
||||
isPrimaryCommandCritical?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### Helper: Creating Results
|
||||
|
||||
```typescript
|
||||
// Navigate to a page
|
||||
{ kind: 'goToPage', args: { pageId: 'my-page', navigationMode: 'push' } }
|
||||
|
||||
// Show a toast
|
||||
{ kind: 'showToast', args: { message: 'Done!' } }
|
||||
|
||||
// Confirmation dialog
|
||||
{ kind: 'confirm', args: { title: 'Delete?', description: 'This cannot be undone.' } }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Item Types
|
||||
|
||||
### ICommandItem
|
||||
|
||||
A selectable item shown in lists.
|
||||
|
||||
```typescript
|
||||
interface ICommandItem {
|
||||
command: ICommand;
|
||||
moreCommands?: ContextItem[]; // Right-click / overflow menu
|
||||
icon?: IconInfo | null;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### IListItem
|
||||
|
||||
Extended list item with metadata.
|
||||
|
||||
```typescript
|
||||
interface IListItem extends ICommandItem {
|
||||
tags?: Tag[];
|
||||
details?: Details;
|
||||
section?: string; // Section header text (creates visual grouping)
|
||||
textToSuggest?: string; // Text to fill into search box on selection
|
||||
}
|
||||
```
|
||||
|
||||
### IFallbackCommandItem
|
||||
|
||||
A command that receives the user's search query in real-time.
|
||||
|
||||
```typescript
|
||||
interface IFallbackCommandItem extends ICommandItem {
|
||||
fallbackHandler?: IFallbackHandler;
|
||||
displayTitle?: string; // Dynamic title that updates as user types
|
||||
}
|
||||
|
||||
interface IFallbackHandler {
|
||||
updateQuery(query: string): void;
|
||||
}
|
||||
```
|
||||
|
||||
### ContextItem
|
||||
|
||||
An action in a right-click or overflow menu.
|
||||
|
||||
```typescript
|
||||
interface ContextItem {
|
||||
command: ICommand;
|
||||
icon?: IconInfo | null;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
isCritical?: boolean; // Show in red/destructive style
|
||||
requestedShortcut?: KeyChord; // Keyboard shortcut hint
|
||||
}
|
||||
```
|
||||
|
||||
### Separator
|
||||
|
||||
A visual divider in lists.
|
||||
|
||||
```typescript
|
||||
import { Separator } from '@microsoft/cmdpal-sdk';
|
||||
|
||||
// Untitled separator (horizontal line)
|
||||
new Separator()
|
||||
|
||||
// Section header separator
|
||||
new Separator('Section Title')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Details Panel
|
||||
|
||||
Rich metadata shown alongside a selected list item.
|
||||
|
||||
```typescript
|
||||
interface Details {
|
||||
heroImage?: IconInfo | null;
|
||||
title?: string;
|
||||
body?: string; // Markdown-formatted body text
|
||||
metadata?: DetailsElement[];
|
||||
}
|
||||
|
||||
interface DetailsElement {
|
||||
key: string; // Label shown to the left
|
||||
data: DetailsData; // Value shown to the right
|
||||
}
|
||||
|
||||
// Discriminated union of detail data types
|
||||
type DetailsData =
|
||||
| DetailsTags // { type: 'tags', tags: Tag[] }
|
||||
| DetailsLink // { type: 'link', link: string, text: string }
|
||||
| DetailsCommands // { type: 'commands', commands: ICommand[] }
|
||||
| DetailsSeparator; // { type: 'separator' }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Page Types
|
||||
|
||||
### IListPage
|
||||
|
||||
A page that shows a scrollable list of items.
|
||||
|
||||
```typescript
|
||||
interface IListPage extends IPage {
|
||||
searchText?: string;
|
||||
placeholderText?: string;
|
||||
showDetails?: boolean; // Show details panel
|
||||
filters?: Filters | null; // Filter bar
|
||||
gridProperties?: GridProperties | null; // Grid/gallery layout
|
||||
hasMoreItems?: boolean; // Infinite scroll
|
||||
emptyContent?: ICommandItem | null;
|
||||
getItems(): IListItem[] | Promise<IListItem[]>;
|
||||
loadMore?(): void | Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
### IDynamicListPage
|
||||
|
||||
A list page that receives search input in real-time.
|
||||
|
||||
```typescript
|
||||
interface IDynamicListPage extends IListPage {
|
||||
setSearchText(text: string): void;
|
||||
}
|
||||
```
|
||||
|
||||
### IContentPage
|
||||
|
||||
A page that displays rich content (markdown, forms, images, trees).
|
||||
|
||||
```typescript
|
||||
interface IContentPage extends IPage {
|
||||
getContent(): Content[] | Promise<Content[]>;
|
||||
details?: Details | null;
|
||||
commands?: ContextItem[];
|
||||
}
|
||||
```
|
||||
|
||||
### Filters
|
||||
|
||||
```typescript
|
||||
interface Filter {
|
||||
id: string;
|
||||
name: string;
|
||||
icon?: IconInfo | null;
|
||||
}
|
||||
|
||||
interface Filters {
|
||||
currentFilterId: string;
|
||||
filters: Array<Filter | { separator: true }>;
|
||||
}
|
||||
```
|
||||
|
||||
### Grid Properties
|
||||
|
||||
```typescript
|
||||
type GridLayoutType = 'small' | 'medium' | 'gallery';
|
||||
|
||||
interface GridProperties {
|
||||
type: GridLayoutType;
|
||||
showTitle?: boolean;
|
||||
showSubtitle?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Content Types
|
||||
|
||||
Content pages display an array of `Content` items:
|
||||
|
||||
```typescript
|
||||
type ContentType = 'markdown' | 'form' | 'tree' | 'plainText' | 'image';
|
||||
|
||||
interface MarkdownContent {
|
||||
type: 'markdown';
|
||||
body: string;
|
||||
}
|
||||
|
||||
interface FormContent {
|
||||
type: 'form';
|
||||
templateJson: string; // Adaptive Card JSON template
|
||||
dataJson: string; // Form data values JSON
|
||||
stateJson?: string;
|
||||
submitForm(inputs: string, data: string): CommandResult | Promise<CommandResult>;
|
||||
}
|
||||
|
||||
interface ImageContent {
|
||||
type: 'image';
|
||||
image: IconInfo; // Base64-encoded image data (use iconFromUrl/iconFromFile)
|
||||
maxWidth?: number;
|
||||
maxHeight?: number;
|
||||
}
|
||||
|
||||
interface PlainTextContent {
|
||||
type: 'plainText';
|
||||
text: string;
|
||||
fontFamily?: 'userInterface' | 'monospace';
|
||||
wrapWords?: boolean;
|
||||
}
|
||||
|
||||
interface TreeContent {
|
||||
type: 'tree';
|
||||
rootContent: Content;
|
||||
getChildren(): Content[] | Promise<Content[]>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Settings
|
||||
|
||||
```typescript
|
||||
import { Settings, ToggleSetting, TextSetting, ChoiceSetSetting } from '@microsoft/cmdpal-sdk';
|
||||
|
||||
const settings = new Settings();
|
||||
|
||||
// Toggle (boolean)
|
||||
settings.add(new ToggleSetting('darkMode', 'Dark Mode', true, 'Enable dark theme'));
|
||||
|
||||
// Text input
|
||||
settings.add(new TextSetting('apiKey', 'API Key', '', 'Your API key'));
|
||||
|
||||
// Choice set (dropdown)
|
||||
settings.add(new ChoiceSetSetting('language', 'Language', [
|
||||
{ title: 'English', value: 'en' },
|
||||
{ title: 'Spanish', value: 'es' },
|
||||
], 'en'));
|
||||
|
||||
// Read values
|
||||
const darkMode = settings.getSetting<ToggleSetting>('darkMode')?.value;
|
||||
|
||||
// Expose in provider
|
||||
class MyProvider extends CommandProviderBase {
|
||||
settings = settings;
|
||||
// ... settings page auto-generated from settings definitions
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Base Classes
|
||||
|
||||
### CommandProviderBase
|
||||
|
||||
The entry point for every extension.
|
||||
|
||||
```typescript
|
||||
class MyProvider extends CommandProviderBase implements ICommandProvider {
|
||||
readonly id = 'my-extension';
|
||||
readonly displayName = 'My Extension';
|
||||
readonly icon = iconFromGlyph('\uE8A5');
|
||||
|
||||
topLevelCommands(): ICommandItem[] {
|
||||
return [ /* ... */ ];
|
||||
}
|
||||
|
||||
// Optional
|
||||
fallbackCommands?(): IFallbackCommandItem[];
|
||||
getCommand?(id: string): ICommand | null;
|
||||
settings?: ICommandSettings;
|
||||
initializeWithHost?(host: IExtensionHost): void;
|
||||
dispose?(): void;
|
||||
}
|
||||
```
|
||||
|
||||
### ListPageBase
|
||||
|
||||
```typescript
|
||||
class MyListPage extends ListPageBase implements IListPage {
|
||||
readonly id = 'my-list';
|
||||
readonly name = 'My List';
|
||||
readonly title = 'My List Page';
|
||||
|
||||
getItems(): IListItem[] {
|
||||
return [ /* ... */ ];
|
||||
}
|
||||
|
||||
// Optional
|
||||
showDetails?: boolean;
|
||||
filters?: Filters;
|
||||
gridProperties?: GridProperties;
|
||||
placeholderText?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### DynamicListPageBase
|
||||
|
||||
```typescript
|
||||
class MySearchPage extends DynamicListPageBase implements IDynamicListPage {
|
||||
readonly id = 'my-search';
|
||||
readonly name = 'Search';
|
||||
readonly title = 'Search Page';
|
||||
private query = '';
|
||||
|
||||
setSearchText(text: string): void {
|
||||
this.query = text;
|
||||
this.notifyItemsChanged(); // Tell host to re-fetch items
|
||||
}
|
||||
|
||||
getItems(): IListItem[] {
|
||||
return allItems.filter(item => item.title.includes(this.query));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ContentPageBase
|
||||
|
||||
```typescript
|
||||
class MyContentPage extends ContentPageBase implements IContentPage {
|
||||
readonly id = 'my-content';
|
||||
readonly name = 'Content';
|
||||
readonly title = 'My Content Page';
|
||||
|
||||
getContent(): Content[] {
|
||||
return [
|
||||
{ type: 'markdown', body: '# Hello\n\nThis is **markdown** content.' },
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### InvokableCommandBase
|
||||
|
||||
```typescript
|
||||
class MyCommand extends InvokableCommandBase implements IInvokableCommand {
|
||||
readonly id = 'my-command';
|
||||
readonly name = 'Do Something';
|
||||
|
||||
invoke(): CommandResult {
|
||||
// Do work...
|
||||
return { kind: 'showToast', args: { message: 'Done!' } };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Built-In Commands
|
||||
|
||||
| Class | Purpose | `invoke()` returns |
|
||||
|-------|---------|-------------------|
|
||||
| `NoOpCommand` | Does nothing | `{ kind: 'keepOpen' }` |
|
||||
| `OpenUrlCommand` | Opens a URL in the default browser | `{ kind: 'dismiss' }` |
|
||||
| `CopyTextCommand` | Copies text to clipboard | Toast with copy confirmation |
|
||||
| `ConfirmableCommand` | Shows a confirmation dialog | `{ kind: 'confirm', args: {...} }` |
|
||||
|
||||
---
|
||||
|
||||
## Icon Helpers
|
||||
|
||||
```typescript
|
||||
import { iconFromGlyph, iconFromBase64, iconFromUrl, iconFromFile } from '@microsoft/cmdpal-sdk';
|
||||
|
||||
// Font glyph (Segoe Fluent Icons)
|
||||
const icon = iconFromGlyph('\uE91B');
|
||||
|
||||
// Base64-encoded image data
|
||||
const icon = iconFromBase64('iVBORw0KGgoAAAANSUhEUg...');
|
||||
|
||||
// Fetch image from URL (async — downloads and encodes as base64)
|
||||
const icon = await iconFromUrl('https://example.com/icon.png');
|
||||
|
||||
// Read local file (async — reads and encodes as base64)
|
||||
const icon = await iconFromFile('./assets/icon.png');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Runtime API
|
||||
|
||||
### ExtensionHost
|
||||
|
||||
Static bridge for communicating with the CmdPal host.
|
||||
|
||||
```typescript
|
||||
import { ExtensionHost } from '@microsoft/cmdpal-sdk';
|
||||
|
||||
// Logging
|
||||
ExtensionHost.log('Something happened');
|
||||
ExtensionHost.log('Error occurred', 'error');
|
||||
|
||||
// Status bar
|
||||
ExtensionHost.showStatus('Loading...', 'info', { isIndeterminate: true });
|
||||
ExtensionHost.hideStatus('loading-id');
|
||||
|
||||
// Clipboard
|
||||
ExtensionHost.copyToClipboard('Hello, clipboard!');
|
||||
```
|
||||
|
||||
### Activation
|
||||
|
||||
```typescript
|
||||
import { activate, startJsonRpcServer } from '@microsoft/cmdpal-sdk';
|
||||
|
||||
// Standard activation pattern
|
||||
startJsonRpcServer(() => {
|
||||
return new MyProvider();
|
||||
});
|
||||
```
|
||||
|
||||
### Notifications
|
||||
|
||||
```typescript
|
||||
import { sendNotification } from '@microsoft/cmdpal-sdk';
|
||||
|
||||
// Tell the host that a list page's items have changed
|
||||
sendNotification('listPage/itemsChanged', { pageId: 'my-list' });
|
||||
|
||||
// Tell the host that a command's properties changed
|
||||
sendNotification('command/propChanged', {
|
||||
commandId: 'my-fallback',
|
||||
properties: { displayTitle: 'Search: query text' },
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## C# Toolkit Equivalence
|
||||
|
||||
| C# Toolkit | TypeScript SDK |
|
||||
|------------|----------------|
|
||||
| `ListPage` | `ListPageBase` |
|
||||
| `DynamicListPage` | `DynamicListPageBase` |
|
||||
| `ContentPage` | `ContentPageBase` |
|
||||
| `InvokableCommand` | `InvokableCommandBase` |
|
||||
| `CommandItem` | `CommandItemBase` |
|
||||
| `ListItem` | `ListItemBase` |
|
||||
| `FallbackCommandItem` | `FallbackCommandItemBase` |
|
||||
| `Separator` | `Separator` |
|
||||
| `NoOpCommand` | `NoOpCommand` |
|
||||
| `OpenUrlCommand` | `OpenUrlCommand` |
|
||||
| `CopyTextCommand` | `CopyTextCommand` |
|
||||
| `ConfirmableCommand` | `ConfirmableCommand` |
|
||||
| `Settings`/`SettingsPage` | `Settings` (auto-generates `IContentPage`) |
|
||||
| `ToggleSetting` | `ToggleSetting` |
|
||||
| `TextSetting` | `TextSetting` |
|
||||
| `ChoiceSetSetting` | `ChoiceSetSetting` |
|
||||
| `IconHelpers.FromRelativePath` | `iconFromFile` |
|
||||
| `IconInfo.FromStream` | `iconFromBase64` / `iconFromUrl` |
|
||||
509
src/modules/cmdpal/doc/json-rpc-spec/03-jsonrpc-protocol.md
Normal file
509
src/modules/cmdpal/doc/json-rpc-spec/03-jsonrpc-protocol.md
Normal file
@@ -0,0 +1,509 @@
|
||||
# 03 - JSON-RPC Protocol Specification
|
||||
|
||||
## Overview
|
||||
|
||||
Communication between the CmdPal host and JavaScript extensions uses **JSON-RPC 2.0** over **stdio** with **LSP-style `Content-Length` framing**.
|
||||
|
||||
### Framing Format
|
||||
|
||||
Every message (request, response, and notification) is preceded by a header:
|
||||
|
||||
```http
|
||||
Content-Length: <byte-count>\r\n
|
||||
\r\n
|
||||
<UTF-8 JSON body>
|
||||
```
|
||||
|
||||
Where `<byte-count>` is the byte length (not character length) of the JSON body.
|
||||
|
||||
### Connection Properties
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Transport | stdin (host→extension), stdout (extension→host) |
|
||||
| Encoding | UTF-8 |
|
||||
| Protocol | JSON-RPC 2.0 |
|
||||
| Request timeout | 10 seconds |
|
||||
| Concurrency | Requests are serialized (one at a time); notifications can interleave |
|
||||
|
||||
### Message Types
|
||||
|
||||
| Type | Has `id` | Has `method` | Direction |
|
||||
|------|----------|-------------|-----------|
|
||||
| Request | ✅ | ✅ | Host → Extension |
|
||||
| Response | ✅ | ❌ | Extension → Host |
|
||||
| Notification | ❌ | ✅ | Either direction |
|
||||
|
||||
---
|
||||
|
||||
## Host → Extension Requests
|
||||
|
||||
### `initialize`
|
||||
|
||||
Called once after the Node.js process starts. The extension should initialize its provider and return capabilities.
|
||||
|
||||
**Parameters:**
|
||||
```json
|
||||
{
|
||||
"extensionId": "my-extension"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"capabilities": ["commands"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `provider/getTopLevelCommands`
|
||||
|
||||
Fetches the extension's top-level command items (shown in the main CmdPal list).
|
||||
|
||||
**Parameters:** `null`
|
||||
|
||||
**Response:** Array of command items:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "cmd-1",
|
||||
"title": "My Command",
|
||||
"displayName": "My Command",
|
||||
"subtitle": "Does something useful",
|
||||
"command": {
|
||||
"id": "cmd-1",
|
||||
"name": "My Command",
|
||||
"icon": { "light": { "icon": "\uE8A5" } },
|
||||
"pageType": "dynamicListPage"
|
||||
},
|
||||
"icon": { "light": { "icon": "\uE8A5" } },
|
||||
"moreCommands": []
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
The `command` object includes a `pageType` field when the command represents a page:
|
||||
- `"listPage"` — static list page
|
||||
- `"dynamicListPage"` — search-enabled list page
|
||||
- `"contentPage"` — rich content page
|
||||
- Absent — invokable command (no page)
|
||||
|
||||
---
|
||||
|
||||
### `provider/getFallbackCommands`
|
||||
|
||||
Fetches commands that receive the user's search query when no other results match.
|
||||
|
||||
**Parameters:** `null`
|
||||
|
||||
**Response:** Array of fallback command items, or `null`:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "search-web",
|
||||
"title": "Search the web",
|
||||
"displayName": "Search the web",
|
||||
"command": { "id": "search-web", "name": "Search the web" }
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `provider/getCommand`
|
||||
|
||||
Fetches a specific command/page by ID. Used when navigating to a page.
|
||||
|
||||
**Parameters:**
|
||||
```json
|
||||
{
|
||||
"commandId": "my-page-id"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** Command object or `null`:
|
||||
```json
|
||||
{
|
||||
"id": "my-page-id",
|
||||
"name": "My Page",
|
||||
"pageType": "listPage",
|
||||
"icon": { "light": { "icon": "\uE8A5" } }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `provider/getSettings`
|
||||
|
||||
Fetches the extension's settings page ID.
|
||||
|
||||
**Parameters:** `null`
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": "settings-page-id"
|
||||
}
|
||||
```
|
||||
|
||||
Or `null` if the extension has no settings.
|
||||
|
||||
---
|
||||
|
||||
### `command/invoke`
|
||||
|
||||
Invokes a command by ID.
|
||||
|
||||
**Parameters:**
|
||||
```json
|
||||
{
|
||||
"commandId": "my-command-id"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** Command result:
|
||||
```json
|
||||
{
|
||||
"Kind": 6,
|
||||
"Args": {
|
||||
"Message": "Operation complete!"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`Kind` values:
|
||||
| Value | Name | Description |
|
||||
|-------|------|-------------|
|
||||
| 0 | Dismiss | Close CmdPal |
|
||||
| 1 | GoHome | Navigate to home |
|
||||
| 2 | GoBack | Navigate back |
|
||||
| 3 | Hide | Hide CmdPal (keep state) |
|
||||
| 4 | KeepOpen | Stay on current page |
|
||||
| 5 | GoToPage | Navigate to page (requires `PageId` in args) |
|
||||
| 6 | ShowToast | Show toast notification (requires `Message` in args) |
|
||||
| 7 | Confirm | Show confirmation dialog |
|
||||
|
||||
---
|
||||
|
||||
### `listPage/getItems`
|
||||
|
||||
Fetches items for a list page.
|
||||
|
||||
**Parameters:**
|
||||
```json
|
||||
{
|
||||
"pageId": "my-list-page"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": "item-1",
|
||||
"title": "Item One",
|
||||
"subtitle": "Description",
|
||||
"section": "Group A",
|
||||
"command": { "id": "item-1-cmd", "name": "Item One" },
|
||||
"icon": { "light": { "icon": "\uE8A5" } },
|
||||
"tags": [{ "text": "New", "foreground": { "hasValue": true, "color": { "r": 255, "g": 255, "b": 255, "a": 255 } } }],
|
||||
"details": {
|
||||
"title": "Item One Details",
|
||||
"body": "**Rich** markdown description",
|
||||
"metadata": [
|
||||
{ "key": "Author", "data": { "type": "tags", "tags": [{ "text": "mjolley" }] } },
|
||||
{ "key": "Link", "data": { "type": "link", "link": "https://github.com", "text": "GitHub" } }
|
||||
]
|
||||
},
|
||||
"moreCommands": [
|
||||
{
|
||||
"command": { "id": "copy-cmd", "name": "Copy" },
|
||||
"title": "Copy to clipboard",
|
||||
"icon": { "light": { "icon": "\uE8C8" } }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Items with `_isSeparator: true` are rendered as visual separators:
|
||||
```json
|
||||
{
|
||||
"title": "Section Header",
|
||||
"section": "Section Header",
|
||||
"_isSeparator": true,
|
||||
"command": null
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `listPage/setSearchText`
|
||||
|
||||
Updates the search text for a dynamic list page.
|
||||
|
||||
**Parameters:**
|
||||
```json
|
||||
{
|
||||
"pageId": "my-dynamic-page",
|
||||
"searchText": "user query"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `null`
|
||||
|
||||
The extension should update its internal state and send a `listPage/itemsChanged` notification when items are ready.
|
||||
|
||||
---
|
||||
|
||||
### `listPage/setFilter`
|
||||
|
||||
Updates the active filter for a list page.
|
||||
|
||||
**Parameters:**
|
||||
```json
|
||||
{
|
||||
"pageId": "my-filtered-page",
|
||||
"filterId": "recent"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `null`
|
||||
|
||||
---
|
||||
|
||||
### `listPage/loadMore`
|
||||
|
||||
Requests additional items for infinite scroll.
|
||||
|
||||
**Parameters:**
|
||||
```json
|
||||
{
|
||||
"pageId": "my-page"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `null`
|
||||
|
||||
The extension should append items and send a `listPage/itemsChanged` notification.
|
||||
|
||||
---
|
||||
|
||||
### `fallback/updateQuery`
|
||||
|
||||
Updates the search query for a fallback command.
|
||||
|
||||
**Parameters:**
|
||||
```json
|
||||
{
|
||||
"commandId": "search-fallback",
|
||||
"query": "user typed text"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `null`
|
||||
|
||||
The extension should update its internal state and send a `command/propChanged` notification to update the display title.
|
||||
|
||||
---
|
||||
|
||||
### `contentPage/getContent`
|
||||
|
||||
Fetches content for a content page.
|
||||
|
||||
**Parameters:**
|
||||
```json
|
||||
{
|
||||
"pageId": "my-content-page"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** Array of content items:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"type": "markdown",
|
||||
"body": "# Hello\n\nMarkdown content"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"image": { "light": { "data": "iVBORw0KGgo..." } },
|
||||
"maxWidth": 600,
|
||||
"maxHeight": 400
|
||||
},
|
||||
{
|
||||
"type": "form",
|
||||
"templateJson": "{...adaptive card JSON...}",
|
||||
"dataJson": "{...data values...}"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `form/submit`
|
||||
|
||||
Submits form data from a content page or form content.
|
||||
|
||||
**Parameters:**
|
||||
```json
|
||||
{
|
||||
"pageId": "my-form-page",
|
||||
"inputs": "{\"name\":\"John\"}",
|
||||
"data": "{}"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** Command result (same format as `command/invoke`).
|
||||
|
||||
---
|
||||
|
||||
### `dispose`
|
||||
|
||||
Notification sent before the host kills the extension process.
|
||||
|
||||
**Parameters:** `null`
|
||||
|
||||
**Note:** This is a notification (no `id`), not a request. The extension should clean up resources.
|
||||
|
||||
---
|
||||
|
||||
## Extension → Host Notifications
|
||||
|
||||
### `listPage/itemsChanged`
|
||||
|
||||
Tells the host to re-fetch items for a list page.
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "listPage/itemsChanged",
|
||||
"params": {
|
||||
"pageId": "my-list-page"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `command/propChanged`
|
||||
|
||||
Tells the host that a command's properties have changed (e.g., fallback display title).
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "command/propChanged",
|
||||
"params": {
|
||||
"commandId": "my-fallback",
|
||||
"properties": {
|
||||
"displayTitle": "Search: new query"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `host/logMessage`
|
||||
|
||||
Sends a log message to the host's logging system.
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "host/logMessage",
|
||||
"params": {
|
||||
"message": "Extension initialized successfully",
|
||||
"state": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
State values: 0 = Info, 1 = Success, 2 = Warning, 3 = Error
|
||||
|
||||
---
|
||||
|
||||
### `host/showStatus`
|
||||
|
||||
Shows a status message in the CmdPal status bar.
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "host/showStatus",
|
||||
"params": {
|
||||
"message": {
|
||||
"Message": "Loading data...",
|
||||
"State": 0
|
||||
},
|
||||
"context": "extension"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `host/hideStatus`
|
||||
|
||||
Hides a previously shown status message.
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "host/hideStatus",
|
||||
"params": {
|
||||
"message": {
|
||||
"Message": "Loading data...",
|
||||
"State": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `host/copyText`
|
||||
|
||||
Copies text to the system clipboard (since Node.js doesn't have clipboard access).
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "host/copyText",
|
||||
"params": {
|
||||
"text": "Text to copy to clipboard"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Protocol Summary Table
|
||||
|
||||
| Method | Direction | Type | Purpose |
|
||||
|--------|-----------|------|---------|
|
||||
| `initialize` | Host → Ext | Request | Initialize extension |
|
||||
| `provider/getTopLevelCommands` | Host → Ext | Request | Get top-level commands |
|
||||
| `provider/getFallbackCommands` | Host → Ext | Request | Get fallback commands |
|
||||
| `provider/getCommand` | Host → Ext | Request | Get command by ID |
|
||||
| `provider/getSettings` | Host → Ext | Request | Get settings page |
|
||||
| `command/invoke` | Host → Ext | Request | Invoke a command |
|
||||
| `listPage/getItems` | Host → Ext | Request | Get list page items |
|
||||
| `listPage/setSearchText` | Host → Ext | Request | Update search query |
|
||||
| `listPage/setFilter` | Host → Ext | Request | Update active filter |
|
||||
| `listPage/loadMore` | Host → Ext | Request | Load more items |
|
||||
| `fallback/updateQuery` | Host → Ext | Request | Update fallback query |
|
||||
| `contentPage/getContent` | Host → Ext | Request | Get content page content |
|
||||
| `form/submit` | Host → Ext | Request | Submit form data |
|
||||
| `dispose` | Host → Ext | Notification | Clean up before exit |
|
||||
| `listPage/itemsChanged` | Ext → Host | Notification | Items have changed |
|
||||
| `command/propChanged` | Ext → Host | Notification | Command props changed |
|
||||
| `host/logMessage` | Ext → Host | Notification | Log message |
|
||||
| `host/showStatus` | Ext → Host | Notification | Show status bar message |
|
||||
| `host/hideStatus` | Ext → Host | Notification | Hide status bar message |
|
||||
| `host/copyText` | Ext → Host | Notification | Copy text to clipboard |
|
||||
326
src/modules/cmdpal/doc/json-rpc-spec/04-manifest-packaging.md
Normal file
326
src/modules/cmdpal/doc/json-rpc-spec/04-manifest-packaging.md
Normal file
@@ -0,0 +1,326 @@
|
||||
# 04 - Manifest, Packaging, and Installation
|
||||
|
||||
## Extension Project Structure
|
||||
|
||||
A CmdPal JavaScript extension is a standard Node.js project. Extension metadata is declared in the `cmdpal` field of `package.json` (similar to how VS Code uses `contributes`):
|
||||
|
||||
```
|
||||
my-extension/
|
||||
├── package.json # Node.js manifest + "cmdpal" section (required)
|
||||
├── dist/
|
||||
│ └── index.js # Compiled entry point
|
||||
├── src/
|
||||
│ └── index.ts # TypeScript source
|
||||
├── tsconfig.json # TypeScript config
|
||||
├── node_modules/ # Dependencies (ideally bundled)
|
||||
└── icon.png # Extension icon (optional)
|
||||
```
|
||||
|
||||
The key files:
|
||||
- **`package.json`** — Standard Node.js package manifest with an added `cmdpal` section for CmdPal-specific metadata
|
||||
- **`dist/index.js`** — The compiled JavaScript entry point that CmdPal will execute
|
||||
|
||||
---
|
||||
|
||||
## `package.json` Schema
|
||||
|
||||
CmdPal discovers extensions by finding directories with a `package.json` that contains a `cmdpal` object. Top-level npm fields provide identity; the `cmdpal` section provides CmdPal-specific metadata.
|
||||
|
||||
### Full Example
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@microsoft/cmdpal-my-extension",
|
||||
"version": "1.0.0",
|
||||
"description": "A brief description of the extension",
|
||||
"main": "dist/index.js",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"cmdpal": {
|
||||
"displayName": "My Extension",
|
||||
"icon": "icon.png",
|
||||
"publisher": "your-name",
|
||||
"capabilities": ["commands"],
|
||||
"debug": false,
|
||||
"debugPort": 9230
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/cmdpal-sdk": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"keywords": ["cmdpal", "powertoys", "command-palette"]
|
||||
}
|
||||
```
|
||||
|
||||
### Field Reference
|
||||
|
||||
#### Top-level fields (standard npm)
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `name` | `string` | ✅ | Package identifier. Must be unique across installed extensions. Used as the extension ID. |
|
||||
| `version` | `string` | ❌ | Semantic version string (e.g., `"1.0.0"`). |
|
||||
| `description` | `string` | ❌ | Brief description shown in the extension gallery and settings. |
|
||||
| `main` | `string` | ✅ | Relative path to the entry point JavaScript file. This is what `node` executes. |
|
||||
| `engines.node` | `string` | ❌ | Node.js version requirement (e.g., `">=18"`). |
|
||||
|
||||
#### `cmdpal` section fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `displayName` | `string` | ❌ | Human-readable name shown in CmdPal UI. Falls back to `name` if not provided. |
|
||||
| `icon` | `string` | ❌ | Icon glyph character (e.g., `"\uE943"`) or relative path to an icon file (PNG recommended). |
|
||||
| `publisher` | `string` | ❌ | Author or publisher name. |
|
||||
| `capabilities` | `string[]` | ❌ | List of capabilities the extension provides (e.g., `["commands"]`). |
|
||||
| `debug` | `boolean` | ❌ | When `true`, starts Node.js with `--inspect` for debugger attachment. Default: `false`. |
|
||||
| `debugPort` | `integer` | ❌ | Inspector port when `debug` is `true`. If not specified, auto-assigned starting at 9229. |
|
||||
| `main` | `string` | ❌ | Optional override of the top-level `main` field (for packages where the CmdPal entry point differs from the npm main). |
|
||||
|
||||
### Validation Rules
|
||||
|
||||
A `package.json` is recognized as a CmdPal extension if:
|
||||
1. It contains a `cmdpal` object (even if empty: `"cmdpal": {}`)
|
||||
2. `name` is present and non-empty
|
||||
3. Either `cmdpal.main` or top-level `main` resolves to an existing file
|
||||
|
||||
---
|
||||
|
||||
## Installation Directory
|
||||
|
||||
Extensions are installed to:
|
||||
|
||||
```
|
||||
%LOCALAPPDATA%\Microsoft\PowerToys\CommandPalette\JSExtensions\
|
||||
```
|
||||
|
||||
Each extension occupies its own subdirectory:
|
||||
|
||||
```
|
||||
JSExtensions/
|
||||
├── my-extension/
|
||||
│ ├── package.json ← contains "cmdpal" section
|
||||
│ ├── dist/
|
||||
│ │ └── index.js
|
||||
│ └── node_modules/
|
||||
├── another-extension/
|
||||
│ ├── package.json
|
||||
│ └── ...
|
||||
```
|
||||
|
||||
### Discovery
|
||||
|
||||
The `JsonRpcExtensionService` watches this directory with a `FileSystemWatcher`:
|
||||
- **New directory with valid `package.json`** → extension is loaded automatically
|
||||
- **Directory removed** → extension is unloaded, Node.js process is terminated
|
||||
- **`*.js` file changed** within an extension → hot-reload (500ms debounce)
|
||||
|
||||
This means:
|
||||
- Installing an extension = copying its directory to `JSExtensions/`
|
||||
- Uninstalling = deleting the directory
|
||||
- Updating = replacing files (hot-reload handles `*.js` changes)
|
||||
|
||||
---
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Creating a New Extension
|
||||
|
||||
1. **Create the project directory:**
|
||||
```bash
|
||||
mkdir my-extension && cd my-extension
|
||||
npm init -y
|
||||
```
|
||||
|
||||
2. **Install the SDK:**
|
||||
```bash
|
||||
npm install @microsoft/cmdpal-sdk
|
||||
```
|
||||
|
||||
3. **Add the `cmdpal` section to `package.json`:**
|
||||
```json
|
||||
{
|
||||
"name": "my-extension",
|
||||
"version": "1.0.0",
|
||||
"description": "My awesome CmdPal extension",
|
||||
"main": "dist/index.js",
|
||||
"cmdpal": {
|
||||
"displayName": "My Extension",
|
||||
"debug": true
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/cmdpal-sdk": "^1.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. **Create `tsconfig.json`:**
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
```
|
||||
|
||||
5. **Write your extension** in `src/index.ts` (see [05-getting-started.md](./05-getting-started.md))
|
||||
|
||||
6. **Build:**
|
||||
```bash
|
||||
npx tsc
|
||||
```
|
||||
|
||||
### Development Installation
|
||||
|
||||
For development, symlink or copy your extension to the JSExtensions directory:
|
||||
|
||||
```powershell
|
||||
# Option 1: Copy
|
||||
Copy-Item -Recurse ./my-extension "$env:LOCALAPPDATA\Microsoft\PowerToys\CommandPalette\JSExtensions\my-extension"
|
||||
|
||||
# Option 2: Junction link (recommended for development)
|
||||
New-Item -ItemType Junction -Path "$env:LOCALAPPDATA\Microsoft\PowerToys\CommandPalette\JSExtensions\my-extension" -Target (Resolve-Path ./my-extension)
|
||||
```
|
||||
|
||||
With a junction link, changes to your source files are reflected immediately (after build). The `*.js` file watcher triggers hot-reload automatically.
|
||||
|
||||
### Debugging
|
||||
|
||||
1. Set `"debug": true` in the `cmdpal` section of `package.json`
|
||||
2. Optionally set `"debugPort": 9230` (or any available port)
|
||||
3. Open Chrome DevTools: `chrome://inspect` or attach VS Code's debugger
|
||||
4. The Node.js process starts with `--inspect=0.0.0.0:<port>`, ready for debugger attachment
|
||||
|
||||
---
|
||||
|
||||
## Production Packaging
|
||||
|
||||
### npm Package Structure
|
||||
|
||||
Extensions are distributed as standard npm packages. The recommended `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@publisher/cmdpal-my-extension",
|
||||
"version": "1.0.0",
|
||||
"description": "My CmdPal extension",
|
||||
"main": "dist/index.js",
|
||||
"cmdpal": {
|
||||
"displayName": "My Extension",
|
||||
"icon": "icon.png",
|
||||
"publisher": "your-name",
|
||||
"capabilities": ["commands"]
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"files": [
|
||||
"dist/",
|
||||
"icon.png"
|
||||
],
|
||||
"dependencies": {
|
||||
"@microsoft/cmdpal-sdk": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"keywords": ["cmdpal", "powertoys", "command-palette"],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Naming Convention
|
||||
|
||||
Recommended npm package naming: `@publisher/cmdpal-<name>` or `cmdpal-<name>`.
|
||||
|
||||
The `cmdpal-` prefix helps with discoverability and could be used for future npm-based discovery.
|
||||
|
||||
---
|
||||
|
||||
## Extension Gallery Integration
|
||||
|
||||
|
||||
### Gallery Manifest Entry
|
||||
|
||||
The existing CmdPal extension gallery pulls from a manifest that lists available extensions. For JavaScript extensions, the manifest entry includes npm package details:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-extension",
|
||||
"displayName": "My Extension",
|
||||
"description": "Does amazing things",
|
||||
"publisher": "your-name",
|
||||
"version": "1.0.0",
|
||||
"icon": "https://example.com/icon.png",
|
||||
"type": "jsonrpc",
|
||||
"npm": {
|
||||
"package": "@publisher/cmdpal-my-extension",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `type: "jsonrpc"` field distinguishes JavaScript extensions from COM-based extensions.
|
||||
|
||||
### Installation Flow
|
||||
|
||||
When a user clicks "Install" for a JavaScript extension in the gallery:
|
||||
|
||||
1. CmdPal runs `npm install <package>` in the JSExtensions directory
|
||||
2. The package is installed to `JSExtensions/<name>/`
|
||||
3. `FileSystemWatcher` detects the new directory → extension loads automatically
|
||||
4. The extension appears in the main CmdPal list
|
||||
|
||||
### Uninstallation Flow
|
||||
|
||||
When a user clicks "Uninstall":
|
||||
|
||||
1. CmdPal terminates the extension's Node.js process
|
||||
2. The extension directory is deleted from `JSExtensions/`
|
||||
3. `FileSystemWatcher` detects the removal → extension is unloaded
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Process Isolation
|
||||
|
||||
Each JavaScript extension runs in its own Node.js process:
|
||||
- Separate memory space
|
||||
- Separate event loop
|
||||
- No direct access to other extensions or CmdPal internals
|
||||
- Communication only through the JSON-RPC protocol
|
||||
|
||||
### Permissions
|
||||
|
||||
Currently, JavaScript extensions have the same permissions as the Node.js process:
|
||||
- File system access
|
||||
- Network access
|
||||
- Process spawning
|
||||
|
||||
Future considerations:
|
||||
- Extension permission declarations in `package.json` `cmdpal` section
|
||||
- User consent prompts for sensitive permissions
|
||||
- Sandboxing via Node.js `--experimental-policy` or similar mechanisms
|
||||
|
||||
### Trust Model
|
||||
|
||||
- Extensions installed from the gallery are implicitly trusted by the user
|
||||
- Sideloaded extensions (copied to JSExtensions/) have no verification
|
||||
484
src/modules/cmdpal/doc/json-rpc-spec/05-getting-started.md
Normal file
484
src/modules/cmdpal/doc/json-rpc-spec/05-getting-started.md
Normal file
@@ -0,0 +1,484 @@
|
||||
# 05 — Getting Started: Build Your First CmdPal Extension
|
||||
|
||||
This guide walks you through building a CmdPal JavaScript extension from scratch. By the end, you'll have a working extension with a searchable list page, a content page, and settings.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Node.js 18+** installed and on your PATH
|
||||
- **PowerToys** with Command Palette enabled
|
||||
- A text editor (VS Code recommended)
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Scaffold the Project
|
||||
|
||||
```bash
|
||||
mkdir my-first-extension && cd my-first-extension
|
||||
npm init -y
|
||||
npm install @microsoft/cmdpal-sdk
|
||||
npm install --save-dev typescript
|
||||
```
|
||||
|
||||
Create `tsconfig.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
```
|
||||
|
||||
Add the `cmdpal` section to your `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-first-extension",
|
||||
"version": "1.0.0",
|
||||
"description": "A tutorial CmdPal extension",
|
||||
"main": "dist/index.js",
|
||||
"cmdpal": {
|
||||
"displayName": "My First Extension",
|
||||
"debug": true
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/cmdpal-sdk": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Create a Simple Command
|
||||
|
||||
Create `src/index.ts`:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
CommandProvider,
|
||||
Command,
|
||||
ListPage,
|
||||
ListItem,
|
||||
CommandResultKind,
|
||||
run,
|
||||
iconFromGlyph,
|
||||
} from "@microsoft/cmdpal-sdk";
|
||||
|
||||
// Define a simple command that shows a toast
|
||||
class GreetCommand extends Command {
|
||||
constructor() {
|
||||
super({
|
||||
id: "greet",
|
||||
name: "Say Hello",
|
||||
icon: iconFromGlyph("\uE76E"), // Checkmark icon
|
||||
});
|
||||
}
|
||||
|
||||
async invoke(): Promise<{ kind: CommandResultKind }> {
|
||||
return {
|
||||
kind: CommandResultKind.ShowToast,
|
||||
args: { Message: "Hello from my extension!" },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Define the main list page
|
||||
class MainPage extends ListPage {
|
||||
constructor() {
|
||||
super({
|
||||
id: "main-page",
|
||||
name: "My First Extension",
|
||||
icon: iconFromGlyph("\uE8A5"),
|
||||
});
|
||||
}
|
||||
|
||||
async getItems(): Promise<ListItem[]> {
|
||||
return [
|
||||
new ListItem({
|
||||
title: "Say Hello",
|
||||
subtitle: "Shows a greeting toast",
|
||||
command: new GreetCommand(),
|
||||
icon: iconFromGlyph("\uE76E"),
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Define the provider
|
||||
class MyProvider extends CommandProvider {
|
||||
constructor() {
|
||||
super("my-first-extension", "My First Extension");
|
||||
}
|
||||
|
||||
getTopLevelCommands() {
|
||||
return [
|
||||
new ListItem({
|
||||
title: "My First Extension",
|
||||
subtitle: "A tutorial extension",
|
||||
command: new MainPage(),
|
||||
icon: iconFromGlyph("\uE8A5"),
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Start the extension
|
||||
run(new MyProvider());
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Build and Install
|
||||
|
||||
```bash
|
||||
# Build
|
||||
npx tsc
|
||||
|
||||
# Install (junction link for development)
|
||||
$extensionsDir = "$env:LOCALAPPDATA\Microsoft\PowerToys\CommandPalette\JSExtensions"
|
||||
New-Item -ItemType Junction -Path "$extensionsDir\my-first-extension" -Target (Resolve-Path .)
|
||||
```
|
||||
|
||||
Open CmdPal — you should see "My First Extension" in the list. Click it to see your command, then click "Say Hello" to see the toast!
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Add a Dynamic List Page (Search)
|
||||
|
||||
Let's add a searchable list that filters items as you type:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
DynamicListPage,
|
||||
// ... other imports
|
||||
} from "@microsoft/cmdpal-sdk";
|
||||
|
||||
class SearchablePage extends DynamicListPage {
|
||||
private allItems = [
|
||||
{ title: "Apple", emoji: "🍎" },
|
||||
{ title: "Banana", emoji: "🍌" },
|
||||
{ title: "Cherry", emoji: "🍒" },
|
||||
{ title: "Dragon Fruit", emoji: "🐉" },
|
||||
{ title: "Elderberry", emoji: "🫐" },
|
||||
];
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: "searchable-page",
|
||||
name: "Fruit Search",
|
||||
icon: iconFromGlyph("\uE721"), // Search icon
|
||||
placeholderText: "Search fruits...",
|
||||
});
|
||||
}
|
||||
|
||||
async getItems(): Promise<ListItem[]> {
|
||||
const query = this.searchText?.toLowerCase() ?? "";
|
||||
return this.allItems
|
||||
.filter((item) => item.title.toLowerCase().includes(query))
|
||||
.map(
|
||||
(item) =>
|
||||
new ListItem({
|
||||
title: `${item.emoji} ${item.title}`,
|
||||
subtitle: "A delicious fruit",
|
||||
command: new ToastCommand(item.title),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ToastCommand extends Command {
|
||||
private fruit: string;
|
||||
|
||||
constructor(fruit: string) {
|
||||
super({ id: `toast-${fruit}`, name: `Select ${fruit}` });
|
||||
this.fruit = fruit;
|
||||
}
|
||||
|
||||
async invoke() {
|
||||
return {
|
||||
kind: CommandResultKind.ShowToast,
|
||||
args: { Message: `You selected ${this.fruit}!` },
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Add the searchable page to your `MainPage.getItems()`:
|
||||
|
||||
```typescript
|
||||
new ListItem({
|
||||
title: "Fruit Search",
|
||||
subtitle: "Search through a list of fruits",
|
||||
command: new SearchablePage(),
|
||||
icon: iconFromGlyph("\uE721"),
|
||||
}),
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Add a Content Page
|
||||
|
||||
Content pages display rich content — markdown, images, and forms:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
ContentPage,
|
||||
MarkdownContent,
|
||||
ImageContent,
|
||||
FormContent,
|
||||
iconFromUrl,
|
||||
// ... other imports
|
||||
} from "@microsoft/cmdpal-sdk";
|
||||
|
||||
class AboutPage extends ContentPage {
|
||||
constructor() {
|
||||
super({
|
||||
id: "about-page",
|
||||
name: "About",
|
||||
icon: iconFromGlyph("\uE946"), // Info icon
|
||||
});
|
||||
}
|
||||
|
||||
async getContent() {
|
||||
return [
|
||||
new MarkdownContent({
|
||||
body: `# My First Extension
|
||||
|
||||
This extension was built with the CmdPal TypeScript SDK.
|
||||
|
||||
## Features
|
||||
- **Simple commands** with toast notifications
|
||||
- **Searchable lists** with dynamic filtering
|
||||
- **Rich content** with markdown and images
|
||||
`,
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Add Settings
|
||||
|
||||
Extensions can have a settings page using Adaptive Cards:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
FormPage,
|
||||
// ... other imports
|
||||
} from "@microsoft/cmdpal-sdk";
|
||||
|
||||
class SettingsPage extends FormPage {
|
||||
constructor() {
|
||||
super({
|
||||
id: "settings",
|
||||
name: "Settings",
|
||||
icon: iconFromGlyph("\uE713"),
|
||||
});
|
||||
}
|
||||
|
||||
async getContent() {
|
||||
return [
|
||||
new FormContent({
|
||||
templateJson: JSON.stringify({
|
||||
type: "AdaptiveCard",
|
||||
body: [
|
||||
{
|
||||
type: "Input.Text",
|
||||
id: "greeting",
|
||||
label: "Custom Greeting",
|
||||
placeholder: "Enter a greeting...",
|
||||
value: "Hello",
|
||||
},
|
||||
{
|
||||
type: "Input.Toggle",
|
||||
id: "showEmoji",
|
||||
title: "Show emoji in results",
|
||||
value: "true",
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
type: "Action.Submit",
|
||||
title: "Save",
|
||||
},
|
||||
],
|
||||
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
version: "1.5",
|
||||
}),
|
||||
dataJson: JSON.stringify({
|
||||
greeting: "Hello",
|
||||
showEmoji: "true",
|
||||
}),
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
async onFormSubmit(inputs: Record<string, string>) {
|
||||
// Save settings (e.g., to a file or state)
|
||||
console.log("Settings saved:", inputs);
|
||||
return {
|
||||
kind: CommandResultKind.ShowToast,
|
||||
args: { Message: "Settings saved!" },
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Register the settings page in your provider:
|
||||
|
||||
```typescript
|
||||
class MyProvider extends CommandProvider {
|
||||
constructor() {
|
||||
super("my-first-extension", "My First Extension");
|
||||
}
|
||||
|
||||
getSettings() {
|
||||
return new SettingsPage();
|
||||
}
|
||||
|
||||
// ... getTopLevelCommands
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 7: Add Context Commands and Details
|
||||
|
||||
List items can have context menu commands and a details panel:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
CopyTextCommand,
|
||||
OpenUrlCommand,
|
||||
Details,
|
||||
DetailsMetadata,
|
||||
TagsData,
|
||||
Tag,
|
||||
LinkData,
|
||||
// ... other imports
|
||||
} from "@microsoft/cmdpal-sdk";
|
||||
|
||||
// In your getItems():
|
||||
new ListItem({
|
||||
title: "GitHub",
|
||||
subtitle: "Open GitHub in your browser",
|
||||
command: new OpenUrlCommand({
|
||||
id: "open-github",
|
||||
name: "Open GitHub",
|
||||
url: "https://github.com",
|
||||
}),
|
||||
icon: iconFromGlyph("\uE774"),
|
||||
tags: [
|
||||
new Tag({ text: "Web", foreground: { r: 100, g: 200, b: 255, a: 255 } }),
|
||||
],
|
||||
details: new Details({
|
||||
title: "GitHub",
|
||||
body: "The world's leading software development platform.",
|
||||
heroImage: iconFromGlyph("\uE774"),
|
||||
metadata: [
|
||||
new DetailsMetadata({
|
||||
key: "URL",
|
||||
data: new LinkData({
|
||||
link: "https://github.com",
|
||||
text: "github.com",
|
||||
}),
|
||||
}),
|
||||
new DetailsMetadata({
|
||||
key: "Tags",
|
||||
data: new TagsData({
|
||||
tags: [new Tag({ text: "Development" }), new Tag({ text: "Git" })],
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
moreCommands: [
|
||||
{
|
||||
title: "Copy URL",
|
||||
command: new CopyTextCommand({
|
||||
id: "copy-github-url",
|
||||
name: "Copy URL",
|
||||
text: "https://github.com",
|
||||
}),
|
||||
icon: iconFromGlyph("\uE8C8"),
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 8: Rebuild and Test
|
||||
|
||||
```bash
|
||||
# Rebuild after changes
|
||||
npx tsc
|
||||
```
|
||||
|
||||
CmdPal watches for `*.js` file changes and hot-reloads automatically. After running `tsc`, your extension will reload within ~500ms.
|
||||
|
||||
---
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
### Enable Debug Mode
|
||||
|
||||
Set `"debug": true` in the `cmdpal` section of your `package.json`. The Node.js process starts with `--inspect`, allowing you to attach a debugger.
|
||||
|
||||
### Attach VS Code Debugger
|
||||
|
||||
Add to `.vscode/launch.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "Attach to CmdPal Extension",
|
||||
"port": 9229,
|
||||
"restart": true,
|
||||
"skipFiles": ["<node_internals>/**"]
|
||||
}
|
||||
```
|
||||
|
||||
### View Logs
|
||||
|
||||
Use `host.log()` in your extension to send messages to the CmdPal log:
|
||||
|
||||
```typescript
|
||||
// Inside any command or page method:
|
||||
host.log("Fetching items...", MessageState.Info);
|
||||
host.log("Error occurred!", MessageState.Error);
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| Extension not showing | Check `package.json` has a `cmdpal` section and valid `name` + `main` |
|
||||
| Blank page | Check `getItems()` or `getContent()` is returning data |
|
||||
| Command does nothing | Ensure `invoke()` returns a valid `CommandResultKind` |
|
||||
| Images not loading | Use `iconFromUrl()` or `iconFromBase64()` helpers |
|
||||
| Settings crash | Ensure form template JSON is valid Adaptive Card schema |
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Read the [TypeScript SDK Reference](./02-typescript-sdk.md) for the full API
|
||||
- Read the [JSON-RPC Protocol Specification](./03-jsonrpc-protocol.md) to understand the wire format
|
||||
- Check out the [Sample Extension](../src/modules/cmdpal/ext/SampleJSExtension/) for a comprehensive example
|
||||
- Read the [Architecture Overview](./01-architecture.md) for how it all fits together
|
||||
83
src/modules/cmdpal/doc/json-rpc-spec/overview.md
Normal file
83
src/modules/cmdpal/doc/json-rpc-spec/overview.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Command Palette JavaScript Extension System Design Specification
|
||||
|
||||
> **Status:** Draft — seeking community and internal feedback<br/>
|
||||
> **Last updated:** 2006-06-17
|
||||
|
||||
## Table of Contents
|
||||
|
||||
| Document | Description |
|
||||
|----------|-------------|
|
||||
| [01 — Architecture Overview](01-architecture.md) | Process model, extension lifecycle, transport, and security |
|
||||
| [02 — TypeScript SDK Reference](02-typescript-sdk.md) | Full API surface: types, base classes, helpers, and runtime |
|
||||
| [03 — JSON-RPC Protocol](03-jsonrpc-protocol.md) | Complete protocol specification: methods, notifications, framing |
|
||||
| [04 — Extension Manifest & Packaging](04-manifest-packaging.md) | `package.json` schema, project structure, distribution |
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Command Palette (CmdPal) is extending its extension model beyond in-process WinRT/COM extensions to support **JavaScript and TypeScript extensions** that run as isolated Node.js processes, communicating with the host over JSON-RPC 2.0 via stdio.
|
||||
|
||||
### Goals
|
||||
|
||||
1. **Developer accessibility** — Let web developers build CmdPal extensions using familiar tools (TypeScript, npm, Node.js)
|
||||
2. **Process isolation** — Extension crashes don't take down CmdPal; extensions can't corrupt host state
|
||||
3. **Type-safe SDK** — Full TypeScript type definitions mirroring the C# toolkit surface
|
||||
4. **Developer experience** — Hot-reload on file changes, debugger attachment, familiar project structure
|
||||
5. **Feature parity** — JS extensions can create list pages, content pages, forms, grids, settings, and more
|
||||
|
||||
### Non-Goals (v1)
|
||||
|
||||
- Browser/WebView-based extension UI rendering
|
||||
- Sandboxed filesystem access or permission model
|
||||
- Extension marketplace / auto-update infrastructure
|
||||
- Multi-language JSONRPC support beyond JavaScript/TypeScript (Python, Go, etc.)
|
||||
|
||||
### Architecture at a Glance
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Host["Command Palette (WinUI Host)"]
|
||||
subgraph JS["JsonRpcExtensionService"]
|
||||
A["JSExtensionWrapper (ext A)"] --> ARPC["JsonRpcConnection\n(stdio, LSP framing)"]
|
||||
B["JSExtensionWrapper (ext B)"] --> BRPC["JsonRpcConnection\n(stdio, LSP framing)"]
|
||||
N["... one process per extension"]
|
||||
end
|
||||
|
||||
W["WinRTExtensionService\n(existing COM/WinRT extensions)"]
|
||||
BI["BuiltInExtensionService\n(built-in extensions)"]
|
||||
end
|
||||
|
||||
ARPC <-->|"JSON-RPC 2.0 over stdio"| NODEA["node ext-a.js\n(TS SDK runtime)"]
|
||||
BRPC <-->|"JSON-RPC 2.0 over stdio"| NODEB["node ext-b.js\n(TS SDK runtime)"]
|
||||
```
|
||||
|
||||
Each JS extension runs in its own Node.js process. The host spawns the process, establishes a JSON-RPC 2.0 connection over stdin/stdout with LSP-style `Content-Length` framing, sends an `initialize` request, and then queries the extension for commands, pages, and content as the user navigates.
|
||||
|
||||
### Key Design Decisions
|
||||
|
||||
| Decision | Choice | Rationale |
|
||||
|----------|--------|-----------|
|
||||
| Process model | One Node.js process per extension | Isolation, independent crash recovery, independent debugging |
|
||||
| Transport | stdio with LSP framing | No port conflicts, no network exposure, proven by LSP ecosystem |
|
||||
| Protocol | JSON-RPC 2.0 | Standard, well-tooled, bidirectional |
|
||||
| SDK language | TypeScript | Type safety, npm ecosystem, familiar to web developers |
|
||||
| Entry point | `cmdpal` field in `package.json` | Simple, declarative, same pattern as VS Code's contributions |
|
||||
| Icon data | Base64-encoded in JSON | No filesystem sharing needed, works with generated/fetched images |
|
||||
| Hot-reload | FileSystemWatcher on `*.js` | Immediate feedback during development |
|
||||
|
||||
---
|
||||
|
||||
## Feedback Requested
|
||||
|
||||
We are seeking feedback on the following areas:
|
||||
|
||||
1. **API surface** — Are the base classes and types intuitive? What's missing?
|
||||
2. **Extension lifecycle** — Is the initialize → query → dispose model sufficient?
|
||||
3. **Manifest schema** — What additional fields would be useful?
|
||||
4. **Distribution** — Should we support npm-based installation? Local-only? Both?
|
||||
5. **Security** — What permission boundaries should exist for JS extensions?
|
||||
6. **Developer experience** — What tooling (CLI scaffolding, debugging, testing) is most important?
|
||||
7. **Performance** — Are there concerns about per-extension Node.js processes?
|
||||
|
||||
Please file issues with the tag `[CmdPal-JS-SDK]`.
|
||||
Reference in New Issue
Block a user