Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7b8f87d325 | |||
| ee84b2aba9 |
@@ -29,6 +29,7 @@ import { ChatTopBar } from "./components/ChatTopBar";
|
||||
import { GitPanel } from "./components/GitPanel/GitPanel";
|
||||
import { RightPanel } from "./components/RightPanel/RightPanel";
|
||||
import { SidebarTabView } from "./components/Sidebar/SidebarTabView";
|
||||
import { useDesktopLandscape } from "./hooks/useDesktopMode";
|
||||
import type { ChatDetailError } from "./utils/usageLimitMessage";
|
||||
|
||||
type ChatStoreHandle = ReturnType<typeof useChatStore>["store"];
|
||||
@@ -216,6 +217,11 @@ export const AgentChatPageView: FC<AgentChatPageViewProps> = ({
|
||||
// the user clicks the inline desktop preview card).
|
||||
const [sidebarTabId, setSidebarTabId] = useState<string | null>(null);
|
||||
|
||||
// Lock landscape orientation when the Desktop tab is fullscreened
|
||||
// on mobile. Exiting fullscreen (isRightPanelExpanded = false)
|
||||
// automatically restores portrait.
|
||||
useDesktopLandscape(isRightPanelExpanded && sidebarTabId === "desktop");
|
||||
|
||||
const handleOpenDesktop = () => {
|
||||
onSetShowSidebarPanel(true);
|
||||
setSidebarTabId("desktop");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
FullscreenIcon,
|
||||
MaximizeIcon,
|
||||
MinimizeIcon,
|
||||
PanelLeftIcon,
|
||||
@@ -159,6 +160,12 @@ export const SidebarTabView: FC<SidebarTabViewProps> = ({
|
||||
scrollRight: scrollTabsRight,
|
||||
} = useTabScroll();
|
||||
|
||||
// Whether to show the mobile fullscreen button for the
|
||||
// desktop tab. Visible only on touch devices when the
|
||||
// Desktop tab is active and the panel is not yet expanded.
|
||||
const showDesktopFullscreen =
|
||||
effectiveTabId === "desktop" && desktopChatId && !isExpanded;
|
||||
|
||||
if (tabs.length === 0 && !desktopChatId) {
|
||||
return (
|
||||
<div className="flex h-full min-w-0 flex-col overflow-hidden bg-surface-primary">
|
||||
@@ -307,8 +314,8 @@ export const SidebarTabView: FC<SidebarTabViewProps> = ({
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Right side: close (mobile) / expand (desktop) */}
|
||||
{onClose && (
|
||||
{/* Right side: close (mobile) / fullscreen (mobile desktop) / expand (desktop) */}
|
||||
{onClose && !isExpanded && (
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="icon"
|
||||
@@ -319,6 +326,31 @@ export const SidebarTabView: FC<SidebarTabViewProps> = ({
|
||||
<XIcon />
|
||||
</Button>
|
||||
)}
|
||||
{/* Mobile fullscreen for Desktop tab — enters expanded
|
||||
mode which the parent ties to landscape orientation. */}
|
||||
{showDesktopFullscreen && (
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="icon"
|
||||
onClick={onToggleExpanded}
|
||||
aria-label="Fullscreen desktop"
|
||||
className="h-7 w-7 shrink-0 text-content-secondary hover:text-content-primary md:hidden"
|
||||
>
|
||||
<FullscreenIcon />
|
||||
</Button>
|
||||
)}
|
||||
{/* Exit fullscreen — visible on mobile when expanded */}
|
||||
{isExpanded && (
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="icon"
|
||||
onClick={onToggleExpanded}
|
||||
aria-label="Exit fullscreen"
|
||||
className="h-7 w-7 shrink-0 text-content-secondary hover:text-content-primary md:hidden"
|
||||
>
|
||||
<MinimizeIcon />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="icon"
|
||||
|
||||
@@ -3,7 +3,9 @@ import { useEffect } from "react";
|
||||
/**
|
||||
* Injects PWA-related <head> tags while the Agents page is mounted
|
||||
* (manifest, apple-touch-icon, mobile-web-app metas) and tweaks the
|
||||
* viewport to prevent zooming. Everything is cleaned up on unmount.
|
||||
* viewport to prevent zooming. On mobile it also locks orientation
|
||||
* to portrait; the Desktop panel can override this at runtime.
|
||||
* Everything is cleaned up on unmount.
|
||||
*/
|
||||
export function useAgentsPWA() {
|
||||
useEffect(() => {
|
||||
@@ -57,6 +59,24 @@ export function useAgentsPWA() {
|
||||
"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no";
|
||||
}
|
||||
|
||||
// -- Orientation lock ---------------------------------------------------
|
||||
// Lock portrait by default for a native-app feel on mobile.
|
||||
// The useDesktopMode hook overrides this when the user opens
|
||||
// the Desktop panel in landscape. The lock is best-effort —
|
||||
// it only works in PWA standalone mode on Android and is
|
||||
// silently ignored everywhere else.
|
||||
let orientationLocked = false;
|
||||
try {
|
||||
screen.orientation
|
||||
.lock("portrait-primary")
|
||||
.then(() => {
|
||||
orientationLocked = true;
|
||||
})
|
||||
.catch(() => {});
|
||||
} catch {
|
||||
// screen.orientation.lock may not exist at all.
|
||||
}
|
||||
|
||||
// -- Cleanup ------------------------------------------------------------
|
||||
return () => {
|
||||
for (const el of injected) {
|
||||
@@ -65,6 +85,13 @@ export function useAgentsPWA() {
|
||||
if (viewport) {
|
||||
viewport.content = prevViewportContent;
|
||||
}
|
||||
if (orientationLocked) {
|
||||
try {
|
||||
screen.orientation.unlock();
|
||||
} catch {
|
||||
// Ignored.
|
||||
}
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
|
||||
/**
|
||||
* Try to lock the screen orientation. This only works in certain
|
||||
* contexts (PWA standalone mode on Android, fullscreen) and throws
|
||||
* or rejects everywhere else. All failures are silently ignored.
|
||||
*/
|
||||
async function tryOrientationLock(
|
||||
orientation: OrientationLockType,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
await screen.orientation.lock(orientation);
|
||||
return true;
|
||||
} catch {
|
||||
// Expected to fail on iOS Safari, non-fullscreen browsers,
|
||||
// and desktop browsers. This is not an error condition.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function tryOrientationUnlock(): void {
|
||||
try {
|
||||
screen.orientation.unlock();
|
||||
} catch {
|
||||
// Same as above — expected to fail in most contexts.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages landscape orientation for the Desktop panel fullscreen
|
||||
* mode on mobile. The caller tells the hook whether landscape
|
||||
* should be active; the hook handles the Screen Orientation API
|
||||
* and cleans up on unmount.
|
||||
*
|
||||
* Portrait is restored automatically when `isLandscape` flips
|
||||
* back to false or when the component unmounts.
|
||||
*/
|
||||
export function useDesktopLandscape(isLandscape: boolean): void {
|
||||
const wasLandscapeRef = useRef(false);
|
||||
|
||||
const restorePortrait = useCallback(() => {
|
||||
void tryOrientationLock("portrait-primary").then((locked) => {
|
||||
if (!locked) {
|
||||
tryOrientationUnlock();
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLandscape) {
|
||||
void tryOrientationLock("landscape");
|
||||
wasLandscapeRef.current = true;
|
||||
} else if (wasLandscapeRef.current) {
|
||||
restorePortrait();
|
||||
wasLandscapeRef.current = false;
|
||||
}
|
||||
|
||||
return () => {
|
||||
// Restore portrait on unmount so navigating away
|
||||
// returns the device to its normal orientation.
|
||||
if (wasLandscapeRef.current) {
|
||||
restorePortrait();
|
||||
wasLandscapeRef.current = false;
|
||||
}
|
||||
};
|
||||
}, [isLandscape, restorePortrait]);
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
"start_url": "/agents",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait",
|
||||
"background_color": "#17172E",
|
||||
"theme_color": "#17172E",
|
||||
"icons": [
|
||||
|
||||
Reference in New Issue
Block a user