fix(app): refine mobile session layout (#32796)

This commit is contained in:
Brendan Allan
2026-06-23 16:58:05 +08:00
committed by GitHub
parent a379c7956b
commit bbc88eb8e3
5 changed files with 58 additions and 44 deletions
@@ -74,6 +74,7 @@ test("stages a submitted line comment in the prompt context", async ({ page }) =
await review.locator('[data-slot="line-comment-action"][data-variant="primary"]').click() await review.locator('[data-slot="line-comment-action"][data-variant="primary"]').click()
await expect(review.getByText("Use the existing value instead", { exact: true })).toBeVisible() await expect(review.getByText("Use the existing value instead", { exact: true })).toBeVisible()
await page.getByRole("tab", { name: "Session" }).click()
const context = page.getByText("Use the existing value instead", { exact: true }).last() const context = page.getByText("Use the existing value instead", { exact: true }).last()
await expect(context).toBeVisible() await expect(context).toBeVisible()
await expect(context.locator("..")).toContainText("review.ts:2") await expect(context.locator("..")).toContainText("review.ts:2")
@@ -10,6 +10,7 @@ import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { getFilename } from "@opencode-ai/core/util/path" import { getFilename } from "@opencode-ai/core/util/path"
import { createEffect, createMemo, createSignal, For, onMount, Show } from "solid-js" import { createEffect, createMemo, createSignal, For, onMount, Show } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { createMediaQuery } from "@solid-primitives/media"
import { Portal } from "solid-js/web" import { Portal } from "solid-js/web"
import { useCommand } from "@/context/command" import { useCommand } from "@/context/command"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
@@ -158,6 +159,7 @@ export function SessionHeader() {
const isV2 = settings.general.newLayoutDesigns const isV2 = settings.general.newLayoutDesigns
const search = settings.visibility.search const search = settings.visibility.search
const status = settings.visibility.status const status = settings.visibility.status
const isDesktop = createMediaQuery("(min-width: 768px)")
const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({ const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({
finder: true, finder: true,
@@ -236,6 +238,7 @@ export function SessionHeader() {
statusLabel: language.t("status.popover.trigger"), statusLabel: language.t("status.popover.trigger"),
reviewLabel: language.t("command.review.toggle"), reviewLabel: language.t("command.review.toggle"),
reviewKeybind: command.keybind("review.toggle"), reviewKeybind: command.keybind("review.toggle"),
reviewVisible: isDesktop(),
reviewOpened: view().reviewPanel.opened(), reviewOpened: view().reviewPanel.opened(),
onReviewToggle: () => view().reviewPanel.toggle(), onReviewToggle: () => view().reviewPanel.toggle(),
})) }))
@@ -518,6 +521,7 @@ type SessionHeaderV2ActionsState = {
statusLabel: string statusLabel: string
reviewLabel: string reviewLabel: string
reviewKeybind: string reviewKeybind: string
reviewVisible: boolean
reviewOpened: boolean reviewOpened: boolean
onReviewToggle: () => void onReviewToggle: () => void
} }
@@ -530,6 +534,7 @@ function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) {
<StatusPopoverV2 /> <StatusPopoverV2 />
</Tooltip> </Tooltip>
</Show> </Show>
<Show when={props.state.reviewVisible}>
<TooltipKeybind title={props.state.reviewLabel} keybind={props.state.reviewKeybind}> <TooltipKeybind title={props.state.reviewLabel} keybind={props.state.reviewKeybind}>
<IconButtonV2 <IconButtonV2
type="button" type="button"
@@ -544,6 +549,7 @@ function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) {
icon={<IconV2 name="sidebar-right" />} icon={<IconV2 name="sidebar-right" />}
/> />
</TooltipKeybind> </TooltipKeybind>
</Show>
</div> </div>
) )
} }
+32 -27
View File
@@ -1568,6 +1568,31 @@ export default function Page() {
/> />
) )
const mobileTabs = (compact = false) => (
<Tabs value={store.mobileTab} class="h-auto">
<Tabs.List class={compact ? "!h-9" : undefined}>
<Tabs.Trigger
value="session"
class="!w-1/2 !max-w-none"
classes={{ button: compact ? "w-full !py-2" : "w-full" }}
onClick={() => setStore("mobileTab", "session")}
>
{language.t("session.tab.session")}
</Tabs.Trigger>
<Tabs.Trigger
value="changes"
class="!w-1/2 !max-w-none !border-r-0"
classes={{ button: compact ? "w-full !py-2" : "w-full" }}
onClick={() => setStore("mobileTab", "changes")}
>
{hasReview()
? language.t("session.review.filesChanged", { count: reviewCount() })
: language.t("session.review.change.other")}
</Tabs.Trigger>
</Tabs.List>
</Tabs>
)
return ( return (
<div class="relative size-full overflow-hidden flex flex-col"> <div class="relative size-full overflow-hidden flex flex-col">
{sessionSync() ?? ""} {sessionSync() ?? ""}
@@ -1578,30 +1603,7 @@ export default function Page() {
"gap-2 p-2": settings.general.newLayoutDesigns(), "gap-2 p-2": settings.general.newLayoutDesigns(),
}} }}
> >
<Show when={!isDesktop() && !!params.id}> <Show when={!isDesktop() && !!params.id && !settings.general.newLayoutDesigns()}>{mobileTabs()}</Show>
<Tabs value={store.mobileTab} class="h-auto">
<Tabs.List>
<Tabs.Trigger
value="session"
class="!w-1/2 !max-w-none"
classes={{ button: "w-full" }}
onClick={() => setStore("mobileTab", "session")}
>
{language.t("session.tab.session")}
</Tabs.Trigger>
<Tabs.Trigger
value="changes"
class="!w-1/2 !max-w-none !border-r-0"
classes={{ button: "w-full" }}
onClick={() => setStore("mobileTab", "changes")}
>
{hasReview()
? language.t("session.review.filesChanged", { count: reviewCount() })
: language.t("session.review.change.other")}
</Tabs.Trigger>
</Tabs.List>
</Tabs>
</Show>
<div <div
classList={{ classList={{
@@ -1620,6 +1622,9 @@ export default function Page() {
"shadow-[var(--v2-elevation-raised)]": settings.general.newLayoutDesigns() && !!params.id, "shadow-[var(--v2-elevation-raised)]": settings.general.newLayoutDesigns() && !!params.id,
}} }}
> >
<Show when={!isDesktop() && !!params.id && settings.general.newLayoutDesigns()}>
{mobileTabs(true)}
</Show>
<div class="flex-1 min-h-0 overflow-hidden"> <div class="flex-1 min-h-0 overflow-hidden">
<Switch> <Switch>
<Match when={params.id && mobileChanges()}> <Match when={params.id && mobileChanges()}>
@@ -1627,8 +1632,8 @@ export default function Page() {
{reviewContent({ {reviewContent({
diffStyle: "unified", diffStyle: "unified",
classes: { classes: {
root: "pb-8", root: "pb-8 [&_[data-slot=session-review-list]]:pb-0",
header: "px-4", header: "px-4 !h-16 !pb-4",
container: "px-4", container: "px-4",
}, },
loadingClass: "px-4 py-4 text-text-weak", loadingClass: "px-4 py-4 text-text-weak",
@@ -1684,7 +1689,7 @@ export default function Page() {
</Switch> </Switch>
</div> </div>
<Show when={params.id || !newSessionDesign()}>{composerRegion("dock")}</Show> <Show when={(params.id || !newSessionDesign()) && !mobileChanges()}>{composerRegion("dock")}</Show>
</div> </div>
<Show when={desktopReviewOpen()}> <Show when={desktopReviewOpen()}>
@@ -1300,7 +1300,9 @@ export function MessageTimeline(props: {
"sticky top-0 z-30 bg-[linear-gradient(to_bottom,var(--background-stronger)_48px,transparent)]": true, "sticky top-0 z-30 bg-[linear-gradient(to_bottom,var(--background-stronger)_48px,transparent)]": true,
"w-full": true, "w-full": true,
"pb-4": true, "pb-4": true,
"pl-2 pr-3 md:pl-4 md:pr-3": true, "pr-3": true,
"pl-4": settings.general.newLayoutDesigns(),
"pl-2 md:pl-4": !settings.general.newLayoutDesigns(),
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered, "md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
}} }}
> >
@@ -386,7 +386,7 @@ export const SessionReview = (props: SessionReviewProps) => {
> >
<div data-slot="session-review-container" class={props.classes?.container}> <div data-slot="session-review-container" class={props.classes?.container}>
<Show when={hasDiffs()} fallback={props.empty}> <Show when={hasDiffs()} fallback={props.empty}>
<div class="pb-6"> <div data-slot="session-review-list" class="pb-6">
<Accordion multiple value={open()} onChange={handleChange}> <Accordion multiple value={open()} onChange={handleChange}>
<For each={files()}> <For each={files()}>
{(file) => { {(file) => {