From 38bbd8832ec4ccead8b9b01fb6b5731394f47d55 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Oct 2025 18:25:43 +0000 Subject: [PATCH 1/3] Initial plan From 62f42ceb1c085b501512bbc479c8210460b3a662 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Oct 2025 18:37:06 +0000 Subject: [PATCH 2/3] Add web:useragenttype meta key with mobile emulation support Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> --- frontend/app/view/webview/webview.tsx | 106 +++++++++++++++++++++++--- frontend/types/gotypes.d.ts | 1 + pkg/waveobj/metaconsts.go | 1 + pkg/waveobj/wtypemeta.go | 7 +- 4 files changed, 100 insertions(+), 15 deletions(-) diff --git a/frontend/app/view/webview/webview.tsx b/frontend/app/view/webview/webview.tsx index a55814ca56..952e7202de 100644 --- a/frontend/app/view/webview/webview.tsx +++ b/frontend/app/view/webview/webview.tsx @@ -22,6 +22,12 @@ import { Atom, PrimitiveAtom, atom, useAtomValue, useSetAtom } from "jotai"; import { Fragment, createRef, memo, useCallback, useEffect, useRef, useState } from "react"; import "./webview.scss"; +// User agent strings for mobile emulation +const USER_AGENT_IPHONE = + "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1"; +const USER_AGENT_ANDROID = + "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.43 Mobile Safari/537.36"; + let webviewPreloadUrl = null; function getWebviewPreloadUrl() { @@ -61,6 +67,7 @@ export class WebViewModel implements ViewModel { searchAtoms?: SearchAtoms; typeaheadOpen: PrimitiveAtom; partitionOverride: PrimitiveAtom | null; + userAgentType: Atom; constructor(blockId: string, nodeModel: BlockNodeModel) { this.nodeModel = nodeModel; @@ -87,6 +94,7 @@ export class WebViewModel implements ViewModel { this.hideNav = getBlockMetaKeyAtom(blockId, "web:hidenav"); this.typeaheadOpen = atom(false); this.partitionOverride = null; + this.userAgentType = getBlockMetaKeyAtom(blockId, "web:useragenttype"); this.mediaPlaying = atom(false); this.mediaMuted = atom(false); @@ -161,20 +169,36 @@ export class WebViewModel implements ViewModel { return null; } const url = get(this.url); - return [ - { + const userAgentType = get(this.userAgentType); + const buttons: IconButtonDecl[] = []; + + // Add mobile indicator icon if using mobile user agent + if (userAgentType === "mobile:iphone" || userAgentType === "mobile:android") { + const mobileIcon = userAgentType === "mobile:iphone" ? "mobile-screen" : "mobile-screen-button"; + const mobileTitle = + userAgentType === "mobile:iphone" ? "Mobile User Agent: iPhone" : "Mobile User Agent: Android"; + buttons.push({ elemtype: "iconbutton", - icon: "arrow-up-right-from-square", - title: "Open in External Browser", - click: () => { - console.log("open external", url); - if (url != null && url != "") { - const externalUrl = this.modifyExternalUrl?.(url) ?? url; - return getApi().openExternal(externalUrl); - } - }, + icon: mobileIcon, + title: mobileTitle, + noAction: true, + }); + } + + buttons.push({ + elemtype: "iconbutton", + icon: "arrow-up-right-from-square", + title: "Open in External Browser", + click: () => { + console.log("open external", url); + if (url != null && url != "") { + const externalUrl = this.modifyExternalUrl?.(url) ?? url; + return getApi().openExternal(externalUrl); + } }, - ]; + }); + + return buttons; }); } @@ -595,6 +619,50 @@ export class WebViewModel implements ViewModel { zoomSubMenu.push(makeZoomFactorMenuItem("175%", 1.75)); zoomSubMenu.push(makeZoomFactorMenuItem("200%", 2)); + // User Agent Type submenu + const curUserAgentType = globalStore.get(this.userAgentType) || "default"; + const userAgentSubMenu: ContextMenuItem[] = [ + { + label: "Default", + type: "checkbox", + click: () => { + fireAndForget(() => { + return RpcApi.SetMetaCommand(TabRpcClient, { + oref: WOS.makeORef("block", this.blockId), + meta: { "web:useragenttype": null }, + }); + }); + }, + checked: curUserAgentType === "default" || curUserAgentType === "", + }, + { + label: "Mobile: iPhone", + type: "checkbox", + click: () => { + fireAndForget(() => { + return RpcApi.SetMetaCommand(TabRpcClient, { + oref: WOS.makeORef("block", this.blockId), + meta: { "web:useragenttype": "mobile:iphone" }, + }); + }); + }, + checked: curUserAgentType === "mobile:iphone", + }, + { + label: "Mobile: Android", + type: "checkbox", + click: () => { + fireAndForget(() => { + return RpcApi.SetMetaCommand(TabRpcClient, { + oref: WOS.makeORef("block", this.blockId), + meta: { "web:useragenttype": "mobile:android" }, + }); + }); + }, + checked: curUserAgentType === "mobile:android", + }, + ]; + const isNavHidden = globalStore.get(this.hideNav); return [ { @@ -622,6 +690,10 @@ export class WebViewModel implements ViewModel { }); }), }, + { + label: "User Agent Type", + submenu: userAgentSubMenu, + }, { label: "Set Zoom Factor", submenu: zoomSubMenu, @@ -735,6 +807,15 @@ const WebView = memo(({ model, onFailLoad, blockRef, initialSrc }: WebViewProps) const partitionOverride = useAtomValueSafe(model.partitionOverride); const metaPartition = useAtomValue(getBlockMetaKeyAtom(model.blockId, "web:partition")); const webPartition = partitionOverride || metaPartition || undefined; + const userAgentType = useAtomValue(model.userAgentType) || "default"; + + // Determine user agent string based on type + let userAgent: string | undefined = undefined; + if (userAgentType === "mobile:iphone") { + userAgent = USER_AGENT_IPHONE; + } else if (userAgentType === "mobile:android") { + userAgent = USER_AGENT_ANDROID; + } // Search const searchProps = useSearch({ anchorRef: model.webviewRef, viewModel: model }); @@ -957,6 +1038,7 @@ const WebView = memo(({ model, onFailLoad, blockRef, initialSrc }: WebViewProps) // @ts-ignore This is a discrepancy between the React typing and the Chromium impl for webviewTag. Chrome webviewTag expects a string, while React expects a boolean. allowpopups="true" partition={webPartition} + useragent={userAgent} /> {errorText && (
diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 123576e21c..678126ed93 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -671,6 +671,7 @@ declare global { "web:zoom"?: number; "web:hidenav"?: boolean; "web:partition"?: string; + "web:useragenttype"?: string; "markdown:fontsize"?: number; "markdown:fixedfontsize"?: number; "tsunami:*"?: boolean; diff --git a/pkg/waveobj/metaconsts.go b/pkg/waveobj/metaconsts.go index e35d21e167..ba6ab067b1 100644 --- a/pkg/waveobj/metaconsts.go +++ b/pkg/waveobj/metaconsts.go @@ -116,6 +116,7 @@ const ( MetaKey_WebZoom = "web:zoom" MetaKey_WebHideNav = "web:hidenav" MetaKey_WebPartition = "web:partition" + MetaKey_WebUserAgentType = "web:useragenttype" MetaKey_MarkdownFontSize = "markdown:fontsize" MetaKey_MarkdownFixedFontSize = "markdown:fixedfontsize" diff --git a/pkg/waveobj/wtypemeta.go b/pkg/waveobj/wtypemeta.go index 7bb357b9e7..9649e4a189 100644 --- a/pkg/waveobj/wtypemeta.go +++ b/pkg/waveobj/wtypemeta.go @@ -117,9 +117,10 @@ type MetaTSType struct { TermShiftEnterNewline *bool `json:"term:shiftenternewline,omitempty"` TermConnDebug string `json:"term:conndebug,omitempty"` // null, info, debug - WebZoom float64 `json:"web:zoom,omitempty"` - WebHideNav *bool `json:"web:hidenav,omitempty"` - WebPartition string `json:"web:partition,omitempty"` + WebZoom float64 `json:"web:zoom,omitempty"` + WebHideNav *bool `json:"web:hidenav,omitempty"` + WebPartition string `json:"web:partition,omitempty"` + WebUserAgentType string `json:"web:useragenttype,omitempty"` MarkdownFontSize float64 `json:"markdown:fontsize,omitempty"` MarkdownFixedFontSize float64 `json:"markdown:fixedfontsize,omitempty"` From 056186d95450f82054b9cfc4be5a1e185a5c2c26 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 17 Oct 2025 17:43:55 -0700 Subject: [PATCH 3/3] must update useragent programatically after initial load --- frontend/app/view/webview/webview.tsx | 32 +++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/frontend/app/view/webview/webview.tsx b/frontend/app/view/webview/webview.tsx index 952e7202de..5695f99e63 100644 --- a/frontend/app/view/webview/webview.tsx +++ b/frontend/app/view/webview/webview.tsx @@ -680,6 +680,13 @@ export class WebViewModel implements ViewModel { { type: "separator", }, + { + label: "User Agent Type", + submenu: userAgentSubMenu, + }, + { + type: "separator", + }, { label: isNavHidden ? "Un-Hide Navigation" : "Hide Navigation", click: () => @@ -690,10 +697,6 @@ export class WebViewModel implements ViewModel { }); }), }, - { - label: "User Agent Type", - submenu: userAgentSubMenu, - }, { label: "Set Zoom Factor", submenu: zoomSubMenu, @@ -871,6 +874,7 @@ const WebView = memo(({ model, onFailLoad, blockRef, initialSrc }: WebViewProps) // The initial value of the block metadata URL when the component first renders. Used to set the starting src value for the webview. const [metaUrlInitial] = useState(initialSrc || metaUrl); + const prevUserAgentTypeRef = useRef(userAgentType); const [webContentsId, setWebContentsId] = useState(null); const domReady = useAtomValue(model.domReady); @@ -936,6 +940,26 @@ const WebView = memo(({ model, onFailLoad, blockRef, initialSrc }: WebViewProps) } }, [metaUrl, initialSrc]); + // Reload webview when user agent type changes + useEffect(() => { + if (prevUserAgentTypeRef.current !== userAgentType && domReady && model.webviewRef.current) { + let newUserAgent: string | undefined = undefined; + if (userAgentType === "mobile:iphone") { + newUserAgent = USER_AGENT_IPHONE; + } else if (userAgentType === "mobile:android") { + newUserAgent = USER_AGENT_ANDROID; + } + + if (newUserAgent) { + model.webviewRef.current.setUserAgent(newUserAgent); + } else { + model.webviewRef.current.setUserAgent(""); + } + model.webviewRef.current.reload(); + } + prevUserAgentTypeRef.current = userAgentType; + }, [userAgentType, domReady]); + useEffect(() => { const webview = model.webviewRef.current; if (!webview) {