Kunkun

Custom UI Extension

Build extensions with full UI control using any frontend framework

Custom-View extensions are static SPAs (Single Page Applications) loaded into isolated BrowserWindows via the kunkun-ext:// custom protocol. You have full control over the UI using any frontend framework you prefer.

When to Use Custom-View

Choose custom-view when you need:

  • Full control over the UI design
  • Complex interactive components
  • Existing web apps to convert to extensions
  • Framework-specific features (Svelte, Vue, etc.)

Project Setup

# Create a new SvelteKit project
npx sv create my-extension

# Install the Kunkun API
cd my-extension
pnpm add @kunkunsh/api

Configure for Static Build

Create or update src/routes/+layout.ts:

export const prerender = true;
export const ssr = false;

This ensures SvelteKit generates a static SPA suitable for Kunkun.

Manifest Configuration

Add the kunkun key to your package.json:

{
  "name": "my-extension",
  "version": "1.0.0",
  "license": "MIT",
  "$schema": "../../packages/api/manifest-schema.json",
  "kunkun": {
    "identifier": "com.yourname.my-extension",
    "name": "My Extension",
    "icon": {
      "type": "iconify",
      "value": "mdi:puzzle",
      "invert": true
    },
    "shortDescription": "A brief description of your extension",
    "permissions": [
      "clipboard-read",
      "clipboard-write",
      "storage"
    ],
    "commands": [
      {
        "name": "main",
        "title": "My Command",
        "description": "What this command does",
        "icon": { "type": "iconify", "value": "mdi:puzzle", "invert": true },
        "mode": "custom-view",
        "main": "/",
        "dist": "build",
        "devMain": "http://localhost:5173"
      }
    ]
  }
}

Command Fields Explained

FieldDescription
nameInternal command identifier
titleDisplay name in the command palette
descriptionBrief description shown in UI
modeMust be "custom-view"
mainRoute path within your SPA
distBuild output directory (default: "build")
devMainDev server URL for hot reload

Window Configuration

Customize the window appearance:

{
  "name": "window-demo",
  "title": "Window Demo",
  "mode": "custom-view",
  "main": "/window-demo",
  "dist": "build",
  "devMain": "http://localhost:5173/window-demo",
  "window": {
    "titleBarStyle": "overlay",
    "transparent": true,
    "vibrancy": "sidebar",
    "width": 700,
    "height": 550
  }
}

Window options:

  • titleBarStyle: "default" | "hidden" | "overlay" (macOS traffic lights visible)
  • transparent: Enable transparent window background
  • vibrancy: macOS blur effect ("sidebar", "content", etc.)
  • width / height: Initial window dimensions

Using the API

Import APIs from @kunkunsh/api:

<script lang="ts">
  import { Clipboard, showToast, Toast, LocalStorage } from '@kunkunsh/api'
  
  let clipboardText = ''
  
  async function readClipboard() {
    try {
      clipboardText = await Clipboard.readText()
      await showToast({
        title: 'Clipboard Read',
        style: Toast.Style.Success
      })
    } catch (err) {
      await showToast({
        title: 'Error',
        message: String(err),
        style: Toast.Style.Failure
      })
    }
  }
</script>

<button onclick={readClipboard}>Read Clipboard</button>
<p>Content: {clipboardText}</p>

Permissions

Declare permissions your extension needs:

{
  "permissions": [
    "clipboard-read",
    "clipboard-write",
    "notifications",
    "storage",
    "path",
    "shell",
    "dialog",
    "open-url",
    "open-file",
    "open-folder",
    "window-control",
    { "permission": "fs-read", "allow": ["$EXTENSION_SUPPORT/**"] },
    { "permission": "fs-write", "allow": ["$EXTENSION_SUPPORT/**"] },
    { "permission": "network", "domains": ["*.github.com", "api.example.com"] }
  ]
}

Scoped Permissions

For filesystem and network access, use scoped permissions:

{
  "permission": "fs-read",
  "allow": ["$HOME/Documents/**", "$DESKTOP/*.png"]
}

Path aliases:

  • $HOME - User home directory
  • $DESKTOP - Desktop folder
  • $DOCUMENT - Documents folder
  • $DOWNLOAD - Downloads folder
  • $EXTENSION - Extension root directory
  • $EXTENSION_SUPPORT - Extension data directory

Building

Build your extension as a static SPA:

# For SvelteKit
pnpm build

# The build output goes to the 'dist' folder specified in manifest

Development Mode

For hot reload during development, Kunkun loads from devMain URL instead:

  1. Start your dev server: pnpm dev
  2. Open Kunkun and navigate to your extension
  3. Changes are reflected immediately via HMR

Example: Complete SvelteKit Extension

<!-- src/routes/+page.svelte -->
<script lang="ts">
  import { onMounted, ref } from 'svelte'
  import { Clipboard, showToast, Toast, LocalStorage, getEnvironment } from '@kunkunsh/api'
  
  let clipboardText = $state('')
  let storedValue = $state('')
  let envInfo = $state<any>(null)
  
  async function readClipboard() {
    clipboardText = await Clipboard.readText()
  }
  
  async function writeClipboard() {
    await Clipboard.copy('Hello from Kunkun!')
    await showToast({ title: 'Copied!', style: Toast.Style.Success })
  }
  
  async function saveValue() {
    await LocalStorage.setItem('demo-key', `Saved at ${new Date().toLocaleTimeString()}`)
    await showToast({ title: 'Saved!', style: Toast.Style.Success })
  }
  
  async function loadValue() {
    storedValue = await LocalStorage.getItem('demo-key') || '(empty)'
  }
  
  async function loadEnv() {
    envInfo = await getEnvironment()
  }
</script>

<div class="p-6">
  <h1 class="text-2xl font-bold mb-4">My Extension</h1>
  
  <div class="space-y-4">
    <div>
      <h2 class="font-semibold mb-2">Clipboard</h2>
      <div class="flex gap-2">
        <button onclick={readClipboard}>Read</button>
        <button onclick={writeClipboard}>Write</button>
      </div>
      {#if clipboardText}
        <p class="mt-2">Content: {clipboardText}</p>
      {/if}
    </div>
    
    <div>
      <h2 class="font-semibold mb-2">Storage</h2>
      <div class="flex gap-2">
        <button onclick={saveValue}>Save</button>
        <button onclick={loadValue}>Load</button>
      </div>
      {#if storedValue}
        <p class="mt-2">Value: {storedValue}</p>
      {/if}
    </div>
    
    <div>
      <h2 class="font-semibold mb-2">Environment</h2>
      <button onclick={loadEnv}>Get Info</button>
      {#if envInfo}
        <pre class="mt-2">{JSON.stringify(envInfo, null, 2)}</pre>
      {/if}
    </div>
  </div>
</div>

Full Example

See the complete sample extension at apps/sample-ext in the repository. It demonstrates:

  • Clipboard monitoring
  • File system operations
  • Shell command execution
  • Dialog and file picker APIs
  • Window control and vibrancy effects
  • Dynamic permissions
  • Network fetch with CORS bypass
  • Backend process spawning

On this page