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
14 changes: 8 additions & 6 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,11 +197,14 @@ function AppShell() {
<EventProvider>
<KeyboardShortcutsProvider>
<GlobalShortcuts />
<div ref={rootRef} className="flex h-dvh w-full min-w-0">
<DesktopSidebar />
<main className="flex-1 min-w-0 min-h-0 flex flex-col">
<Outlet />
</main>
<div className="flex h-dvh w-full min-w-0 flex-col">
<PwaUpdatePrompt />
<div ref={rootRef} className="flex flex-1 min-h-0 w-full min-w-0">
<DesktopSidebar />
<main className="flex-1 min-w-0 min-h-0 flex flex-col">
<Outlet />
</main>
</div>
</div>
<MobileTabBar />
<MobileSheetHost />
Expand All @@ -210,7 +213,6 @@ function AppShell() {
<SettingsDialog />
<HealthMonitor />
<VersionNotifier />
<PwaUpdatePrompt />
<ThemedToaster />
</KeyboardShortcutsProvider>
</EventProvider>
Expand Down
73 changes: 73 additions & 0 deletions frontend/src/components/PwaUpdatePrompt.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { render, screen, fireEvent, act } from '@testing-library/react'
import { PwaUpdatePrompt } from './PwaUpdatePrompt'

const mocks = vi.hoisted(() => ({
onServiceWorkerUpdate: vi.fn(),
offServiceWorkerUpdate: vi.fn(),
}))

vi.mock('@/lib/serviceWorker', () => ({
onServiceWorkerUpdate: mocks.onServiceWorkerUpdate,
offServiceWorkerUpdate: mocks.offServiceWorkerUpdate,
}))

function triggerUpdate() {
const callback = mocks.onServiceWorkerUpdate.mock.calls[0]?.[0]
if (typeof callback !== 'function') throw new Error('update callback was not registered')
act(() => callback())
}

describe('PwaUpdatePrompt', () => {
let reloadMock: ReturnType<typeof vi.fn>

beforeEach(() => {
mocks.onServiceWorkerUpdate.mockReset()
mocks.offServiceWorkerUpdate.mockReset()
reloadMock = vi.fn()
Object.defineProperty(window, 'location', {
configurable: true,
writable: true,
value: { ...window.location, reload: reloadMock },
})
})

afterEach(() => {
vi.restoreAllMocks()
})

it('renders nothing until an update is signalled', () => {
const { container } = render(<PwaUpdatePrompt />)

expect(container).toBeEmptyDOMElement()
expect(screen.queryByRole('status')).toBeNull()
})

it('shows a status bar with a Reload button when an update is signalled', () => {
render(<PwaUpdatePrompt />)

triggerUpdate()

expect(screen.getByRole('status')).toHaveTextContent(
'A new version of OpenCode Manager is available.',
)
expect(screen.getByRole('button', { name: 'Reload' })).toBeInTheDocument()
})

it('reloads the page when Reload is clicked', () => {
render(<PwaUpdatePrompt />)

triggerUpdate()
fireEvent.click(screen.getByRole('button', { name: 'Reload' }))

expect(reloadMock).toHaveBeenCalledTimes(1)
})

it('unsubscribes from updates on unmount', () => {
const { unmount } = render(<PwaUpdatePrompt />)

unmount()

expect(mocks.offServiceWorkerUpdate).toHaveBeenCalledTimes(1)
})
})
43 changes: 30 additions & 13 deletions frontend/src/components/PwaUpdatePrompt.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,38 @@
import { useEffect } from 'react'
import { showToast } from '@/lib/toast'
import { useEffect, useState } from 'react'
import { RefreshCw } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { onServiceWorkerUpdate, offServiceWorkerUpdate } from '@/lib/serviceWorker'

export function PwaUpdatePrompt() {
const [visible, setVisible] = useState(false)

useEffect(() => {
onServiceWorkerUpdate(() => {
showToast.info('New build deployed', {
description: 'Refresh to load the latest changes.',
action: {
label: 'Refresh',
onClick: () => window.location.reload(),
},
duration: Infinity,
})
})
onServiceWorkerUpdate(() => setVisible(true))
return () => offServiceWorkerUpdate()
}, [])

return null
if (!visible) return null

return (
<div
role="status"
aria-live="polite"
className="w-full shrink-0 bg-primary pt-safe text-primary-foreground"
>
<div className="flex flex-wrap items-center justify-center gap-3 px-4 py-2">
<RefreshCw className="h-4 w-4 shrink-0" aria-hidden="true" />
<span className="text-sm font-medium">
A new version of OpenCode Manager is available.
</span>
<Button
type="button"
variant="secondary"
size="sm"
onClick={() => window.location.reload()}
>
Reload
</Button>
</div>
</div>
)
}
2 changes: 1 addition & 1 deletion frontend/src/components/ui/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function Sidebar({
return (
<aside
className={cn(
'relative z-20 flex-shrink-0 border-r border-border bg-card/50 backdrop-blur-sm h-dvh flex flex-col pt-safe pb-safe transition-[width] duration-200',
'relative z-20 flex-shrink-0 border-r border-border bg-card/50 backdrop-blur-sm h-full flex flex-col pt-safe pb-safe transition-[width] duration-200',
collapsed ? collapsedWidthClass : widthClass,
className
)}
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/AssistantRedirect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ export function AssistantRedirect() {
}

return (
<div className="flex h-dvh max-h-dvh min-w-0">
<div className="flex-1 min-w-0 h-dvh max-h-dvh overflow-hidden bg-gradient-to-br from-background via-background to-background flex flex-col pb-[calc(env(safe-area-inset-bottom)+56px)] sm:pb-0">
<div className="flex h-full max-h-full min-w-0">
<div className="flex-1 min-w-0 h-full max-h-full overflow-hidden bg-gradient-to-br from-background via-background to-background flex flex-col pb-[calc(env(safe-area-inset-bottom)+56px)] sm:pb-0">
<Header>
<Header.BackButton to="/" />
<Header.Title>Assistant</Header.Title>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/GlobalSchedules.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ export function GlobalSchedules() {
const selectedRunLookupError = selectedRunMissing && selectedRunError && selectedRun === null

return (
<div className="h-dvh max-h-dvh overflow-hidden bg-background flex flex-col">
<div className="h-full max-h-full overflow-hidden bg-background flex flex-col">
<Header>
<Header.BackButton to="/" />
<div className="min-w-0 flex-1 px-3">
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ export function Login() {
const hasCredentials = config.enabledProviders.includes('credentials')

return (
<div className="h-dvh flex flex-col items-center justify-center bg-gradient-to-br from-background via-background to-background p-4">
<div className="w-full max-w-sm space-y-6">
<div className="h-full overflow-y-auto bg-gradient-to-br from-background via-background to-background p-4">
<div className="mx-auto flex min-h-full w-full max-w-sm flex-col justify-center space-y-6">
<div className="flex flex-col items-center space-y-2">
<img
src={theme === 'light' ? "/opencode-wordmark-light.svg" : "/opencode-wordmark-dark.svg"}
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/Register.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ export function Register() {
}

return (
<div className="h-dvh flex flex-col items-center justify-center bg-gradient-to-br from-background via-background to-background p-4">
<div className="w-full max-w-sm space-y-6">
<div className="h-full overflow-y-auto bg-gradient-to-br from-background via-background to-background p-4">
<div className="mx-auto flex min-h-full w-full max-w-sm flex-col justify-center space-y-6">
<div className="flex flex-col items-center space-y-2">
<img
src={theme === 'light' ? "/opencode-wordmark-light.svg" : "/opencode-wordmark-dark.svg"}
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/RepoDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,9 @@ export function RepoDetail() {
const isWorktree = repo.isWorktree || false;

return (
<div className="flex h-dvh max-h-dvh min-w-0">
<div className="flex h-full max-h-full min-w-0">
<div
className="flex-1 min-w-0 h-dvh max-h-dvh overflow-hidden bg-gradient-to-br from-background via-background to-background flex flex-col pb-[calc(env(safe-area-inset-bottom)+56px)] sm:pb-0"
className="flex-1 min-w-0 h-full max-h-full overflow-hidden bg-gradient-to-br from-background via-background to-background flex flex-col pb-[calc(env(safe-area-inset-bottom)+56px)] sm:pb-0"
>
<Header>
<div className="flex items-center gap-2 sm:gap-3 min-w-0 flex-1">
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/Repos.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ export function Repos() {
};

return (
<div className="flex h-dvh max-h-dvh min-w-0">
<div className="flex-1 min-w-0 h-dvh max-h-dvh overflow-hidden bg-gradient-to-br from-background via-background to-background flex flex-col">
<div className="flex h-full max-h-full min-w-0">
<div className="flex-1 min-w-0 h-full max-h-full overflow-hidden bg-gradient-to-br from-background via-background to-background flex flex-col">
<Header>
<div className="flex items-center gap-3">
<Header.Title logo>OpenCode</Header.Title>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/Schedules.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function Schedules() {
const backHref = getReturnToPath(location.search, scheduleTarget.backHref)

return (
<div className="h-dvh max-h-dvh overflow-hidden bg-background flex flex-col">
<div className="h-full max-h-full overflow-hidden bg-background flex flex-col">
<Header>
<Header.BackButton to={backHref} />
<div className="min-w-0 flex-1 px-3">
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/SessionDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,7 @@ export function SessionDetail() {

return (
<div
className="h-dvh max-h-dvh overflow-hidden bg-gradient-to-br from-background via-background to-background flex flex-col"
className="h-full max-h-full overflow-hidden bg-gradient-to-br from-background via-background to-background flex flex-col"
>
<div
data-testid="session-header-region"
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/Setup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ export function Setup() {
}

return (
<div className="h-dvh flex flex-col items-center justify-center bg-gradient-to-br from-background via-background to-background p-4">
<div className="w-full max-w-sm space-y-6">
<div className="h-full overflow-y-auto bg-gradient-to-br from-background via-background to-background p-4">
<div className="mx-auto flex min-h-full w-full max-w-sm flex-col justify-center space-y-6">
<div className="flex flex-col items-center space-y-2">
<img
src={theme === 'light' ? "/opencode-wordmark-light.svg" : "/opencode-wordmark-dark.svg"}
Expand Down