
Cross-platform WebView UI and Playwright-style browser automation with AXTree extraction, CDP-based physical clicks, CSP-safe element location, anti-detection interactions, headless operation and screenshot capture.
Work in Progress — APIs are subject to change without notice. iOS and Android platforms have not been tested.
English | 简体中文
KBrowser is a Kotlin Multiplatform library that provides:
KBWebView — A cross-platform WebView UI component for Android, iOS, Desktop (JVM), and WasmJs (Browser). It is a pure WebView abstraction with a unified API similar to WKWebView / Android WebView.KBPage — A Playwright-inspired browser automation wrapper around KBWebView for Desktop (JVM). Built on Chrome DevTools Protocol (CDP), it provides AXTree extraction, CSP-safe element location, anti-detection physical clicks, screenshot capture, and coroutine-based thread safety.| Platform | KBWebView UI | KBPage Automation | Test Status |
|---|---|---|---|
| Desktop (JVM) | ✅ | ✅ Primary target | ✅ Actively tested |
| WasmJs (Browser) | ✅ | ❌ | |
| Android | ✅ | ❌ Not tested | |
| iOS | ✅ | ❌ Not tested |
Automation features (AXTree, CDP-based interactions, screenshots) are Desktop-only. On Android and iOS,
KBLocatorfalls back to JS injection. On WasmJs,KBWebViewrenders via an HTML<iframe>overlay on top of the Compose canvas; automation APIs are not yet implemented.
Must use JetBrains Runtime (JBR) with JCEF. Standard JDK will not work. The library uses JCEF directly from JBR — JCEF is not bundled.
Distribution: JetBrains Runtime
Package: JDK + JCEF
| Platform | Minimum Version |
|---|---|
| Android | API 34 (Android 14) |
| iOS | iOS 17.0+ |
On WasmJs, Compose renders to an HTML <canvas> via Skia. Skia has its own font system - it does not read document.fonts or CSS @font-face. KBrowser provides WithFontResourcesLoaded to solve this, with three modes:
Mode 1: Chrome-only (FontMode.CHROME_ONLY)
queryLocalFonts()) to enumerate all system fontsMode 2: Chrome with fallback (FontMode.CHROME_WITH_FALLBACK) - default
composeResources/font/
font-paths Gradle plugin (see below)Mode 3: Custom only (FontMode.CUSTOM_ONLY)
queryLocalFonts()
Apply the font-paths plugin in your build.gradle.kts:
plugins {
id("xyz.kbrowser.font-paths")
}
kbrowserFontPaths {
packageName.set("com.example.app") // must match your wasmJsMain package
}Place font files under src/commonMain/composeResources/font/ (e.g. NotoSansSC.ttf, NotoSansArabic.ttf). The plugin auto-discovers all .ttf/.otf/.woff/.woff2 files at build time and generates a FontPaths.generated.kt file. No manual path listing needed.
import xyz.kbrowser.WithFontResourcesLoaded
import xyz.kbrowser.FontMode
// Default: Chrome with fallback to bundled fonts
ComposeViewport {
WithFontResourcesLoaded {
App()
}
}
// Chrome only (no bundled fonts)
ComposeViewport {
WithFontResourcesLoaded(mode = FontMode.CHROME_ONLY) {
App()
}
}
// Custom only (no Chrome API, uses bundled fonts exclusively)
ComposeViewport {
WithFontResourcesLoaded(mode = FontMode.CUSTOM_ONLY) {
App()
}
}Chrome 103+: queryLocalFonts() enumerates all system fonts → reads binary data via blob() → transfers to Kotlin via base64 → registers into Skia via Font(identity, bytes) + FontFamilyResolver.preload(). Supports any language - Arabic, Chinese, Thai, Hebrew, etc.
Non-Chrome (Safari/Firefox): Reads bundled font files from composeResources/font/ (auto-discovered by the Gradle plugin) → registers into Skia. Developer is responsible for choosing which fonts to bundle.
Cross-platform: commonMain UI code is shared. Desktop (JVM) uses system fonts natively. WasmJs uses WithFontResourcesLoaded. No platform-specific font code in UI layer.
font-paths Gradle plugin is in buildSrc/. It's part of the KBrowser repo. Consumer projects apply it via id("xyz.kbrowser.font-paths").composeResources/font/, the generated list is empty. Mode 2 and Mode 3 will have no fonts to load on non-Chrome browsers.In gradle/libs.versions.toml:
[versions]
kbrowser = "0.1.0-alpha45"
[libraries]
kbrowser = { module = "io.github.lzdev42:kbrowser", version.ref = "kbrowser" }In your module's build.gradle.kts:
implementation(libs.kbrowser)Configure your IDE or build tool to use JBR with JCEF as the project runtime. In compose.desktop configuration, the following JVM arguments are required:
compose.desktop {
application {
jvmArgs += listOf(
"--enable-native-access=jcef",
"--add-opens=jcef/com.jetbrains.cef.remote.browser=ALL-UNNAMED",
"--add-opens=jcef/com.jetbrains.cef.remote=ALL-UNNAMED"
)
}
}
⚠️ Important: Without these JVM arguments, OSR mode will not support Chinese/CJK text input (English input is unaffected). In OSR mode, JCEF renders off-screen with no native window handling IME. Chinese input relies on reflective calls to JCEF internal classes, and--add-opensgrants access to those classes. Without them, IME events are silently dropped, but English works via key events — easy to misdiagnose as "IME broken" rather than "missing configuration". In non-OSR mode, JCEF uses a native window where IME is handled natively by the OS, so no special arguments are needed.
On JVM, JCEF supports two rendering modes. The mode is determined at initialization time via KBrowser.initializeConfig(useOsr = ...) and cannot be changed after the application starts.
| Mode | useOsr |
Overlay Compose UI | Event Handling | Performance | Chinese Input |
|---|---|---|---|---|---|
| OSR (Off-Screen Rendering) — default | true |
✅ Can overlay Compose UI on top of JCEF | Lower (pixel round-trip) | ||
| Non-OSR (Native Window) | false |
❌ Cannot overlay Compose UI on top of JCEF | ✅ Normal | ✅ Best (native window) | ✅ Native support |
Known Issue (OSR mode): In OSR mode, JCEF renders off-screen, allowing Compose UI to be layered on top. However, mouse and keyboard events are received by the underlying JCEF native view, not by the Compose overlay. This means interactive Compose components placed over the JCEF area will not respond to user input. This issue has not been investigated yet and is currently low priority.
Chinese Input in OSR Mode: Besides the JVM arguments above, OSR mode also requires focus synchronization for Chinese input — KBrowser handles this internally, no user action needed. For technical details, see the Architecture Document.
Recommendation: OSR (useOsr = true, the default) is recommended for most applications — it is the only mode that supports overlaying Compose UI on top of the browser. Use non-OSR (useOsr = false) only when you need maximum rendering performance and can guarantee no Compose UI is ever drawn on top of the browser view. The API is identical for both modes; only the rendering pipeline differs.
macOS live-resize caveat (non-OSR): In non-OSR mode on macOS, browser content does not update while dragging window or splitter edges — it refreshes once the drag is released. This is a CEF + Core Animation architecture limitation (the AWT event queue is blocked and Core Animation does not commit frames during live-resize) that cannot be worked around from Java/AWT. See jcef-resize-fix-plan.md.
This project includes a full demo application showcasing all KBrowser features.
Desktop: On launch, you first choose a rendering mode (OSR / Non-OSR) — this is desktop-specific, letting you compare the two modes (OSR is the default and supports Compose overlay; non-OSR offers best performance but cannot overlay Compose UI). After selecting OSR, the main page offers:
Selecting Non-OSR directly shows a WebGL scene (demonstrating the limitation that Compose UI cannot be overlaid in non-OSR mode).
Mobile: No rendering mode selection (mobile WebView has no OSR concept), goes directly to the feature list. The 6 WebView component demo pages share code with the desktop. The browser automation page shows a warning that some features may not work on mobile.
WasmJs (Browser): On launch, the app requests Local Font Access permission. Once granted, all system fonts are loaded into Skia and the main UI renders. The KBWebView component uses an HTML <iframe> overlay positioned on top of the Compose canvas. WebView demo pages (basic browsing, HTML content, JS communication) are available; automation features are not yet supported.
KBrowser.initializeConfig() and initializeKBrowser() must be called before application {}:
import xyz.kbrowser.webview.KBrowser
import xyz.kbrowser.webview.initializeKBrowser
import androidx.compose.ui.window.application
fun main() {
// 1. Configure cache directory and rendering mode (must be called once at startup)
KBrowser.initializeConfig(
storageDir = "/path/to/cache",
useOsr = true // default; set to false only for maximum performance with no Compose overlay
)
// 2. Initialize JCEF engine (suspend function, must be called before any UI)
kotlinx.coroutines.runBlocking {
initializeKBrowser()
}
// 3. Start Compose application
application {
Window(onCloseRequest = ::exitApplication) { App() }
}
}On WasmJs, Compose renders to a <canvas> via Skia, which has a separate font system from the browser's CSS. Without explicit font loading, Chinese/CJK and other non-Latin text will render as tofu boxes. KBrowser provides WithFontResourcesLoaded to handle this.
Step 1: Apply the font-paths plugin in build.gradle.kts:
plugins {
id("xyz.kbrowser.font-paths")
}
kbrowserFontPaths {
packageName.set("com.example.app") // match your wasmJsMain package
}Step 2 (optional): Place font files under src/commonMain/composeResources/font/. The plugin auto-discovers them. Skip this if you only need Chrome support.
Step 3: Wrap your content:
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.ComposeViewport
import xyz.kbrowser.WithFontResourcesLoaded
@OptIn(ExperimentalComposeUiApi::class)
fun main() {
ComposeViewport {
WithFontResourcesLoaded {
App()
}
}
}On Chrome 103+, all system fonts are loaded automatically (any language). On non-Chrome browsers, bundled fonts from composeResources/font/ are used as fallback. See the WasmJs section above for mode details.
// commonMain - shared UI, no platform-specific font code
@Composable
expect fun App()
// wasmJsMain
fun main() = ComposeViewport {
WithFontResourcesLoaded { App() }
}
// jvmMain - standard desktop, no font wrapper needed
fun main() = application {
Window(onCloseRequest = ::exitApplication) { App() }
}In a Compose Multiplatform project targeting both Desktop (JVM) and WasmJs, use expect/actual to isolate the platform-specific entry point:
// commonMain
@Composable
expect fun App()
// wasmJsMain - wrap with font loading
@OptIn(ExperimentalComposeUiApi::class)
fun main() = ComposeViewport {
WithFontResourcesLoaded { App() }
}
// jvmMain - standard desktop entry, no font wrapper needed
fun main() = application {
Window(onCloseRequest = ::exitApplication) { App() }
}Desktop (JVM) uses system fonts natively via Skia's font manager. WasmJs needs WithFontResourcesLoaded because Skia in the browser sandbox has no system font access. The shared App() composable works on both platforms without modification.
KBWebView is a pure WebView component. Use it when you need to display web content in your Compose UI:
@Composable
fun BrowserScreen() {
val webView = rememberKBWebView(initialUrl = "https://example.com")
LaunchedEffect(webView) {
webView.onNewWindowRequest = { url ->
webView.loadUrl(url)
}
}
Column(Modifier.fillMaxSize()) {
KBWebView(webView = webView, modifier = Modifier.weight(1f))
Row {
Button(onClick = { webView.goBack() }) { Text("←") }
Button(onClick = { webView.goForward() }) { Text("→") }
Button(onClick = { webView.reload() }) { Text("↺") }
}
}
}KBPage is a coroutine-based automation wrapper around KBWebView. It provides:
loadUrl suspends until page finishes loading)Mutex for writes and @Volatile for readsval page = KBrowser.newPage()
page.onNewPage = { url -> println("New page request: $url") }
page.loadUrl("https://example.com")
// Coordinate mode (physical events, anti-detection)
page.getByLabel("Username").fill("admin")
page.getByLabel("Password").type("secret")
page.getByRole("button", name = "Login").click()
// JS mode (DOM event simulation, bypasses occlusion)
page.getByLabel("Username").jsFill("admin")
page.getByLabel("Password").jsType("secret")
page.getByRole("button", name = "Login").jsClick()
// AXTree extraction
val tree = page.snapshot().rawTree.getCleanedAxTree()
println("Visible nodes: ${tree.visibleElements}")
// Get page snapshot (YAML + raw data from the same fetch)
val result = page.snapshot(SnapshotMode.VIEWPORT)
val yaml = result.yaml // For AI
val rawTree = result.rawTree // Raw data, refids consistent with yaml
// Screenshot
val png = page.screenshot()
page.close()suspend methods of KBPage internally switch to Dispatchers.Main via withContext, so they can be called from any coroutine context.KBPage node cache uses Mutex for write serialization and @Volatile for read visibility. Read operations (e.g., click) will never deadlock with write operations (e.g., getRawAxTree).AxTreeData.getCleanedAxTree(), AxTreeData.toYamlSnapshot()) are pure Kotlin extension functions that execute in the caller's coroutine context without switching threads. getCleanedAxTree() actually filters nodes within the current viewport (same viewport-range logic as toYamlSnapshot(VIEWPORT)).KBrowser provides two distinct page-creation APIs, each with a clear single responsibility:
KBrowser.newPage(profile: KBProfile? = null) — Creates a UI page for display in a Compose window via the KBWebView Composable. Render size is determined by the Compose modifier.KBrowser.newHeadlessTab(profile: KBProfile? = null, viewportWidth = 1280, viewportHeight = 720) — Creates a headless page for background automation (screenshots, CDP operations, AX Tree extraction). Render size is determined by a transparent JFrame (opacity = 0) that hosts the JCEF component. Never mount a headless page onto the KBWebView Composable — it will cause size anomalies.Both APIs only create the page; navigation is done via page.loadUrl(url), which is a suspend function that returns when loading completes:
val page = KBrowser.newHeadlessTab() // create
page.loadUrl("https://example.com") // navigate (suspend)
val png = page.screenshot() // readyLimitations:
useOsr = true).Xvfb) is required.Apache License 2.0 — see LICENSE.
Portions of the JVM/Desktop implementation are derived from IntelliJ IDEA (JetBrains s.r.o.), licensed under Apache 2.0. Modified files retain original copyright notices.
Work in Progress — APIs are subject to change without notice. iOS and Android platforms have not been tested.
English | 简体中文
KBrowser is a Kotlin Multiplatform library that provides:
KBWebView — A cross-platform WebView UI component for Android, iOS, Desktop (JVM), and WasmJs (Browser). It is a pure WebView abstraction with a unified API similar to WKWebView / Android WebView.KBPage — A Playwright-inspired browser automation wrapper around KBWebView for Desktop (JVM). Built on Chrome DevTools Protocol (CDP), it provides AXTree extraction, CSP-safe element location, anti-detection physical clicks, screenshot capture, and coroutine-based thread safety.| Platform | KBWebView UI | KBPage Automation | Test Status |
|---|---|---|---|
| Desktop (JVM) | ✅ | ✅ Primary target | ✅ Actively tested |
| WasmJs (Browser) | ✅ | ❌ | |
| Android | ✅ | ❌ Not tested | |
| iOS | ✅ | ❌ Not tested |
Automation features (AXTree, CDP-based interactions, screenshots) are Desktop-only. On Android and iOS,
KBLocatorfalls back to JS injection. On WasmJs,KBWebViewrenders via an HTML<iframe>overlay on top of the Compose canvas; automation APIs are not yet implemented.
Must use JetBrains Runtime (JBR) with JCEF. Standard JDK will not work. The library uses JCEF directly from JBR — JCEF is not bundled.
Distribution: JetBrains Runtime
Package: JDK + JCEF
| Platform | Minimum Version |
|---|---|
| Android | API 34 (Android 14) |
| iOS | iOS 17.0+ |
On WasmJs, Compose renders to an HTML <canvas> via Skia. Skia has its own font system - it does not read document.fonts or CSS @font-face. KBrowser provides WithFontResourcesLoaded to solve this, with three modes:
Mode 1: Chrome-only (FontMode.CHROME_ONLY)
queryLocalFonts()) to enumerate all system fontsMode 2: Chrome with fallback (FontMode.CHROME_WITH_FALLBACK) - default
composeResources/font/
font-paths Gradle plugin (see below)Mode 3: Custom only (FontMode.CUSTOM_ONLY)
queryLocalFonts()
Apply the font-paths plugin in your build.gradle.kts:
plugins {
id("xyz.kbrowser.font-paths")
}
kbrowserFontPaths {
packageName.set("com.example.app") // must match your wasmJsMain package
}Place font files under src/commonMain/composeResources/font/ (e.g. NotoSansSC.ttf, NotoSansArabic.ttf). The plugin auto-discovers all .ttf/.otf/.woff/.woff2 files at build time and generates a FontPaths.generated.kt file. No manual path listing needed.
import xyz.kbrowser.WithFontResourcesLoaded
import xyz.kbrowser.FontMode
// Default: Chrome with fallback to bundled fonts
ComposeViewport {
WithFontResourcesLoaded {
App()
}
}
// Chrome only (no bundled fonts)
ComposeViewport {
WithFontResourcesLoaded(mode = FontMode.CHROME_ONLY) {
App()
}
}
// Custom only (no Chrome API, uses bundled fonts exclusively)
ComposeViewport {
WithFontResourcesLoaded(mode = FontMode.CUSTOM_ONLY) {
App()
}
}Chrome 103+: queryLocalFonts() enumerates all system fonts → reads binary data via blob() → transfers to Kotlin via base64 → registers into Skia via Font(identity, bytes) + FontFamilyResolver.preload(). Supports any language - Arabic, Chinese, Thai, Hebrew, etc.
Non-Chrome (Safari/Firefox): Reads bundled font files from composeResources/font/ (auto-discovered by the Gradle plugin) → registers into Skia. Developer is responsible for choosing which fonts to bundle.
Cross-platform: commonMain UI code is shared. Desktop (JVM) uses system fonts natively. WasmJs uses WithFontResourcesLoaded. No platform-specific font code in UI layer.
font-paths Gradle plugin is in buildSrc/. It's part of the KBrowser repo. Consumer projects apply it via id("xyz.kbrowser.font-paths").composeResources/font/, the generated list is empty. Mode 2 and Mode 3 will have no fonts to load on non-Chrome browsers.In gradle/libs.versions.toml:
[versions]
kbrowser = "0.1.0-alpha45"
[libraries]
kbrowser = { module = "io.github.lzdev42:kbrowser", version.ref = "kbrowser" }In your module's build.gradle.kts:
implementation(libs.kbrowser)Configure your IDE or build tool to use JBR with JCEF as the project runtime. In compose.desktop configuration, the following JVM arguments are required:
compose.desktop {
application {
jvmArgs += listOf(
"--enable-native-access=jcef",
"--add-opens=jcef/com.jetbrains.cef.remote.browser=ALL-UNNAMED",
"--add-opens=jcef/com.jetbrains.cef.remote=ALL-UNNAMED"
)
}
}
⚠️ Important: Without these JVM arguments, OSR mode will not support Chinese/CJK text input (English input is unaffected). In OSR mode, JCEF renders off-screen with no native window handling IME. Chinese input relies on reflective calls to JCEF internal classes, and--add-opensgrants access to those classes. Without them, IME events are silently dropped, but English works via key events — easy to misdiagnose as "IME broken" rather than "missing configuration". In non-OSR mode, JCEF uses a native window where IME is handled natively by the OS, so no special arguments are needed.
On JVM, JCEF supports two rendering modes. The mode is determined at initialization time via KBrowser.initializeConfig(useOsr = ...) and cannot be changed after the application starts.
| Mode | useOsr |
Overlay Compose UI | Event Handling | Performance | Chinese Input |
|---|---|---|---|---|---|
| OSR (Off-Screen Rendering) — default | true |
✅ Can overlay Compose UI on top of JCEF | Lower (pixel round-trip) | ||
| Non-OSR (Native Window) | false |
❌ Cannot overlay Compose UI on top of JCEF | ✅ Normal | ✅ Best (native window) | ✅ Native support |
Known Issue (OSR mode): In OSR mode, JCEF renders off-screen, allowing Compose UI to be layered on top. However, mouse and keyboard events are received by the underlying JCEF native view, not by the Compose overlay. This means interactive Compose components placed over the JCEF area will not respond to user input. This issue has not been investigated yet and is currently low priority.
Chinese Input in OSR Mode: Besides the JVM arguments above, OSR mode also requires focus synchronization for Chinese input — KBrowser handles this internally, no user action needed. For technical details, see the Architecture Document.
Recommendation: OSR (useOsr = true, the default) is recommended for most applications — it is the only mode that supports overlaying Compose UI on top of the browser. Use non-OSR (useOsr = false) only when you need maximum rendering performance and can guarantee no Compose UI is ever drawn on top of the browser view. The API is identical for both modes; only the rendering pipeline differs.
macOS live-resize caveat (non-OSR): In non-OSR mode on macOS, browser content does not update while dragging window or splitter edges — it refreshes once the drag is released. This is a CEF + Core Animation architecture limitation (the AWT event queue is blocked and Core Animation does not commit frames during live-resize) that cannot be worked around from Java/AWT. See jcef-resize-fix-plan.md.
This project includes a full demo application showcasing all KBrowser features.
Desktop: On launch, you first choose a rendering mode (OSR / Non-OSR) — this is desktop-specific, letting you compare the two modes (OSR is the default and supports Compose overlay; non-OSR offers best performance but cannot overlay Compose UI). After selecting OSR, the main page offers:
Selecting Non-OSR directly shows a WebGL scene (demonstrating the limitation that Compose UI cannot be overlaid in non-OSR mode).
Mobile: No rendering mode selection (mobile WebView has no OSR concept), goes directly to the feature list. The 6 WebView component demo pages share code with the desktop. The browser automation page shows a warning that some features may not work on mobile.
WasmJs (Browser): On launch, the app requests Local Font Access permission. Once granted, all system fonts are loaded into Skia and the main UI renders. The KBWebView component uses an HTML <iframe> overlay positioned on top of the Compose canvas. WebView demo pages (basic browsing, HTML content, JS communication) are available; automation features are not yet supported.
KBrowser.initializeConfig() and initializeKBrowser() must be called before application {}:
import xyz.kbrowser.webview.KBrowser
import xyz.kbrowser.webview.initializeKBrowser
import androidx.compose.ui.window.application
fun main() {
// 1. Configure cache directory and rendering mode (must be called once at startup)
KBrowser.initializeConfig(
storageDir = "/path/to/cache",
useOsr = true // default; set to false only for maximum performance with no Compose overlay
)
// 2. Initialize JCEF engine (suspend function, must be called before any UI)
kotlinx.coroutines.runBlocking {
initializeKBrowser()
}
// 3. Start Compose application
application {
Window(onCloseRequest = ::exitApplication) { App() }
}
}On WasmJs, Compose renders to a <canvas> via Skia, which has a separate font system from the browser's CSS. Without explicit font loading, Chinese/CJK and other non-Latin text will render as tofu boxes. KBrowser provides WithFontResourcesLoaded to handle this.
Step 1: Apply the font-paths plugin in build.gradle.kts:
plugins {
id("xyz.kbrowser.font-paths")
}
kbrowserFontPaths {
packageName.set("com.example.app") // match your wasmJsMain package
}Step 2 (optional): Place font files under src/commonMain/composeResources/font/. The plugin auto-discovers them. Skip this if you only need Chrome support.
Step 3: Wrap your content:
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.ComposeViewport
import xyz.kbrowser.WithFontResourcesLoaded
@OptIn(ExperimentalComposeUiApi::class)
fun main() {
ComposeViewport {
WithFontResourcesLoaded {
App()
}
}
}On Chrome 103+, all system fonts are loaded automatically (any language). On non-Chrome browsers, bundled fonts from composeResources/font/ are used as fallback. See the WasmJs section above for mode details.
// commonMain - shared UI, no platform-specific font code
@Composable
expect fun App()
// wasmJsMain
fun main() = ComposeViewport {
WithFontResourcesLoaded { App() }
}
// jvmMain - standard desktop, no font wrapper needed
fun main() = application {
Window(onCloseRequest = ::exitApplication) { App() }
}In a Compose Multiplatform project targeting both Desktop (JVM) and WasmJs, use expect/actual to isolate the platform-specific entry point:
// commonMain
@Composable
expect fun App()
// wasmJsMain - wrap with font loading
@OptIn(ExperimentalComposeUiApi::class)
fun main() = ComposeViewport {
WithFontResourcesLoaded { App() }
}
// jvmMain - standard desktop entry, no font wrapper needed
fun main() = application {
Window(onCloseRequest = ::exitApplication) { App() }
}Desktop (JVM) uses system fonts natively via Skia's font manager. WasmJs needs WithFontResourcesLoaded because Skia in the browser sandbox has no system font access. The shared App() composable works on both platforms without modification.
KBWebView is a pure WebView component. Use it when you need to display web content in your Compose UI:
@Composable
fun BrowserScreen() {
val webView = rememberKBWebView(initialUrl = "https://example.com")
LaunchedEffect(webView) {
webView.onNewWindowRequest = { url ->
webView.loadUrl(url)
}
}
Column(Modifier.fillMaxSize()) {
KBWebView(webView = webView, modifier = Modifier.weight(1f))
Row {
Button(onClick = { webView.goBack() }) { Text("←") }
Button(onClick = { webView.goForward() }) { Text("→") }
Button(onClick = { webView.reload() }) { Text("↺") }
}
}
}KBPage is a coroutine-based automation wrapper around KBWebView. It provides:
loadUrl suspends until page finishes loading)Mutex for writes and @Volatile for readsval page = KBrowser.newPage()
page.onNewPage = { url -> println("New page request: $url") }
page.loadUrl("https://example.com")
// Coordinate mode (physical events, anti-detection)
page.getByLabel("Username").fill("admin")
page.getByLabel("Password").type("secret")
page.getByRole("button", name = "Login").click()
// JS mode (DOM event simulation, bypasses occlusion)
page.getByLabel("Username").jsFill("admin")
page.getByLabel("Password").jsType("secret")
page.getByRole("button", name = "Login").jsClick()
// AXTree extraction
val tree = page.snapshot().rawTree.getCleanedAxTree()
println("Visible nodes: ${tree.visibleElements}")
// Get page snapshot (YAML + raw data from the same fetch)
val result = page.snapshot(SnapshotMode.VIEWPORT)
val yaml = result.yaml // For AI
val rawTree = result.rawTree // Raw data, refids consistent with yaml
// Screenshot
val png = page.screenshot()
page.close()suspend methods of KBPage internally switch to Dispatchers.Main via withContext, so they can be called from any coroutine context.KBPage node cache uses Mutex for write serialization and @Volatile for read visibility. Read operations (e.g., click) will never deadlock with write operations (e.g., getRawAxTree).AxTreeData.getCleanedAxTree(), AxTreeData.toYamlSnapshot()) are pure Kotlin extension functions that execute in the caller's coroutine context without switching threads. getCleanedAxTree() actually filters nodes within the current viewport (same viewport-range logic as toYamlSnapshot(VIEWPORT)).KBrowser provides two distinct page-creation APIs, each with a clear single responsibility:
KBrowser.newPage(profile: KBProfile? = null) — Creates a UI page for display in a Compose window via the KBWebView Composable. Render size is determined by the Compose modifier.KBrowser.newHeadlessTab(profile: KBProfile? = null, viewportWidth = 1280, viewportHeight = 720) — Creates a headless page for background automation (screenshots, CDP operations, AX Tree extraction). Render size is determined by a transparent JFrame (opacity = 0) that hosts the JCEF component. Never mount a headless page onto the KBWebView Composable — it will cause size anomalies.Both APIs only create the page; navigation is done via page.loadUrl(url), which is a suspend function that returns when loading completes:
val page = KBrowser.newHeadlessTab() // create
page.loadUrl("https://example.com") // navigate (suspend)
val png = page.screenshot() // readyLimitations:
useOsr = true).Xvfb) is required.Apache License 2.0 — see LICENSE.
Portions of the JVM/Desktop implementation are derived from IntelliJ IDEA (JetBrains s.r.o.), licensed under Apache 2.0. Modified files retain original copyright notices.