Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand All @@ -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
```

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions docs/extension-development.md
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion scripts/check-sdk-package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const packageJson = JSON.parse(await readFile(path.join(sdk, "package.json"), "u
exports: Record<string, unknown>;
};

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"]) {
Expand Down
4 changes: 3 additions & 1 deletion sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>;
showToast(message: string, options?: {kind?: "info" | "success" | "warning" | "error"; timeoutMs?: number}): ExtensionDisposable;
showDialog(options: {title: string; message: string; confirmLabel?: string; cancelLabel?: string}): Promise<boolean>;
experimental: {
Expand All @@ -684,7 +686,7 @@ export type RendererExtension = {
pageId: string,
container: HTMLElement,
context: RendererExtensionContext,
) => void | (() => void) | ExtensionDisposable;
) => ExtensionActivationResult | Promise<ExtensionActivationResult>;
/** Legacy API v1 page lifecycle. */
mount?: (
container: HTMLElement,
Expand Down
2 changes: 1 addition & 1 deletion sdk/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/extension-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>("host:chooseDirectory");
},
showToast: (message, options) => disposables.add(ui.showToast(pluginId, capabilities, message, options)),
showDialog: (options) => ui.showDialog(capabilities, options),
experimental: {
Expand Down
2 changes: 1 addition & 1 deletion src/shared/constants.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/serve-renderer-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
});
}
Expand Down
34 changes: 34 additions & 0 deletions tests/renderer-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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",
Expand All @@ -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); },
Expand All @@ -207,6 +240,7 @@ function createBridge(granted: ExtensionManifest["capabilities"] = [], legacyDef
value,
get subscriptions() { return subscriptions; },
get unsubscriptions() { return unsubscriptions; },
get directorySelections() { return directorySelections; },
};
}

Expand Down