From 8d660a82c5784f28f6bd078eb35bb1f4eeacaa66 Mon Sep 17 00:00:00 2001 From: "Zeyi (Rice) Fan" Date: Mon, 10 Aug 2026 15:36:47 -0700 Subject: [PATCH] fix(android): hide the Databricks workspace nav chrome in the WebView shell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Related issue Closes # ## Summary - A workspace-hosted Omnigent is mounted as a Databricks workspace *page*, so the workspace wraps the SPA in its top-nav shell (the dark bar with the workspace switcher). In the Android shell that bar was still painted: it wastes vertical space and, worse, lets a user navigate into another workspace app with no way back into Omnigent. - The electron and iOS shells already hide it; port the same fix to Android. New `WorkspaceChromeScript` holds the CSS plus the install-once JS, and `OmnigentWebViewClient.onPageFinished` evaluates it on every finished pinned-origin load. - Keyed on the pinned origin, never on the URL path: the workspace serves the SPA on more than one mount (`/ml/omnigents`, `/omnigent`) and an auth redirect can land on neither, so a path guard leaves the chrome visible. The rule targets Omnigent's own `.omnigent-app` root rather than the monolith-owned nav markup, so it can't silently break when Databricks reshuffles its chrome, and is a no-op on standalone builds. ## Test Plan - `cd web/android && ./gradlew :app:testDebugUnitTest --tests '*WorkspaceChromeScriptTest' --tests '*OmnigentWebViewClientTest'` — 22 tests, all green. - New `WorkspaceChromeScriptTest` covers the CSS contract, the install-once guard, and that the CSS is embedded as an escaped JS string literal. - `OmnigentWebViewClientTest` now asserts injection order (chrome CSS before the facade, whose callback declares the page ready), injection without the facade fallback, that injection is *not* gated on the UI mount path, and that an off-origin load injects nothing. ## Demo N/A — logic-only parity port; the CSS is unchanged from the electron and iOS shells, which already ship this behaviour. ## Type of change - [x] Bug fix - [ ] Feature - [ ] UI / frontend change - [ ] Refactor / chore - [ ] Docs - [ ] Test / CI - [ ] Breaking change ## Test coverage - [x] Unit tests added / updated - [ ] Integration tests added / updated - [ ] E2E tests added / updated - [ ] Manual verification completed - [ ] Existing tests cover this change - [ ] Not applicable ## Coverage notes Unit tests cover the script contents and the injection points in the WebView client. The visual result is the same CSS the electron and iOS shells already apply. ## Changelog The Android app no longer shows the Databricks workspace navigation bar around Omnigent when connecting to a workspace-hosted server. Signed-off-by: Zeyi (Rice) Fan --- .../omnigent/android/OmnigentWebViewClient.kt | 13 +++++ .../omnigent/android/WorkspaceChromeScript.kt | 57 +++++++++++++++++++ .../android/OmnigentWebViewClientTest.kt | 50 ++++++++++++++-- .../android/WorkspaceChromeScriptTest.kt | 51 +++++++++++++++++ 4 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 web/android/app/src/main/java/ai/omnigent/android/WorkspaceChromeScript.kt create mode 100644 web/android/app/src/test/java/ai/omnigent/android/WorkspaceChromeScriptTest.kt diff --git a/web/android/app/src/main/java/ai/omnigent/android/OmnigentWebViewClient.kt b/web/android/app/src/main/java/ai/omnigent/android/OmnigentWebViewClient.kt index d1b5722fd4..f1b155559a 100644 --- a/web/android/app/src/main/java/ai/omnigent/android/OmnigentWebViewClient.kt +++ b/web/android/app/src/main/java/ai/omnigent/android/OmnigentWebViewClient.kt @@ -19,6 +19,9 @@ import android.webkit.WebViewClient * The facade is normally registered with `addDocumentStartJavaScript` in * `MainActivity`. Older WebViews that support the message listener but not * document-start scripts inject it after the pinned page finishes. + * + * Also injects [WorkspaceChromeScript] once each pinned-origin document finishes, + * so a workspace-hosted server's nav chrome stays hidden. */ class OmnigentWebViewClient( private val pinnedOrigin: () -> String?, @@ -101,6 +104,16 @@ class OmnigentWebViewClient( // An app page loaded, so the mount works: re-arm the bounce budget for // the next time the user lands back on the workspace root. if (onPinnedOrigin && databricksWorkspaceUiUrl(url) == null) rootBounces = 0 + // Databricks workspace-hosted Omnigent renders inside the workspace's + // top-nav chrome (the SPA is a workspace page). Hide it by overlaying + // Omnigent's own root — see [WorkspaceChromeScript], which also explains why + // this is keyed on the pinned origin and never on the URL's path. Re-applied + // on every full load (a server switch is a fresh document); the SPA's + // client-side routing keeps the same document, so the injected stylesheet + // persists across in-app navigation. + if (onPinnedOrigin) { + view.evaluateJavascript(WorkspaceChromeScript.source, null) + } if (onPinnedOrigin && shouldInjectBridgeAtPageReady()) { view.evaluateJavascript(NativeBridgeScript.source) { onPageReady(url) } return diff --git a/web/android/app/src/main/java/ai/omnigent/android/WorkspaceChromeScript.kt b/web/android/app/src/main/java/ai/omnigent/android/WorkspaceChromeScript.kt new file mode 100644 index 0000000000..2ea68bfd92 --- /dev/null +++ b/web/android/app/src/main/java/ai/omnigent/android/WorkspaceChromeScript.kt @@ -0,0 +1,57 @@ +package ai.omnigent.android + +/** + * Hiding the Databricks workspace navigation chrome around a workspace-hosted + * Omnigent SPA. Kept in its own `WebView`-free object so the script is + * unit-testable without a live WebView, matching [NativeBridgeScript] and + * [BlobDownloadScript]. + * + * Ported from `web/electron/src/workspace-chrome.js`; the iOS shell carries the + * same logic in `WorkspaceChromeScript.swift`. Keep the CSS identical in all + * three so a fix in one shell isn't silently missing from the others. + */ +object WorkspaceChromeScript { + /** + * CSS that hides the Databricks workspace navigation chrome. + * + * On a workspace the SPA is mounted as a workspace *page*, so Databricks wraps + * it in its top-nav shell (the dark bar with the workspace switcher). In a + * dedicated app window that chrome is just noise. We promote Omnigent's own + * root — `.omnigent-app`, which the embed entry sets (`web/src/embed.tsx`) — to + * a full-viewport overlay so it paints over the workspace bar. Keying on + * Omnigent's wrapper (defined in THIS repo) rather than the monolith-owned, + * unstable workspace nav markup keeps this from silently breaking when + * Databricks reshuffles its chrome; on a standalone (non-embed) build there is + * no `.omnigent-app`, so the rule is a harmless no-op. + */ + val css: String = + """ + .omnigent-app { + position: fixed !important; + inset: 0 !important; + z-index: 2147483647 !important; + } + """.trimIndent() + + /** + * JS that installs [css] into the current document, at most once. + * + * The caller injects this on every finished main-frame load of the pinned + * origin, and must NOT gate it on the URL's path. The workspace serves the SPA + * on more than one mount (`/ml/omnigents` for the desktop shells, `/omnigent` + * in `omnigent/conversation_browser.py`) and an auth redirect can land on + * neither, so a path guard leaves the workspace switcher visible — from there a + * user navigates into another workspace app with no way back. Do not + * reintroduce a path guard. + */ + val source: String = + """ + (() => { + if (document.querySelector("style[data-omnigent-workspace-chrome]")) return; + const style = document.createElement("style"); + style.dataset.omnigentWorkspaceChrome = "true"; + style.textContent = ${jsString(css)}; + document.documentElement.appendChild(style); + })(); + """.trimIndent() +} diff --git a/web/android/app/src/test/java/ai/omnigent/android/OmnigentWebViewClientTest.kt b/web/android/app/src/test/java/ai/omnigent/android/OmnigentWebViewClientTest.kt index 676e739b4f..329f0d5a63 100644 --- a/web/android/app/src/test/java/ai/omnigent/android/OmnigentWebViewClientTest.kt +++ b/web/android/app/src/test/java/ai/omnigent/android/OmnigentWebViewClientTest.kt @@ -25,7 +25,7 @@ class OmnigentWebViewClientTest { client.onPageStarted(webView, PINNED_URL, null) - assertNull(webView.evaluatedScript) + assertTrue(webView.evaluatedScripts.isEmpty()) } @Test @@ -39,13 +39,55 @@ class OmnigentWebViewClientTest { client.onPageFinished(webView, PINNED_URL) - assertEquals(NativeBridgeScript.source, webView.evaluatedScript) + // Chrome-hide CSS first, then the facade — the facade's callback is what + // declares the page ready, so it has to be the last script evaluated. + assertEquals( + listOf(WorkspaceChromeScript.source, NativeBridgeScript.source), + webView.evaluatedScripts, + ) assertNull(readyUrl) webView.completeEvaluation() assertEquals(PINNED_URL, readyUrl) } + @Test + fun `pinned page finish hides the workspace chrome without the facade fallback`() { + val webView = RecordingWebView(ApplicationProvider.getApplicationContext()) + val client = client(shouldInjectBridgeAtPageReady = false) + + client.onPageFinished(webView, PINNED_URL) + + assertEquals(listOf(WorkspaceChromeScript.source), webView.evaluatedScripts) + } + + @Test + fun `workspace chrome hide is not gated on the ui mount path`() { + val webView = RecordingWebView(ApplicationProvider.getApplicationContext()) + val client = client(shouldInjectBridgeAtPageReady = false) + + // A post-login landing on the pinned server's root, and the `/omnigent` + // mount the CLI records — neither starts with `/ml/omnigents`. Both must + // still get the CSS or the workspace switcher stays visible. + client.onPageFinished(webView, "$PINNED_ORIGIN/") + client.onPageFinished(webView, "$PINNED_ORIGIN/omnigent/c/abc") + + assertEquals( + listOf(WorkspaceChromeScript.source, WorkspaceChromeScript.source), + webView.evaluatedScripts, + ) + } + + @Test + fun `off-origin page finish injects nothing`() { + val webView = RecordingWebView(ApplicationProvider.getApplicationContext()) + val client = client(shouldInjectBridgeAtPageReady = true) + + client.onPageFinished(webView, IDP_URL) + + assertTrue(webView.evaluatedScripts.isEmpty()) + } + @Test fun `idp redirect loads inline when the server authenticates in the webview`() { val webView = RecordingWebView(ApplicationProvider.getApplicationContext()) @@ -282,7 +324,7 @@ class OmnigentWebViewClientTest { private class RecordingWebView( context: Context, ) : WebView(context) { - var evaluatedScript: String? = null + val evaluatedScripts = mutableListOf() var stopLoadingCalled = false var currentUrl: String? = null var loadedUrl: String? = null @@ -302,7 +344,7 @@ class OmnigentWebViewClientTest { script: String, resultCallback: ValueCallback?, ) { - evaluatedScript = script + evaluatedScripts += script callback = resultCallback } diff --git a/web/android/app/src/test/java/ai/omnigent/android/WorkspaceChromeScriptTest.kt b/web/android/app/src/test/java/ai/omnigent/android/WorkspaceChromeScriptTest.kt new file mode 100644 index 0000000000..fe28166229 --- /dev/null +++ b/web/android/app/src/test/java/ai/omnigent/android/WorkspaceChromeScriptTest.kt @@ -0,0 +1,51 @@ +package ai.omnigent.android + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class WorkspaceChromeScriptTest { + /** + * The rule must key on Omnigent's own embed root, not on the monolith-owned + * workspace nav markup, and must win over it (fixed + full-inset + top layer). + */ + @Test + fun `css promotes the embed root to a full-viewport overlay`() { + assertEquals( + """ + .omnigent-app { + position: fixed !important; + inset: 0 !important; + z-index: 2147483647 !important; + } + """.trimIndent(), + WorkspaceChromeScript.css, + ) + } + + /** + * The script runs on every finished load, so re-running it on a document that + * already carries the stylesheet must not stack duplicate `