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
Using SvelteKit (Recommended)
# Create a new SvelteKit project
npx sv create my-extension
# Install the Kunkun API
cd my-extension
pnpm add @kunkunsh/apiConfigure 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
| Field | Description |
|---|---|
name | Internal command identifier |
title | Display name in the command palette |
description | Brief description shown in UI |
mode | Must be "custom-view" |
main | Route path within your SPA |
dist | Build output directory (default: "build") |
devMain | Dev 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 backgroundvibrancy: 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 manifestDevelopment Mode
For hot reload during development, Kunkun loads from devMain URL instead:
- Start your dev server:
pnpm dev - Open Kunkun and navigate to your extension
- 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