Custom UI Extension
Full UI control with any frontend framework, loaded as a static SPA.
Custom-View extensions are static SPAs loaded into an isolated BrowserWindow via the kunkun-ext:// protocol. Use React, Vue, Svelte — anything that builds to a static frontend.
Choose custom-view when you need full UI control, are converting an existing web app, or want framework-specific features. If you want Raycast-style list/form UIs without managing your own rendering, use a Worker Extension instead.
Project structure
Project setup
Scaffold with Vite — pick any framework that builds to a static frontend. React is shown first because it's the most common; the other tabs are the same steps.
bun create vite my-extension --template react-ts
cd my-extension
pnpm add @kunkunsh/sdkbun create vite my-extension --template vue-ts
cd my-extension
pnpm add @kunkunsh/sdkbun create vite my-extension --template svelte-ts
cd my-extension
pnpm add @kunkunsh/sdkbun create vite my-extension --template vanilla-ts
cd my-extension
pnpm add @kunkunsh/sdknpm create vite@latest / pnpm create vite work the same way.
Custom views load from the extension's own kunkun-ext:// origin, so asset URLs must be relative. Set base: './' in vite.config.ts:
import { defineConfig } from 'vite'
// import framework plugin: react() / vue() / svelte()
export default defineConfig({
base: './', // required: assets load relative to kunkun-ext://
// plugins: [react()],
})Prefer a meta-framework like SvelteKit or Nuxt? That's fine — just emit a static SPA (no SSR). For SvelteKit use @sveltejs/adapter-static with ssr = false and hash routing so client routes resolve under kunkun-ext://. The main / dist / devMain manifest fields below are identical.
Need multiple pages? Keep it purely client-side — add a router like React Router or TanStack Router (React), or Vue Router (Vue). Prefer hash or memory history so deep links resolve under the kunkun-ext:// origin without a server. You don't need a meta-framework just to get routing.
Manifest
The manifest is the kunkun field of your project's existing package.json — add it alongside the $schema hint; don't create a separate file:
{
"name": "my-extension",
"version": "0.1.0",
// ...your normal package.json fields
"$schema": "https://schema.kunkun.sh/",
"kunkun": {
"identifier": "com.you.my-extension",
"name": "My Extension",
"icon": { "type": "iconify", "value": "mdi:puzzle", "invert": true },
"shortDescription": "A brief description",
"permissions": ["clipboard-read", "clipboard-write", "storage"],
"commands": [
{
"name": "main",
"title": "My Command",
"mode": "custom-view",
"main": "/",
"dist": "dist",
"devMain": "http://localhost:5173"
}
]
}
}| Field | Meaning |
|---|---|
main | Route path within your SPA |
dist | Build output directory |
devMain | Dev server URL (HMR in development) |
Window options
{
"name": "window-demo",
"mode": "custom-view",
"main": "/window-demo",
"dist": "dist",
"window": { "titleBarStyle": "overlay", "transparent": true, "vibrancy": "sidebar", "width": 700, "height": 550 }
}titleBarStyle: "default" | "hidden" | "overlay" · transparent, vibrancy (macOS blur), width/height. See window-control to change these at runtime.
Using the SDK
Import @kunkunsh/sdk/ui/custom once in your entry file, before the app mounts — this side-effect import wires the extension ↔ host RPC channel. Everything else (Clipboard, db, fs, showToast, …) comes from @kunkunsh/sdk.
// src/main.tsx (React) / src/main.ts (Vue, Svelte, vanilla)
import "@kunkunsh/sdk/ui/custom"; // must come first
// ...your normal bootstrap: createRoot(el).render(<App />) / mount(App, ...) / createApp(App).mount(...)Then call the APIs from your components:
import { useState } from "react";
import { Clipboard, showToast, Toast } from "@kunkunsh/sdk";
export default function App() {
const [text, setText] = useState("");
async function read() {
try {
setText(await Clipboard.readText());
await showToast({ title: "Read", style: Toast.Style.Success });
} catch (err) {
await showToast({ title: "Error", message: String(err), style: Toast.Style.Failure });
}
}
return (
<>
<button onClick={read}>Read clipboard</button>
<p>{text}</p>
</>
);
}<script setup lang="ts">
import { ref } from "vue";
import { Clipboard, showToast, Toast } from "@kunkunsh/sdk";
const text = ref("");
async function read() {
try {
text.value = await Clipboard.readText();
await showToast({ title: "Read", style: Toast.Style.Success });
} catch (err) {
await showToast({ title: "Error", message: String(err), style: Toast.Style.Failure });
}
}
</script>
<template>
<button @click="read">Read clipboard</button>
<p>{{ text }}</p>
</template><script lang="ts">
import { Clipboard, showToast, Toast } from '@kunkunsh/sdk'
let text = $state('')
async function read() {
try {
text = await Clipboard.readText()
await showToast({ title: 'Read', style: Toast.Style.Success })
} catch (err) {
await showToast({ title: 'Error', message: String(err), style: Toast.Style.Failure })
}
}
</script>
<button onclick={read}>Read clipboard</button>
<p>{text}</p>For persistent structured data (lists, history, bookmarks), reach for Record Storage instead of stuffing JSON into LocalStorage.
Permissions
Declare what you need; scope filesystem and network tightly:
{
"permissions": [
"clipboard-read", "clipboard-write", "notifications", "storage",
{ "permission": "fs-read", "allow": ["$EXTENSION_SUPPORT/**", "$HOME/Documents/**"] },
{ "permission": "fs-write", "allow": ["$EXTENSION_SUPPORT/**"] },
{ "permission": "network", "domains": ["*.github.com", "api.example.com"] }
]
}See Permissions for path aliases and scoped forms.
Build & dev
pnpm build # outputs static files into `dist`/`build`
pnpm dev # Kunkun loads `devMain` with HMRThe installed tarball is served as-is — bundle your dependencies. Kunkun does not npm install inside an installed extension.
Real examples
The custom-view samples in the repo happen to be written in Svelte, but the setup above is identical across frameworks — the React/Vue code in this guide is authored inline.
sample-custom-view-dev— a minimal Svelte + Vite starter (the canonicalbase: './'+main:"/"+distshape)ai-config-manager— a real SvelteKit custom view managing AI config in a scoped directorykkterminal/space-lens— custom views backed by a spawned backend (see CLI + Plugin)