CmdPal JS/TS Extensions - Phase 1: TypeScript SDK - #49321
Open
michaeljolley wants to merge 8 commits into
Open
Conversation
Introduce the extension-author-facing TypeScript SDK for building PowerToys Command Palette extensions that run as isolated Node.js processes and talk to the host over JSON-RPC 2.0 via stdio with LSP-style Content-Length framing. This is phase 1 of a stacked series (tracking issue #48707) and adds source only. No C# host code is touched in this phase. The dist/ output and node_modules/ are git-ignored. Includes: the full type surface, base classes, icon helpers, a JSON-RPC runtime (framing, dispatch, serialization, host bridge, stdio server), package/tsconfig/eslint/prettier config, a README, and unit tests (framing round-trip, dispatch, command-result mapping, icons). Co-authored-by: Copilot App <[email protected]> Copilot-Session: 3786df5b-8423-47c5-96d2-3601e7120778
… execution OpenUrlCommand now opens the URL as an in-process side effect through an injectable UrlOpener and returns a plain dismiss result. The wire CommandResult only serializes args for goToPage/showToast/confirm, and the Phase 1 protocol has no host/openUrl notification, so the URL could not be carried across the boundary and was being dropped. Cache priming is now lazy: setProvider no longer runs the provider command factories, so a normal startup (provider/getTopLevelCommands and provider/getFallbackCommands) invokes each factory once instead of twice. A guarded primeCaches() still covers the rare invoke-by-id-before-listing path. Co-authored-by: Copilot App <[email protected]> Copilot-Session: 3786df5b-8423-47c5-96d2-3601e7120778
Context items (the moreCommands / overflow entries on list items and command items) could not carry nested sub-commands. The ContextItem type had no moreCommands field and serialize.ts contextItems() never emitted sub-commands, so authored nested context menus were dropped on the wire. Add an optional recursive moreCommands field to ContextItem and serialize it recursively so the host receives the nested JSON. Pairs with a Phase 3 host-parse change. Co-authored-by: Copilot App <[email protected]> Copilot-Session: 4c56d9b0-6728-4dad-b8c7-2d6e9c05f080
Document the exported developer surface of @microsoft/cmdpal-sdk so extension authors get rich hover tooltips, parameter hints, and autocomplete docs. This adds or expands TSDoc on interfaces, type members, base classes, options bags, settings, built-in commands, and runtime entry points. Documentation only: no code, signatures, exports, or runtime behavior changed. Co-authored-by: Copilot App <[email protected]> Copilot-Session: 92cf3d8a-6ad5-4c12-b0fb-5db1cd2735ff
Harden the Command Palette TypeScript SDK per the locked wire contract, keeping every change additive so the integrated host and sample keep working until later phases upgrade them. - stdout ownership: claim the protocol stdout before author code runs and redirect console.log/info/debug and direct stdout writes to stderr. - init-failure propagation: track pending/ready/failed init state, answer a JSON-RPC error and set a nonzero exit code on failure, reject later requests. - graceful async disposal: dispose is void or Promise, awaited with a bounded host-supplied timeout, preferring process.exitCode over process.exit. - settings: add a SettingsChanged event and onSave hook fired after update, ship a JsonSettingsStore file helper, return a neutral goHome result. - command-id collisions: hash the full command payload and honor explicit ids, warn on duplicate ids within one response. - status identity: mint a client-side statusId, carry it plus the message text in host/showStatus and host/hideStatus, support update by id. - form identity: add a required formId per serialized form, route submissions by (pageId, formId) to the handler captured at serialize time, traverse nested tree content, fall back to the first form when a formId is absent. - bounded registry: scope command registrations per page generation and evict retired ids after a refresh. - CommandResult discriminated union with runtime validation, keeping the wire bytes byte-identical (numeric Kind map unchanged). - toast continuation retained and covered by a shared wire fixture. - handshake: return protocolVersion and sdkVersion, read host versions, reject an incompatible major, treat a missing version as legacy/compatible. - metadata transport: default provider.frozen to true, serialize frozen and accentColor faithfully. - npm publication: add publishConfig access public; files allowlist ships only dist and README.md. - tests and canonical wire fixtures consumed by later C# phases. Co-authored-by: Copilot App <[email protected]> Copilot-Session: fc2b159c-0183-4db6-9323-160526791eef
The ts-sdk production build was green but `npm run typecheck` (and thus `npm run check`) was red: tsconfig.json type-checks the test files while vitest/esbuild strip types without checking. Three stale test/fixture spots did not match the accepted SDK types. - registry.test.ts: build the list item command as a typed IInvokableCommand variable instead of an inline object literal on a field typed as the base ICommand, so excess-property checking no longer rejects invoke(). The base ICommand and ICommandItem.command stay as is. - status.test.ts: rewrite the three showStatus/updateStatus call sites and their wire assertions to the real ProgressState shape (isIndeterminate/progressPercent) that the host bridge forwards verbatim. - wire-fixtures.build.ts: set the plainText fixture fontFamily to the closed FontFamily union value 'monospace' and regenerate the committed golden content-plainText.json via the UPDATE_FIXTURES path. No production SDK types changed and no suppressions were added. Co-authored-by: Copilot App <[email protected]> Copilot-Session: 311db8aa-f8db-47bb-943f-5447037946c7
Address the eight grounded findings in the Command Palette TypeScript SDK: r2-p1-01: add a bootstrap loader that claims the protocol stdout writer and redirects console/stdout to stderr before dynamically importing the extension entry, so a stray top-level write cannot corrupt JSON-RPC framing. r2-p1-02: scope command registrations to their owner (provider generation, fallback handler, resolved results) and release them on refresh so the id registry no longer grows without bound. r2-p1-03: dispose immediately when the disposal timeout is zero or negative. r2-p1-04: validate the goToPage navigation mode against the known enum set. r2-p1-05: emit the Adaptive Card isRequired and errorMessage inputs for required settings. r2-p1-06: reject a present-but-malformed protocol version while still treating an absent one as the legacy default. r2-p1-07: serialize hasMoreItems and support the loadMore continuation so list pagination works end to end. r2-p1-08: return the full serialized settings page rather than only its id. Co-authored-by: Copilot App <[email protected]> Copilot-Session: 311db8aa-f8db-47bb-943f-5447037946c7
…nt command scopes Round 3 Phase 1 remediation for the Command Palette TypeScript SDK. r3-p1-02: Ship an executable bootstrap bin. Add a checked-in bin wrapper (bin/cmdpal-bootstrap.mjs) that carries the Node shebang and delegates to the compiled bootstrap, point the package bin at it, include bin in the published files, and add scripts/verify-pack.mjs (wired into prepublishOnly) to assert the packed bin is listed and starts with the shebang. A ts-sdk .gitignore negation keeps the bin tracked despite the repository root [Bb]in/ ignore. r3-p1-01: Document the bootstrap as the mandatory launch path in the README and prove the guarantee. The bootstrap claims stdout before dynamically importing the user entry, so a log emitted at static-import time of the entry cannot reach the framed protocol stream. Adds fixtures and a bootstrap test covering the static-import case. r3-p1-03: Recursively retire descendant command scopes. When an owner command or page is retired, its nested result commands and child-page scopes are now walked and unregistered together, so a stale nested or child command id can no longer be invoked and does not accumulate until process shutdown. Adds registry tests for both the child-page and nested-result cases. Co-authored-by: Copilot App <[email protected]> Copilot-Session: 311db8aa-f8db-47bb-943f-5447037946c7
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
This PR is the first of seven stacked PRs (#49321 → #49323 → #49324 → #49325 → #49326 → #49329 → #49364) that together add JavaScript/TypeScript extension support to Command Palette.
To test the complete implementation, run the branch from PR #49364.
Closes: #48707
What is this effort?
This chain of PRs enables developers to write Command Palette extensions in TypeScript or JavaScript instead of C# via WinRT COM. Extensions run as isolated Node.js processes and communicate with the C# host over JSON-RPC 2.0 using LSP-style
Content-Lengthframing over stdio. From the host's perspective, a JS extension looks like any otherICommandProvider; the translation layers built across these phases handle the bridging.The seven phases at a glance
@microsoft/cmdpal-sdknpm package: types, base classes, helpers, and the JSON-RPC runtime that extension authors use to build and test extensions without any C# involvement.JsonRpcConnection(framing, request correlation, timeout, dispatch) andJSExtensionManifest(parsespackage.jsonto find and validate CmdPal extensions).ICommandProvider,IListPage,IContentPage, etc.), plus all the command/page/icon mapping logic.initializehandshake, owns process lifecycle, discovers extensions from%LOCALAPPDATA%\\Microsoft\\CmdPal\\JSExtensions, and registers everything into the host's DI and command-manager pipeline.type: jsonrpc+npmblock support to gallery entries so users can install JS extensions vianpm install <package>. Handles process teardown before delete and guards against npm not being present.json-rpc-spec/) and a TypeScript sample extension (SampleJSExtension) that mirrors the C#SamplePagesExtensionview-for-view, exercising the full protocol surface. Also fixes the npm install layout so the discovery scanner finds the hoistedpackage.json.Detailssize (small/medium/large) support end-to-end, fixes the image content sample, lockstextToSuggestwire serialization, and cleans up the sample extension after dogfooding.How a JS extension loads, step by step
Startup scan.
JsonRpcExtensionServicewalksJSExtensions/, callingJSExtensionManifest.TryParseon eachpackage.json. A valid extension must have acmdpalsection, a non-emptyname, and a resolvable entry point file.Process spawn. For each valid manifest,
JSExtensionWrapperrunsnode <entrypoint>with stdio redirected. Ifcmdpal.debugis set in the manifest,--inspectis passed with an auto-assigned port starting at 9229.Handshake. The wrapper sends an
initializerequest over theJsonRpcConnectionwith Content-Length framing. The Node.js side, running the Phase 1 SDK runtime, responds and begins listening for provider requests.Provider surfacing.
JSCommandProviderProxywraps the connection and implementsICommandProvider.JSExtensionWrapperhands this to aCommandProviderWrapper, which plugs into the sameIExtensionServicepipeline that WinRT extensions already use.TopLevelCommandManagerpicks it up from there.Live requests. Every
getTopLevelCommands,invoke,getItems,setSearchText, and similar call becomes a JSON-RPC request on the stdio channel. Responses flow back through the Phase 3 adapter/proxy layer and materialize as native CmdPal types.Host notifications. The extension can call
ExtensionHost.log(),showStatus(),hideStatus(), andcopyText()at any time. These arrive as one-way JSON-RPC notifications and are dispatched byJSCommandProviderProxy.Hot reload. A
FileSystemWatcheron theJSExtensionstree fires on*.jschanges, debounced by 500 ms and ignoringnode_modules. The service tears down the old wrapper, kills the process, and constructs a fresh one.Shutdown. Each extension receives a
disposenotification and has a 2-second grace period before the process tree is forcibly terminated.What is not yet supported
These gaps are documented in the spec (
json-rpc-spec/overview.md) and deferred to a future phase:OnLoad/OnUnload): no WinRT ABI notification exists.GetDockBandsandIParameterRunare not exposed over the JSON-RPC protocol.