Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.kitakkun.jetwhale.demo.shared

import com.kitakkun.jetwhale.plugins.example.agent.ExampleAgentPlugin
import com.kitakkun.jetwhale.plugins.example.agent.ExampleWebAgentPlugin
import com.kitakkun.jetwhale.plugins.network.agent.JetWhaleNetworkAgentPlugin
import com.kitakkun.jetwhale.plugins.network.agent.ktor.ktorClientPlugin
import io.ktor.client.HttpClient
Expand All @@ -11,6 +12,9 @@ import io.ktor.client.request.header
object DIModule {
val exampleAgentPlugin: ExampleAgentPlugin by lazy { ExampleAgentPlugin() }

/** Agent counterpart of the experimental web-based host plugin. */
val exampleWebAgentPlugin: ExampleWebAgentPlugin by lazy { ExampleWebAgentPlugin() }

val networkAgentPlugin: JetWhaleNetworkAgentPlugin by lazy { JetWhaleNetworkAgentPlugin() }

/** A demo Ktor client wired to the Network Inspector so its traffic shows up in the debugger. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ fun initializeJetWhale() {

plugins {
register(DIModule.exampleAgentPlugin)
register(DIModule.exampleWebAgentPlugin)
register(DIModule.networkAgentPlugin)
}
}
Expand Down
100 changes: 100 additions & 0 deletions docs/guide/developing-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,106 @@ Read it (`LocalJetWhaleDarkTheme.current`) to pick theme-appropriate colors inst
`isSystemInDarkTheme()`, which reflects the OS setting and can disagree with the host's own Theme
option.

## Web-based UI <Badge type="warning" text="experimental" />

A host plugin's UI can be a web page (React, Vue, plain HTML — anything that builds to static assets
or runs on a dev server) instead of Compose. The page is shown by an embedded Chromium browser and
talks to its agent counterpart through an injected `window.jetwhale` bridge.

The recommended form is a **pure web plugin**: no plugin code to write or compile. You ship only a
manifest plus your built web assets — the host provides the plugin implementation and wires the
bridge to the agent for you.

### Pure web plugin (no Kotlin)

Declare a `web` block in the manifest instead of a `factoryClass`, and put your built output under
`src/main/resources/<resourceRoot>/`:

```json
{
"plugins": [
{
"pluginId": "com.example.myplugin",
"pluginName": "My Web Plugin",
"version": "1.0.0",
"web": {
"entry": "index.html",
"resourceRoot": "web/myplugin",
"devServerUrlProperty": "myplugin.devServer"
},
"agentVersionRange": { "min": "1.0.0", "max": "1.0.0" }
}
]
}
```

- **Bundled assets** are served over a loopback `http://127.0.0.1` origin (so `fetch()` and relative
URLs behave as on a real web server), then the browser opens `entry`. Copy a Vite `dist/` (etc.)
into `src/main/resources/web/myplugin/`.
- **Dev server / HMR:** set `devServerUrlProperty` and launch with e.g.
`-Dmyplugin.devServer=http://localhost:5173` to load a running dev server instead of the bundle.

That is the whole plugin — only the manifest and the assets. It still needs an agent counterpart
advertising the same `pluginId` to appear (like any messaging plugin).

Because there is no code to compile, you do not need Gradle or any JVM tooling: zip the two entries
and install the archive directly. Lay the files out as they sit inside the archive —

```
myplugin/
├── META-INF/jetwhale/plugin-manifest.json
└── web/myplugin/ # matches "resourceRoot"
├── index.html # matches "entry"
└── assets/…
```

— then `cd myplugin && zip -r ../myplugin.zip .` and install `myplugin.zip` from **Settings →
Plugins** (the picker accepts `.jar` and `.zip`). A Kotlin-authored plugin still ships as the usual
`.jar`; a `.zip` is just a plugin archive with no compiled classes.

### The `window.jetwhale` bridge

The page reaches the agent through a bridge injected once the document loads (a `jetwhale:ready`
event also fires on `window`):

```js
// fire-and-forget event to the agent
window.jetwhale.send("example/note", JSON.stringify({ text: "hi" }));

// request-reply; resolves with the agent's reply payload (a JSON string)
window.jetwhale.request("example/ping", "{}")
.then(reply => console.log(reply))
.catch(err => console.error(err.message));

// every agent event arrives here as (wireType, payloadJson)
window.jetwhale.onMessage((type, payloadJson) => { /* ... */ });
```

`type` is the message's wire name — its `@SerialName` (or fully-qualified name). For TypeScript, copy
`jetwhale.d.ts` (next to the example page) into your project for a typed `window.jetwhale`.

### Advanced: Kotlin-authored web plugin

Write plugin code only when you need custom Kotlin logic (MCP tools, storage migrations, transforming
messages). Implement `JetWhaleWebHostPluginUi` and call `JetWhaleWebView` from `Content`, passing the
plugin's `messenger`, a `JetWhaleWebBridge`, and a `JetWhaleWebSource`
(`BundledAsset(classLoader = javaClass.classLoader, …)` or `DevServer(url)`). Forward agent messages
to the page with `bridge.emit(type, payloadJson)` from a `configure { … }` handler.

### Things to know

- The APIs are `@ExperimentalJetWhaleApi` and may change between releases.
- The Chromium runtime is downloaded and initialized **the first time any web plugin is shown** (a
one-time download); plugins that never open one are unaffected.
- A web plugin **cannot be captured by `jetwhale.screenshot`**, and Compose overlays the host draws
may be occluded by the browser surface.
- Agent → web-UI **events** are delivered generically. Agent-initiated **requests** to the UI need a
Kotlin `onRawRequest` handler (advanced) — the pure form forwards events only.

A complete in-repo example — a pure web manifest, its bundled page + `jetwhale.d.ts`, and its agent
counterpart — lives in `jetwhale-plugins/example` (`plugin-manifest.json` `com.kitakkun.jetwhale.example.web`,
`resources/web/example/`, `ExampleWebAgentPlugin`).

## Persistent storage

Every host plugin instance gets a persistent key-value store via the protected `storage` property,
Expand Down
10 changes: 10 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ conveyorControl = "1.1"

bouncyCastle = "1.83"

# Embedded browser for experimental web-based host plugins (Compose-independent JCEF wrapper).
kcef = "2025.03.23"

[libraries]
# Gradle Plugins
kotlinGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
Expand All @@ -55,11 +58,16 @@ mavenPublishGradlePlugin = { module = "com.vanniktech:gradle-maven-publish-plugi
# kotlin
kotlinTest = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }

# webview (experimental web-based host plugins)
kcef = { module = "dev.datlag:kcef", version.ref = "kcef" }

# kotlinx
kotlinxSerializationCore = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerializationJson" }
kotlinxSerializationJson = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
kotlinxCollectionsImmutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" }
kotlinxCoroutinesCore = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" }
# Swing dispatcher required by KCEF (the embedded browser drives its callbacks on the AWT/Swing thread).
kotlinxCoroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinxCoroutines" }
byteBuddyAgent = { module = "net.bytebuddy:byte-buddy-agent", version.ref = "byteBuddy" }
javaKeyring = { module = "com.github.javakeyring:java-keyring", version.ref = "javaKeyring" }
kotlinxDatetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
Expand All @@ -73,6 +81,8 @@ androidxDatastoreCoreOkio = { module = "androidx.datastore:datastore-core-okio",

# compose
jetbrainsComposeRuntime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "jetbrainsCompose" }
jetbrainsComposeFoundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "jetbrainsCompose" }
jetbrainsComposeUi = { module = "org.jetbrains.compose.ui:ui", version.ref = "jetbrainsCompose" }
jetbrainsComposePreview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "jetbrainsCompose" }
jetbrainsComposeResources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "jetbrainsCompose" }
jetbrainsComposeSplitPane = { module = "org.jetbrains.compose.components:components-splitpane", version.ref = "jetbrainsCompose" }
Expand Down
101 changes: 97 additions & 4 deletions jetwhale-host-sdk/api/jetwhale-host-sdk.api
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,18 @@ public abstract interface class com/kitakkun/jetwhale/host/sdk/JetWhaleHostPlugi
public final class com/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest {
public static final field $stable I
public static final field Companion Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$Companion;
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$AgentVersionRange;Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$Icon;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$AgentVersionRange;Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$Icon;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$AgentVersionRange;Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$Icon;Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$WebUi;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$AgentVersionRange;Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$Icon;Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$WebUi;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/lang/String;
public final fun component3 ()Ljava/lang/String;
public final fun component4 ()Ljava/lang/String;
public final fun component5 ()Z
public final fun component6 ()Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$AgentVersionRange;
public final fun component7 ()Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$Icon;
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$AgentVersionRange;Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$Icon;)Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest;
public static synthetic fun copy$default (Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$AgentVersionRange;Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$Icon;ILjava/lang/Object;)Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest;
public final fun component8 ()Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$WebUi;
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$AgentVersionRange;Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$Icon;Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$WebUi;)Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest;
public static synthetic fun copy$default (Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$AgentVersionRange;Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$Icon;Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$WebUi;ILjava/lang/Object;)Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest;
public fun equals (Ljava/lang/Object;)Z
public final fun getAgentVersionRange ()Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$AgentVersionRange;
public final fun getFactoryClass ()Ljava/lang/String;
Expand All @@ -54,6 +55,7 @@ public final class com/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest {
public final fun getPluginName ()Ljava/lang/String;
public final fun getRequiresAgent ()Z
public final fun getVersion ()Ljava/lang/String;
public final fun getWeb ()Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$WebUi;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
Expand Down Expand Up @@ -140,6 +142,40 @@ public final class com/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$Ico
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public final class com/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$WebUi {
public static final field $stable I
public static final field Companion Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$WebUi$Companion;
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/lang/String;
public final fun component3 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$WebUi;
public static synthetic fun copy$default (Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$WebUi;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$WebUi;
public fun equals (Ljava/lang/Object;)Z
public final fun getDevServerUrlProperty ()Ljava/lang/String;
public final fun getEntry ()Ljava/lang/String;
public final fun getResourceRoot ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final synthetic class com/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$WebUi$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field $stable I
public static final field INSTANCE Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$WebUi$$serializer;
public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$WebUi;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$WebUi;)V
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}

public final class com/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifest$WebUi$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public final class com/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifestFile {
public static final field $stable I
public static final field Companion Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginManifestFile$Companion;
Expand Down Expand Up @@ -320,3 +356,60 @@ public final class com/kitakkun/jetwhale/host/sdk/ScreenshotCaptureKt {
public static final fun getLocalIsScreenshotCapture ()Landroidx/compose/runtime/ProvidableCompositionLocal;
}

public final class com/kitakkun/jetwhale/host/sdk/web/JetWhaleWebBridge {
public static final field $stable I
public fun <init> ()V
public final fun emit (Ljava/lang/String;Ljava/lang/String;)V
}

public abstract interface class com/kitakkun/jetwhale/host/sdk/web/JetWhaleWebHostPluginUi : com/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginUi {
}

public abstract interface class com/kitakkun/jetwhale/host/sdk/web/JetWhaleWebSource {
}

public final class com/kitakkun/jetwhale/host/sdk/web/JetWhaleWebSource$BundledAsset : com/kitakkun/jetwhale/host/sdk/web/JetWhaleWebSource {
public static final field $stable I
public fun <init> (Ljava/lang/ClassLoader;Ljava/lang/String;Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/ClassLoader;
public final fun component2 ()Ljava/lang/String;
public final fun component3 ()Ljava/lang/String;
public final fun copy (Ljava/lang/ClassLoader;Ljava/lang/String;Ljava/lang/String;)Lcom/kitakkun/jetwhale/host/sdk/web/JetWhaleWebSource$BundledAsset;
public static synthetic fun copy$default (Lcom/kitakkun/jetwhale/host/sdk/web/JetWhaleWebSource$BundledAsset;Ljava/lang/ClassLoader;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/kitakkun/jetwhale/host/sdk/web/JetWhaleWebSource$BundledAsset;
public fun equals (Ljava/lang/Object;)Z
public final fun getClassLoader ()Ljava/lang/ClassLoader;
public final fun getEntry ()Ljava/lang/String;
public final fun getResourceRoot ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class com/kitakkun/jetwhale/host/sdk/web/JetWhaleWebSource$DevServer : com/kitakkun/jetwhale/host/sdk/web/JetWhaleWebSource {
public static final field $stable I
public fun <init> (Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;)Lcom/kitakkun/jetwhale/host/sdk/web/JetWhaleWebSource$DevServer;
public static synthetic fun copy$default (Lcom/kitakkun/jetwhale/host/sdk/web/JetWhaleWebSource$DevServer;Ljava/lang/String;ILjava/lang/Object;)Lcom/kitakkun/jetwhale/host/sdk/web/JetWhaleWebSource$DevServer;
public fun equals (Ljava/lang/Object;)Z
public final fun getUrl ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class com/kitakkun/jetwhale/host/sdk/web/JetWhaleWebViewKt {
public static final fun JetWhaleWebView (Lcom/kitakkun/jetwhale/protocol/messaging/JetWhaleMessenger;Lcom/kitakkun/jetwhale/host/sdk/web/JetWhaleWebBridge;Lcom/kitakkun/jetwhale/host/sdk/web/JetWhaleWebSource;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
}

public final class com/kitakkun/jetwhale/host/sdk/web/WebManifestHostPluginKt {
public static final fun webManifestPluginFactory (Lcom/kitakkun/jetwhale/host/sdk/web/WebPluginConfig;)Lcom/kitakkun/jetwhale/host/sdk/JetWhaleHostPluginFactory;
}

public final class com/kitakkun/jetwhale/host/sdk/web/WebPluginConfig {
public static final field $stable I
public fun <init> (Ljava/lang/ClassLoader;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public final fun getClassLoader ()Ljava/lang/ClassLoader;
public final fun getDevServerUrl ()Ljava/lang/String;
public final fun getEntry ()Ljava/lang/String;
public final fun getResourceRoot ()Ljava/lang/String;
}

9 changes: 9 additions & 0 deletions jetwhale-host-sdk/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ dependencies {
// Exposed in public API: JetWhalePluginStorage returns Flow and rememberPersistent uses coroutines.
api(libs.kotlinxCoroutinesCore)
api(projects.jetwhaleProtocol.core)

// Experimental web-based host plugins: JetWhaleWebView embeds a Chromium browser (KCEF) into the
// plugin UI via SwingPanel, so the SDK needs Compose Foundation/UI (desktop) and the KCEF runtime.
implementation(libs.jetbrainsComposeFoundation)
implementation(libs.jetbrainsComposeUi)
implementation(libs.kcef)
// KCEF drives its browser callbacks on the AWT/Swing thread via the Swing coroutine dispatcher.
implementation(libs.kotlinxCoroutinesSwing)

testImplementation(libs.kotlinTest)
}

Expand Down
Loading
Loading