Kunkun

Worker Extension

Build Raycast-style extensions with React components

Worker extensions run React components in sandboxed environments (Web Workers or Node.js processes) with the UI rendered by the host application. This provides a Raycast-like development experience with consistent UI.

Extension Modes

ModeRuntimeUINode.js Access
worker-viewBrowser Web WorkerHost rendersNo
node-viewNode.js processHost rendersYes
worker-headlessBrowser Web WorkerNoneNo
node-headlessNode.js processNoneYes

Project Setup

Create Project

mkdir my-worker-ext
cd my-worker-ext
pnpm init
pnpm add @kunkunsh/api react valibot
pnpm add -D @types/react @types/bun

Create Build Script

Create build.ts using Bun's bundler:

import { dedupeReact, kunkunCommandPlugin } from "@kunkunsh/api/build";
import type { BunPlugin } from "bun";

// Browser target: runs in Web Worker
await Bun.build({
  entrypoints: ["./src/App.tsx"],
  outdir: "./dist",
  target: "browser",
  format: "esm",
  minify: true,
  sourcemap: "external",
  naming: "[name].js",
  plugins: [kunkunCommandPlugin({ mode: "view" }) as BunPlugin, dedupeReact(import.meta.dir)],
});

// Node target: runs in Node.js process
await Bun.build({
  entrypoints: ["./src/App.tsx"],
  outdir: "./dist/node",
  target: "node",
  format: "esm",
  minify: true,
  sourcemap: "external",
  naming: "[name].js",
  plugins: [kunkunCommandPlugin({ mode: "view" }) as BunPlugin, dedupeReact(import.meta.dir)],
});

The kunkunCommandPlugin automatically:

  • Injects the bootstrap code
  • Wraps your component with the necessary RPC setup
  • Handles communication with the host

Writing the Component

Create src/App.tsx:

import { useState } from "react";
import { Button, Div, H2, H3, P, Code, Pre, Hr } from "@kunkunsh/api/ui";
import {
  popToRoot,
  showToast,
  Toast,
  Clipboard,
  getEnvironment,
  LocalStorage,
} from "@kunkunsh/api";

export default function App() {
  const [clipboardText, setClipboardText] = useState("");
  const [envInfo, setEnvInfo] = useState("");
  const [storageValue, setStorageValue] = useState("");
  const [status, setStatus] = useState("");

  return (
    <Div className="p-6" style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
      <H2>My Worker Extension</H2>
      <P>Demonstrates worker-view plugin APIs.</P>

      {/* Navigation */}
      <Hr />
      <H3>Navigation</H3>
      <Button
        title="Go Back"
        variant="outline"
        onClick={() => popToRoot()}
      />

      {/* Toast */}
      <Hr />
      <H3>Toast</H3>
      <Button
        title="Show Toast"
        variant="primary"
        onClick={() => {
          showToast({
            title: "Hello!",
            message: "Toast notification works.",
            style: Toast.Style.Success,
          });
        }}
      />

      {/* Clipboard */}
      <Hr />
      <H3>Clipboard</H3>
      <Div style={{ display: "flex", gap: "8px" }}>
        <Button
          title="Read Clipboard"
          variant="outline"
          onClick={async () => {
            const text = await Clipboard.readText();
            setClipboardText(text);
          }}
        />
        <Button
          title="Write to Clipboard"
          variant="outline"
          onClick={async () => {
            await Clipboard.copy("Hello Kunkun!");
          }}
        />
      </Div>
      {clipboardText && <Pre><Code>{clipboardText}</Code></Pre>}

      {/* Storage */}
      <Hr />
      <H3>Storage</H3>
      <Div style={{ display: "flex", gap: "8px" }}>
        <Button
          title="Store Value"
          variant="outline"
          onClick={async () => {
            await LocalStorage.setItem("key", `Saved at ${new Date().toLocaleTimeString()}`);
          }}
        />
        <Button
          title="Read Value"
          variant="outline"
          onClick={async () => {
            const val = await LocalStorage.getItem("key");
            setStorageValue(val ?? "(empty)");
          }}
        />
      </Div>
      {storageValue && <P>Stored: <Code>{storageValue}</Code></P>}
    </Div>
  );
}

UI Components

Import primitive components from @kunkunsh/api/ui:

import {
  Button,
  Div,
  H2, H3, H4,
  P,
  Code,
  Pre,
  Hr,
  Image,
  List,
  ListItem,
  Grid,
  Form,
  TextField,
  TextArea,
  Select,
  Checkbox,
} from "@kunkunsh/api/ui";

These components are rendered by the host Svelte app, ensuring consistent styling.

List Component

<List>
  <List.Item
    title="Item Title"
    subtitle="Subtitle"
    icon="mdi:file"
    onClick={() => console.log("clicked")}
  />
</List>

Form Components

<Form>
  <TextField
    title="Name"
    placeholder="Enter your name"
    value={name}
    onChange={setName}
  />
  <TextArea
    title="Description"
    placeholder="Enter description"
    value={desc}
    onChange={setDesc}
  />
  <Select
    title="Choice"
    value={choice}
    onChange={setChoice}
    options={[
      { label: "Option A", value: "a" },
      { label: "Option B", value: "b" },
    ]}
  />
</Form>

Manifest Configuration

{
  "name": "my-worker-ext",
  "kunkun": {
    "identifier": "com.example.my-worker-ext",
    "name": "My Worker Extension",
    "source": "kunkun",
    "icon": {
      "type": "iconify",
      "invert": true,
      "value": "mdi:puzzle"
    },
    "permissions": [
      "clipboard-read",
      "clipboard-write",
      "storage",
      "notifications"
    ],
    "commands": [
      {
        "name": "main",
        "title": "My Worker Command",
        "description": "A worker-view command",
        "mode": "worker-view",
        "main": "dist/App.js"
      },
      {
        "name": "node-version",
        "title": "Node Version",
        "description": "A node-view command with filesystem access",
        "mode": "node-view",
        "main": "dist/node/App.js"
      }
    ]
  }
}

Command Modes

ModeMain PathRuntime
worker-viewdist/App.jsBrowser Web Worker
node-viewdist/node/App.jsNode.js process
worker-headlessdist/Headless.jsBrowser Web Worker (no UI)
node-headlessdist/node/Headless.jsNode.js process (no UI)

Building

bun run build.ts

This produces:

  • dist/App.js - Browser target (worker-view)
  • dist/node/App.js - Node target (node-view)

Available APIs

Worker extensions have access to these APIs:

APIDescription
ClipboardRead/write clipboard content
LocalStoragePersistent key-value storage
showToastShow system notifications
showHUDShow head-up display overlay
popToRootClose plugin and return to launcher
getEnvironmentGet extension environment info
fetchNetwork requests with CORS bypass
pathPath operations and aliases

Node-view extensions additionally have:

APIDescription
fsFile system operations
shellExecute shell commands
dialogNative file/dialogs
permissionsRequest/check permissions

Full Example

See apps/sample-worker-ext in the repository for a complete example demonstrating:

  • Worker-view and node-view commands
  • Headless commands
  • Service plugins
  • Permission verification
  • Deno runtime detection

On this page