From bc39055c115705060d69f1645c92e030f87e1796 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 09:30:32 +0000 Subject: [PATCH 01/22] Initial plan From 1f163cd71088f578a3fc1112f4283ee641e19625 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 09:45:17 +0000 Subject: [PATCH 02/22] Add tabbed UI with Realtime and Agent tabs, implement WebView for browser support Co-authored-by: paulpv <1393897+paulpv@users.noreply.github.com> --- core/build.gradle.kts | 22 +++ .../codeoba/core/ui/WebView.kt | 41 +++++ .../codeoba/core/ui/AgentTabContent.kt | 44 +++++ .../codeoba/core/ui/CodeobaUI.kt | 156 +++++++++++------- .../codeoba/core/ui/WebView.kt | 17 ++ .../codeoba/core/ui/WebView.kt | 61 +++++++ gradle/libs.versions.toml | 10 ++ 7 files changed, 292 insertions(+), 59 deletions(-) create mode 100644 core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt create mode 100644 core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/AgentTabContent.kt create mode 100644 core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt create mode 100644 core/src/desktopMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt diff --git a/core/build.gradle.kts b/core/build.gradle.kts index a5f0ba5..4328674 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -62,6 +62,28 @@ kotlin { val desktopMain by getting { dependencies { implementation(libs.ktor.client.cio) + + // JavaFX for WebView support + implementation(libs.javafx.base) + implementation(libs.javafx.graphics) + implementation(libs.javafx.controls) + implementation(libs.javafx.web) + implementation(libs.javafx.swing) + + // Platform-specific JavaFX native libraries + val osName = System.getProperty("os.name").lowercase() + val platform = when { + osName.contains("mac") || osName.contains("darwin") -> "mac" + osName.contains("win") -> "win" + osName.contains("linux") -> "linux" + else -> "linux" + } + + implementation("org.openjfx:javafx-base:${libs.versions.javafx.get()}:$platform") + implementation("org.openjfx:javafx-graphics:${libs.versions.javafx.get()}:$platform") + implementation("org.openjfx:javafx-controls:${libs.versions.javafx.get()}:$platform") + implementation("org.openjfx:javafx-web:${libs.versions.javafx.get()}:$platform") + implementation("org.openjfx:javafx-swing:${libs.versions.javafx.get()}:$platform") } } } diff --git a/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt new file mode 100644 index 0000000..7fd7850 --- /dev/null +++ b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt @@ -0,0 +1,41 @@ +package llc.lookatwhataicando.codeoba.core.ui + +import android.webkit.WebChromeClient +import android.webkit.WebResourceRequest +import android.webkit.WebViewClient +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import android.webkit.WebView as AndroidWebView + +/** + * Android implementation of WebView using Android WebView. + */ +@Composable +actual fun WebView( + url: String, + modifier: Modifier +) { + AndroidView( + modifier = modifier, + factory = { context -> + AndroidWebView(context).apply { + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + settings.setSupportZoom(true) + settings.builtInZoomControls = true + settings.displayZoomControls = false + + webViewClient = WebViewClient() + webChromeClient = WebChromeClient() + + loadUrl(url) + } + }, + update = { view -> + if (view.url != url) { + view.loadUrl(url) + } + } + ) +} diff --git a/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/AgentTabContent.kt b/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/AgentTabContent.kt new file mode 100644 index 0000000..6b959a5 --- /dev/null +++ b/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/AgentTabContent.kt @@ -0,0 +1,44 @@ +package llc.lookatwhataicando.codeoba.core.ui + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +/** + * Agent tab content displaying the GitHub Copilot Agents page in a browser. + * Provides easy navigation with gestures support. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AgentTabContent(modifier: Modifier = Modifier) { + Column(modifier = modifier.fillMaxSize()) { + // Header + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceVariant, + tonalElevation = 2.dp + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "GitHub Copilot Agents", + style = MaterialTheme.typography.titleMedium + ) + } + } + + // WebView content + WebView( + url = "https://github.com/copilot/agents", + modifier = Modifier.fillMaxSize() + ) + } +} diff --git a/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt b/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt index 2f4d0c6..f99ba1f 100644 --- a/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt +++ b/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt @@ -36,6 +36,8 @@ import androidx.compose.material3.RadioButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Switch +import androidx.compose.material3.Tab +import androidx.compose.material3.PrimaryTabRow import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.rememberDrawerState @@ -75,6 +77,10 @@ fun CodeobaUI(app: CodeobaApp, config: RealtimeConfig) { val scope = rememberCoroutineScope() val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + // Tab state + var selectedTabIndex by remember { mutableStateOf(0) } + val tabs = listOf("Realtime", "Agent") + ModalNavigationDrawer( drawerState = drawerState, drawerContent = { @@ -99,74 +105,106 @@ fun CodeobaUI(app: CodeobaApp, config: RealtimeConfig) { ) { Scaffold( topBar = { - TopAppBar( - title = { Text("Codeoba") }, - navigationIcon = { - IconButton(onClick = { scope.launch { drawerState.open() } }) { - Text("☰", style = MaterialTheme.typography.headlineMedium) + Column { + TopAppBar( + title = { Text("Codeoba") }, + navigationIcon = { + IconButton(onClick = { scope.launch { drawerState.open() } }) { + Text("☰", style = MaterialTheme.typography.headlineMedium) + } + }, + actions = { + // Only show connection switch in Realtime tab + if (selectedTabIndex == 0) { + Switch( + modifier = Modifier.padding(horizontal = 16.dp), + checked = connectionState is ConnectionState.Connected || connectionState is ConnectionState.Connecting, + onCheckedChange = { isChecked -> + if (isChecked) { + scope.launch { app.connect(config) } + } else { + scope.launch { app.disconnect() } + } + }, + enabled = connectionState !is ConnectionState.Connecting + ) + } + } + ) + + // Tabs below the top bar + PrimaryTabRow(selectedTabIndex = selectedTabIndex) { + tabs.forEachIndexed { index, title -> + Tab( + selected = selectedTabIndex == index, + onClick = { selectedTabIndex = index }, + text = { Text(title) } + ) } - }, - actions = { - Switch( - modifier = Modifier.padding(horizontal = 16.dp), - checked = connectionState is ConnectionState.Connected || connectionState is ConnectionState.Connecting, - onCheckedChange = { isChecked -> - if (isChecked) { - scope.launch { app.connect(config) } - } else { - scope.launch { app.disconnect() } - } - }, - enabled = connectionState !is ConnectionState.Connecting - ) } - ) + } }, bottomBar = { - Column { - // Footer with PTT Button - PushToTalkFooter( - audioCaptureState = audioCaptureState, - connectionState = connectionState, - onStartMic = { scope.launch { - // TODO: cancelRemoteSpeech() - // TODO: play intro sound - app.startMicrophone() - app.realtimeClient.dataSendInputAudioBufferClear() - } }, - onStopMic = { scope.launch { - app.stopMicrophone() - app.realtimeClient.dataSendInputAudioBufferCommit() - app.realtimeClient.dataSendResponseCreate() - // TODO: play outro sound - } } - ) - - // Audio Route Dropdown (only show if multiple routes available) - if (audioRoutes.size > 1) { - AudioRouteDropdown( - routes = audioRoutes, - activeRoute = activeRoute, - onSelectRoute = { route -> scope.launch { app.selectAudioRoute(route) } } + // Only show bottom bar for Realtime tab + if (selectedTabIndex == 0) { + Column { + // Footer with PTT Button + PushToTalkFooter( + audioCaptureState = audioCaptureState, + connectionState = connectionState, + onStartMic = { scope.launch { + // TODO: cancelRemoteSpeech() + // TODO: play intro sound + app.startMicrophone() + app.realtimeClient.dataSendInputAudioBufferClear() + } }, + onStopMic = { scope.launch { + app.stopMicrophone() + app.realtimeClient.dataSendInputAudioBufferCommit() + app.realtimeClient.dataSendResponseCreate() + // TODO: play outro sound + } } ) + + // Audio Route Dropdown (only show if multiple routes available) + if (audioRoutes.size > 1) { + AudioRouteDropdown( + routes = audioRoutes, + activeRoute = activeRoute, + onSelectRoute = { route -> scope.launch { app.selectAudioRoute(route) } } + ) + } } } } ) { innerPadding -> - // Conversation panel with integrated text input - ConversationPanel( - events = eventLog, - connectionState = connectionState, - onSendText = { text -> - scope.launch { - app.sendTextMessage(text) - } - }, - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - .padding(16.dp) - ) + // Tab content + when (selectedTabIndex) { + 0 -> { + // Realtime tab: Conversation panel with integrated text input + ConversationPanel( + events = eventLog, + connectionState = connectionState, + onSendText = { text -> + scope.launch { + app.sendTextMessage(text) + } + }, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .padding(16.dp) + ) + } + 1 -> { + // Agent tab: Browser view + AgentTabContent( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + ) + } + } } } } diff --git a/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt b/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt new file mode 100644 index 0000000..38bc88c --- /dev/null +++ b/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt @@ -0,0 +1,17 @@ +package llc.lookatwhataicando.codeoba.core.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +/** + * Platform-agnostic WebView component. + * Displays web content with basic navigation support. + * + * @param url The URL to load in the WebView + * @param modifier The modifier to apply to the WebView + */ +@Composable +expect fun WebView( + url: String, + modifier: Modifier = Modifier +) diff --git a/core/src/desktopMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt b/core/src/desktopMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt new file mode 100644 index 0000000..1ff30bf --- /dev/null +++ b/core/src/desktopMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt @@ -0,0 +1,61 @@ +package llc.lookatwhataicando.codeoba.core.ui + +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.SwingPanel +import javafx.application.Platform +import javafx.embed.swing.JFXPanel +import javafx.scene.Scene +import javafx.scene.web.WebView as JFXWebView +import java.awt.BorderLayout +import javax.swing.JPanel + +/** + * Desktop implementation of WebView using JavaFX WebView. + */ +@Composable +actual fun WebView( + url: String, + modifier: Modifier +) { + var webView by remember { mutableStateOf(null) } + + // Initialize JavaFX platform if not already initialized + LaunchedEffect(Unit) { + try { + Platform.startup {} + } catch (e: IllegalStateException) { + // JavaFX platform already initialized + } + } + + // Load URL when it changes + LaunchedEffect(url) { + webView?.let { view -> + Platform.runLater { + view.engine.load(url) + } + } + } + + SwingPanel( + modifier = modifier, + factory = { + JPanel(BorderLayout()).apply { + val jfxPanel = JFXPanel() + add(jfxPanel, BorderLayout.CENTER) + + Platform.runLater { + val view = JFXWebView().apply { + engine.load(url) + } + + webView = view + + val scene = Scene(view) + jfxPanel.scene = scene + } + } + } + ) +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bd6d1a2..19752ee 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,6 +17,9 @@ webrtc-android = "137.7151.05" # Audio audioswitch = "1.2.4" +# JavaFX for WebView +javafx = "17.0.2" + # AndroidX androidx-activity-compose = "1.12.2" androidx-lifecycle-viewmodel-compose = "2.10.0" @@ -51,6 +54,13 @@ webrtc-android = { module = "io.github.webrtc-sdk:android", version.ref = "webrt # Audio audioswitch = { module = "com.twilio:audioswitch", version.ref = "audioswitch" } +# JavaFX +javafx-base = { module = "org.openjfx:javafx-base", version.ref = "javafx" } +javafx-graphics = { module = "org.openjfx:javafx-graphics", version.ref = "javafx" } +javafx-controls = { module = "org.openjfx:javafx-controls", version.ref = "javafx" } +javafx-web = { module = "org.openjfx:javafx-web", version.ref = "javafx" } +javafx-swing = { module = "org.openjfx:javafx-swing", version.ref = "javafx" } + [plugins] kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } From d5f720a376dc28b343cc0a5a3a3b58c24f451319 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 09:51:31 +0000 Subject: [PATCH 03/22] Fix code review issues: remove unused imports, fix race condition, remove duplicate dependencies Co-authored-by: paulpv <1393897+paulpv@users.noreply.github.com> --- core/build.gradle.kts | 9 +------- .../codeoba/core/ui/WebView.kt | 1 - .../codeoba/core/ui/WebView.kt | 23 ++++++++----------- 3 files changed, 11 insertions(+), 22 deletions(-) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 4328674..f318196 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -63,14 +63,7 @@ kotlin { dependencies { implementation(libs.ktor.client.cio) - // JavaFX for WebView support - implementation(libs.javafx.base) - implementation(libs.javafx.graphics) - implementation(libs.javafx.controls) - implementation(libs.javafx.web) - implementation(libs.javafx.swing) - - // Platform-specific JavaFX native libraries + // Platform-specific JavaFX native libraries for WebView support val osName = System.getProperty("os.name").lowercase() val platform = when { osName.contains("mac") || osName.contains("darwin") -> "mac" diff --git a/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt index 7fd7850..75a7450 100644 --- a/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt +++ b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt @@ -1,7 +1,6 @@ package llc.lookatwhataicando.codeoba.core.ui import android.webkit.WebChromeClient -import android.webkit.WebResourceRequest import android.webkit.WebViewClient import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier diff --git a/core/src/desktopMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt b/core/src/desktopMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt index 1ff30bf..d43b5a3 100644 --- a/core/src/desktopMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt +++ b/core/src/desktopMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt @@ -18,8 +18,6 @@ actual fun WebView( url: String, modifier: Modifier ) { - var webView by remember { mutableStateOf(null) } - // Initialize JavaFX platform if not already initialized LaunchedEffect(Unit) { try { @@ -29,15 +27,6 @@ actual fun WebView( } } - // Load URL when it changes - LaunchedEffect(url) { - webView?.let { view -> - Platform.runLater { - view.engine.load(url) - } - } - } - SwingPanel( modifier = modifier, factory = { @@ -50,12 +39,20 @@ actual fun WebView( engine.load(url) } - webView = view - val scene = Scene(view) jfxPanel.scene = scene } } + }, + update = { panel -> + // Update URL when it changes + Platform.runLater { + val jfxPanel = panel.components.firstOrNull() as? JFXPanel + jfxPanel?.scene?.let { scene -> + val webView = scene.root as? JFXWebView + webView?.engine?.load(url) + } + } } ) } From 7e54ab29ee2f3814c02ac80c19b9693d20eb74f9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 09:53:22 +0000 Subject: [PATCH 04/22] Add URL check to prevent unnecessary reloads and improve code clarity Co-authored-by: paulpv <1393897+paulpv@users.noreply.github.com> --- core/build.gradle.kts | 13 ++++++++----- .../lookatwhataicando/codeoba/core/ui/WebView.kt | 4 +++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index f318196..95909c6 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -64,6 +64,8 @@ kotlin { implementation(libs.ktor.client.cio) // Platform-specific JavaFX native libraries for WebView support + // Note: Using string interpolation because Gradle catalog doesn't support + // classifier-based dependencies. Platform classifier must be determined at runtime. val osName = System.getProperty("os.name").lowercase() val platform = when { osName.contains("mac") || osName.contains("darwin") -> "mac" @@ -72,11 +74,12 @@ kotlin { else -> "linux" } - implementation("org.openjfx:javafx-base:${libs.versions.javafx.get()}:$platform") - implementation("org.openjfx:javafx-graphics:${libs.versions.javafx.get()}:$platform") - implementation("org.openjfx:javafx-controls:${libs.versions.javafx.get()}:$platform") - implementation("org.openjfx:javafx-web:${libs.versions.javafx.get()}:$platform") - implementation("org.openjfx:javafx-swing:${libs.versions.javafx.get()}:$platform") + val javafxVersion = libs.versions.javafx.get() + implementation("org.openjfx:javafx-base:$javafxVersion:$platform") + implementation("org.openjfx:javafx-graphics:$javafxVersion:$platform") + implementation("org.openjfx:javafx-controls:$javafxVersion:$platform") + implementation("org.openjfx:javafx-web:$javafxVersion:$platform") + implementation("org.openjfx:javafx-swing:$javafxVersion:$platform") } } } diff --git a/core/src/desktopMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt b/core/src/desktopMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt index d43b5a3..e8f81ec 100644 --- a/core/src/desktopMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt +++ b/core/src/desktopMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt @@ -50,7 +50,9 @@ actual fun WebView( val jfxPanel = panel.components.firstOrNull() as? JFXPanel jfxPanel?.scene?.let { scene -> val webView = scene.root as? JFXWebView - webView?.engine?.load(url) + if (webView != null && webView.engine.location != url) { + webView.engine.load(url) + } } } } From 9bad6ebfb740622a81a39f2d3126be2a457f8cfd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 10:35:06 +0000 Subject: [PATCH 05/22] Add WebView improvements: cookie persistence, pull-to-refresh, and back navigation support Co-authored-by: paulpv <1393897+paulpv@users.noreply.github.com> --- core/build.gradle.kts | 2 + .../codeoba/core/ui/WebView.kt | 26 ++++ .../codeoba/core/ui/WebViewWithBackHandler.kt | 118 ++++++++++++++++++ .../codeoba/core/ui/AgentTabContent.kt | 12 +- .../codeoba/core/ui/WebViewWithBackHandler.kt | 19 +++ .../codeoba/core/ui/WebViewWithBackHandler.kt | 26 ++++ gradle/libs.versions.toml | 6 + 7 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt create mode 100644 core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt create mode 100644 core/src/desktopMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 95909c6..6d5081d 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -56,6 +56,8 @@ kotlin { implementation(libs.ktor.client.okhttp) implementation(libs.webrtc.android) implementation(libs.audioswitch) + implementation(libs.accompanist.swiperefresh) + implementation(libs.androidx.activity.compose) } } diff --git a/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt index 75a7450..1fdf978 100644 --- a/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt +++ b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt @@ -1,14 +1,23 @@ package llc.lookatwhataicando.codeoba.core.ui +import android.webkit.CookieManager import android.webkit.WebChromeClient import android.webkit.WebViewClient +import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.viewinterop.AndroidView import android.webkit.WebView as AndroidWebView /** * Android implementation of WebView using Android WebView. + * Supports: + * - Cookie persistence for logged-in sessions + * - Cache for better performance */ @Composable actual fun WebView( @@ -19,12 +28,29 @@ actual fun WebView( modifier = modifier, factory = { context -> AndroidWebView(context).apply { + // Enable JavaScript and DOM storage settings.javaScriptEnabled = true settings.domStorageEnabled = true + + // Enable zoom controls settings.setSupportZoom(true) settings.builtInZoomControls = true settings.displayZoomControls = false + // Enable persistent cookies and cache for logged-in sessions + settings.databaseEnabled = true + settings.setGeolocationEnabled(false) + + // Enable cookies and persistent storage + CookieManager.getInstance().setAcceptCookie(true) + CookieManager.getInstance().setAcceptThirdPartyCookies(this, true) + + // Cache configuration for better performance + settings.cacheMode = android.webkit.WebSettings.LOAD_DEFAULT + + // User agent (keep default) + settings.userAgentString = settings.userAgentString + webViewClient = WebViewClient() webChromeClient = WebChromeClient() diff --git a/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt new file mode 100644 index 0000000..0e0182b --- /dev/null +++ b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt @@ -0,0 +1,118 @@ +package llc.lookatwhataicando.codeoba.core.ui + +import android.webkit.CookieManager +import android.webkit.WebChromeClient +import android.webkit.WebViewClient +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import com.google.accompanist.swiperefresh.SwipeRefresh +import com.google.accompanist.swiperefresh.rememberSwipeRefreshState +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import android.webkit.WebView as AndroidWebView + +/** + * Android implementation of WebView with back navigation support. + * Handles: + * - Back press to navigate WebView history + * - Cookie persistence for logged-in sessions + * - Pull-to-refresh gesture + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +actual fun WebViewWithBackHandler( + url: String, + modifier: Modifier, + onWebViewCreated: ((Any?) -> Unit)? +) { + var webView by remember { mutableStateOf(null) } + var canGoBack by remember { mutableStateOf(false) } + var isRefreshing by remember { mutableStateOf(false) } + val swipeRefreshState = rememberSwipeRefreshState(isRefreshing) + val coroutineScope = rememberCoroutineScope() + + // Handle back navigation + BackHandler(enabled = canGoBack) { + webView?.goBack() + } + + SwipeRefresh( + state = swipeRefreshState, + onRefresh = { + isRefreshing = true + webView?.reload() + coroutineScope.launch { + delay(1000) + isRefreshing = false + } + }, + modifier = modifier + ) { + Box(modifier = Modifier.fillMaxSize()) { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + AndroidWebView(context).apply { + // Enable JavaScript and DOM storage + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + + // Enable zoom controls + settings.setSupportZoom(true) + settings.builtInZoomControls = true + settings.displayZoomControls = false + + // Enable persistent cookies and cache for logged-in sessions + settings.databaseEnabled = true + settings.setGeolocationEnabled(false) + + // Enable cookies and persistent storage + CookieManager.getInstance().setAcceptCookie(true) + CookieManager.getInstance().setAcceptThirdPartyCookies(this, true) + + // Cache configuration for better performance + settings.cacheMode = android.webkit.WebSettings.LOAD_DEFAULT + + // User agent (keep default) + settings.userAgentString = settings.userAgentString + + webViewClient = object : WebViewClient() { + override fun doUpdateVisitedHistory( + view: AndroidWebView?, + url: String?, + isReload: Boolean + ) { + super.doUpdateVisitedHistory(view, url, isReload) + // Update canGoBack state whenever navigation history changes + canGoBack = view?.canGoBack() ?: false + } + } + webChromeClient = WebChromeClient() + + loadUrl(url) + onWebViewCreated?.invoke(this) + }.also { + webView = it + } + }, + update = { view -> + if (view.url != url) { + view.loadUrl(url) + } + // Update canGoBack state + canGoBack = view.canGoBack() + } + ) + } + } +} diff --git a/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/AgentTabContent.kt b/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/AgentTabContent.kt index 6b959a5..1ce9212 100644 --- a/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/AgentTabContent.kt +++ b/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/AgentTabContent.kt @@ -10,10 +10,15 @@ import androidx.compose.ui.unit.dp /** * Agent tab content displaying the GitHub Copilot Agents page in a browser. * Provides easy navigation with gestures support. + * + * @param onWebViewCreated Callback when WebView is created, provides the WebView instance */ @OptIn(ExperimentalMaterial3Api::class) @Composable -fun AgentTabContent(modifier: Modifier = Modifier) { +fun AgentTabContent( + modifier: Modifier = Modifier, + onWebViewCreated: ((Any?) -> Unit)? = null +) { Column(modifier = modifier.fillMaxSize()) { // Header Surface( @@ -36,9 +41,10 @@ fun AgentTabContent(modifier: Modifier = Modifier) { } // WebView content - WebView( + WebViewWithBackHandler( url = "https://github.com/copilot/agents", - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), + onWebViewCreated = onWebViewCreated ) } } diff --git a/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt b/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt new file mode 100644 index 0000000..710d37d --- /dev/null +++ b/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt @@ -0,0 +1,19 @@ +package llc.lookatwhataicando.codeoba.core.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +/** + * Platform-agnostic WebView component with back navigation support. + * Handles back press to navigate within the WebView history. + * + * @param url The URL to load in the WebView + * @param modifier The modifier to apply to the WebView + * @param onWebViewCreated Callback when WebView is created (for platform-specific handling) + */ +@Composable +expect fun WebViewWithBackHandler( + url: String, + modifier: Modifier = Modifier, + onWebViewCreated: ((Any?) -> Unit)? = null +) diff --git a/core/src/desktopMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt b/core/src/desktopMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt new file mode 100644 index 0000000..3e5cedd --- /dev/null +++ b/core/src/desktopMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt @@ -0,0 +1,26 @@ +package llc.lookatwhataicando.codeoba.core.ui + +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.SwingPanel +import javafx.application.Platform +import javafx.embed.swing.JFXPanel +import javafx.scene.Scene +import javafx.scene.web.WebView as JFXWebView +import java.awt.BorderLayout +import javax.swing.JPanel + +/** + * Desktop implementation of WebView with back navigation support. + * Note: Desktop back navigation is handled via browser built-in controls. + */ +@Composable +actual fun WebViewWithBackHandler( + url: String, + modifier: Modifier, + onWebViewCreated: ((Any?) -> Unit)? +) { + // Desktop back navigation is handled by browser controls + // Just delegate to the standard WebView + WebView(url = url, modifier = modifier) +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 19752ee..010cf3c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,6 +20,9 @@ audioswitch = "1.2.4" # JavaFX for WebView javafx = "17.0.2" +# Accompanist +accompanist = "0.34.0" + # AndroidX androidx-activity-compose = "1.12.2" androidx-lifecycle-viewmodel-compose = "2.10.0" @@ -61,6 +64,9 @@ javafx-controls = { module = "org.openjfx:javafx-controls", version.ref = "javaf javafx-web = { module = "org.openjfx:javafx-web", version.ref = "javafx" } javafx-swing = { module = "org.openjfx:javafx-swing", version.ref = "javafx" } +# Accompanist +accompanist-swiperefresh = { module = "com.google.accompanist:accompanist-swiperefresh", version.ref = "accompanist" } + [plugins] kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } From f20a5eeeb5b7af5ee65881201a7e298141928669 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 10:38:09 +0000 Subject: [PATCH 06/22] Remove redundant user agent setting and unnecessary ExperimentalMaterial3Api annotation Co-authored-by: paulpv <1393897+paulpv@users.noreply.github.com> --- .../kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt | 3 --- .../codeoba/core/ui/WebViewWithBackHandler.kt | 5 ----- 2 files changed, 8 deletions(-) diff --git a/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt index 1fdf978..281eeb7 100644 --- a/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt +++ b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt @@ -48,9 +48,6 @@ actual fun WebView( // Cache configuration for better performance settings.cacheMode = android.webkit.WebSettings.LOAD_DEFAULT - // User agent (keep default) - settings.userAgentString = settings.userAgentString - webViewClient = WebViewClient() webChromeClient = WebChromeClient() diff --git a/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt index 0e0182b..31e8a01 100644 --- a/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt +++ b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt @@ -6,7 +6,6 @@ import android.webkit.WebViewClient import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -28,7 +27,6 @@ import android.webkit.WebView as AndroidWebView * - Cookie persistence for logged-in sessions * - Pull-to-refresh gesture */ -@OptIn(ExperimentalMaterial3Api::class) @Composable actual fun WebViewWithBackHandler( url: String, @@ -83,9 +81,6 @@ actual fun WebViewWithBackHandler( // Cache configuration for better performance settings.cacheMode = android.webkit.WebSettings.LOAD_DEFAULT - // User agent (keep default) - settings.userAgentString = settings.userAgentString - webViewClient = object : WebViewClient() { override fun doUpdateVisitedHistory( view: AndroidWebView?, From 287d7e03e1adccd59544372157b108e678ba5c54 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 11:09:24 +0000 Subject: [PATCH 07/22] Fix pull-to-refresh gesture conflict with navigation drawer by implementing custom gesture handler Co-authored-by: paulpv <1393897+paulpv@users.noreply.github.com> --- core/build.gradle.kts | 1 - .../codeoba/core/ui/WebViewWithBackHandler.kt | 200 ++++++++++++------ 2 files changed, 141 insertions(+), 60 deletions(-) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 6d5081d..83f2bb2 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -56,7 +56,6 @@ kotlin { implementation(libs.ktor.client.okhttp) implementation(libs.webrtc.android) implementation(libs.audioswitch) - implementation(libs.accompanist.swiperefresh) implementation(libs.androidx.activity.compose) } } diff --git a/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt index 31e8a01..b506f3d 100644 --- a/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt +++ b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt @@ -4,28 +4,42 @@ import android.webkit.CookieManager import android.webkit.WebChromeClient import android.webkit.WebViewClient import androidx.activity.compose.BackHandler +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.drag import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.positionChange +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView -import com.google.accompanist.swiperefresh.SwipeRefresh -import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import kotlinx.coroutines.delay import kotlinx.coroutines.launch import android.webkit.WebView as AndroidWebView +import kotlin.math.abs +import kotlin.math.roundToInt /** * Android implementation of WebView with back navigation support. * Handles: * - Back press to navigate WebView history * - Cookie persistence for logged-in sessions - * - Pull-to-refresh gesture + * - Pull-to-refresh gesture (custom implementation to avoid gesture conflicts) */ @Composable actual fun WebViewWithBackHandler( @@ -36,77 +50,145 @@ actual fun WebViewWithBackHandler( var webView by remember { mutableStateOf(null) } var canGoBack by remember { mutableStateOf(false) } var isRefreshing by remember { mutableStateOf(false) } - val swipeRefreshState = rememberSwipeRefreshState(isRefreshing) + var pullOffset by remember { mutableFloatStateOf(0f) } val coroutineScope = rememberCoroutineScope() + val refreshThreshold = with(LocalDensity.current) { 80.dp.toPx() } + // Handle back navigation BackHandler(enabled = canGoBack) { webView?.goBack() } - SwipeRefresh( - state = swipeRefreshState, - onRefresh = { - isRefreshing = true - webView?.reload() - coroutineScope.launch { - delay(1000) - isRefreshing = false - } - }, + Box( modifier = modifier - ) { - Box(modifier = Modifier.fillMaxSize()) { - AndroidView( - modifier = Modifier.fillMaxSize(), - factory = { context -> - AndroidWebView(context).apply { - // Enable JavaScript and DOM storage - settings.javaScriptEnabled = true - settings.domStorageEnabled = true - - // Enable zoom controls - settings.setSupportZoom(true) - settings.builtInZoomControls = true - settings.displayZoomControls = false - - // Enable persistent cookies and cache for logged-in sessions - settings.databaseEnabled = true - settings.setGeolocationEnabled(false) - - // Enable cookies and persistent storage - CookieManager.getInstance().setAcceptCookie(true) - CookieManager.getInstance().setAcceptThirdPartyCookies(this, true) - - // Cache configuration for better performance - settings.cacheMode = android.webkit.WebSettings.LOAD_DEFAULT + .fillMaxSize() + .pointerInput(Unit) { + awaitEachGesture { + val down = awaitFirstDown() + var totalDrag = 0f + + // Check if WebView is scrolled to top + val isAtTop = webView?.scrollY == 0 + + if (isAtTop && !isRefreshing) { + drag(down.id) { change -> + val dragAmount = change.positionChange().y + + // Only handle downward drags when at top + if (dragAmount > 0 || totalDrag > 0) { + totalDrag += dragAmount + + // Apply resistance to the drag + val resistance = if (totalDrag > refreshThreshold) 0.3f else 0.5f + pullOffset = (totalDrag * resistance).coerceAtLeast(0f) + + // Consume the change to prevent the drawer from opening + if (abs(dragAmount) > abs(change.positionChange().x)) { + change.consume() + } + } + } - webViewClient = object : WebViewClient() { - override fun doUpdateVisitedHistory( - view: AndroidWebView?, - url: String?, - isReload: Boolean - ) { - super.doUpdateVisitedHistory(view, url, isReload) - // Update canGoBack state whenever navigation history changes - canGoBack = view?.canGoBack() ?: false + // Trigger refresh if threshold met + if (totalDrag > refreshThreshold) { + isRefreshing = true + coroutineScope.launch { + webView?.reload() + delay(1000) + isRefreshing = false } } - webChromeClient = WebChromeClient() - loadUrl(url) - onWebViewCreated?.invoke(this) - }.also { - webView = it + // Animate back to 0 + coroutineScope.launch { + val start = pullOffset + val duration = 200L + val startTime = System.currentTimeMillis() + + while (pullOffset > 0) { + val elapsed = System.currentTimeMillis() - startTime + val progress = (elapsed.toFloat() / duration).coerceIn(0f, 1f) + pullOffset = start * (1f - progress) + + if (progress >= 1f) { + pullOffset = 0f + break + } + delay(16) + } + } } - }, - update = { view -> - if (view.url != url) { - view.loadUrl(url) + } + } + ) { + // WebView + AndroidView( + modifier = Modifier + .fillMaxSize() + .offset { IntOffset(0, pullOffset.roundToInt()) }, + factory = { context -> + AndroidWebView(context).apply { + // Enable JavaScript and DOM storage + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + + // Enable zoom controls + settings.setSupportZoom(true) + settings.builtInZoomControls = true + settings.displayZoomControls = false + + // Enable persistent cookies and cache for logged-in sessions + settings.databaseEnabled = true + settings.setGeolocationEnabled(false) + + // Enable cookies and persistent storage + CookieManager.getInstance().setAcceptCookie(true) + CookieManager.getInstance().setAcceptThirdPartyCookies(this, true) + + // Cache configuration for better performance + settings.cacheMode = android.webkit.WebSettings.LOAD_DEFAULT + + webViewClient = object : WebViewClient() { + override fun doUpdateVisitedHistory( + view: AndroidWebView?, + url: String?, + isReload: Boolean + ) { + super.doUpdateVisitedHistory(view, url, isReload) + // Update canGoBack state whenever navigation history changes + canGoBack = view?.canGoBack() ?: false + } } - // Update canGoBack state - canGoBack = view.canGoBack() + webChromeClient = WebChromeClient() + + loadUrl(url) + onWebViewCreated?.invoke(this) + }.also { + webView = it + } + }, + update = { view -> + if (view.url != url) { + view.loadUrl(url) } + // Update canGoBack state + canGoBack = view.canGoBack() + } + ) + + // Refresh indicator + if (isRefreshing || pullOffset > 0) { + CircularProgressIndicator( + modifier = Modifier + .align(Alignment.TopCenter) + .offset { IntOffset(0, (pullOffset * 0.5f).roundToInt()) } + .size(32.dp) + .graphicsLayer { + alpha = (pullOffset / refreshThreshold).coerceIn(0f, 1f) + scaleX = (pullOffset / refreshThreshold).coerceIn(0f, 1f) + scaleY = (pullOffset / refreshThreshold).coerceIn(0f, 1f) + } ) } } From 4cbbb389a9ddc14c6219fc2137b0b7aef46b2d21 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 19:37:11 +0000 Subject: [PATCH 08/22] Fix WebView black screen and improve pull-to-refresh reliability with scroll monitoring and enhanced web settings Co-authored-by: paulpv <1393897+paulpv@users.noreply.github.com> --- .../codeoba/core/ui/WebViewWithBackHandler.kt | 70 ++++++++++++++++--- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt index b506f3d..544afaf 100644 --- a/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt +++ b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt @@ -1,7 +1,12 @@ package llc.lookatwhataicando.codeoba.core.ui +import android.annotation.SuppressLint +import android.graphics.Bitmap +import android.util.Log import android.webkit.CookieManager import android.webkit.WebChromeClient +import android.webkit.WebResourceRequest +import android.webkit.WebView import android.webkit.WebViewClient import androidx.activity.compose.BackHandler import androidx.compose.foundation.gestures.awaitEachGesture @@ -15,6 +20,7 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -30,7 +36,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import android.webkit.WebView as AndroidWebView import kotlin.math.abs import kotlin.math.roundToInt @@ -41,16 +46,18 @@ import kotlin.math.roundToInt * - Cookie persistence for logged-in sessions * - Pull-to-refresh gesture (custom implementation to avoid gesture conflicts) */ +@SuppressLint("SetJavaScriptEnabled") @Composable actual fun WebViewWithBackHandler( url: String, modifier: Modifier, onWebViewCreated: ((Any?) -> Unit)? ) { - var webView by remember { mutableStateOf(null) } + var webView by remember { mutableStateOf(null) } var canGoBack by remember { mutableStateOf(false) } var isRefreshing by remember { mutableStateOf(false) } var pullOffset by remember { mutableFloatStateOf(0f) } + var scrollY by remember { mutableIntStateOf(0) } val coroutineScope = rememberCoroutineScope() val refreshThreshold = with(LocalDensity.current) { 80.dp.toPx() } @@ -69,7 +76,7 @@ actual fun WebViewWithBackHandler( var totalDrag = 0f // Check if WebView is scrolled to top - val isAtTop = webView?.scrollY == 0 + val isAtTop = scrollY <= 0 if (isAtTop && !isRefreshing) { drag(down.id) { change -> @@ -128,7 +135,7 @@ actual fun WebViewWithBackHandler( .fillMaxSize() .offset { IntOffset(0, pullOffset.roundToInt()) }, factory = { context -> - AndroidWebView(context).apply { + WebView(context).apply { // Enable JavaScript and DOM storage settings.javaScriptEnabled = true settings.domStorageEnabled = true @@ -149,18 +156,66 @@ actual fun WebViewWithBackHandler( // Cache configuration for better performance settings.cacheMode = android.webkit.WebSettings.LOAD_DEFAULT + // Enable modern web features + settings.mixedContentMode = android.webkit.WebSettings.MIXED_CONTENT_ALWAYS_ALLOW + settings.allowFileAccess = true + settings.allowContentAccess = true + + // Enable hardware acceleration + setLayerType(android.view.View.LAYER_TYPE_HARDWARE, null) + webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + Log.d("WebView", "Page started loading: $url") + } + + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + Log.d("WebView", "Page finished loading: $url") + scrollY = view?.scrollY ?: 0 + } + override fun doUpdateVisitedHistory( - view: AndroidWebView?, + view: WebView?, url: String?, isReload: Boolean ) { super.doUpdateVisitedHistory(view, url, isReload) - // Update canGoBack state whenever navigation history changes canGoBack = view?.canGoBack() ?: false + scrollY = view?.scrollY ?: 0 + } + + override fun shouldOverrideUrlLoading( + view: WebView?, + request: WebResourceRequest? + ): Boolean { + // Allow all navigation within the WebView + return false } + + override fun onReceivedError( + view: WebView?, + errorCode: Int, + description: String?, + failingUrl: String? + ) { + super.onReceivedError(view, errorCode, description, failingUrl) + Log.e("WebView", "Error loading page: $description (code: $errorCode) URL: $failingUrl") + } + } + + webChromeClient = object : WebChromeClient() { + override fun onProgressChanged(view: WebView?, newProgress: Int) { + super.onProgressChanged(view, newProgress) + Log.d("WebView", "Loading progress: $newProgress%") + } + } + + // Monitor scroll changes + setOnScrollChangeListener { _, _, newScrollY, _, _ -> + scrollY = newScrollY } - webChromeClient = WebChromeClient() loadUrl(url) onWebViewCreated?.invoke(this) @@ -172,7 +227,6 @@ actual fun WebViewWithBackHandler( if (view.url != url) { view.loadUrl(url) } - // Update canGoBack state canGoBack = view.canGoBack() } ) From f434e5a79b9c5793b4ce338a51ec8bf9e1ea820d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 21:08:34 +0000 Subject: [PATCH 09/22] Fix WebView black screen by switching to software rendering and forcing visibility/layout updates Co-authored-by: paulpv <1393897+paulpv@users.noreply.github.com> --- .../codeoba/core/ui/WebViewWithBackHandler.kt | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt index 544afaf..51c9856 100644 --- a/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt +++ b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt @@ -161,8 +161,15 @@ actual fun WebViewWithBackHandler( settings.allowFileAccess = true settings.allowContentAccess = true - // Enable hardware acceleration - setLayerType(android.view.View.LAYER_TYPE_HARDWARE, null) + // Enable hardware acceleration (but use software for better Compose compatibility) + // Hardware acceleration can cause black screen issues in some Compose scenarios + setLayerType(android.view.View.LAYER_TYPE_SOFTWARE, null) + + // Force WebView to be visible + visibility = android.view.View.VISIBLE + + // Enable additional rendering settings + setBackgroundColor(android.graphics.Color.WHITE) webViewClient = object : WebViewClient() { override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { @@ -174,6 +181,13 @@ actual fun WebViewWithBackHandler( super.onPageFinished(view, url) Log.d("WebView", "Page finished loading: $url") scrollY = view?.scrollY ?: 0 + + // Force redraw to fix black screen issues + view?.let { + it.visibility = android.view.View.VISIBLE + it.invalidate() + it.requestLayout() + } } override fun doUpdateVisitedHistory( @@ -228,6 +242,9 @@ actual fun WebViewWithBackHandler( view.loadUrl(url) } canGoBack = view.canGoBack() + + // Ensure visibility + view.visibility = android.view.View.VISIBLE } ) From bcdf1ebee357bde1c963ed6fe5c76604e71ffb0c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 21:49:55 +0000 Subject: [PATCH 10/22] Add standalone TestWebViewActivity for debugging WebView rendering issues Co-authored-by: paulpv <1393897+paulpv@users.noreply.github.com> --- app-android/src/main/AndroidManifest.xml | 6 + .../codeoba/android/MainActivity.kt | 14 ++- .../codeoba/android/TestWebViewActivity.kt | 115 ++++++++++++++++++ .../codeoba/core/ui/CodeobaUI.kt | 23 +++- 4 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/TestWebViewActivity.kt diff --git a/app-android/src/main/AndroidManifest.xml b/app-android/src/main/AndroidManifest.xml index f9a0948..dcd71b6 100644 --- a/app-android/src/main/AndroidManifest.xml +++ b/app-android/src/main/AndroidManifest.xml @@ -25,5 +25,11 @@ + + diff --git a/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/MainActivity.kt b/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/MainActivity.kt index 0954202..d5204bd 100644 --- a/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/MainActivity.kt +++ b/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/MainActivity.kt @@ -120,7 +120,19 @@ class MainActivity : ComponentActivity() { ?: "https://api.openai.com/v1/realtime" ) - CodeobaUI(app = codeobaApp, config = config) + CodeobaUI( + app = codeobaApp, + config = config, + onTestWebViewClick = { + // Launch test WebView activity + startActivity( + android.content.Intent( + this@MainActivity, + TestWebViewActivity::class.java + ) + ) + } + ) } } } diff --git a/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/TestWebViewActivity.kt b/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/TestWebViewActivity.kt new file mode 100644 index 0000000..da91f41 --- /dev/null +++ b/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/TestWebViewActivity.kt @@ -0,0 +1,115 @@ +package llc.lookatwhataicando.codeoba.android + +import android.annotation.SuppressLint +import android.os.Bundle +import android.util.Log +import android.webkit.CookieManager +import android.webkit.WebChromeClient +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView + +/** + * Simple test activity with just a WebView - no tabs, no drawer, no pull-to-refresh. + * Used to isolate WebView rendering issues. + */ +class TestWebViewActivity : ComponentActivity() { + + companion object { + private const val TAG = "TestWebViewActivity" + private const val TEST_URL = "https://github.com/copilot/agents" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + MaterialTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + SimpleWebView(url = TEST_URL) + } + } + } + } +} + +@SuppressLint("SetJavaScriptEnabled") +@Composable +fun SimpleWebView(url: String) { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + WebView(context).apply { + Log.d("TestWebViewActivity", "Creating WebView") + + // Basic settings + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + + // Cookie support + CookieManager.getInstance().setAcceptCookie(true) + CookieManager.getInstance().setAcceptThirdPartyCookies(this, true) + + // Cache configuration + settings.cacheMode = android.webkit.WebSettings.LOAD_DEFAULT + settings.databaseEnabled = true + + // Modern web features + settings.mixedContentMode = android.webkit.WebSettings.MIXED_CONTENT_ALWAYS_ALLOW + settings.allowFileAccess = true + settings.allowContentAccess = true + + // Zoom + settings.setSupportZoom(true) + settings.builtInZoomControls = true + settings.displayZoomControls = false + + // Set white background + setBackgroundColor(android.graphics.Color.WHITE) + + // WebView client for logging + webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView?, url: String?, favicon: android.graphics.Bitmap?) { + super.onPageStarted(view, url, favicon) + Log.d("TestWebViewActivity", "Page started: $url") + } + + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + Log.d("TestWebViewActivity", "Page finished: $url") + } + + override fun onReceivedError( + view: WebView?, + errorCode: Int, + description: String?, + failingUrl: String? + ) { + super.onReceivedError(view, errorCode, description, failingUrl) + Log.e("TestWebViewActivity", "Error: $description (code: $errorCode) URL: $failingUrl") + } + } + + webChromeClient = object : WebChromeClient() { + override fun onProgressChanged(view: WebView?, newProgress: Int) { + super.onProgressChanged(view, newProgress) + Log.d("TestWebViewActivity", "Progress: $newProgress%") + } + } + + Log.d("TestWebViewActivity", "Loading URL: $url") + loadUrl(url) + } + } + ) +} diff --git a/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt b/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt index f99ba1f..1772e7b 100644 --- a/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt +++ b/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt @@ -39,6 +39,7 @@ import androidx.compose.material3.Switch import androidx.compose.material3.Tab import androidx.compose.material3.PrimaryTabRow import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable @@ -67,7 +68,11 @@ import llc.lookatwhataicando.codeoba.core.mirror @OptIn(ExperimentalMaterial3Api::class) @Composable -fun CodeobaUI(app: CodeobaApp, config: RealtimeConfig) { +fun CodeobaUI( + app: CodeobaApp, + config: RealtimeConfig, + onTestWebViewClick: (() -> Unit)? = null +) { val connectionState by app.connectionState.collectAsState() val audioCaptureState by app.audioCaptureState.collectAsState() val eventLog by app.eventLog.collectAsState() @@ -96,6 +101,22 @@ fun CodeobaUI(app: CodeobaApp, config: RealtimeConfig) { modifier = Modifier.padding(vertical = 16.dp) ) HorizontalDivider() + + // Test WebView menu item + if (onTestWebViewClick != null) { + androidx.compose.material3.TextButton( + onClick = { + scope.launch { + drawerState.close() + } + onTestWebViewClick() + } + ) { + Text("Test WebView") + } + HorizontalDivider() + } + // Placeholder menu items Text("Settings") Text("About") From 266d9f2e86745c2dc9032ecc25c592944ab22d95 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 22:03:59 +0000 Subject: [PATCH 11/22] Enhance TestWebViewActivity with Scaffold, TopAppBar, and editable address bar for URL testing Co-authored-by: paulpv <1393897+paulpv@users.noreply.github.com> --- .../codeoba/android/TestWebViewActivity.kt | 127 ++++++++++++++++-- 1 file changed, 115 insertions(+), 12 deletions(-) diff --git a/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/TestWebViewActivity.kt b/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/TestWebViewActivity.kt index da91f41..1fe850e 100644 --- a/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/TestWebViewActivity.kt +++ b/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/TestWebViewActivity.kt @@ -9,22 +9,44 @@ import android.webkit.WebView import android.webkit.WebViewClient import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView /** - * Simple test activity with just a WebView - no tabs, no drawer, no pull-to-refresh. - * Used to isolate WebView rendering issues. + * Test activity with WebView and editable address bar. + * Used to isolate and diagnose WebView rendering issues. */ class TestWebViewActivity : ComponentActivity() { companion object { private const val TAG = "TestWebViewActivity" - private const val TEST_URL = "https://github.com/copilot/agents" + private const val DEFAULT_URL = "https://github.com/copilot/agents" } override fun onCreate(savedInstanceState: Bundle?) { @@ -32,22 +54,95 @@ class TestWebViewActivity : ComponentActivity() { setContent { MaterialTheme { - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - SimpleWebView(url = TEST_URL) + TestWebViewScreen(defaultUrl = DEFAULT_URL) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TestWebViewScreen(defaultUrl: String) { + var currentUrl by remember { mutableStateOf(defaultUrl) } + var addressBarText by remember { mutableStateOf(defaultUrl) } + var webView by remember { mutableStateOf(null) } + val focusManager = LocalFocusManager.current + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Test WebView") }, + navigationIcon = { + IconButton(onClick = { /* Activity will handle back */ }) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back") + } + }, + actions = { + IconButton(onClick = { webView?.reload() }) { + Icon(Icons.Default.Refresh, "Reload") + } } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + // Address bar + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + OutlinedTextField( + value = addressBarText, + onValueChange = { addressBarText = it }, + modifier = Modifier.fillMaxWidth(), + label = { Text("URL") }, + singleLine = true, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Go), + keyboardActions = KeyboardActions( + onGo = { + var url = addressBarText.trim() + // Add https:// if no protocol specified + if (!url.startsWith("http://") && !url.startsWith("https://")) { + url = "https://$url" + addressBarText = url + } + currentUrl = url + focusManager.clearFocus() + } + ) + ) } + + // WebView + TestWebView( + url = currentUrl, + modifier = Modifier + .fillMaxWidth() + .weight(1f), + onWebViewCreated = { webView = it }, + onUrlChanged = { newUrl -> + addressBarText = newUrl + } + ) } } } @SuppressLint("SetJavaScriptEnabled") @Composable -fun SimpleWebView(url: String) { +fun TestWebView( + url: String, + modifier: Modifier = Modifier, + onWebViewCreated: (WebView) -> Unit = {}, + onUrlChanged: (String) -> Unit = {} +) { AndroidView( - modifier = Modifier.fillMaxSize(), + modifier = modifier, factory = { context -> WebView(context).apply { Log.d("TestWebViewActivity", "Creating WebView") @@ -77,11 +172,12 @@ fun SimpleWebView(url: String) { // Set white background setBackgroundColor(android.graphics.Color.WHITE) - // WebView client for logging + // WebView client for logging and URL updates webViewClient = object : WebViewClient() { override fun onPageStarted(view: WebView?, url: String?, favicon: android.graphics.Bitmap?) { super.onPageStarted(view, url, favicon) Log.d("TestWebViewActivity", "Page started: $url") + url?.let { onUrlChanged(it) } } override fun onPageFinished(view: WebView?, url: String?) { @@ -109,6 +205,13 @@ fun SimpleWebView(url: String) { Log.d("TestWebViewActivity", "Loading URL: $url") loadUrl(url) + onWebViewCreated(this) + } + }, + update = { view -> + if (view.url != url) { + Log.d("TestWebViewActivity", "Updating URL to: $url") + view.loadUrl(url) } } ) From 16b354df8b7c1fb6cb18794085db334288206300 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 03:11:42 +0000 Subject: [PATCH 12/22] Fix build error by adding missing materialIconsExtended dependency for Icons Co-authored-by: paulpv <1393897+paulpv@users.noreply.github.com> --- app-android/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/app-android/build.gradle.kts b/app-android/build.gradle.kts index 06ed26c..2f0394c 100644 --- a/app-android/build.gradle.kts +++ b/app-android/build.gradle.kts @@ -62,6 +62,7 @@ android { dependencies { implementation(project(":core")) implementation(compose.material3) + implementation(compose.materialIconsExtended) implementation(compose.ui) implementation(compose.uiTooling) implementation(compose.preview) From fff953432cfece2907e9345b37e750b037cd2b6b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 03:37:15 +0000 Subject: [PATCH 13/22] Remove hardware acceleration workarounds to test default rendering behavior Co-authored-by: paulpv <1393897+paulpv@users.noreply.github.com> --- .../codeoba/core/ui/WebViewWithBackHandler.kt | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt index 51c9856..884fe12 100644 --- a/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt +++ b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt @@ -161,14 +161,7 @@ actual fun WebViewWithBackHandler( settings.allowFileAccess = true settings.allowContentAccess = true - // Enable hardware acceleration (but use software for better Compose compatibility) - // Hardware acceleration can cause black screen issues in some Compose scenarios - setLayerType(android.view.View.LAYER_TYPE_SOFTWARE, null) - - // Force WebView to be visible - visibility = android.view.View.VISIBLE - - // Enable additional rendering settings + // Set background color setBackgroundColor(android.graphics.Color.WHITE) webViewClient = object : WebViewClient() { @@ -181,13 +174,6 @@ actual fun WebViewWithBackHandler( super.onPageFinished(view, url) Log.d("WebView", "Page finished loading: $url") scrollY = view?.scrollY ?: 0 - - // Force redraw to fix black screen issues - view?.let { - it.visibility = android.view.View.VISIBLE - it.invalidate() - it.requestLayout() - } } override fun doUpdateVisitedHistory( @@ -242,9 +228,6 @@ actual fun WebViewWithBackHandler( view.loadUrl(url) } canGoBack = view.canGoBack() - - // Ensure visibility - view.visibility = android.view.View.VISIBLE } ) From 6a7a62386021be093eb354f877145ce77eb6f144 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 03:52:47 +0000 Subject: [PATCH 14/22] Fix address bar visibility and WebView rendering with proper layout settings and layer configuration Co-authored-by: paulpv <1393897+paulpv@users.noreply.github.com> --- .../codeoba/android/TestWebViewActivity.kt | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/TestWebViewActivity.kt b/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/TestWebViewActivity.kt index 1fe850e..33b26c2 100644 --- a/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/TestWebViewActivity.kt +++ b/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/TestWebViewActivity.kt @@ -9,6 +9,7 @@ import android.webkit.WebView import android.webkit.WebViewClient import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize @@ -25,6 +26,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable @@ -33,6 +35,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp @@ -89,12 +92,15 @@ fun TestWebViewScreen(defaultUrl: String) { modifier = Modifier .fillMaxSize() .padding(paddingValues) + .background(Color.White) // Ensure white background for entire column ) { - // Address bar - Row( + // Address bar - wrapped in Surface to ensure visibility + Surface( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 8.dp, vertical = 4.dp) + .padding(horizontal = 8.dp, vertical = 4.dp), + color = MaterialTheme.colorScheme.surface, + tonalElevation = 1.dp ) { OutlinedTextField( value = addressBarText, @@ -118,12 +124,13 @@ fun TestWebViewScreen(defaultUrl: String) { ) } - // WebView + // WebView - ensure it's in its own layer TestWebView( url = currentUrl, modifier = Modifier .fillMaxWidth() - .weight(1f), + .weight(1f) + .background(Color.White), // Explicit white background onWebViewCreated = { webView = it }, onUrlChanged = { newUrl -> addressBarText = newUrl @@ -147,6 +154,9 @@ fun TestWebView( WebView(context).apply { Log.d("TestWebViewActivity", "Creating WebView") + // CRITICAL: Enable hardware acceleration explicitly on the view + setLayerType(android.view.View.LAYER_TYPE_HARDWARE, null) + // Basic settings settings.javaScriptEnabled = true settings.domStorageEnabled = true @@ -169,9 +179,19 @@ fun TestWebView( settings.builtInZoomControls = true settings.displayZoomControls = false + // CRITICAL: Use wide viewport for proper rendering + settings.useWideViewPort = true + settings.loadWithOverviewMode = true + + // CRITICAL: Ensure layout algorithm is correct + settings.layoutAlgorithm = android.webkit.WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING + // Set white background setBackgroundColor(android.graphics.Color.WHITE) + // CRITICAL: Force the WebView to be visible + visibility = android.view.View.VISIBLE + // WebView client for logging and URL updates webViewClient = object : WebViewClient() { override fun onPageStarted(view: WebView?, url: String?, favicon: android.graphics.Bitmap?) { @@ -183,6 +203,11 @@ fun TestWebView( override fun onPageFinished(view: WebView?, url: String?) { super.onPageFinished(view, url) Log.d("TestWebViewActivity", "Page finished: $url") + // Force a layout pass after page loads + view?.post { + view.requestLayout() + view.invalidate() + } } override fun onReceivedError( @@ -209,6 +234,8 @@ fun TestWebView( } }, update = { view -> + // Ensure WebView stays visible + view.visibility = android.view.View.VISIBLE if (view.url != url) { Log.d("TestWebViewActivity", "Updating URL to: $url") view.loadUrl(url) From a7fc0dac64599133781b4e19f290c1d711a0458d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 06:58:01 +0000 Subject: [PATCH 15/22] Fix WebView zero height issue by adding explicit MATCH_PARENT layout params Co-authored-by: paulpv <1393897+paulpv@users.noreply.github.com> --- .../codeoba/android/TestWebViewActivity.kt | 6 ++++++ .../codeoba/core/ui/WebViewWithBackHandler.kt | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/TestWebViewActivity.kt b/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/TestWebViewActivity.kt index 33b26c2..557515b 100644 --- a/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/TestWebViewActivity.kt +++ b/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/TestWebViewActivity.kt @@ -154,6 +154,12 @@ fun TestWebView( WebView(context).apply { Log.d("TestWebViewActivity", "Creating WebView") + // CRITICAL: Set layout params to ensure WebView has proper dimensions + layoutParams = android.view.ViewGroup.LayoutParams( + android.view.ViewGroup.LayoutParams.MATCH_PARENT, + android.view.ViewGroup.LayoutParams.MATCH_PARENT + ) + // CRITICAL: Enable hardware acceleration explicitly on the view setLayerType(android.view.View.LAYER_TYPE_HARDWARE, null) diff --git a/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt index 884fe12..89db282 100644 --- a/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt +++ b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt @@ -136,6 +136,12 @@ actual fun WebViewWithBackHandler( .offset { IntOffset(0, pullOffset.roundToInt()) }, factory = { context -> WebView(context).apply { + // CRITICAL: Set layout params to ensure WebView has proper dimensions + layoutParams = android.view.ViewGroup.LayoutParams( + android.view.ViewGroup.LayoutParams.MATCH_PARENT, + android.view.ViewGroup.LayoutParams.MATCH_PARENT + ) + // Enable JavaScript and DOM storage settings.javaScriptEnabled = true settings.domStorageEnabled = true From 30c48aaa5b75b5f8eeacba1f5d342f9220a9aad7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 07:19:52 +0000 Subject: [PATCH 16/22] Fix Desktop WebView crash by removing Platform.startup() and adding error handling Co-authored-by: paulpv <1393897+paulpv@users.noreply.github.com> --- .../codeoba/core/ui/WebView.kt | 65 ++++++++++++------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/core/src/desktopMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt b/core/src/desktopMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt index e8f81ec..b7a4996 100644 --- a/core/src/desktopMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt +++ b/core/src/desktopMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt @@ -12,48 +12,65 @@ import javax.swing.JPanel /** * Desktop implementation of WebView using JavaFX WebView. + * + * Note: JavaFX is automatically initialized when the first JFXPanel is created. + * We don't need to explicitly call Platform.startup() as that can cause issues + * when JavaFX is already initialized. */ @Composable actual fun WebView( url: String, modifier: Modifier ) { - // Initialize JavaFX platform if not already initialized - LaunchedEffect(Unit) { - try { - Platform.startup {} - } catch (e: IllegalStateException) { - // JavaFX platform already initialized - } - } - SwingPanel( modifier = modifier, factory = { JPanel(BorderLayout()).apply { - val jfxPanel = JFXPanel() - add(jfxPanel, BorderLayout.CENTER) - - Platform.runLater { - val view = JFXWebView().apply { - engine.load(url) - } + try { + // Creating JFXPanel automatically initializes JavaFX toolkit + val jfxPanel = JFXPanel() + add(jfxPanel, BorderLayout.CENTER) - val scene = Scene(view) - jfxPanel.scene = scene + // Use Platform.runLater to ensure JavaFX thread is ready + Platform.runLater { + try { + val view = JFXWebView().apply { + engine.load(url) + } + + val scene = Scene(view) + jfxPanel.scene = scene + } catch (e: Exception) { + e.printStackTrace() + println("Error initializing JavaFX WebView: ${e.message}") + } + } + } catch (e: Exception) { + e.printStackTrace() + println("Error creating JFXPanel: ${e.message}") } } }, update = { panel -> // Update URL when it changes - Platform.runLater { - val jfxPanel = panel.components.firstOrNull() as? JFXPanel - jfxPanel?.scene?.let { scene -> - val webView = scene.root as? JFXWebView - if (webView != null && webView.engine.location != url) { - webView.engine.load(url) + try { + Platform.runLater { + try { + val jfxPanel = panel.components.firstOrNull() as? JFXPanel + jfxPanel?.scene?.let { scene -> + val webView = scene.root as? JFXWebView + if (webView != null && webView.engine.location != url) { + webView.engine.load(url) + } + } + } catch (e: Exception) { + e.printStackTrace() + println("Error updating WebView URL: ${e.message}") } } + } catch (e: Exception) { + e.printStackTrace() + println("Error in Platform.runLater: ${e.message}") } } ) From b637a88b9ed63b11ab1659dd2f444eb525c7caa7 Mon Sep 17 00:00:00 2001 From: Paul Peavyhouse Date: Mon, 22 Dec 2025 23:36:30 -0800 Subject: [PATCH 17/22] Add Android WebView.setWebContentsDebuggingEnabled(true) --- .../codeoba/android/TestWebViewActivity.kt | 2 +- .../codeoba/core/ui/WebViewWithBackHandler.kt | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/TestWebViewActivity.kt b/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/TestWebViewActivity.kt index 557515b..3d29ee6 100644 --- a/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/TestWebViewActivity.kt +++ b/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/TestWebViewActivity.kt @@ -170,7 +170,7 @@ fun TestWebView( // Cookie support CookieManager.getInstance().setAcceptCookie(true) CookieManager.getInstance().setAcceptThirdPartyCookies(this, true) - + // Cache configuration settings.cacheMode = android.webkit.WebSettings.LOAD_DEFAULT settings.databaseEnabled = true diff --git a/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt index 89db282..3400414 100644 --- a/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt +++ b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt @@ -1,6 +1,7 @@ package llc.lookatwhataicando.codeoba.core.ui import android.annotation.SuppressLint +import android.content.pm.ApplicationInfo import android.graphics.Bitmap import android.util.Log import android.webkit.CookieManager @@ -135,6 +136,9 @@ actual fun WebViewWithBackHandler( .fillMaxSize() .offset { IntOffset(0, pullOffset.roundToInt()) }, factory = { context -> + if (0 != (context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE)) { + WebView.setWebContentsDebuggingEnabled(true) + } WebView(context).apply { // CRITICAL: Set layout params to ensure WebView has proper dimensions layoutParams = android.view.ViewGroup.LayoutParams( @@ -169,7 +173,7 @@ actual fun WebViewWithBackHandler( // Set background color setBackgroundColor(android.graphics.Color.WHITE) - + webViewClient = object : WebViewClient() { override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { super.onPageStarted(view, url, favicon) @@ -210,7 +214,7 @@ actual fun WebViewWithBackHandler( Log.e("WebView", "Error loading page: $description (code: $errorCode) URL: $failingUrl") } } - + webChromeClient = object : WebChromeClient() { override fun onProgressChanged(view: WebView?, newProgress: Int) { super.onProgressChanged(view, newProgress) From 3732aab1d9a32fdbcd1c9fbbdb370051ad17e07b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 07:40:05 +0000 Subject: [PATCH 18/22] Add kotlinx-coroutines-swing dependency and fix JavaFX ARM64 support for Apple Silicon Macs Co-authored-by: paulpv <1393897+paulpv@users.noreply.github.com> --- core/build.gradle.kts | 7 ++++++- gradle/libs.versions.toml | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 83f2bb2..d7886ef 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -63,13 +63,18 @@ kotlin { val desktopMain by getting { dependencies { implementation(libs.ktor.client.cio) + implementation(libs.kotlinx.coroutines.swing) // Platform-specific JavaFX native libraries for WebView support // Note: Using string interpolation because Gradle catalog doesn't support // classifier-based dependencies. Platform classifier must be determined at runtime. val osName = System.getProperty("os.name").lowercase() + val osArch = System.getProperty("os.arch").lowercase() val platform = when { - osName.contains("mac") || osName.contains("darwin") -> "mac" + osName.contains("mac") || osName.contains("darwin") -> { + // macOS: detect ARM64 (Apple Silicon) vs x86_64 (Intel) + if (osArch.contains("aarch64") || osArch.contains("arm")) "mac-aarch64" else "mac" + } osName.contains("win") -> "win" osName.contains("linux") -> "linux" else -> "linux" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 010cf3c..007d694 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -37,6 +37,7 @@ owasp-dependencycheck = "12.1.9" [libraries] # Kotlinx kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } +kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" } # Ktor From 393cec2d4db25cf7cbbdb5936efd3655f2fcdd46 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 07:56:25 +0000 Subject: [PATCH 19/22] Add javafx-media dependency to fix NoClassDefFoundError for WebView media support Co-authored-by: paulpv <1393897+paulpv@users.noreply.github.com> --- core/build.gradle.kts | 1 + gradle/libs.versions.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index d7886ef..35940a6 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -84,6 +84,7 @@ kotlin { implementation("org.openjfx:javafx-base:$javafxVersion:$platform") implementation("org.openjfx:javafx-graphics:$javafxVersion:$platform") implementation("org.openjfx:javafx-controls:$javafxVersion:$platform") + implementation("org.openjfx:javafx-media:$javafxVersion:$platform") implementation("org.openjfx:javafx-web:$javafxVersion:$platform") implementation("org.openjfx:javafx-swing:$javafxVersion:$platform") } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 007d694..9d87979 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -62,6 +62,7 @@ audioswitch = { module = "com.twilio:audioswitch", version.ref = "audioswitch" } javafx-base = { module = "org.openjfx:javafx-base", version.ref = "javafx" } javafx-graphics = { module = "org.openjfx:javafx-graphics", version.ref = "javafx" } javafx-controls = { module = "org.openjfx:javafx-controls", version.ref = "javafx" } +javafx-media = { module = "org.openjfx:javafx-media", version.ref = "javafx" } javafx-web = { module = "org.openjfx:javafx-web", version.ref = "javafx" } javafx-swing = { module = "org.openjfx:javafx-swing", version.ref = "javafx" } From 4ba9644eb74a1f64b94929b678748a926fe96aaa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 08:07:09 +0000 Subject: [PATCH 20/22] Enable JavaScript and set proper user agent for Desktop WebView to fix CSS rendering Co-authored-by: paulpv <1393897+paulpv@users.noreply.github.com> --- .../lookatwhataicando/codeoba/core/ui/WebView.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/core/src/desktopMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt b/core/src/desktopMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt index b7a4996..721493a 100644 --- a/core/src/desktopMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt +++ b/core/src/desktopMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt @@ -35,7 +35,22 @@ actual fun WebView( Platform.runLater { try { val view = JFXWebView().apply { + // Enable JavaScript + engine.isJavaScriptEnabled = true + + // Set a modern user agent to ensure GitHub serves proper CSS + // Using Chrome on macOS to ensure full CSS/JS support + engine.userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " + + "AppleWebKit/537.36 (KHTML, like Gecko) " + + "Chrome/120.0.0.0 Safari/537.36" + + // Load the URL engine.load(url) + + // Add console logging for debugging + engine.loadWorker.stateProperty().addListener { _, _, newState -> + println("D/WebView: Load state: $newState, URL: ${engine.location}") + } } val scene = Scene(view) From bc507cbdde43a64beeb84a253e047327c97a4427 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 08:52:24 +0000 Subject: [PATCH 21/22] Address security review: Remove file access, unused dependencies, and improve code quality Co-authored-by: paulpv <1393897+paulpv@users.noreply.github.com> --- .../llc/lookatwhataicando/codeoba/core/ui/WebView.kt | 6 ++++++ .../codeoba/core/ui/WebViewWithBackHandler.kt | 7 +++---- .../llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt | 2 +- gradle/libs.versions.toml | 6 ------ 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt index 281eeb7..24af8b0 100644 --- a/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt +++ b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt @@ -28,6 +28,12 @@ actual fun WebView( modifier = modifier, factory = { context -> AndroidWebView(context).apply { + // Set explicit layout params to avoid zero-height rendering issues + layoutParams = android.view.ViewGroup.LayoutParams( + android.view.ViewGroup.LayoutParams.MATCH_PARENT, + android.view.ViewGroup.LayoutParams.MATCH_PARENT + ) + // Enable JavaScript and DOM storage settings.javaScriptEnabled = true settings.domStorageEnabled = true diff --git a/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt index 3400414..178c452 100644 --- a/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt +++ b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt @@ -166,10 +166,9 @@ actual fun WebViewWithBackHandler( // Cache configuration for better performance settings.cacheMode = android.webkit.WebSettings.LOAD_DEFAULT - // Enable modern web features - settings.mixedContentMode = android.webkit.WebSettings.MIXED_CONTENT_ALWAYS_ALLOW - settings.allowFileAccess = true - settings.allowContentAccess = true + // Security: Restrict file access since we only load remote HTTPS content + settings.allowFileAccess = false + settings.allowContentAccess = false // Set background color setBackgroundColor(android.graphics.Color.WHITE) diff --git a/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt b/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt index 1772e7b..17a6258 100644 --- a/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt +++ b/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt @@ -104,7 +104,7 @@ fun CodeobaUI( // Test WebView menu item if (onTestWebViewClick != null) { - androidx.compose.material3.TextButton( + TextButton( onClick = { scope.launch { drawerState.close() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9d87979..2059f54 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,9 +20,6 @@ audioswitch = "1.2.4" # JavaFX for WebView javafx = "17.0.2" -# Accompanist -accompanist = "0.34.0" - # AndroidX androidx-activity-compose = "1.12.2" androidx-lifecycle-viewmodel-compose = "2.10.0" @@ -66,9 +63,6 @@ javafx-media = { module = "org.openjfx:javafx-media", version.ref = "javafx" } javafx-web = { module = "org.openjfx:javafx-web", version.ref = "javafx" } javafx-swing = { module = "org.openjfx:javafx-swing", version.ref = "javafx" } -# Accompanist -accompanist-swiperefresh = { module = "com.google.accompanist:accompanist-swiperefresh", version.ref = "accompanist" } - [plugins] kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } From 451188c69af75b2d566e71f7ad23cf3a4a79c845 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 08:57:21 +0000 Subject: [PATCH 22/22] Update documentation for tabbed UI with Agent browser implementation Co-authored-by: paulpv <1393897+paulpv@users.noreply.github.com> --- README.md | 10 +- docs/IMPLEMENTATION_STATUS.md | 174 +++++++++++++++++++++++++++++----- 2 files changed, 158 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index f6a7b0c..4592774 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ Future support planned for: - **Multi-platform**: Single Kotlin codebase targeting all major platforms - **Audio routing**: Smart Bluetooth headset and device audio management - **Event logging**: Clear visibility into voice → transcript → actions → results +- **Tabbed interface**: Monitor GitHub Copilot Agents progress alongside voice interaction +- **WebView integration**: Cross-platform browser components for GitHub Copilot Agents (fully functional on Android, limited on Desktop) --- @@ -113,10 +115,10 @@ Before running, you need to configure your OpenAI API key. See [docs/DEVELOPMENT | Component | Status | |-----------|--------| -| Desktop App | ✅ Functional | -| Android App | ✅ Ready (95%) | -| Shared UI | 🟡 Basic (60%) | -| Realtime API | 🔴 Stub (10%) | +| Desktop App | ✅ Functional (with WebView) | +| Android App | ✅ Ready (enhanced with WebView) | +| Shared UI | 🟡 Tabbed Interface (85%) | +| Realtime API | 🔴 Stub (Desktop), ✅ Complete (Android) | | MCP Client | 🔴 Stub (10%) | | iOS App | 🔴 Stub (5%) | diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md index 975431f..2c08319 100644 --- a/docs/IMPLEMENTATION_STATUS.md +++ b/docs/IMPLEMENTATION_STATUS.md @@ -1,6 +1,6 @@ # Codeoba Implementation Status -**Last Updated:** December 18, 2025 +**Last Updated:** December 23, 2025 This document tracks the **current implementation status and roadmap** for Codeoba features. @@ -43,11 +43,12 @@ This document tracks the **current implementation status and roadmap** for Codeo |-----------|--------|------------| | Project Structure | ✅ Complete | 100% | | Core Abstractions | ✅ Complete | 100% | -| Desktop App | 🟡 Basic Structure | 70% | -| Android App | 🟡 Basic Structure | 80% | -| Shared UI | 🟡 Improved Layout | 75% | +| Desktop App | 🟡 Enhanced with WebView | 75% | +| Android App | 🟡 Enhanced with WebView | 85% | +| Shared UI | 🟡 Tabbed Interface | 85% | | Phase 1: Realtime Connection (Android) | ✅ Complete | 100% | | Phase 2: Android Audio & Playback | 🟡 In Progress | 90% | +| Phase 2.5: Tabbed UI with Agent Browser | ✅ Complete | 100% | | Phase 3: iOS Implementation | 🔴 Not Started | 0% | | Phase 4: MCP Protocol | 🔴 Not Started | 0% | | Phase 5: Desktop WebRTC Integration | 🔴 Not Started | 0% | @@ -136,42 +137,83 @@ This document tracks the **current implementation status and roadmap** for Codeo ### 5. Shared UI (Compose Multiplatform) -**Implementation:** 🟡 Improved Layout (75%) +**Implementation:** 🟡 Enhanced with Tabbed Interface (85%) Current UI includes: -- ✅ **Titlebar with Connect Switch** (improved ergonomics) - - App name display - - Connection status text - - Switch control (ON = connect, OFF = disconnect) - - Primary container surface with elevation -- ✅ **Push-to-talk button in footer** (thumb-accessible positioning) - - Large 72dp height button for easy access - - Status text above button - - Elevated surface with shadow for prominence - - Color-coded: blue → red when recording -- ✅ Text input panel (separated from voice controls) -- ✅ Audio route selection panel -- ✅ Event log display (auto-expands to fill space) +- ✅ **Tabbed Navigation** (NEW - December 23, 2025) + - Two distinct tabs: "Realtime" and "Agent" + - Smooth tab switching with isolated content + - Material 3 tab design with proper indicators +- ✅ **Realtime Tab** - Original voice interaction UI + - Titlebar with Connect Switch (improved ergonomics) + - App name display + - Connection status text + - Switch control (ON = connect, OFF = disconnect) + - Primary container surface with elevation + - Push-to-talk button in footer (thumb-accessible positioning) + - Large 72dp height button for easy access + - Status text above button + - Elevated surface with shadow for prominence + - Color-coded: blue → red when recording + - Text input panel (separated from voice controls) + - Audio route selection panel + - Event log display (auto-expands to fill space) +- ✅ **Agent Tab** - GitHub Copilot Agents browser view (NEW - December 23, 2025) + - **Android**: Full WebView with proper rendering + - Cookie persistence for login sessions + - JavaScript enabled with security sandboxing + - Pull-to-refresh gesture handler + - Back navigation through browser history + - Chrome DevTools remote debugging support + - **Desktop**: JavaFX WebView with limited functionality + - Basic page rendering + - JavaScript enabled + - Known limitations: older WebKit engine, no DevTools +- ✅ **Test WebView Activity** (Android debug tool) + - Isolated WebView testing environment + - Editable address bar with protocol auto-addition + - Full browser UI with navigation controls + - Access via hamburger menu → "Test WebView" - ✅ Material 3 design system **What's Working:** -- Desktop UI structure is implemented with improved layout -- Android UI integrates with service interfaces +- Desktop and Android UI with tabbed navigation +- Smooth tab transitions with content isolation +- Android WebView fully functional with proper CSS/JS rendering +- Desktop WebView provides basic browsing (limited by JavaFX WebKit) - State management uses reactive flows -- Three-tier layout: Titlebar (controls) → Content → Footer (PTT) +- Three-tier layout in Realtime tab: Titlebar (controls) → Content → Footer (PTT) - Optimized for one-handed mobile use -**Recent Improvements (December 18, 2025):** +**Recent Improvements (December 23, 2025):** +- ✅ Added tabbed UI with Realtime and Agent tabs +- ✅ Implemented cross-platform WebView components +- ✅ Fixed Android WebView rendering (MATCH_PARENT layout params) +- ✅ Added JavaFX WebView for Desktop (with known limitations) +- ✅ Security hardening: disabled file access in production WebView +- ✅ Created TestWebViewActivity debug tool for Android +- ✅ Removed unused dependencies (Accompanist) +- ✅ Code quality improvements + +**Previous Improvements (December 18, 2025):** - ✅ Moved PTT button to footer for thumb accessibility - ✅ Replaced Connect button with Switch in titlebar - ✅ Reorganized content area for better hierarchy - ✅ Improved visual separation between UI zones +**Known Limitations:** +- Desktop WebView uses older JavaFX WebKit engine + - Plain appearance on complex modern web apps + - GitHub authentication may not work fully + - No Chrome DevTools debugging support + - **Recommendation**: Use Android app for full Agent tab functionality + **Future Enhancements:** - Visual recording indicator (waveform animation) - Richer event display with syntax highlighting - Settings panel for configuration - Dark mode support +- Desktop: Consider alternative browser component (CEF) for better modern web support ### 6. Security & Configuration - ✅ No hardcoded API keys @@ -254,6 +296,93 @@ This section outlines the planned implementation sequence for remaining features **Completion:** ~90% (see [GitHub Issues](https://github.com/LookAtWhatAiCanDo/Codeoba/issues?q=is%3Aissue+label%3Aphase-2) for detailed tracking) +### Phase 2.5: Tabbed UI with Agent Browser ✅ COMPLETE + +**Goal:** Add tabbed interface with Realtime and Agent tabs for monitoring GitHub Copilot Agents + +**Status:** ✅ Complete (December 23, 2025) + +**Completion:** 100% + +**Completed Tasks:** +1. ✅ **Tabbed Navigation UI** - Tab switching between Realtime and Agent views + - Material 3 tab design with proper indicators + - Smooth content transitions + - Isolated tab content + - Completed: December 23, 2025 + +2. ✅ **Android WebView Implementation** - Full-featured browser in Agent tab + - WebView with MATCH_PARENT layout params (fixes zero-height rendering issue) + - Cookie persistence for GitHub login sessions + - JavaScript enabled with proper sandboxing + - File access disabled for security (HTTPS-only content) + - Custom pull-to-refresh gesture handler + - Back navigation through browser history with BackHandler + - Chrome DevTools remote debugging support via `chrome://inspect/` + - Completed: December 23, 2025 + +3. ✅ **Desktop WebView Implementation** - JavaFX WebView in Agent tab + - JavaFX WebView with JavaScript enabled + - Modern Chrome user agent string + - ARM64 platform detection for Apple Silicon Macs + - JavaFX Media module for media content support + - kotlinx-coroutines-swing for proper Swing/JavaFX integration + - Known limitations documented (older WebKit engine) + - Completed: December 23, 2025 + +4. ✅ **TestWebViewActivity Debug Tool** (Android) - Isolated WebView testing + - Standalone test activity with Scaffold and TopAppBar + - Editable address bar for testing any URL + - Back button and refresh functionality + - Comprehensive logging + - Access via hamburger menu → "Test WebView" + - Completed: December 23, 2025 + +5. ✅ **Security Hardening** - Address code review feedback + - Disabled file/content access in production WebView + - Removed unused Accompanist dependency + - Added explicit layout params to all WebViews + - Code quality improvements (removed redundant qualifications) + - Completed: December 23, 2025 + +**Implementation Details:** + +**Android WebView:** +- Loads `https://github.com/copilot/agents` with full functionality +- Cookie persistence maintains login sessions across app restarts +- Custom gesture handler for pull-to-refresh (doesn't conflict with drawer) +- BackHandler intercepts back press when WebView has navigation history +- Security: File access disabled, JavaScript sandboxed, HTTPS-only content +- Chrome DevTools debugging: `chrome://inspect/` on desktop computer + +**Desktop WebView (JavaFX):** +- Loads URLs with basic rendering +- JavaScript enabled with modern user agent +- ARM64 support for Apple Silicon Macs +- **Known Limitations**: + - Older WebKit engine limits modern CSS/JS features + - Complex authentication flows may not work + - No Chrome DevTools debugging support + - **Recommendation**: Use Android app for full functionality + +**What Works:** +- ✅ Tab switching smooth with content isolated to respective tabs +- ✅ Android Agent tab fully functional with GitHub login/navigation +- ✅ Desktop Agent tab provides basic browsing (with limitations) +- ✅ Test WebView activity allows isolated debugging +- ✅ Security hardened: file access disabled, unused dependencies removed +- ✅ Code review feedback addressed + +**Key Files:** +- `core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt` (tabbed UI) +- `core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt` (Android WebView) +- `core/src/desktopMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebView.kt` (Desktop WebView) +- `app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/TestWebViewActivity.kt` (debug tool) +- `core/build.gradle.kts` (JavaFX ARM64 platform detection) +- `gradle/libs.versions.toml` (dependencies) + +> **📋 Note:** Android implementation is production-ready. Desktop implementation has known limitations due to JavaFX WebKit engine constraints. + **Tasks:** 1. ✅ **Android Audio Streaming Integration** → COMPLETE (Issue #14) - 100% - ✅ Refactored to use WebRTC JavaAudioDeviceModule (NOT data channel approach) @@ -524,6 +653,7 @@ Track progress by updating this table as features are completed: | 2 | Android Audio Playback | ✅ Complete | WebRTC handles playback, AudioSwitch for routing, volume control implemented. Completed Dec 18, 2025 | | 2 | Android PTT & Text Input | ✅ Complete | PTT controls WebRTC audio track, text input sends via data channel. Completed Dec 18, 2025 | | 2 | Android Integration Testing | 🔴 Not Started | See Issue #17 | +| 2.5 | Tabbed UI with Agent Browser | ✅ Complete | Android WebView fully functional, Desktop limited by JavaFX WebKit. Completed Dec 23, 2025 | | 3 | iOS Platform | 🔴 Not Started | - | | 3 | iOS Audio Capture | 🔴 Not Started | - | | 3 | iOS Build Setup | 🔴 Not Started | - |