Worker Extension
Raycast-style React extensions rendered by the host, in a Web Worker or Node.js process.
Worker extensions run React components in a sandbox (Web Worker or Node.js/Deno process). You write React with primitives from @kunkunsh/sdk/ui; the host renders them as native components — a Raycast-like experience with consistent styling. The mechanics are in How Extensions Run.
Modes
| Mode | Runtime | UI | Node.js |
|---|---|---|---|
worker-view | Web Worker | host renders | No |
node-view | Node.js / Deno process | host renders | Yes |
worker-headless | Web Worker | none | No |
node-headless | Node.js / Deno process | none | Yes |
Project structure
Setup
mkdir my-worker-ext && cd my-worker-ext
pnpm init && pnpm add @kunkunsh/sdk react valibot
pnpm add -D @types/react @types/bunBuild script
// build.ts
import { dedupeReact, kunkunCommandPlugin } from "@kunkunsh/sdk/build";
import type { BunPlugin } from "bun";
// Browser target → worker-view
await Bun.build({
entrypoints: ["./src/App.tsx"],
outdir: "./dist",
target: "browser", format: "esm", minify: true,
plugins: [kunkunCommandPlugin({ mode: "view" }) as BunPlugin, dedupeReact(import.meta.dir)],
});
// Node target → node-view
await Bun.build({
entrypoints: ["./src/App.tsx"],
outdir: "./dist/node",
target: "node", format: "esm", minify: true,
plugins: [kunkunCommandPlugin({ mode: "view" }) as BunPlugin, dedupeReact(import.meta.dir)],
});kunkunCommandPlugin injects the bootstrap and RPC wiring. dedupeReact is required — two React copies would break the reconciler.
dist must be self-contained: Bun bundles JS deps by default, so avoid external entries unless the host provides the dependency. Native .node addons can't be inlined — package their loader + platform files, or lazy-load them.
The component
import { useState } from "react";
import { Button, Div, H2, H3, P, Hr } from "@kunkunsh/sdk/ui";
import { popToRoot, showToast, Toast, Clipboard, LocalStorage } from "@kunkunsh/sdk";
export default function App() {
const [text, setText] = useState("");
return (
<Div className="p-6" style={{ display: "flex", flexDirection: "column", gap: 16 }}>
<H2>My Worker Extension</H2>
<Hr />
<Button title="Read Clipboard" variant="outline" onClick={async () => setText(await Clipboard.readText())} />
<Button title="Toast" variant="primary" onClick={() => showToast({ title: "Hi", style: Toast.Style.Success })} />
<Button title="Back" variant="outline" onClick={() => popToRoot()} />
{text && <P>{text}</P>}
</Div>
);
}Import UI primitives from @kunkunsh/sdk/ui:
import { Button, Div, H2, H3, P, Code, Pre, Hr, Image, List, Grid, Form, TextField, TextArea, Select, Checkbox } from "@kunkunsh/sdk/ui";<List>
<List.Item title="Item" subtitle="Subtitle" icon="mdi:file" onClick={() => {}} />
</List>
<Form>
<TextField title="Name" value={name} onChange={setName} />
<Select title="Choice" value={choice} onChange={setChoice}
options={[{ label: "A", value: "a" }, { label: "B", value: "b" }]} />
</Form>Manifest
{
"kunkun": {
"identifier": "com.you.my-worker-ext",
"name": "My Worker Extension",
"permissions": ["clipboard-read", "clipboard-write", "storage"],
"commands": [
{ "name": "main", "title": "My Command", "mode": "worker-view", "main": "dist/App.js" },
{ "name": "with-fs", "title": "Node Version", "mode": "node-view", "main": "dist/node/App.js" }
]
}
}Available APIs
Both worker-view and node-view get the full host API surface over RPC — Clipboard, LocalStorage, db, showToast, showHUD, popToRoot, getEnvironment, network.fetch, path, and also fs, shell, dialog, permissions, and spawnBackend. Every call is permission-gated: declare what you use in the manifest (e.g. a scoped shell permission listing the programs you run).
What node-view additionally offers is the runtime itself: your bundle runs in a Node.js/Deno process, so you can use Node built-ins (child_process, fs, native .node addons) directly instead of going through the host API. Prefer worker-view unless you need that.
See the full surface in the SDK Reference.
Live-updating lists
Worker and node views can bind directly to record storage with useQuery, so the list re-renders whenever the data changes — no manual refetch:
import { useQuery } from "@kunkunsh/sdk/utils";
import { List } from "@kunkunsh/sdk/ui";
function Bookmarks() {
const { data } = useQuery<{ title: string; url: string }>("bookmarks", { orderBy: [["createdAt", "desc"]] });
return <List>{data?.map((r) => <List.Item key={r.id} title={r.data.title} subtitle={r.data.url} />)}</List>;
}Real examples
youtube-tools(service + worker) andyoutube-podcast-generator(worker-view consumer)port-manager— worker view, no-view command, and a menu-bar command in one extensiongit-repos— a React view scanning local repos