mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-02 06:56:21 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b467432ba4 |
@@ -0,0 +1,69 @@
|
||||
import { expect, test } from "@playwright/test"
|
||||
import { fixture, pageMessages } from "../performance/timeline/session-timeline-stress.fixture"
|
||||
import { installStressSessionTabs, stressSessionHref } from "../performance/timeline/timeline-test-helpers"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
|
||||
for (const direction of ["ltr", "rtl"] as const) {
|
||||
test(`session header groups controls and exposes server status in ${direction}`, async ({ page }) => {
|
||||
await mockOpenCodeServer(page, {
|
||||
directory: fixture.directory,
|
||||
project: fixture.project,
|
||||
sessions: fixture.sessions,
|
||||
provider: fixture.provider,
|
||||
pageMessages,
|
||||
})
|
||||
await installStressSessionTabs(page)
|
||||
await page.addInitScript(() => {
|
||||
const settings = JSON.parse(localStorage.getItem("settings.v3") ?? "{}")
|
||||
localStorage.setItem(
|
||||
"settings.v3",
|
||||
JSON.stringify({ ...settings, general: { ...settings.general, showStatus: true } }),
|
||||
)
|
||||
})
|
||||
await page.goto(stressSessionHref(fixture.targetID))
|
||||
const header = page.locator("[data-session-title]")
|
||||
const more = header.getByRole("button", { name: "More options", exact: true })
|
||||
const review = header.getByRole("button", { name: "Toggle review", exact: true })
|
||||
const details = header.getByRole("button", { name: "Session details", exact: true })
|
||||
await expect(header.getByRole("heading")).toHaveText(fixture.expected.targetTitle)
|
||||
await page.evaluate((direction) => document.documentElement.setAttribute("dir", direction), direction)
|
||||
await expect(review).toBeVisible()
|
||||
await expect(details).toBeVisible()
|
||||
const status = page.locator('[data-slot="titlebar-v2"]').getByRole("button", { name: "Status" })
|
||||
await expect(status).toBeVisible()
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const boxes = await Promise.all(
|
||||
[header.getByRole("heading"), more, review, details].map((button) => button.boundingBox()),
|
||||
)
|
||||
const [title, menu, sidebar, summary] = boxes
|
||||
if (!title || !menu || !sidebar || !summary) return false
|
||||
return direction === "ltr"
|
||||
? Math.abs(title.x + title.width - menu.x) <= 1 &&
|
||||
menu.x + menu.width <= summary.x &&
|
||||
summary.x + summary.width <= sidebar.x
|
||||
: Math.abs(menu.x + menu.width - title.x) <= 1 &&
|
||||
sidebar.x + sidebar.width <= summary.x &&
|
||||
summary.x + summary.width <= menu.x
|
||||
})
|
||||
.toBe(true)
|
||||
|
||||
await review.click()
|
||||
await expect(review).toHaveAttribute("aria-expanded", "true")
|
||||
await expect(page.locator("#review-panel")).toBeVisible()
|
||||
await review.click()
|
||||
await expect(review).toHaveAttribute("aria-expanded", "false")
|
||||
|
||||
await more.click()
|
||||
await expect(page.getByRole("menuitem", { name: "Server status", exact: true })).toHaveCount(0)
|
||||
await page.keyboard.press("Escape")
|
||||
await status.click()
|
||||
const mcp = page.getByRole("tab", { name: "MCP", exact: true })
|
||||
const plugins = page.getByRole("tab", { name: "Plugins", exact: true })
|
||||
await expect(mcp).toHaveAttribute("aria-selected", "true")
|
||||
await plugins.click()
|
||||
await expect(plugins).toHaveAttribute("aria-selected", "true")
|
||||
await page.keyboard.press("Escape")
|
||||
await expect(mcp).toBeHidden()
|
||||
})
|
||||
}
|
||||
@@ -22,8 +22,7 @@ test("navigates to a subagent child session missing from the session list", asyn
|
||||
await expectSessionTitle(page, taskDescription)
|
||||
await expect(page.getByRole("heading", { name: parentTitle })).toHaveCount(0)
|
||||
|
||||
const titlebarRight = page.locator("#opencode-titlebar-right")
|
||||
await expect(titlebarRight.getByRole("button", { name: "Toggle review" })).toHaveCount(1)
|
||||
await expect(page.getByRole("button", { name: "Toggle review", exact: true })).toBeVisible()
|
||||
})
|
||||
|
||||
test("returns to the parent session with Escape", async ({ page }) => {
|
||||
|
||||
@@ -121,7 +121,10 @@ test("vertical tabs show project details, resize, and navigate", async ({ page }
|
||||
await mockServer(page)
|
||||
await page.addInitScript(
|
||||
({ server, sessionA, sessionB }) => {
|
||||
localStorage.setItem("settings.v3", JSON.stringify({ appearance: { tabLayout: "vertical" } }))
|
||||
localStorage.setItem(
|
||||
"settings.v3",
|
||||
JSON.stringify({ appearance: { tabLayout: "vertical" }, general: { showStatus: true } }),
|
||||
)
|
||||
localStorage.setItem(
|
||||
"opencode.window.browser.dat:tabs",
|
||||
JSON.stringify([
|
||||
@@ -144,7 +147,26 @@ test("vertical tabs show project details, resize, and navigate", async ({ page }
|
||||
await expect(tabA).toContainText(sessionA.title)
|
||||
await expect(tabB).toContainText(sessionB.title)
|
||||
await expect(tabB.locator('[data-slot="tab-project"]')).toHaveText("tab-project")
|
||||
await expect(sidebar.getByRole("button", { name: "Home", exact: true })).toHaveText("Home")
|
||||
await expect(sidebar.getByRole("button", { name: "New session" })).toBeVisible()
|
||||
await expect(sidebar.locator('[data-slot="vertical-tabs-footer"]')).toBeVisible()
|
||||
const status = sidebar.getByRole("button", { name: "Status", exact: true })
|
||||
await expect(status).toBeVisible()
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const bounds = await sidebar.boundingBox()
|
||||
const button = await status.boundingBox()
|
||||
return !!bounds && !!button && bounds.x + bounds.width - button.x - button.width <= 12
|
||||
})
|
||||
.toBe(true)
|
||||
await expect(page.locator('[data-slot="titlebar-v2"]')).toBeHidden()
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const button = await sidebar.getByRole("button", { name: "New session" }).boundingBox()
|
||||
const tab = await tabA.boundingBox()
|
||||
return !!button && !!tab && button.y + button.height < tab.y
|
||||
})
|
||||
.toBe(true)
|
||||
await expect(page.locator('[data-slot="titlebar-tabs"]')).toHaveCount(0)
|
||||
|
||||
const handle = sidebar.locator('[data-component="resize-handle"]')
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Show, type JSX } from "solid-js"
|
||||
import { Show } from "solid-js"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
|
||||
export type SessionHeaderActionsState = {
|
||||
status?: { label: string; content: () => JSX.Element }
|
||||
reviewLabel: string
|
||||
reviewKeybind: string[]
|
||||
reviewVisible: boolean
|
||||
@@ -16,13 +15,6 @@ export type SessionHeaderActionsState = {
|
||||
export function SessionHeaderActions(props: { state: SessionHeaderActionsState }) {
|
||||
return (
|
||||
<div class="flex items-center gap-2">
|
||||
<Show when={props.state.status}>
|
||||
{(status) => (
|
||||
<Tooltip appearance="standard" placement="bottom" value={status().label}>
|
||||
{status().content()}
|
||||
</Tooltip>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={props.state.reviewVisible}>
|
||||
<Tooltip
|
||||
class="shrink-0"
|
||||
@@ -40,7 +32,7 @@ export function SessionHeaderActions(props: { state: SessionHeaderActionsState }
|
||||
type="button"
|
||||
variant="ghost-muted"
|
||||
size="large"
|
||||
class="!w-9 shrink-0"
|
||||
class="shrink-0"
|
||||
state={props.state.reviewOpened ? "pressed" : undefined}
|
||||
onClick={props.state.onReviewToggle}
|
||||
aria-label={props.state.reviewLabel}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createMemo } from "solid-js"
|
||||
import { createMemo, Show } from "solid-js"
|
||||
import { createMediaQuery } from "@solid-primitives/media"
|
||||
import { useCommand } from "@/shell/commands/command"
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
@@ -7,6 +7,7 @@ import { useSessionLayout } from "@/session/session-layout"
|
||||
import { reviewTooltipKeybind } from "@/shell/commands/tooltip-keybind"
|
||||
import { StatusPopover } from "@/shell/status/status-popover"
|
||||
import { TitlebarRight } from "@/shell/titlebar/right-slot"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { SessionHeaderActions, type SessionHeaderActionsState } from "./session-header-actions"
|
||||
|
||||
export function SessionHeader() {
|
||||
@@ -15,14 +16,9 @@ export function SessionHeader() {
|
||||
const settings = useSettings()
|
||||
const { view } = useSessionLayout()
|
||||
|
||||
const status = settings.visibility.status
|
||||
const isDesktop = createMediaQuery("(min-width: 768px)")
|
||||
|
||||
const actions = createMemo<SessionHeaderActionsState>(() => ({
|
||||
status:
|
||||
isDesktop() && status()
|
||||
? { label: language.t("status.popover.trigger"), content: () => <StatusPopover /> }
|
||||
: undefined,
|
||||
reviewLabel: language.t("command.review.toggle"),
|
||||
reviewKeybind: reviewTooltipKeybind(command),
|
||||
reviewVisible: isDesktop(),
|
||||
@@ -31,8 +27,15 @@ export function SessionHeader() {
|
||||
}))
|
||||
|
||||
return (
|
||||
<TitlebarRight>
|
||||
<>
|
||||
<TitlebarRight>
|
||||
<Show when={isDesktop() && settings.visibility.status()}>
|
||||
<Tooltip appearance="standard" placement="bottom" value={language.t("status.popover.trigger")}>
|
||||
<StatusPopover />
|
||||
</Tooltip>
|
||||
</Show>
|
||||
</TitlebarRight>
|
||||
<SessionHeaderActions state={actions()} />
|
||||
</TitlebarRight>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
} from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||
import { SessionHeader } from "@/session/header/session-header"
|
||||
import { MessageTimeline, SessionSummaryPanel } from "@/session/timeline/message-timeline"
|
||||
import { useServer } from "@/runtime/server/current"
|
||||
import { projectForSession } from "@/shell/layout/helpers"
|
||||
@@ -273,7 +272,6 @@ export function SessionScreen(props: { session: SessionModel }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SessionHeader />
|
||||
<div class="flex-1 min-h-0 flex flex-col gap-2 px-2 pb-[var(--shell-bottom-inset,8px)] pt-[var(--shell-top-inset,8px)]">
|
||||
<div ref={screen.panel.ref} class="relative flex-1 min-h-0 flex flex-col md:flex-row gap-2">
|
||||
<div
|
||||
|
||||
@@ -32,6 +32,7 @@ import { parseCommentNote, readPromptPresentation } from "@/composer/comment-not
|
||||
import { useCommand } from "@/shell/commands/command"
|
||||
import { useSettings } from "@/settings/model"
|
||||
import { SessionTitleHeader } from "../session-identity-header"
|
||||
import { SessionHeader } from "@/session/header/session-header"
|
||||
|
||||
type BackgroundTask = {
|
||||
id: string
|
||||
@@ -732,6 +733,58 @@ function MessageTimelineView(
|
||||
/>
|
||||
</Show>
|
||||
</Show>
|
||||
<Show when={sessionID()} keyed>
|
||||
{(id) => (
|
||||
<Menu
|
||||
gutter={6}
|
||||
placement="bottom-end"
|
||||
open={title.menuOpen}
|
||||
onOpenChange={(open) => setTitle("menuOpen", open)}
|
||||
>
|
||||
<Menu.Trigger
|
||||
as={IconButton}
|
||||
icon={<Icon name="outline-dots" />}
|
||||
variant="ghost-muted"
|
||||
size="large"
|
||||
class="shrink-0"
|
||||
aria-label={language.t("common.moreOptions")}
|
||||
aria-expanded={title.menuOpen}
|
||||
/>
|
||||
<Menu.Portal>
|
||||
<Menu.Content
|
||||
style={{ "min-width": "160px" }}
|
||||
onCloseAutoFocus={(event) => {
|
||||
if (!title.pendingRename) return
|
||||
event.preventDefault()
|
||||
setTitle("pendingRename", false)
|
||||
openTitleEditor()
|
||||
}}
|
||||
>
|
||||
<Show when={!parentID()}>
|
||||
<Menu.Item
|
||||
onSelect={() => {
|
||||
setTitle("pendingRename", true)
|
||||
setTitle("menuOpen", false)
|
||||
}}
|
||||
>
|
||||
{language.t("common.rename")}
|
||||
</Menu.Item>
|
||||
<Menu.Item onSelect={() => void props.action.export(id)}>
|
||||
{language.t("common.export")}...
|
||||
</Menu.Item>
|
||||
</Show>
|
||||
<Show when={!parentID()}>
|
||||
{/* TODO: Need a session archive API. */}
|
||||
<Menu.Separator />
|
||||
<Menu.Item onSelect={() => props.action.showDelete(id)}>
|
||||
{language.t("common.delete")}...
|
||||
</Menu.Item>
|
||||
</Show>
|
||||
</Menu.Content>
|
||||
</Menu.Portal>
|
||||
</Menu>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={sessionID()} keyed>
|
||||
@@ -775,56 +828,7 @@ function MessageTimelineView(
|
||||
</Popover>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={!parentID()}>
|
||||
<Menu
|
||||
gutter={6}
|
||||
placement="bottom-end"
|
||||
open={title.menuOpen}
|
||||
onOpenChange={(open) => {
|
||||
setTitle("menuOpen", open)
|
||||
if (open) return
|
||||
}}
|
||||
>
|
||||
<Menu.Trigger
|
||||
as={IconButton}
|
||||
icon={<Icon name="outline-dots" />}
|
||||
variant="ghost-muted"
|
||||
size="large"
|
||||
aria-label={language.t("common.moreOptions")}
|
||||
aria-expanded={title.menuOpen}
|
||||
/>
|
||||
<Menu.Portal>
|
||||
<Menu.Content
|
||||
style={{ width: "120px", "min-width": "120px" }}
|
||||
onCloseAutoFocus={(event) => {
|
||||
if (title.pendingRename) {
|
||||
event.preventDefault()
|
||||
setTitle("pendingRename", false)
|
||||
openTitleEditor()
|
||||
return
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Menu.Item
|
||||
onSelect={() => {
|
||||
setTitle("pendingRename", true)
|
||||
setTitle("menuOpen", false)
|
||||
}}
|
||||
>
|
||||
{language.t("common.rename")}
|
||||
</Menu.Item>
|
||||
<Menu.Item onSelect={() => void props.action.export(id)}>
|
||||
{language.t("common.export")}...
|
||||
</Menu.Item>
|
||||
{/* TODO: Need a session archive API. */}
|
||||
<Menu.Separator />
|
||||
<Menu.Item onSelect={() => props.action.showDelete(id)}>
|
||||
{language.t("common.delete")}...
|
||||
</Menu.Item>
|
||||
</Menu.Content>
|
||||
</Menu.Portal>
|
||||
</Menu>
|
||||
</Show>
|
||||
<SessionHeader />
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
@@ -65,7 +65,7 @@ export default function Layout(props: ParentProps) {
|
||||
<aside
|
||||
ref={(element) => setState("tabsMount", element)}
|
||||
data-slot="vertical-tabs-sidebar"
|
||||
class="relative flex min-h-0 shrink-0 flex-col bg-v2-background-bg-deep px-2.5 pb-[var(--shell-bottom-inset,8px)] pt-[var(--shell-top-inset,8px)]"
|
||||
class="relative flex h-full min-h-0 shrink-0 flex-col bg-v2-background-bg-deep px-2.5 pb-[var(--shell-bottom-inset,8px)] pt-[var(--shell-top-inset,8px)]"
|
||||
style={{
|
||||
width: `${state.tabsWidth}px`,
|
||||
"padding-bottom": "max(8px, env(safe-area-inset-bottom, 0px))",
|
||||
|
||||
@@ -7,6 +7,8 @@ import { useGlobal } from "@/runtime/server/runtime"
|
||||
import { hasNonBlockingServiceIssue, hasServiceNeedingAttention, serverStatusDotClass } from "./indicator"
|
||||
import { useData, useServer } from "@/runtime/server/current"
|
||||
import { useWorkspaceLocation } from "@/workspaces/location"
|
||||
import { useSettings } from "@/settings/model"
|
||||
import { createMediaQuery } from "@solid-primitives/media"
|
||||
|
||||
const Body = lazy(() => import("./body").then((x) => ({ default: x.StatusPopoverBody })))
|
||||
|
||||
@@ -16,6 +18,8 @@ export function StatusPopover() {
|
||||
const global = useGlobal()
|
||||
const data = useData()
|
||||
const sdk = useWorkspaceLocation()
|
||||
const settings = useSettings()
|
||||
const desktop = createMediaQuery("(min-width: 768px)")
|
||||
const [shown, setShown] = createSignal(false)
|
||||
const serverHealth = () => global.servers.health[server.key]?.healthy
|
||||
const mcp = () => data.location.mcp.server.list({ directory: sdk().directory })
|
||||
@@ -37,6 +41,8 @@ export function StatusPopover() {
|
||||
serverHealth: serverHealth(),
|
||||
attention: attention(),
|
||||
issue: issue(),
|
||||
placement: desktop() && settings.appearance.tabLayout() === "vertical" ? "top-start" : "bottom-end",
|
||||
shift: desktop() && settings.appearance.tabLayout() === "vertical" ? 0 : -168,
|
||||
label: language.t("status.popover.trigger"),
|
||||
onOpenChange: setShown,
|
||||
body: () => (
|
||||
@@ -55,6 +61,8 @@ type StatusPopoverState = {
|
||||
serverHealth: boolean | undefined
|
||||
attention: boolean
|
||||
issue: boolean
|
||||
placement: "top-start" | "bottom-end"
|
||||
shift: number
|
||||
label: string
|
||||
onOpenChange: (value: boolean) => void
|
||||
body: () => JSX.Element
|
||||
@@ -77,8 +85,8 @@ function StatusPopoverView(props: { state: StatusPopoverState }) {
|
||||
class:
|
||||
"[&_[data-slot=popover-body]]:p-0 w-[360px] max-w-[calc(100vw-40px)] bg-transparent border-0 shadow-none rounded-xl",
|
||||
gutter: 4,
|
||||
placement: "bottom-end" as const,
|
||||
shift: -168,
|
||||
placement: props.state.placement,
|
||||
shift: props.state.shift,
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -86,6 +86,10 @@
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
[data-slot="vertical-tabs-sidebar"] [data-titlebar-tab-list][data-orientation="vertical"] {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
[data-titlebar-tab-slot] {
|
||||
--tab-separator: var(--v2-background-bg-layer-03);
|
||||
position: relative;
|
||||
|
||||
@@ -37,6 +37,7 @@ const windowsTitlebarHeight = 44 // Includes the content inset; matches the nati
|
||||
const minTitlebarZoom = 0.25
|
||||
const windowsControlsBaseWidth = 138 // 3 native Windows caption buttons at 46px each.
|
||||
const macTrafficLightsBaseWidth = 84
|
||||
const macTrafficLightsTopClearance = 28
|
||||
|
||||
export type TitlebarUpdate = {
|
||||
version: string | undefined
|
||||
@@ -63,6 +64,7 @@ export function Titlebar(props: {
|
||||
const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
|
||||
const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux")
|
||||
const macTrafficLights = createMemo(() => mac() && !platform.windowFullscreen?.())
|
||||
const macVerticalTabs = createMemo(() => mac() && !!props.verticalTabs)
|
||||
const zoom = () => platform.webviewZoom?.() ?? 1
|
||||
const titlebarZoom = () => (windows() ? Math.max(zoom(), minTitlebarZoom) : zoom())
|
||||
const minHeight = () => {
|
||||
@@ -105,6 +107,7 @@ export function Titlebar(props: {
|
||||
const rightState = createMemo<TitlebarRightState>(() => ({
|
||||
update: updateState(),
|
||||
}))
|
||||
const hideVerticalTitlebar = createMemo(() => !!props.verticalTabs && !windows())
|
||||
|
||||
const back = () => {
|
||||
const next = backPath(history)
|
||||
@@ -140,6 +143,7 @@ export function Titlebar(props: {
|
||||
return (
|
||||
<header
|
||||
data-slot="titlebar-v2"
|
||||
hidden={hideVerticalTitlebar()}
|
||||
classList={{
|
||||
"shrink-0 relative flex flex-row h-9 bg-v2-background-bg-deep overflow-visible": true,
|
||||
"order-last": bottom(),
|
||||
@@ -311,6 +315,48 @@ export function Titlebar(props: {
|
||||
}
|
||||
}
|
||||
const toggleHome = () => tabs.toggleHome({ home: layout.route().type === "home", current: currentTab() })
|
||||
const homeButton = (vertical = false) => (
|
||||
<Show
|
||||
when={vertical}
|
||||
fallback={
|
||||
<Tooltip
|
||||
placement="bottom"
|
||||
value={
|
||||
<>
|
||||
{language.t("home.title")}
|
||||
<Keybind keys={command.keybindParts("home.toggle")} variant="neutral" />
|
||||
</>
|
||||
}
|
||||
class="shrink-0"
|
||||
>
|
||||
<IconButton
|
||||
type="button"
|
||||
variant="ghost-muted"
|
||||
size="large"
|
||||
class="!w-9 shrink-0"
|
||||
icon={<Icon name="grid-plus" />}
|
||||
state={layout.route().type === "home" ? "pressed" : undefined}
|
||||
onClick={toggleHome}
|
||||
aria-label={language.t("home.title")}
|
||||
aria-pressed={layout.route().type === "home"}
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
data-action="vertical-tabs-home"
|
||||
data-state={layout.route().type === "home" ? "pressed" : undefined}
|
||||
class="mb-1 flex h-7 w-full shrink-0 items-center gap-1.5 rounded-[6px] px-1.5 text-[13px] leading-4 text-v2-text-text-faint hover:bg-v2-background-bg-layer-02 hover:text-v2-text-text-base data-[state=pressed]:bg-v2-background-bg-layer-02 data-[state=pressed]:text-v2-text-text-base"
|
||||
onClick={toggleHome}
|
||||
aria-label={language.t("home.title")}
|
||||
aria-pressed={layout.route().type === "home"}
|
||||
>
|
||||
<Icon name="grid-plus" />
|
||||
{language.t("home.title")}
|
||||
</button>
|
||||
</Show>
|
||||
)
|
||||
|
||||
command.register("titlebar-home", () => [
|
||||
{
|
||||
@@ -386,36 +432,13 @@ export function Titlebar(props: {
|
||||
"md:pl-4": !macTrafficLights(),
|
||||
}}
|
||||
>
|
||||
<Show when={!mobile()}>
|
||||
<Show when={!mobile() && !props.verticalTabs}>
|
||||
<ChannelIndicator debugTools={props.debugTools} />
|
||||
</Show>
|
||||
<Show when={windows() || linux()}>
|
||||
<WindowsAppMenu command={command} platform={platform} />
|
||||
</Show>
|
||||
<Show when={!mobile()}>
|
||||
<Tooltip
|
||||
placement="bottom"
|
||||
value={
|
||||
<>
|
||||
{language.t("home.title")}
|
||||
<Keybind keys={command.keybindParts("home.toggle")} variant="neutral" />
|
||||
</>
|
||||
}
|
||||
class="shrink-0"
|
||||
>
|
||||
<IconButton
|
||||
type="button"
|
||||
variant="ghost-muted"
|
||||
size="large"
|
||||
class="!w-9 shrink-0"
|
||||
icon={<Icon name="grid-plus" />}
|
||||
state={layout.route().type === "home" ? "pressed" : undefined}
|
||||
onClick={toggleHome}
|
||||
aria-label={language.t("home.title")}
|
||||
aria-pressed={layout.route().type === "home"}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
<Show when={!mobile() && !props.verticalTabs}>{homeButton()}</Show>
|
||||
|
||||
<Show
|
||||
when={!mobile()}
|
||||
@@ -595,31 +618,53 @@ export function Titlebar(props: {
|
||||
{(vertical) => (
|
||||
<Show when={vertical().mount} keyed>
|
||||
{(mount) => (
|
||||
<Portal mount={mount}>
|
||||
<TitlebarTabStrip
|
||||
orientation="vertical"
|
||||
tabs={tabsStore}
|
||||
currentTab={currentTab()}
|
||||
onNavigate={(tab, el) => {
|
||||
tabs.select(tab)
|
||||
el?.scrollIntoView({ behavior: "instant", block: "nearest" })
|
||||
}}
|
||||
onClose={(tab) => {
|
||||
const index = tabsStore.findIndex((item) => tabKey(item) === tabKey(tab))
|
||||
if (index !== -1) tabsStoreActions.closeTab(index)
|
||||
}}
|
||||
onReorder={(keys) => tabsStoreActions.reorder(keys)}
|
||||
/>
|
||||
<Portal
|
||||
mount={mount}
|
||||
ref={(element) => (element.className = "flex size-full min-h-0 flex-col")}
|
||||
>
|
||||
<Show when={macVerticalTabs()}>
|
||||
<div
|
||||
class="relative w-full shrink-0"
|
||||
style={{ height: `${macTrafficLightsTopClearance / zoom()}px` }}
|
||||
data-tauri-drag-region
|
||||
></div>
|
||||
</Show>
|
||||
{homeButton(true)}
|
||||
<button
|
||||
type="button"
|
||||
data-action="vertical-tabs-new-session"
|
||||
class="mt-1 flex h-7 w-full shrink-0 items-center gap-1.5 rounded-[6px] px-1.5 text-[13px] leading-4 text-v2-text-text-faint hover:bg-v2-background-bg-layer-02 hover:text-v2-text-text-base"
|
||||
class="flex h-7 w-full shrink-0 items-center gap-1.5 rounded-[6px] px-1.5 text-[13px] leading-4 text-v2-text-text-faint hover:bg-v2-background-bg-layer-02 hover:text-v2-text-text-base"
|
||||
onClick={openNewTab}
|
||||
aria-label={language.t("command.session.new")}
|
||||
>
|
||||
<Icon name="plus" />
|
||||
{language.t("command.session.new")}
|
||||
</button>
|
||||
<div class="my-1 h-px w-full shrink-0 bg-v2-border-border-muted" aria-hidden="true" />
|
||||
<div class="flex min-h-0 flex-1 flex-col gap-1">
|
||||
<TitlebarTabStrip
|
||||
orientation="vertical"
|
||||
tabs={tabsStore}
|
||||
currentTab={currentTab()}
|
||||
onNavigate={(tab, el) => {
|
||||
tabs.select(tab)
|
||||
el?.scrollIntoView({ behavior: "instant", block: "nearest" })
|
||||
}}
|
||||
onClose={(tab) => {
|
||||
const index = tabsStore.findIndex((item) => tabKey(item) === tabKey(tab))
|
||||
if (index !== -1) tabsStoreActions.closeTab(index)
|
||||
}}
|
||||
onReorder={(keys) => tabsStoreActions.reorder(keys)}
|
||||
/>
|
||||
</div>
|
||||
<div data-slot="vertical-tabs-footer" class="relative mt-auto h-9 w-full shrink-0">
|
||||
<div class="absolute bottom-0 left-0 flex h-9 items-center">
|
||||
<ChannelIndicator debugTools={props.debugTools} />
|
||||
</div>
|
||||
<div class="absolute bottom-0 right-0 flex h-9 items-center">
|
||||
<TitlebarRightMount />
|
||||
</div>
|
||||
</div>
|
||||
</Portal>
|
||||
)}
|
||||
</Show>
|
||||
@@ -629,7 +674,7 @@ export function Titlebar(props: {
|
||||
<Show when={!mobile()}>
|
||||
<div class="flex-1" />
|
||||
</Show>
|
||||
<TitlebarRight state={rightState()} />
|
||||
<TitlebarRight state={rightState()} mount={!props.verticalTabs} />
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
@@ -652,13 +697,15 @@ type TitlebarRightState = {
|
||||
update: TitlebarUpdatePillState
|
||||
}
|
||||
|
||||
function TitlebarRight(props: { state: TitlebarRightState }) {
|
||||
function TitlebarRight(props: { state: TitlebarRightState; mount?: boolean }) {
|
||||
return (
|
||||
<div class="relative z-20 flex shrink-0 items-center justify-end gap-0 overflow-visible">
|
||||
<Show when={props.state.update.visible}>
|
||||
<TitlebarUpdateIconButton state={props.state.update} />
|
||||
</Show>
|
||||
<TitlebarRightMount />
|
||||
<Show when={props.mount !== false}>
|
||||
<TitlebarRightMount />
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user