feat(app): add browser pane UI
This commit is contained in:
parent
fb11917aa7
commit
541b9b5c78
10 changed files with 222 additions and 44 deletions
|
|
@ -94,7 +94,7 @@ test("shows a pending permission dock", async ({ page }) => {
|
||||||
await expect(permission).toBeVisible()
|
await expect(permission).toBeVisible()
|
||||||
await expect(permission.getByText("git status")).toBeVisible()
|
await expect(permission.getByText("git status")).toBeVisible()
|
||||||
await expect(permission.getByText("git diff")).toBeVisible()
|
await expect(permission.getByText("git diff")).toBeVisible()
|
||||||
await expect(permission.locator('[data-slot="permission-footer-actions"] button')).toHaveCount(3)
|
await expect(permission.locator('[data-slot="permission-footer-actions"] button')).toHaveCount(2)
|
||||||
await expect(page.locator('[data-component="session-composer"]')).toHaveCount(0)
|
await expect(page.locator('[data-component="session-composer"]')).toHaveCount(0)
|
||||||
|
|
||||||
const reply = page.waitForRequest((request) => request.method() === "POST")
|
const reply = page.waitForRequest((request) => request.method() === "POST")
|
||||||
|
|
|
||||||
|
|
@ -527,8 +527,6 @@ type SessionHeaderV2ActionsState = {
|
||||||
}
|
}
|
||||||
|
|
||||||
function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) {
|
function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) {
|
||||||
const language = useLanguage()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Show when={props.state.statusVisible}>
|
<Show when={props.state.statusVisible}>
|
||||||
|
|
|
||||||
|
|
@ -711,9 +711,9 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setStore("fileTree", "tab", tab)
|
setStore("fileTree", "tab", tab)
|
||||||
},
|
},
|
||||||
open() {
|
open() {
|
||||||
if (!store.fileTree) {
|
if (!store.fileTree) {
|
||||||
setStore("fileTree", { opened: true, width: DEFAULT_FILE_TREE_WIDTH, tab: "changes" })
|
setStore("fileTree", { opened: true, width: DEFAULT_FILE_TREE_WIDTH, tab: "changes" })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -726,12 +726,13 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||||
}
|
}
|
||||||
setStore("fileTree", "opened", false)
|
setStore("fileTree", "opened", false)
|
||||||
},
|
},
|
||||||
toggle() {
|
toggle() {
|
||||||
if (!store.fileTree) {
|
const next = !(store.fileTree?.opened ?? true)
|
||||||
|
if (!store.fileTree) {
|
||||||
setStore("fileTree", { opened: true, width: DEFAULT_FILE_TREE_WIDTH, tab: "changes" })
|
setStore("fileTree", { opened: true, width: DEFAULT_FILE_TREE_WIDTH, tab: "changes" })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setStore("fileTree", "opened", (x) => !x)
|
setStore("fileTree", "opened", next)
|
||||||
},
|
},
|
||||||
resize(width: number) {
|
resize(width: number) {
|
||||||
if (!store.fileTree) {
|
if (!store.fileTree) {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ const appLocales = [
|
||||||
"zht",
|
"zht",
|
||||||
] as const
|
] as const
|
||||||
const desktopLocales = appLocales.filter((locale) => locale !== "th" && locale !== "tr")
|
const desktopLocales = appLocales.filter((locale) => locale !== "th" && locale !== "tr")
|
||||||
|
|
||||||
const domains = [
|
const domains = [
|
||||||
{
|
{
|
||||||
name: "app",
|
name: "app",
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
Match,
|
Match,
|
||||||
Switch,
|
Switch,
|
||||||
createMemo,
|
createMemo,
|
||||||
|
createResource,
|
||||||
createEffect,
|
createEffect,
|
||||||
createComputed,
|
createComputed,
|
||||||
createSignal,
|
createSignal,
|
||||||
|
|
@ -50,7 +51,8 @@ import { useLayout } from "@/context/layout"
|
||||||
import { ModelsProvider } from "@/context/models"
|
import { ModelsProvider } from "@/context/models"
|
||||||
import { useNotification } from "@/context/notification"
|
import { useNotification } from "@/context/notification"
|
||||||
import { PromptProvider, usePrompt } from "@/context/prompt"
|
import { PromptProvider, usePrompt } from "@/context/prompt"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { browserPaneAvailable, createBrowserPaneBinding, usePlatform } from "@/context/platform"
|
||||||
|
import type { BrowserPaneRegistration } from "@/context/platform"
|
||||||
import { SDKProvider, useSDK } from "@/context/sdk"
|
import { SDKProvider, useSDK } from "@/context/sdk"
|
||||||
import { useServerSDK } from "@/context/server-sdk"
|
import { useServerSDK } from "@/context/server-sdk"
|
||||||
import { ServerConnection, serverName, useServer } from "@/context/server"
|
import { ServerConnection, serverName, useServer } from "@/context/server"
|
||||||
|
|
@ -103,6 +105,7 @@ import { legacySessionHref, requireServerKey, sessionHref } from "@/utils/sessio
|
||||||
import { useUsageExceededDialogs } from "./session/usage-exceeded-dialogs"
|
import { useUsageExceededDialogs } from "./session/usage-exceeded-dialogs"
|
||||||
import { createSessionOwnership } from "./session/session-ownership"
|
import { createSessionOwnership } from "./session/session-ownership"
|
||||||
import { createSessionLineage } from "./session/session-lineage"
|
import { createSessionLineage } from "./session/session-lineage"
|
||||||
|
import { SessionBrowserPane } from "./session/browser-pane"
|
||||||
|
|
||||||
type FollowupItem = FollowupDraft & { id: string }
|
type FollowupItem = FollowupDraft & { id: string }
|
||||||
type FollowupEdit = Pick<FollowupItem, "id" | "prompt" | "context">
|
type FollowupEdit = Pick<FollowupItem, "id" | "prompt" | "context">
|
||||||
|
|
@ -352,6 +355,7 @@ function SessionPanelFrame(props: ParentProps<{ newLayout: boolean; raised?: boo
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const serverSync = useServerSync()
|
const serverSync = useServerSync()
|
||||||
|
const server = useServer()
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
const file = useFile()
|
const file = useFile()
|
||||||
|
|
@ -447,12 +451,87 @@ export default function Page() {
|
||||||
|
|
||||||
const isDesktop = createMediaQuery("(min-width: 768px)")
|
const isDesktop = createMediaQuery("(min-width: 768px)")
|
||||||
const size = createSizing()
|
const size = createSizing()
|
||||||
|
const [serverProtocol] = createResource(
|
||||||
|
() => serverSDK().protocol,
|
||||||
|
(protocol) => protocol,
|
||||||
|
)
|
||||||
|
const browserAvailable = createMemo(() =>
|
||||||
|
browserPaneAvailable({
|
||||||
|
platform: !!platform.browserPane,
|
||||||
|
sessionID: params.id,
|
||||||
|
protocol: serverProtocol(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const browserServer = createMemo(
|
||||||
|
() => {
|
||||||
|
if (!browserAvailable()) return undefined
|
||||||
|
const routeServer = serverSDK().server
|
||||||
|
const serverKey = ServerConnection.key(routeServer)
|
||||||
|
const connection = server.list.find((item) => ServerConnection.key(item) === serverKey) ?? routeServer
|
||||||
|
return {
|
||||||
|
endpoint: {
|
||||||
|
url: connection.http.url,
|
||||||
|
username: connection.http.username,
|
||||||
|
password: connection.http.password,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
equals: (left, right) => {
|
||||||
|
if (!left || !right) return left === right
|
||||||
|
return (
|
||||||
|
left.endpoint.url === right.endpoint.url &&
|
||||||
|
left.endpoint.username === right.endpoint.username &&
|
||||||
|
left.endpoint.password === right.endpoint.password
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const browserBinding = createMemo(() => {
|
||||||
|
if (!params.id) return undefined
|
||||||
|
const target = browserServer()
|
||||||
|
if (!target) return undefined
|
||||||
|
return createBrowserPaneBinding({ sessionID: params.id, endpoint: target.endpoint })
|
||||||
|
})
|
||||||
|
const [browserPane, setBrowserPane] = createStore<{
|
||||||
|
registration?: BrowserPaneRegistration
|
||||||
|
opened: boolean
|
||||||
|
}>({ opened: false })
|
||||||
|
createEffect(() => {
|
||||||
|
const binding = browserBinding()
|
||||||
|
const browser = platform.browserPane
|
||||||
|
if (!binding || !browser) {
|
||||||
|
setBrowserPane({ registration: undefined, opened: false })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const registration = browser.register(binding, () => {
|
||||||
|
view().reviewPanel.close()
|
||||||
|
layout.fileTree.close()
|
||||||
|
setBrowserPane("opened", true)
|
||||||
|
})
|
||||||
|
setBrowserPane("registration", registration)
|
||||||
|
onCleanup(() => {
|
||||||
|
setBrowserPane({ registration: undefined, opened: false })
|
||||||
|
registration.close()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
createEffect(() => {
|
||||||
|
if (!browserPane.opened) return
|
||||||
|
if (!view().reviewPanel.opened() && !layout.fileTree.opened()) return
|
||||||
|
setBrowserPane("opened", false)
|
||||||
|
})
|
||||||
|
const desktopBrowserBinding = createMemo(() => {
|
||||||
|
if (!isDesktop() || !browserAvailable() || !browserPane.opened) return undefined
|
||||||
|
return browserPane.registration
|
||||||
|
})
|
||||||
|
const desktopBrowserOpen = createMemo(() => !!desktopBrowserBinding())
|
||||||
const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
|
const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
|
||||||
const desktopV2ReviewOpen = createMemo(() => newSessionDesign() && desktopReviewOpen() && !!params.id)
|
const desktopV2ReviewOpen = createMemo(() => newSessionDesign() && desktopReviewOpen() && !!params.id)
|
||||||
const terminalOpen = createMemo(() => view().terminal.opened())
|
const terminalOpen = createMemo(() => view().terminal.opened())
|
||||||
const desktopTerminalOpen = createMemo(() => isDesktop() && terminalOpen())
|
const desktopTerminalOpen = createMemo(() => isDesktop() && terminalOpen())
|
||||||
const desktopInlineTerminalOnlyOpen = createMemo(
|
const desktopInlineTerminalOnlyOpen = createMemo(
|
||||||
() => newSessionDesign() && desktopTerminalOpen() && !desktopV2ReviewOpen(),
|
() => newSessionDesign() && desktopTerminalOpen() && !desktopV2ReviewOpen() && !desktopBrowserOpen(),
|
||||||
)
|
)
|
||||||
const desktopFileTreeOpen = createMemo(
|
const desktopFileTreeOpen = createMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|
@ -463,7 +542,9 @@ export default function Page() {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const desktopSessionResizeOpen = createMemo(() =>
|
const desktopSessionResizeOpen = createMemo(() =>
|
||||||
newSessionDesign() ? desktopV2ReviewOpen() || desktopTerminalOpen() : desktopReviewOpen(),
|
newSessionDesign()
|
||||||
|
? desktopV2ReviewOpen() || desktopBrowserOpen() || desktopTerminalOpen()
|
||||||
|
: desktopReviewOpen() || desktopBrowserOpen(),
|
||||||
)
|
)
|
||||||
const desktopSidePanelOpen = createMemo(() => desktopSessionResizeOpen() || desktopFileTreeOpen())
|
const desktopSidePanelOpen = createMemo(() => desktopSessionResizeOpen() || desktopFileTreeOpen())
|
||||||
let panelRow: HTMLDivElement | undefined
|
let panelRow: HTMLDivElement | undefined
|
||||||
|
|
@ -505,6 +586,7 @@ export default function Page() {
|
||||||
const desktopV2PanelLayout = createMemo(() =>
|
const desktopV2PanelLayout = createMemo(() =>
|
||||||
sessionPanelLayout({
|
sessionPanelLayout({
|
||||||
review: desktopV2ReviewOpen(),
|
review: desktopV2ReviewOpen(),
|
||||||
|
browser: desktopBrowserOpen(),
|
||||||
terminal: desktopTerminalOpen(),
|
terminal: desktopTerminalOpen(),
|
||||||
files: desktopFileTreeOpen(),
|
files: desktopFileTreeOpen(),
|
||||||
}),
|
}),
|
||||||
|
|
@ -2298,27 +2380,42 @@ export default function Page() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={!newSessionDesign() && desktopSidePanelOpen()}>
|
<Show when={!newSessionDesign() && desktopSidePanelOpen()}>
|
||||||
<Suspense>
|
<Show
|
||||||
<SessionSidePanel
|
when={desktopBrowserBinding()}
|
||||||
canReview={canReview}
|
keyed
|
||||||
diffs={reviewDiffs}
|
fallback={
|
||||||
diffsReady={reviewReady}
|
<Suspense>
|
||||||
empty={reviewEmptyText}
|
<SessionSidePanel
|
||||||
hasReview={hasReview}
|
canReview={canReview}
|
||||||
reviewHasFocusableContent={hasReview}
|
diffs={reviewDiffs}
|
||||||
reviewCount={reviewCount}
|
diffsReady={reviewReady}
|
||||||
reviewPanel={reviewPanel}
|
empty={reviewEmptyText}
|
||||||
activeDiff={activeReviewFile()}
|
hasReview={hasReview}
|
||||||
focusReviewDiff={focusReviewDiff}
|
reviewHasFocusableContent={hasReview}
|
||||||
reviewSnap={ui.reviewSnap}
|
reviewCount={reviewCount}
|
||||||
size={size}
|
reviewPanel={reviewPanel}
|
||||||
/>
|
activeDiff={activeReviewFile()}
|
||||||
</Suspense>
|
focusReviewDiff={focusReviewDiff}
|
||||||
|
reviewSnap={ui.reviewSnap}
|
||||||
|
size={size}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(registration) => <SessionBrowserPane registration={registration} />}
|
||||||
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={newSessionDesign()}>
|
<Show when={newSessionDesign()}>
|
||||||
<Show when={isDesktop() ? desktopV2PanelLayout().visible : terminalOpen()}>
|
<Show when={isDesktop() ? desktopV2PanelLayout().visible : terminalOpen()}>
|
||||||
<div class="min-w-0 h-full flex flex-1 flex-col">
|
<div class="min-w-0 h-full flex flex-1 flex-col">
|
||||||
<Show when={isDesktop() && (desktopV2ReviewOpen() || desktopFileTreeOpen())}>
|
<Show when={desktopBrowserBinding()} keyed>
|
||||||
|
{(registration) => (
|
||||||
|
<div class="min-h-0 flex-1">
|
||||||
|
<SessionBrowserPane registration={registration} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
<Show when={isDesktop() && !desktopBrowserOpen() && (desktopV2ReviewOpen() || desktopFileTreeOpen())}>
|
||||||
<div class="min-h-0 flex-1">
|
<div class="min-h-0 flex-1">
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<SessionSidePanel
|
<SessionSidePanel
|
||||||
|
|
|
||||||
64
packages/app/src/pages/session/browser-pane.tsx
Normal file
64
packages/app/src/pages/session/browser-pane.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
|
import { createEffect, onCleanup, onMount } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import { usePlatform, type BrowserPaneRegistration } from "@/context/platform"
|
||||||
|
|
||||||
|
export function SessionBrowserPane(props: { registration: BrowserPaneRegistration }) {
|
||||||
|
const platform = usePlatform()
|
||||||
|
const dialog = useDialog()
|
||||||
|
const [store, setStore] = createStore({
|
||||||
|
visible: typeof document === "undefined" || document.visibilityState === "visible",
|
||||||
|
})
|
||||||
|
let surface: HTMLDivElement | undefined
|
||||||
|
let frame: number | undefined
|
||||||
|
let until = 0
|
||||||
|
|
||||||
|
const measure = () => {
|
||||||
|
frame = undefined
|
||||||
|
if (!surface) return
|
||||||
|
const rect = surface.getBoundingClientRect()
|
||||||
|
const zoom = platform.webviewZoom?.() ?? 1
|
||||||
|
const left = Math.round(rect.left * zoom)
|
||||||
|
const top = Math.round(rect.top * zoom)
|
||||||
|
const right = Math.round(rect.right * zoom)
|
||||||
|
const bottom = Math.round(rect.bottom * zoom)
|
||||||
|
props.registration.setLayout({
|
||||||
|
visible: store.visible && !dialog.active,
|
||||||
|
bounds: { x: left, y: top, width: Math.max(0, right - left), height: Math.max(0, bottom - top) },
|
||||||
|
})
|
||||||
|
if (performance.now() < until) frame = requestAnimationFrame(measure)
|
||||||
|
}
|
||||||
|
|
||||||
|
const schedule = (duration = 0) => {
|
||||||
|
until = Math.max(until, performance.now() + duration)
|
||||||
|
if (frame !== undefined) return
|
||||||
|
frame = requestAnimationFrame(measure)
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
props.registration
|
||||||
|
platform.webviewZoom?.()
|
||||||
|
dialog.active
|
||||||
|
store.visible
|
||||||
|
schedule(300)
|
||||||
|
})
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const resize = new ResizeObserver(() => schedule())
|
||||||
|
if (surface) resize.observe(surface)
|
||||||
|
const onResize = () => schedule(300)
|
||||||
|
const onVisibility = () => setStore("visible", document.visibilityState === "visible")
|
||||||
|
window.addEventListener("resize", onResize)
|
||||||
|
document.addEventListener("visibilitychange", onVisibility)
|
||||||
|
schedule(300)
|
||||||
|
onCleanup(() => {
|
||||||
|
resize.disconnect()
|
||||||
|
window.removeEventListener("resize", onResize)
|
||||||
|
document.removeEventListener("visibilitychange", onVisibility)
|
||||||
|
if (frame !== undefined) cancelAnimationFrame(frame)
|
||||||
|
props.registration.setLayout()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return <div ref={surface} class="size-full bg-background-base" />
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { canRememberPermission } from "./session-permission-dock"
|
||||||
|
|
||||||
|
describe("canRememberPermission", () => {
|
||||||
|
test("hides Always when the request has no persistent resources", () => {
|
||||||
|
expect(canRememberPermission({ always: [] })).toBe(false)
|
||||||
|
expect(canRememberPermission({ always: ["https://example.com/*"] })).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -5,6 +5,10 @@ import { DockPrompt } from "@opencode-ai/session-ui/dock-prompt"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
|
||||||
|
export function canRememberPermission(request: Pick<PermissionRequest, "always">) {
|
||||||
|
return request.always.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
export function SessionPermissionDock(props: {
|
export function SessionPermissionDock(props: {
|
||||||
request: PermissionRequest
|
request: PermissionRequest
|
||||||
responding: boolean
|
responding: boolean
|
||||||
|
|
@ -37,14 +41,16 @@ export function SessionPermissionDock(props: {
|
||||||
<Button variant="ghost" size="normal" onClick={() => props.onDecide("reject")} disabled={props.responding}>
|
<Button variant="ghost" size="normal" onClick={() => props.onDecide("reject")} disabled={props.responding}>
|
||||||
{language.t("ui.permission.deny")}
|
{language.t("ui.permission.deny")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Show when={canRememberPermission(props.request)}>
|
||||||
variant="secondary"
|
<Button
|
||||||
size="normal"
|
variant="secondary"
|
||||||
onClick={() => props.onDecide("always")}
|
size="normal"
|
||||||
disabled={props.responding}
|
onClick={() => props.onDecide("always")}
|
||||||
>
|
disabled={props.responding}
|
||||||
{language.t("ui.permission.allowAlways")}
|
>
|
||||||
</Button>
|
{language.t("ui.permission.allowAlways")}
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
<Button variant="primary" size="normal" onClick={() => props.onDecide("once")} disabled={props.responding}>
|
<Button variant="primary" size="normal" onClick={() => props.onDecide("once")} disabled={props.responding}>
|
||||||
{language.t("ui.permission.allowOnce")}
|
{language.t("ui.permission.allowOnce")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,19 @@ import { sessionPanelLayout } from "./session-panel-layout"
|
||||||
|
|
||||||
describe("sessionPanelLayout", () => {
|
describe("sessionPanelLayout", () => {
|
||||||
test("keeps one V2 owner while changing panel geometry", () => {
|
test("keeps one V2 owner while changing panel geometry", () => {
|
||||||
expect(sessionPanelLayout({ review: false, terminal: false, files: false })).toEqual({
|
expect(sessionPanelLayout({ review: false, browser: false, terminal: false, files: false })).toEqual({
|
||||||
visible: false,
|
visible: false,
|
||||||
stacked: false,
|
stacked: false,
|
||||||
})
|
})
|
||||||
expect(sessionPanelLayout({ review: false, terminal: true, files: false })).toEqual({
|
expect(sessionPanelLayout({ review: false, browser: false, terminal: true, files: false })).toEqual({
|
||||||
visible: true,
|
visible: true,
|
||||||
stacked: false,
|
stacked: false,
|
||||||
})
|
})
|
||||||
expect(sessionPanelLayout({ review: true, terminal: true, files: false })).toEqual({
|
expect(sessionPanelLayout({ review: true, browser: false, terminal: true, files: false })).toEqual({
|
||||||
|
visible: true,
|
||||||
|
stacked: true,
|
||||||
|
})
|
||||||
|
expect(sessionPanelLayout({ review: false, browser: true, terminal: true, files: false })).toEqual({
|
||||||
visible: true,
|
visible: true,
|
||||||
stacked: true,
|
stacked: true,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export function sessionPanelLayout(input: { review: boolean; terminal: boolean; files: boolean }) {
|
export function sessionPanelLayout(input: { review: boolean; browser: boolean; terminal: boolean; files: boolean }) {
|
||||||
return {
|
return {
|
||||||
visible: input.review || input.terminal || input.files,
|
visible: input.review || input.browser || input.terminal || input.files,
|
||||||
stacked: input.review && input.terminal,
|
stacked: (input.review || input.browser) && input.terminal,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue