Skip to content
Draft
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
23 changes: 12 additions & 11 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"type": "module",
"exports": {
".": "./src/index.ts",
"./browser-pane": "./src/browser-pane.ts",
"./desktop-menu": "./src/desktop-menu.ts",
"./updater": "./src/updater.ts",
"./wsl/types": "./src/wsl/types.ts",
Expand Down
32 changes: 32 additions & 0 deletions packages/app/src/browser-pane.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export type BrowserPaneBounds = { x: number; y: number; width: number; height: number }

export type BrowserPaneLayout = {
attached: boolean
visible: boolean
destroy?: boolean
background?: string
sessionID?: string
bounds?: BrowserPaneBounds
}

export type BrowserPaneCommand =
| { type: "navigate"; url: string }
| { type: "back" }
| { type: "forward" }
| { type: "reload" }
| { type: "stop" }

export type BrowserPaneState = {
url: string
title: string
loading: boolean
canGoBack: boolean
canGoForward: boolean
error?: string
}

export type BrowserPanePlatform = {
setLayout(layout: BrowserPaneLayout): void
command(command: BrowserPaneCommand): Promise<void>
subscribe(cb: (state: BrowserPaneState) => void): Promise<() => void>
}
78 changes: 77 additions & 1 deletion packages/app/src/components/session/session-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { useCommand } from "@/context/command"
import { useLanguage } from "@/context/language"
import { useLayout } from "@/context/layout"
import { usePlatform } from "@/context/platform"
import { useServer } from "@/context/server"
import { ServerConnection, useServer } from "@/context/server"
import { useSettings } from "@/context/settings"
import { useSync } from "@/context/sync"
import { useTerminal } from "@/context/terminal"
Expand Down Expand Up @@ -242,6 +242,17 @@ export function SessionHeader() {
reviewVisible: isDesktop(),
reviewOpened: view().reviewPanel.opened(),
onReviewToggle: () => view().reviewPanel.toggle(),
browserLabel: language.t("command.browser.toggle"),
browserKeybind: command.keybindParts("browser.toggle"),
browserVisible:
isDesktop() &&
!!params.id &&
!!platform.browserPane &&
settings.general.experimentalBrowser() &&
!!server.current &&
ServerConnection.builtin(server.current),
browserOpened: view().browserPanel.opened(),
onBrowserToggle: () => view().browserPanel.toggle(),
}))

const selectApp = (app: OpenApp) => {
Expand Down Expand Up @@ -478,6 +489,39 @@ export function SessionHeader() {
</Button>
</TooltipKeybind>

<Show
when={
platform.browserPane &&
params.id &&
settings.general.experimentalBrowser() &&
server.current &&
ServerConnection.builtin(server.current)
}
>
<TooltipKeybind
title={language.t("command.browser.toggle")}
keybind={command.keybind("browser.toggle")}
>
<Button
variant="ghost"
class="group/browser-toggle titlebar-icon w-8 h-6 p-0 box-border"
onClick={() => view().browserPanel.toggle()}
aria-label={language.t("command.browser.toggle")}
aria-expanded={view().browserPanel.opened()}
aria-controls="browser-panel"
>
<Icon
size="small"
name="window-cursor"
classList={{
"text-icon-strong": view().browserPanel.opened(),
"text-icon-weak": !view().browserPanel.opened(),
}}
/>
</Button>
</TooltipKeybind>
</Show>

<TooltipKeybind
title={language.t("command.fileTree.toggle")}
keybind={command.keybind("fileTree.toggle")}
Expand Down Expand Up @@ -524,6 +568,11 @@ type SessionHeaderV2ActionsState = {
reviewVisible: boolean
reviewOpened: boolean
onReviewToggle: () => void
browserLabel: string
browserKeybind: string[]
browserVisible: boolean
browserOpened: boolean
onBrowserToggle: () => void
}

function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) {
Expand Down Expand Up @@ -563,6 +612,33 @@ function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) {
/>
</TooltipV2>
</Show>
<Show when={props.state.browserVisible}>
<TooltipV2
class="shrink-0"
placement="bottom"
value={
<>
{props.state.browserLabel}
<Show when={props.state.browserKeybind.length > 0}>
<KeybindV2 keys={props.state.browserKeybind} variant="neutral" />
</Show>
</>
}
>
<IconButtonV2
type="button"
variant="ghost-muted"
size="large"
class="!w-9 shrink-0"
state={props.state.browserOpened ? "pressed" : undefined}
onClick={props.state.onBrowserToggle}
aria-label={props.state.browserLabel}
aria-expanded={props.state.browserOpened}
aria-controls="browser-panel"
icon={<Icon name="window-cursor" size="small" />}
/>
</TooltipV2>
</Show>
</div>
)
}
14 changes: 14 additions & 0 deletions packages/app/src/components/settings-general.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,20 @@ export const SettingsGeneral: Component = () => {
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.advanced")}</h3>

<SettingsList>
<Show when={platform.browserPane}>
<SettingsRow
title={language.t("settings.general.row.experimentalBrowser.title")}
description={language.t("settings.general.row.experimentalBrowser.description")}
>
<div data-action="settings-experimental-browser">
<Switch
checked={settings.general.experimentalBrowser()}
onChange={(checked) => settings.general.setExperimentalBrowser(checked)}
/>
</div>
</SettingsRow>
</Show>

<SettingsRow
title={language.t("settings.general.row.showFileTree.title")}
description={language.t("settings.general.row.showFileTree.description")}
Expand Down
14 changes: 14 additions & 0 deletions packages/app/src/components/settings-v2/general.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,20 @@ export const SettingsGeneralV2: Component<{
<h3 class="settings-v2-section-title">{language.t("settings.general.section.advanced")}</h3>

<SettingsListV2>
<Show when={platform.browserPane}>
<SettingsRowV2
title={language.t("settings.general.row.experimentalBrowser.title")}
description={language.t("settings.general.row.experimentalBrowser.description")}
>
<div data-action="settings-experimental-browser">
<Switch
checked={settings.general.experimentalBrowser()}
onChange={(checked) => settings.general.setExperimentalBrowser(checked)}
/>
</div>
</SettingsRowV2>
</Show>

<SettingsRowV2
title={language.t("settings.general.row.showFileTree.title")}
description={language.t("settings.general.row.showFileTree.description")}
Expand Down
40 changes: 39 additions & 1 deletion packages/app/src/context/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,9 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
diffStyle: "split" as ReviewDiffStyle,
panelOpened: DEFAULT_REVIEW_PANEL_OPENED,
},
browser: {
panelOpened: false,
},
fileTree: {
opened: false,
width: DEFAULT_FILE_TREE_WIDTH,
Expand Down Expand Up @@ -701,6 +704,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setStore("fileTree", "tab", tab)
},
open() {
if (store.browser?.panelOpened) setStore("browser", "panelOpened", false)
if (!store.fileTree) {
setStore("fileTree", { opened: true, width: DEFAULT_FILE_TREE_WIDTH, tab: "changes" })
return
Expand All @@ -715,11 +719,13 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setStore("fileTree", "opened", false)
},
toggle() {
const next = !(store.fileTree?.opened ?? true)
if (next && store.browser?.panelOpened) setStore("browser", "panelOpened", false)
if (!store.fileTree) {
setStore("fileTree", { opened: true, width: DEFAULT_FILE_TREE_WIDTH, tab: "changes" })
return
}
setStore("fileTree", "opened", (x) => !x)
setStore("fileTree", "opened", next)
},
resize(width: number) {
if (!store.fileTree) {
Expand Down Expand Up @@ -807,6 +813,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
})
const terminalOpened = createMemo(() => store.terminal?.opened ?? false)
const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? DEFAULT_REVIEW_PANEL_OPENED)
const browserPanelOpened = createMemo(() => store.browser?.panelOpened ?? false)
const reviewPanelSource = createMemo(() => (reviewPanelOpened() ? ephemeral.reviewPanelSource : "other"))

function setTerminalOpened(next: boolean) {
Expand All @@ -822,6 +829,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}

function setReviewPanelOpened(next: boolean, source: ReviewPanelSource) {
if (next && browserPanelOpened()) setStore("browser", "panelOpened", false)
const nextSource = next ? source : "other"
const current = store.review
if (!current) {
Expand All @@ -843,6 +851,24 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
})
}

function setBrowserPanelOpened(next: boolean) {
const current = store.browser
if (!current) {
batch(() => {
setStore("browser", { panelOpened: next })
if (next) setReviewPanelOpened(false, "other")
if (next && store.fileTree?.opened) setStore("fileTree", "opened", false)
})
return
}
if ((current.panelOpened ?? false) === next) return
batch(() => {
setStore("browser", "panelOpened", next)
if (next) setReviewPanelOpened(false, "other")
if (next && store.fileTree?.opened) setStore("fileTree", "opened", false)
})
}

return {
scroll(tab: string) {
return scroll.scroll(key(), tab)
Expand Down Expand Up @@ -887,6 +913,18 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setReviewPanelOpened(!reviewPanelOpened(), "other")
},
},
browserPanel: {
opened: browserPanelOpened,
open() {
setBrowserPanelOpened(true)
},
close() {
setBrowserPanelOpened(false)
},
toggle() {
setBrowserPanelOpened(!browserPanelOpened())
},
},
review: {
mode: reviewMode,
setMode(mode: ReviewChangeMode) {
Expand Down
11 changes: 11 additions & 0 deletions packages/app/src/context/platform.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ import type { DesktopMenuAction } from "../desktop-menu"
import { ServerConnection } from "./server"
import type { WslServersPlatform } from "../wsl/types"
import type { UpdaterPlatform } from "../updater"
import type { BrowserPanePlatform } from "../browser-pane"
export type {
BrowserPaneBounds,
BrowserPaneCommand,
BrowserPaneLayout,
BrowserPanePlatform,
BrowserPaneState,
} from "../browser-pane"

type PickerPaths = string | string[] | null
type OpenDirectoryPickerOptions = { title?: string; multiple?: boolean }
Expand Down Expand Up @@ -120,6 +128,9 @@ type PlatformBase = {

/** Record a fatal renderer error in platform logs (desktop only) */
recordFatalRendererError?(error: FatalRendererErrorLog): Promise<void>

/** Native browser pane hosted by Electron main (desktop only) */
browserPane?: BrowserPanePlatform
}

export type Platform = PlatformBase &
Expand Down
11 changes: 11 additions & 0 deletions packages/app/src/context/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface Settings {
shellToolPartsExpanded: boolean
editToolPartsExpanded: boolean
showCustomAgents: boolean
experimentalBrowser: boolean
mobileTitlebarPosition: "top" | "bottom"
newLayoutDesigns?: boolean
layoutTransitionEligible?: boolean
Expand All @@ -58,6 +59,7 @@ export const sansDefault = "System Sans"
export const terminalDefault = "JetBrainsMono Nerd Font Mono"
const legacyNewLayoutDesignsDefault = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
export const newLayoutDesignsDefault = true
const experimentalBrowserDefault = false
// Existing users can switch layouts until local midnight on this date. Set new Date(YYYY, M-1, D) to show.
export const oldInterfaceSunset = new Date(2026, 8, 14)
const newLayoutDesignsUpgradeCutoff = "1.17.19"
Expand Down Expand Up @@ -184,6 +186,7 @@ const defaultSettings: Settings = {
shellToolPartsExpanded: false,
editToolPartsExpanded: false,
showCustomAgents: false,
experimentalBrowser: experimentalBrowserDefault,
mobileTitlebarPosition: "top",
},
appearance: {
Expand Down Expand Up @@ -396,6 +399,14 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
setShowCustomAgents(value: boolean) {
setStore("general", "showCustomAgents", value)
},
experimentalBrowser: withFallback(
() => store.general?.experimentalBrowser,
defaultSettings.general.experimentalBrowser,
),
setExperimentalBrowser(value: boolean) {
setStore("general", "experimentalBrowser", value)
if (!value) platform.browserPane?.setLayout({ attached: false, visible: false, destroy: true })
},
mobileTitlebarPosition: withFallback(
() => store.general?.mobileTitlebarPosition,
defaultSettings.general.mobileTitlebarPosition,
Expand Down
Loading
Loading