mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 03:56:55 +03:00
feat(desktop): polish branch search and session spacing (#48150)
Co-authored-by: Hona <10430890+Hona@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
bf4522ed46
commit
eb37a7ebc7
@@ -22,6 +22,10 @@ test("selects a base branch for a new workspace", async ({ page }) => {
|
||||
pageMessages: () => ({ items: [] }),
|
||||
vcsBranches: ["feature/api", "main", "origin/release"],
|
||||
})
|
||||
await page.route("**/api/vcs/branches?*", (route) => {
|
||||
if (new URL(route.request().url()).searchParams.get("search") !== "feature") return route.fallback()
|
||||
return route.fulfill({ json: { location: { directory }, data: ["feature/api"] } })
|
||||
})
|
||||
await page.addInitScript(
|
||||
({ directory, draftID, server }) => {
|
||||
localStorage.setItem(
|
||||
@@ -44,10 +48,28 @@ test("selects a base branch for a new workspace", async ({ page }) => {
|
||||
await page.getByRole("button", { name: "Local", exact: true }).click()
|
||||
await page.getByRole("menuitem", { name: "New worktree", exact: true }).click()
|
||||
await page.getByRole("button", { name: "from main", exact: true }).click()
|
||||
const search = page.getByRole("textbox", { name: "Search branches", exact: true })
|
||||
await expect(search).toBeFocused()
|
||||
await page.keyboard.type("feature")
|
||||
await expect(search).toHaveValue("feature")
|
||||
await expect(page.getByRole("menuitemradio")).toHaveText(["feature/api"])
|
||||
await expect(search).toBeFocused()
|
||||
await page.getByRole("menuitemradio", { name: "feature/api", exact: true }).click()
|
||||
|
||||
const selected = page.getByRole("button", { name: "from feature/api", exact: true })
|
||||
await expect(selected).toBeVisible()
|
||||
await selected.click()
|
||||
await expect(search).toBeFocused()
|
||||
await expect(search).toHaveValue("")
|
||||
await expect(page.getByRole("menuitemradio", { name: "feature/api", exact: true })).toBeChecked()
|
||||
await page.keyboard.press("Escape")
|
||||
await expect(selected).toBeFocused()
|
||||
await page.keyboard.press("Enter")
|
||||
await expect(search).toBeFocused()
|
||||
await page.keyboard.type("feature")
|
||||
await expect(search).toHaveValue("feature")
|
||||
await expect(page.getByRole("menuitemradio")).toHaveText(["feature/api"])
|
||||
await page.getByRole("button", { name: "Clear", exact: true }).click()
|
||||
await expect(search).toHaveValue("")
|
||||
await expect(page.getByRole("menuitemradio")).toHaveText(["feature/api", "main", "origin/release"])
|
||||
})
|
||||
|
||||
@@ -238,16 +238,9 @@ async function expectHeaderClearOfToggle(page: Page, toggle: Locator, progress:
|
||||
})
|
||||
const chatBounds = chat.getBoundingClientRect()
|
||||
const panelBounds = document.querySelector("#review-panel")!.getBoundingClientRect()
|
||||
const summaryBounds = document
|
||||
.querySelector('[data-session-title] button[aria-label="Session details"]')!
|
||||
.getBoundingClientRect()
|
||||
return {
|
||||
row: row.getBoundingClientRect().width,
|
||||
panelWidth: panelBounds.width,
|
||||
timelineControlInset:
|
||||
getComputedStyle(row).direction === "rtl"
|
||||
? summaryBounds.left - chatBounds.left
|
||||
: chatBounds.right - summaryBounds.right,
|
||||
gap:
|
||||
getComputedStyle(row).direction === "rtl"
|
||||
? chatBounds.left - panelBounds.right
|
||||
@@ -257,8 +250,6 @@ async function expectHeaderClearOfToggle(page: Page, toggle: Locator, progress:
|
||||
}
|
||||
}, progress)
|
||||
expect(geometry.gap).toBeCloseTo(8, 1)
|
||||
// Reserve the fixed toggle's 28px width, the 8px control gap, and the 12px header inset.
|
||||
expect(geometry.timelineControlInset).toBeCloseTo(48, 1)
|
||||
if (geometry.panelWidth > 0) expect(Math.abs(geometry.row - geometry.panels)).toBeLessThanOrEqual(1)
|
||||
if (progress === 0.25) {
|
||||
expect(geometry.contentOpacity).toBeGreaterThan(0)
|
||||
|
||||
@@ -242,14 +242,7 @@ export function PromptWorkspaceSelector(props: {
|
||||
class="ms-1 min-w-0 max-w-[220px]"
|
||||
contentClass="max-w-[calc(100vw-32px)] break-all"
|
||||
>
|
||||
<Menu
|
||||
placement="bottom"
|
||||
gutter={4}
|
||||
onOpenChange={(open) => {
|
||||
onOpenChange(open)
|
||||
if (open) requestAnimationFrame(() => branchSearchInput?.focus())
|
||||
}}
|
||||
>
|
||||
<Menu placement="bottom" gutter={4} onOpenChange={onOpenChange}>
|
||||
<Menu.Trigger class="flex h-6 min-w-0 max-w-[220px] items-center gap-1.5 rounded-full bg-v2-background-bg-layer-02 px-2.5 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-faint transition-colors hover:bg-v2-background-bg-layer-03 hover:text-v2-text-text-muted focus-visible:bg-v2-background-bg-layer-03 focus-visible:text-v2-text-text-muted focus-visible:outline-none data-[expanded]:bg-v2-background-bg-layer-03 data-[expanded]:text-v2-text-text-muted">
|
||||
<Icon name="branch-out" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
||||
<span ref={branchTruncation.observe} class="min-w-0 truncate">
|
||||
@@ -258,7 +251,14 @@ export function PromptWorkspaceSelector(props: {
|
||||
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
||||
</Menu.Trigger>
|
||||
<Menu.Portal>
|
||||
<Menu.Content class="w-[243px] overflow-hidden rounded-md border-0 bg-v2-background-bg-layer-01 shadow-[var(--v2-elevation-floating)] focus:outline-none">
|
||||
<Menu.Content
|
||||
class="w-[243px] overflow-hidden rounded-md border-0 bg-v2-background-bg-layer-01 shadow-[var(--v2-elevation-floating)] focus:outline-none"
|
||||
onOpenAutoFocus={(event) => {
|
||||
event.preventDefault()
|
||||
// Kobalte defers its list autofocus until after the focus scope opens.
|
||||
setTimeout(() => requestAnimationFrame(() => branchSearchInput?.focus({ preventScroll: true })))
|
||||
}}
|
||||
>
|
||||
<div class="flex h-7 shrink-0 items-center gap-2 rounded-sm pl-3 pr-2.5 text-v2-icon-icon-muted">
|
||||
<Icon name="magnifying-glass" size="small" class="shrink-0" />
|
||||
<input
|
||||
|
||||
@@ -6,7 +6,7 @@ import { StatusPopover } from "@/shell/status/status-popover"
|
||||
import { TitlebarRight } from "@/shell/titlebar/right-slot"
|
||||
import { Tooltip } from "@opencode/ui/tooltip"
|
||||
|
||||
export function SessionHeader() {
|
||||
export function SessionHeader(props: { reserveReviewToggle: boolean }) {
|
||||
const language = useLanguage()
|
||||
const settings = useSettings()
|
||||
|
||||
@@ -21,8 +21,7 @@ export function SessionHeader() {
|
||||
</Tooltip>
|
||||
</Show>
|
||||
</TitlebarRight>
|
||||
{/* Keep the fixed toggle's slot mounted throughout panel motion. */}
|
||||
<Show when={isDesktop()}>
|
||||
<Show when={isDesktop() && props.reserveReviewToggle}>
|
||||
<div class="size-7 shrink-0" aria-hidden />
|
||||
</Show>
|
||||
</>
|
||||
|
||||
@@ -276,6 +276,7 @@ export function SessionScreen(props: { session: SessionModel }) {
|
||||
onSelectionInteraction={timeline.view.selectionInteraction}
|
||||
pinned={timeline.view.pinned()}
|
||||
centered={screen.centered()}
|
||||
reserveReviewToggle={!sideVisible()}
|
||||
setContentRef={timeline.view.setContentRef}
|
||||
diffs={review.details.diffs}
|
||||
onReview={review.open}
|
||||
|
||||
@@ -363,6 +363,7 @@ type MessageTimelineProps = {
|
||||
onSelectionInteraction: (event: MouseEvent) => void
|
||||
pinned: boolean
|
||||
centered: boolean
|
||||
reserveReviewToggle: boolean
|
||||
setContentRef: (el: HTMLDivElement) => void
|
||||
diffs: Accessor<{ additions: number; deletions: number }[] | undefined>
|
||||
onReview: () => void
|
||||
@@ -845,7 +846,7 @@ function MessageTimelineView(
|
||||
</Popover>
|
||||
)}
|
||||
</Show>
|
||||
<SessionHeader />
|
||||
<SessionHeader reserveReviewToggle={props.reserveReviewToggle} />
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
@@ -1308,7 +1308,7 @@
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user