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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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?,
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class OmnigentWebViewClientTest {

client.onPageStarted(webView, PINNED_URL, null)

assertNull(webView.evaluatedScript)
assertTrue(webView.evaluatedScripts.isEmpty())
}

@Test
Expand All @@ -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())
Expand Down Expand Up @@ -282,7 +324,7 @@ class OmnigentWebViewClientTest {
private class RecordingWebView(
context: Context,
) : WebView(context) {
var evaluatedScript: String? = null
val evaluatedScripts = mutableListOf<String>()
var stopLoadingCalled = false
var currentUrl: String? = null
var loadedUrl: String? = null
Expand All @@ -302,7 +344,7 @@ class OmnigentWebViewClientTest {
script: String,
resultCallback: ValueCallback<String>?,
) {
evaluatedScript = script
evaluatedScripts += script
callback = resultCallback
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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 `<style>` nodes.
*/
@Test
fun `source installs the stylesheet at most once per document`() {
val source = WorkspaceChromeScript.source

assertTrue(
source.contains("""document.querySelector("style[data-omnigent-workspace-chrome]")"""),
)
assertTrue(source.contains("return;"))
assertTrue(source.contains("document.documentElement.appendChild(style)"))
}

/**
* The CSS reaches the page as a JSON-encoded string literal (see [jsString]), so
* a quote or newline in it can't break out of the surrounding script.
*/
@Test
fun `source embeds the css as an escaped string literal`() {
assertTrue(WorkspaceChromeScript.source.contains(jsString(WorkspaceChromeScript.css)))
// trimIndent leaves real newlines in the CSS; they must be escaped, not raw.
assertTrue(WorkspaceChromeScript.source.contains("\\n"))
}
}
Loading