diff --git a/ui/src/components/Layout.tsx b/ui/src/components/Layout.tsx
index d5a76fcd..02bec3de 100644
--- a/ui/src/components/Layout.tsx
+++ b/ui/src/components/Layout.tsx
@@ -17,6 +17,7 @@ import { ToastViewport } from "./ToastViewport";
import { MobileBottomNav } from "./MobileBottomNav";
import { WorktreeBanner } from "./WorktreeBanner";
import { DevRestartBanner } from "./DevRestartBanner";
+import { StandaloneBrowserControls } from "./StandaloneBrowserControls";
import { ResizableSidebarPane } from "./ResizableSidebarPane";
import { SidebarAccountMenu } from "./SidebarAccountMenu";
import { useDialogActions } from "../context/DialogContext";
@@ -424,6 +425,7 @@ export function Layout() {
isMobile && "sticky top-0 z-20 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/85",
)}
>
+
{isMobile && isCompanySettingsRoute ? (
diff --git a/ui/src/components/StandaloneBrowserControls.test.tsx b/ui/src/components/StandaloneBrowserControls.test.tsx
new file mode 100644
index 00000000..1eff16b7
--- /dev/null
+++ b/ui/src/components/StandaloneBrowserControls.test.tsx
@@ -0,0 +1,65 @@
+// @vitest-environment jsdom
+
+import { createRoot } from "react-dom/client";
+import { flushSync } from "react-dom";
+import { afterEach, beforeEach, describe, expect, it } from "vitest";
+import { TooltipProvider } from "@/components/ui/tooltip";
+import { ToastProvider } from "../context/ToastContext";
+import { StandaloneBrowserControls } from "./StandaloneBrowserControls";
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
+
+async function act(callback: () => void | Promise
) {
+ let result: void | Promise = undefined;
+ flushSync(() => {
+ result = callback();
+ });
+ await result;
+}
+
+async function flushReact() {
+ await act(async () => {
+ await Promise.resolve();
+ await new Promise((resolve) => window.setTimeout(resolve, 0));
+ });
+}
+
+describe("StandaloneBrowserControls", () => {
+ let container: HTMLDivElement;
+
+ beforeEach(() => {
+ container = document.createElement("div");
+ document.body.appendChild(container);
+ Object.defineProperty(navigator, "standalone", { configurable: true, value: true });
+ });
+
+ afterEach(() => {
+ delete (navigator as Navigator & { standalone?: boolean }).standalone;
+ container.remove();
+ document.body.innerHTML = "";
+ });
+
+ it("shows refresh, share, and open-in-browser controls in mobile standalone mode", async () => {
+ const root = createRoot(container);
+
+ await act(async () => {
+ root.render(
+
+
+
+
+ ,
+ );
+ });
+ await flushReact();
+
+ expect(container.querySelector('[aria-label="Refresh"]')).not.toBeNull();
+ expect(container.querySelector('[aria-label="Share"]')).not.toBeNull();
+ expect(container.querySelector('[aria-label="Open in Browser"]')).not.toBeNull();
+
+ await act(async () => {
+ root.unmount();
+ });
+ });
+});
diff --git a/ui/src/components/StandaloneBrowserControls.tsx b/ui/src/components/StandaloneBrowserControls.tsx
new file mode 100644
index 00000000..2b0f1e36
--- /dev/null
+++ b/ui/src/components/StandaloneBrowserControls.tsx
@@ -0,0 +1,103 @@
+import { useCallback, useEffect, useState, type ReactNode } from "react";
+import { ExternalLink, RefreshCw, Share2 } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
+import { useOptionalToastActions } from "../context/ToastContext";
+import { isChromelessDisplayMode } from "../lib/pwa-display-mode";
+
+function ControlButton({
+ label,
+ children,
+ onClick,
+}: {
+ label: string;
+ children: ReactNode;
+ onClick: () => void | Promise;
+}) {
+ return (
+
+
+
+
+ {label}
+
+ );
+}
+
+export function StandaloneBrowserControls({ mobile }: { mobile: boolean }) {
+ const [chromeless, setChromeless] = useState(false);
+ const toastActions = useOptionalToastActions();
+
+ useEffect(() => {
+ if (!mobile || typeof window === "undefined") {
+ setChromeless(false);
+ return;
+ }
+
+ const update = () => setChromeless(isChromelessDisplayMode());
+
+ update();
+ if (typeof window.matchMedia !== "function") return;
+
+ const media = window.matchMedia("(display-mode: standalone)");
+ if (typeof media.addEventListener === "function") {
+ media.addEventListener("change", update);
+ return () => media.removeEventListener("change", update);
+ }
+
+ media.addListener(update);
+ return () => media.removeListener(update);
+ }, [mobile]);
+
+ const refresh = useCallback(() => {
+ window.location.reload();
+ }, []);
+
+ const share = useCallback(async () => {
+ const url = window.location.href;
+ try {
+ if (navigator.share) {
+ await navigator.share({ title: document.title || "Paperclip", url });
+ return;
+ }
+ if (navigator.clipboard?.writeText) {
+ await navigator.clipboard.writeText(url);
+ toastActions?.pushToast({ title: "Link copied", tone: "success" });
+ return;
+ }
+ toastActions?.pushToast({ title: "Sharing is unavailable", body: url, tone: "warn" });
+ } catch (error) {
+ if (error instanceof DOMException && error.name === "AbortError") return;
+ toastActions?.pushToast({ title: "Share failed", body: "Try opening the page in your browser.", tone: "error" });
+ }
+ }, [toastActions]);
+
+ const openInBrowser = useCallback(() => {
+ window.open(window.location.href, "_blank", "noopener,noreferrer");
+ }, []);
+
+ if (!mobile || !chromeless) return null;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/ui/src/lib/pwa-display-mode.test.ts b/ui/src/lib/pwa-display-mode.test.ts
new file mode 100644
index 00000000..9e47b2e8
--- /dev/null
+++ b/ui/src/lib/pwa-display-mode.test.ts
@@ -0,0 +1,20 @@
+import { describe, expect, it } from "vitest";
+import { isChromelessDisplayMode } from "./pwa-display-mode";
+
+function matchMode(activeMode: string | null) {
+ return (query: string) => ({ matches: query === `(display-mode: ${activeMode})` });
+}
+
+describe("isChromelessDisplayMode", () => {
+ it("detects standalone display mode from media queries", () => {
+ expect(isChromelessDisplayMode(matchMode("standalone"), false)).toBe(true);
+ });
+
+ it("detects iOS home-screen standalone launches", () => {
+ expect(isChromelessDisplayMode(matchMode(null), true)).toBe(true);
+ });
+
+ it("ignores normal browser launches", () => {
+ expect(isChromelessDisplayMode(matchMode("browser"), false)).toBe(false);
+ });
+});
diff --git a/ui/src/lib/pwa-display-mode.ts b/ui/src/lib/pwa-display-mode.ts
new file mode 100644
index 00000000..4752d733
--- /dev/null
+++ b/ui/src/lib/pwa-display-mode.ts
@@ -0,0 +1,26 @@
+export const CHROMELESS_DISPLAY_MODES = ["standalone", "fullscreen", "window-controls-overlay"] as const;
+
+type DisplayMode = (typeof CHROMELESS_DISPLAY_MODES)[number];
+type MatchDisplayMode = (query: string) => Pick;
+
+function displayModeQuery(mode: DisplayMode) {
+ return `(display-mode: ${mode})`;
+}
+
+function defaultMatchMedia(): MatchDisplayMode | undefined {
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return undefined;
+ return window.matchMedia.bind(window);
+}
+
+export function isChromelessDisplayMode(
+ matchMedia: MatchDisplayMode | undefined = defaultMatchMedia(),
+ iosStandalone: boolean | undefined =
+ typeof navigator === "undefined"
+ ? undefined
+ : (navigator as Navigator & { standalone?: boolean }).standalone,
+) {
+ if (iosStandalone === true) return true;
+ if (!matchMedia) return false;
+
+ return CHROMELESS_DISPLAY_MODES.some((mode) => matchMedia(displayModeQuery(mode)).matches);
+}