diff --git a/README.md b/README.md index c8c30ae..da094f4 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Release executables are currently unsigned. Windows SmartScreen may warn before 3. Verify the archive: ```powershell - $zip = ".\zcode-extensions-v0.3.3-windows-x64.zip" + $zip = ".\zcode-extensions-v0.3.4-windows-x64.zip" $expected = (Get-Content "$zip.sha256").Split()[0].ToLowerInvariant() $actual = (Get-FileHash $zip -Algorithm SHA256).Hash.ToLowerInvariant() if ($actual -ne $expected) { throw "Checksum mismatch" } @@ -52,7 +52,7 @@ Release executables are currently unsigned. Windows SmartScreen may warn before 4. Extract the archive to a permanent location. The ZIP contains a stable `zcode-extensions` directory: ```powershell - Expand-Archive .\zcode-extensions-v0.3.3-windows-x64.zip -DestinationPath D:\ + Expand-Archive .\zcode-extensions-v0.3.4-windows-x64.zip -DestinationPath D:\ Set-Location D:\zcode-extensions ``` @@ -110,7 +110,7 @@ Queued updates replace only the extension bundle on startup. The previous bundle Close ZCode, download the new release, and extract it over the same parent directory: ```powershell -Expand-Archive .\zcode-extensions-v0.3.3-windows-x64.zip -DestinationPath D:\ -Force +Expand-Archive .\zcode-extensions-v0.3.4-windows-x64.zip -DestinationPath D:\ -Force Set-Location D:\zcode-extensions .\bin\zdp.exe repair .\bin\zdp.exe launch @@ -181,7 +181,7 @@ bun run build:example bun run build bun run build:sdk bun run pack:sdk -bun run release:package -- --tag v0.3.3 +bun run release:package -- --tag v0.3.4 ``` See [Developing extensions](docs/extension-development.md) for the public API and [Hello Extension](examples/hello-extension) for a complete minimal project. diff --git a/docs/extension-development.md b/docs/extension-development.md index 6a1f84a..81fee08 100644 --- a/docs/extension-development.md +++ b/docs/extension-development.md @@ -1,6 +1,6 @@ # Developing ZCode Desktop Extensions -This guide describes extension API version 1 as implemented by host/SDK 0.3.3. The public package is [`@notmike101/zcode-extension-sdk`](https://www.npmjs.com/package/@notmike101/zcode-extension-sdk), the source contract is [`sdk/index.ts`](../sdk/index.ts), and [Hello Extension](../examples/hello-extension) remains a complete legacy-lifecycle example. +This guide describes extension API version 1 as implemented by host/SDK 0.3.4. The public package is [`@notmike101/zcode-extension-sdk`](https://www.npmjs.com/package/@notmike101/zcode-extension-sdk), the source contract is [`sdk/index.ts`](../sdk/index.ts), and [Hello Extension](../examples/hello-extension) remains a complete legacy-lifecycle example. Extensions are trusted local code. A main entrypoint runs in ZCode's Electron main process with Node.js access, while an optional renderer entrypoint runs inside the ZCode renderer. Declared capabilities control access through the SDK and are shown during installation; they do not sandbox trusted Node or renderer code. @@ -9,7 +9,7 @@ Extensions are trusted local code. A main entrypoint runs in ZCode's Electron ma For a separate Bun or TypeScript project: ```powershell -bun add -d @notmike101/zcode-extension-sdk@0.3.3 +bun add -d @notmike101/zcode-extension-sdk@0.3.4 ``` Import main-only types and helpers from `@notmike101/zcode-extension-sdk/main`, browser-safe renderer types and helpers from `/renderer`, and unstable raw-channel types from `/experimental`. The root export is also browser-safe. A JSON Schema is available at `/manifest.schema.json`, and `validateExtensionManifest` or `assertExtensionManifest` can validate manifests at runtime. @@ -179,7 +179,7 @@ window.ZDP_REGISTER_PLUGIN_RENDERER?.(defineRendererExtension({ - Namespaced local UI preferences through `storage`; do not use it for prompts or responses. - The capability-gated ZCode read/event proxies available to renderer code. - `ui.activeContext` with current workspace, task, session, turn, message, role, status, and tool-call identifiers when available. -- Toasts, confirmation dialogs, and stable UI contribution slots. +- A native directory picker through `ui.chooseDirectory()`, plus toasts, confirmation dialogs, and stable UI contribution slots. These native/overlay helpers require `ui.overlays`. - `ui.experimental.anchor(...)` for selector-based surfaces when `experimental.ui.dom` is declared. Stable contribution slots are `sidebar.navigation`, `workspace.header.actions`, `task.row.trailing`, `chat.header.actions`, `chat.overlay`, `chat.composer.leading`, `chat.composer.trailing`, `chat.turn.after`, `chat.message.before`, `chat.message.after`, `chat.message.footer`, and `chat.message.overlay`. The host enforces the matching `ui.*` capability. diff --git a/package.json b/package.json index c9286fc..8571e4c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zcode-desktop-extensions", - "version": "0.3.3", + "version": "0.3.4", "description": "An update-resistant extension host for the ZCode Electron desktop application.", "private": true, "license": "MIT", diff --git a/scripts/check-sdk-package.ts b/scripts/check-sdk-package.ts index e51048d..fc20dbb 100644 --- a/scripts/check-sdk-package.ts +++ b/scripts/check-sdk-package.ts @@ -11,7 +11,7 @@ const packageJson = JSON.parse(await readFile(path.join(sdk, "package.json"), "u exports: Record; }; -if (packageJson.name !== "@notmike101/zcode-extension-sdk" || packageJson.version !== "0.3.3") { +if (packageJson.name !== "@notmike101/zcode-extension-sdk" || packageJson.version !== "0.3.4") { throw new Error("Unexpected SDK package identity"); } for (const entry of [".", "./main", "./renderer", "./experimental", "./manifest.schema.json"]) { diff --git a/sdk/index.ts b/sdk/index.ts index a6bea99..a79ef15 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -661,6 +661,8 @@ export type RendererExtensionContext = { mount: UiContributionMount, options?: {order?: number; when?: (context: ActiveUiContext) => boolean}, ): ExtensionDisposable; + /** Opens the host's native directory picker. Requires `ui.overlays`. */ + chooseDirectory(): Promise; showToast(message: string, options?: {kind?: "info" | "success" | "warning" | "error"; timeoutMs?: number}): ExtensionDisposable; showDialog(options: {title: string; message: string; confirmLabel?: string; cancelLabel?: string}): Promise; experimental: { @@ -684,7 +686,7 @@ export type RendererExtension = { pageId: string, container: HTMLElement, context: RendererExtensionContext, - ) => void | (() => void) | ExtensionDisposable; + ) => ExtensionActivationResult | Promise; /** Legacy API v1 page lifecycle. */ mount?: ( container: HTMLElement, diff --git a/sdk/package.json b/sdk/package.json index 421e5f9..23414ec 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@notmike101/zcode-extension-sdk", - "version": "0.3.3", + "version": "0.3.4", "description": "Public TypeScript SDK and authoring helpers for ZCode Desktop Extensions.", "license": "MIT", "type": "module", diff --git a/src/renderer/extension-runtime.ts b/src/renderer/extension-runtime.ts index 234f9b7..540bc94 100644 --- a/src/renderer/extension-runtime.ts +++ b/src/renderer/extension-runtime.ts @@ -229,6 +229,10 @@ function createRendererContext( onDidChange: (listener) => disposables.add(ui.onActiveContextChanged(listener)), }, contribute: (slot, mount, options) => disposables.add(ui.contribute(pluginId, capabilities, slot, mount, options)), + chooseDirectory: async () => { + requireUiCapability(capabilities, "ui.overlays"); + return bridge.invoke("host:chooseDirectory"); + }, showToast: (message, options) => disposables.add(ui.showToast(pluginId, capabilities, message, options)), showDialog: (options) => ui.showDialog(capabilities, options), experimental: { diff --git a/src/shared/constants.ts b/src/shared/constants.ts index e6f3415..dd5fb66 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -1,7 +1,7 @@ import path from "node:path"; export const HOST_NAME = "ZCode Desktop Extensions"; -export const HOST_VERSION = "0.3.3"; +export const HOST_VERSION = "0.3.4"; export const API_VERSION = 1; export const INSTALL_STATE_VERSION = 1; export const DEFAULT_ZCODE_ROOT = path.join( diff --git a/tests/fixtures/serve-renderer-harness.ts b/tests/fixtures/serve-renderer-harness.ts index 043d800..a449372 100644 --- a/tests/fixtures/serve-renderer-harness.ts +++ b/tests/fixtures/serve-renderer-harness.ts @@ -15,7 +15,7 @@ const server = Bun.serve({ }); } if (url.pathname === "/renderer/index.js") { - return new Response(await readFile(path.join(root, "runtime", "versions", "0.3.3", "renderer", "index.js")), { + return new Response(await readFile(path.join(root, "runtime", "versions", "0.3.4", "renderer", "index.js")), { headers: {"content-type": "text/javascript; charset=utf-8"}, }); } diff --git a/tests/renderer-runtime.test.ts b/tests/renderer-runtime.test.ts index 9cc2ae8..23236e2 100644 --- a/tests/renderer-runtime.test.ts +++ b/tests/renderer-runtime.test.ts @@ -115,6 +115,34 @@ describe("renderer extension runtime", () => { .rejects.toThrow("ui.pages"); }); + test("exposes the native directory picker through the capability-gated vNext UI context", async () => { + const allowedBridge = createBridge(["ui.pages", "ui.overlays"]); + const allowed = new RendererExtensionRuntime(allowedBridge.value); + let selected: string | null | undefined; + allowed.register({ + id: "runtime-test", + async mountPage(_pageId, _container, context) { + selected = await context.ui.chooseDirectory(); + }, + }); + allowed.sync([status(1, true, ["ui.pages", "ui.overlays"])]); + const container = window.document.createElement("div"); + await allowed.mountPage("runtime-test", "dashboard", container as never); + expect(selected).toBe("D:\\selected-workspace"); + expect(allowedBridge.directorySelections).toBe(1); + + const denied = new RendererExtensionRuntime(createBridge(["ui.pages"]).value); + denied.register({ + id: "runtime-test", + async mountPage(_pageId, _container, context) { + await context.ui.chooseDirectory(); + }, + }); + denied.sync([status(1, true, ["ui.pages"])]); + await expect(denied.mountPage("runtime-test", "dashboard", container as never)) + .rejects.toThrow("ui.overlays"); + }); + test("supplies the assistant descendant as context for a real ZCode turn shell without turn ids", async () => { const runtime = new RendererExtensionRuntime(createBridge(["ui.chat"]).value); let mountedContext: ActiveUiContext | undefined; @@ -178,6 +206,7 @@ function createBridge(granted: ExtensionManifest["capabilities"] = [], legacyDef const listeners = new Set<(event: string, payload: unknown) => void>(); let subscriptions = 0; let unsubscriptions = 0; + let directorySelections = 0; const capabilities: ExtensionHostCapabilities = { apiVersion: 1, hostVersion: "0.3.0", @@ -199,6 +228,10 @@ function createBridge(granted: ExtensionManifest["capabilities"] = [], legacyDef unsubscriptions += 1; return undefined as T; } + if (method === "host:chooseDirectory") { + directorySelections += 1; + return "D:\\selected-workspace" as T; + } return undefined as T; }, on(listener) { listeners.add(listener); return () => listeners.delete(listener); }, @@ -207,6 +240,7 @@ function createBridge(granted: ExtensionManifest["capabilities"] = [], legacyDef value, get subscriptions() { return subscriptions; }, get unsubscriptions() { return unsubscriptions; }, + get directorySelections() { return directorySelections; }, }; }