Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
22 changes: 22 additions & 0 deletions packages/app/e2e/app/shell-frame.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
desktopShellFrameSelector,
desktopShellMainSelector,
desktopShellSelector,
settingsUpdatesStartupSelector,
titlebarLeftSelector,
titlebarRightSelector,
titlebarShellSelector,
Expand All @@ -17,19 +18,39 @@ test("@smoke shell frame exposes stable desktop hooks", async ({ page, gotoSessi
await expect(page.locator(desktopShellFrameSelector)).toBeVisible()
await expect(page.locator(titlebarShellSelector)).toBeVisible()
await expect(page.locator(desktopShellMainSelector)).toBeVisible()
await expect(page.locator(desktopShellSelector)).toHaveAttribute("data-platform", "web")
await expect(page.locator(desktopShellSelector)).toHaveAttribute("data-shell", "desktop")
await expect(page.locator(titlebarShellSelector)).toHaveAttribute("data-shell", "desktop")
await expect(page.locator(titlebarShellSelector)).toHaveAttribute("data-shell-os", "macos")
await expect(page.locator(titlebarLeftSelector)).toHaveCount(1)
await expect(page.locator(titlebarLeftSelector)).toContainText(/new session/i)
await expect(page.locator(`${titlebarRightSelector} button`).first()).toBeVisible()
await expect(page.getByRole("button", { name: /toggle sidebar/i }).first()).toBeVisible()

const titlebarBox = await page.locator(titlebarShellSelector).boundingBox()
expect(Math.round(titlebarBox?.height ?? 0)).toBe(44)

const settings = await openSettings(page)
await expect(settings.getByRole("heading", { level: 2 })).toBeVisible()
await expect(settings.locator(settingsUpdatesStartupSelector).locator('[data-slot="switch-input"]')).toBeDisabled()
await expect(settings.getByRole("button", { name: /check now/i })).toBeDisabled()
await closeSettingsPanel(page, settings)

const palette = await openPalette(page)
await closeDialog(page, palette)
})

test("web desktop shell keeps the browser-safe project picker fallback", async ({ page }) => {
await page.goto("/")

await page.getByRole("button", { name: /open project/i }).last().click()

const dialog = page.getByRole("dialog")
await expect(dialog).toBeVisible()
await expect(dialog.getByPlaceholder("Search folders")).toBeVisible()
await closeDialog(page, dialog)
})

test("home titlebar left slot shows the current view title instead of the old file search affordance", async ({
page,
gotoSession,
Expand All @@ -53,6 +74,7 @@ test("session titlebar left slot shows a project and session breadcrumb", async
const buttons = left.getByRole("button")

await expect(buttons).toHaveCount(1)
await expect(buttons.first()).toBeDisabled()
await expect(buttons.first()).toContainText(/.+/)
await expect(left).toContainText(title)
await expect(left.getByRole("button", { name: /search files/i })).toHaveCount(0)
Expand Down
1 change: 1 addition & 0 deletions packages/app/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export default defineConfig({
reuseExistingServer: reuse,
timeout: 120_000,
env: {
VITE_PAWWORK_SHELL_OS: "macos",
Comment thread
Astro-Han marked this conversation as resolved.
VITE_OPENCODE_SERVER_HOST: serverHost,
VITE_OPENCODE_SERVER_PORT: serverPort,
},
Expand Down
3 changes: 1 addition & 2 deletions packages/app/src/components/session/session-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,7 @@ export function SessionHeader() {
if (platform.os === "linux") return language.t("session.header.open.fileManager")
return language.t("session.header.open.finder")
})
const canOpenDirectory = (directory?: string) =>
platform.platform === "desktop" && !!platform.openPath && server.isLocal() && !!directory
const canOpenDirectory = (directory?: string) => !!platform.openPath && server.isLocal() && !!directory
const activeWorktreeDirectory = createMemo(() => activeWorktree()?.directory ?? "")
const canOpenProjectDirectory = createMemo(() => canOpenDirectory(projectDirectory()))
const canOpenActiveWorktreeDirectory = createMemo(() => canOpenDirectory(activeWorktreeDirectory()))
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/components/settings-general.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export const SettingsGeneral: Component = () => {
checking: false,
})

const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux")
const linux = createMemo(() => platform.os === "linux" && !!platform.getDisplayBackend && !!platform.setDisplayBackend)
const dir = createMemo(() => decode64(params.dir))
const accepting = createMemo(() => {
const value = dir()
Expand Down
13 changes: 6 additions & 7 deletions packages/app/src/components/titlebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Button } from "@opencode-ai/ui/button"
import { TooltipKeybind } from "@opencode-ai/ui/tooltip"

import { useLayout } from "@/context/layout"
import { usePlatform } from "@/context/platform"
import { isDesktopShell, isMacShell, isWindowsShell, shellAttrs, usePlatform } from "@/context/platform"
import { useCommand } from "@/context/command"
import { useLanguage } from "@/context/language"
import { applyPath, backPath, forwardPath } from "./titlebar-history"
Expand All @@ -20,9 +20,8 @@ export function Titlebar() {
const location = useLocation()
const params = useParams()

const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
const web = createMemo(() => platform.platform === "web")
const mac = createMemo(() => isMacShell(platform))
const windows = createMemo(() => isWindowsShell(platform))
const zoom = () => platform.webviewZoom?.() ?? 1
const currentTitlebarHeight = () =>
mac() ? "var(--shell-titlebar-current-height, var(--shell-titlebar-height, 40px))" : undefined
Expand Down Expand Up @@ -84,9 +83,9 @@ export function Titlebar() {
<header
data-component="titlebar-shell"
data-platform={platform.platform}
data-os={platform.os}
{...shellAttrs(platform)}
class="shrink-0 relative grid grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center"
classList={{ "h-11": platform.platform === "desktop" && !mac() }}
classList={{ "h-11": isDesktopShell(platform) && !mac() }}
style={{ height: currentTitlebarHeight(), "min-height": currentTitlebarHeight() }}
data-shell-drag-region={!windows() || undefined}
>
Expand All @@ -101,7 +100,7 @@ export function Titlebar() {
</Show>
<div class="flex items-center gap-1 shrink-0">
<TooltipKeybind
class={web() ? "flex shrink-0 ml-14" : "flex shrink-0 ml-2"}
class="flex shrink-0 ml-2"
placement="bottom"
title={language.t("command.sidebar.toggle")}
keybind={command.keybind("sidebar.toggle")}
Expand Down
37 changes: 36 additions & 1 deletion packages/app/src/context/platform.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,10 @@ export type Platform = {
/** Platform discriminator */
platform: "web" | "desktop"

/** Desktop OS (desktop only) */
/** Visual shell contract. Runtime identity stays separate from appearance. */
shell?: PlatformShell

/** Runtime desktop OS, Electron only. Visual shell OS lives in shell.os and may be set by Web/E2E. */
os?: "macos" | "windows" | "linux"

/** App version */
Expand Down Expand Up @@ -175,6 +178,38 @@ export type Platform = {

export type DisplayBackend = "auto" | "wayland"

export type PlatformShell = {
kind: "desktop" | "web"
os?: "macos" | "windows" | "linux"
}

export function getShellKind(platform: Pick<Platform, "platform" | "shell">) {
return platform.shell?.kind ?? (platform.platform === "desktop" ? "desktop" : "web")
}

export function getShellOs(platform: Pick<Platform, "shell" | "os">) {
return platform.shell?.os ?? platform.os
}

export function shellAttrs(platform: Pick<Platform, "platform" | "shell" | "os">) {
return {
"data-shell": getShellKind(platform),
"data-shell-os": getShellOs(platform),
}
}

export function isDesktopShell(platform: Pick<Platform, "platform" | "shell">) {
return getShellKind(platform) === "desktop"
}

export function isMacShell(platform: Pick<Platform, "platform" | "shell" | "os">) {
return isDesktopShell(platform) && getShellOs(platform) === "macos"
}

export function isWindowsShell(platform: Pick<Platform, "platform" | "shell" | "os">) {
return isDesktopShell(platform) && getShellOs(platform) === "windows"
}
Comment thread
Astro-Han marked this conversation as resolved.

export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({
name: "Platform",
init: (props: { value: Platform }) => {
Expand Down
11 changes: 11 additions & 0 deletions packages/app/src/entry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,16 @@ const restart: Platform["restart"] = async () => {
window.location.reload()
}

const getShellOs = (): NonNullable<Platform["shell"]>["os"] => {
const override = import.meta.env.VITE_PAWWORK_SHELL_OS
if (override === "macos" || override === "windows" || override === "linux") return override
if (typeof navigator !== "object") return "macos"
const ua = navigator.userAgent
if (ua.includes("Windows")) return "windows"
if (ua.includes("Linux")) return "linux"
return "macos"
}

const root = document.getElementById("root")
if (!(root instanceof HTMLElement) && import.meta.env.DEV) {
throw new Error(getRootNotFoundError())
Expand All @@ -121,6 +131,7 @@ const getDefaultUrl = () => {

const platform: Platform = {
platform: "web",
shell: { kind: "desktop", os: getShellOs() },
version: pkg.version,
openLink,
back,
Expand Down
16 changes: 8 additions & 8 deletions packages/app/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
}
}

[data-component="desktop-shell"][data-platform="desktop"] {
[data-component="desktop-shell"][data-shell="desktop"] {
--shell-titlebar-height: 44px;
--shell-frame-margin: 12px;
--shell-frame-radius: 12px;
Expand All @@ -58,7 +58,7 @@
linear-gradient(180deg, var(--shell-background-base), var(--shell-background-weak));
}

:root[data-color-scheme="dark"] [data-component="desktop-shell"][data-platform="desktop"] {
:root[data-color-scheme="dark"] [data-component="desktop-shell"][data-shell="desktop"] {
--shell-background-base: #212121;
--shell-background-weak: #181818;
--shell-surface-base: #181818;
Expand Down Expand Up @@ -114,7 +114,7 @@
bg. Right-panel boundary is intentionally NOT drawn into the titlebar
(Mail / Finder / Notes do not split their toolbar) so it never overlaps
with breadcrumb or center-portal content when the panel is wide. */
[data-component="titlebar-shell"][data-platform="desktop"][data-os="macos"] {
[data-component="titlebar-shell"][data-shell="desktop"][data-shell-os="macos"] {
padding-inline: 8px;
background: linear-gradient(
to right,
Expand All @@ -125,25 +125,25 @@
);
}

[data-component="titlebar-shell"][data-platform="desktop"][data-os="macos"] [data-shell-slot="left-portal"] {
[data-component="titlebar-shell"][data-shell="desktop"][data-shell-os="macos"] [data-shell-slot="left-portal"] {
padding-inline: 12px;
}

[data-component="titlebar-shell"] [data-shell-slot="left-portal"] > * {
pointer-events: auto;
}

[data-component="titlebar-shell"][data-platform="desktop"][data-os="macos"]
[data-component="titlebar-shell"][data-shell="desktop"][data-shell-os="macos"]
:is([data-component="button"], [data-component="icon-button"]).titlebar-icon[data-variant="ghost"] {
border-radius: 10px;
}

[data-component="titlebar-shell"][data-platform="desktop"][data-os="macos"]
[data-component="titlebar-shell"][data-shell="desktop"][data-shell-os="macos"]
:is([data-component="button"], [data-component="icon-button"]).titlebar-icon[data-variant="ghost"]:hover:not(:disabled) {
background-color: var(--surface-base-hover);
}

[data-component="desktop-shell-main"][data-platform="desktop"][data-os="macos"] {
[data-component="desktop-shell-main"][data-shell="desktop"][data-shell-os="macos"] {
border-top-width: 0;
}

Expand All @@ -153,7 +153,7 @@
}

@media (min-width: 1280px) {
[data-component="desktop-shell-frame"][data-platform="desktop"][data-os="linux"] {
[data-component="desktop-shell-frame"][data-shell="desktop"][data-shell-os="linux"] {
margin: var(--shell-frame-margin);
border: 1px solid var(--shell-frame-border);
border-radius: var(--shell-frame-radius);
Expand Down
11 changes: 5 additions & 6 deletions packages/app/src/pages/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { Button } from "@opencode-ai/ui/button"
import { Dialog } from "@opencode-ai/ui/dialog"
import { getFilename } from "@opencode-ai/util/path"
import { Session, type GlobalSession, type Message } from "@opencode-ai/sdk/v2/client"
import { usePlatform } from "@/context/platform"
import { isMacShell, shellAttrs, usePlatform } from "@/context/platform"
import { useSettings } from "@/context/settings"
import { createStore, produce, reconcile } from "solid-js/store"
import type { DragEvent } from "@thisbeyond/solid-dnd"
Expand Down Expand Up @@ -2097,7 +2097,6 @@ export default function Layout(props: ParentProps) {
)
const sidebarContent = () =>
renderPawworkPanel(pawworkSessions, { directory: currentProject()?.worktree, scope: "main" })

return (
<LayoutPageContext.Provider
value={{
Expand All @@ -2112,15 +2111,15 @@ export default function Layout(props: ParentProps) {
<div
data-component="desktop-shell"
data-platform={platform.platform}
data-os={platform.os}
{...shellAttrs(platform)}
class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text"
classList={{
"[transition:--sidebar-width_200ms_cubic-bezier(0.22,1,0.36,1),--right-panel-width_240ms_cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none":
!state.sizing,
}}
style={{
"--shell-titlebar-current-height":
platform.platform === "desktop" && platform.os === "macos"
isMacShell(platform)
? `calc(var(--shell-titlebar-height, 40px) / ${platform.webviewZoom?.() ?? 1})`
: "var(--shell-titlebar-height, 40px)",
"--sidebar-width": layout.sidebar.opened() ? `${side()}px` : "0px",
Expand All @@ -2131,7 +2130,7 @@ export default function Layout(props: ParentProps) {
<div
data-component="desktop-shell-frame"
data-platform={platform.platform}
data-os={platform.os}
{...shellAttrs(platform)}
class="flex flex-1 min-h-0 min-w-0 flex-col"
>
<Titlebar />
Expand Down Expand Up @@ -2191,7 +2190,7 @@ export default function Layout(props: ParentProps) {
<main
data-component="desktop-shell-main"
data-platform={platform.platform}
data-os={platform.os}
{...shellAttrs(platform)}
classList={{
"size-full overflow-x-hidden flex flex-col items-start contain-strict bg-background-base": true,
}}
Expand Down
Loading
Loading