Skip to content

Commit 9431356

Browse files
authored
fix(app): handle tab overflow and scrolling in titlebar (anomalyco#30886)
1 parent f6197ce commit 9431356

1 file changed

Lines changed: 108 additions & 54 deletions

File tree

packages/app/src/components/titlebar.tsx

Lines changed: 108 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,16 @@
1-
import { createEffect, createMemo, createResource, For, Match, Show, startTransition, Switch, untrack } from "solid-js"
1+
import {
2+
createEffect,
3+
createMemo,
4+
createResource,
5+
createSignal,
6+
For,
7+
Match,
8+
onMount,
9+
Show,
10+
startTransition,
11+
Switch,
12+
untrack,
13+
} from "solid-js"
214
import { createStore } from "solid-js/store"
315
import { useLocation, useMatch, useNavigate, useParams } from "@solidjs/router"
416
import { IconButton } from "@opencode-ai/ui/icon-button"
@@ -230,7 +242,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
230242
: undefined,
231243
"align-self": electronWindows() ? "flex-start" : undefined,
232244
}}
233-
data-tauri-drag-region
245+
// data-tauri-drag-region
234246
onMouseDown={drag}
235247
onDblClick={maximize}
236248
>
@@ -393,9 +405,16 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
393405
].filter((v) => v !== undefined)
394406
})
395407

408+
const [tabsAreOverflowing, setTabsAreOverflowing] = createSignal(false)
409+
let tabScrollRef!: HTMLDivElement
410+
411+
function refreshTabsAreOverflowing() {
412+
setTabsAreOverflowing(tabScrollRef.scrollWidth > tabScrollRef.clientWidth)
413+
}
414+
396415
return (
397416
<div
398-
class="h-full flex-1 flex flex-row items-center gap-1.5 pr-3 pt-2"
417+
class="h-full flex-1 overflow-hidden flex flex-row items-center gap-1.5 pr-3 pt-2"
399418
classList={{
400419
"pl-2": mac(),
401420
"pl-4": !mac(),
@@ -410,60 +429,86 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
410429
size="large"
411430
as="a"
412431
href="/"
413-
class="!w-9"
432+
class="!w-9 shrink-0"
414433
icon={<IconV2 name="grid-plus" />}
415434
state={!!homeMatch() ? "pressed" : undefined}
416435
/>
417436

418-
<div class="flex min-w-0 flex-1 flex-row items-center gap-1.5 overflow-hidden">
419-
<div class="flex min-w-0 flex-row items-center gap-1.5 overflow-hidden">
420-
<For each={tabsStore}>
421-
{(tab, i) => (
422-
<>
423-
{i() !== 0 && (
424-
<div class="w-[1.5px] h-3 shrink-0 rounded-full bg-[var(--v2-background-bg-layer-02)]" />
425-
)}
426-
<TabNavItem
427-
href={tabHref(tab)}
428-
server={tab.server}
429-
directory={decode64(tab.dirBase64)!}
430-
sessionId={tab.sessionId}
431-
onNavigate={() => navigateTab(tab)}
432-
onClose={() => tabsStoreActions.removeTab(i())}
433-
active={currentTab() === tab}
434-
activeServer={tab.server === server.key}
435-
/>
436-
</>
437-
)}
437+
<div class="flex min-w-0 flex-row items-center gap-1.5 overflow-x-auto no-scrollbar" ref={tabScrollRef}>
438+
<div class="flex min-w-0 flex-row items-center gap-1.5">
439+
<For each={[...tabsStore, ...tabsStore, ...tabsStore]}>
440+
{(tab, i) => {
441+
let ref!: HTMLDivElement
442+
443+
onMount(() => {
444+
refreshTabsAreOverflowing()
445+
})
446+
447+
return (
448+
<>
449+
{i() !== 0 && (
450+
<div class="w-[1.5px] h-3 shrink-0 rounded-full bg-[var(--v2-background-bg-layer-02)]" />
451+
)}
452+
<TabNavItem
453+
ref={ref}
454+
href={tabHref(tab)}
455+
server={tab.server}
456+
directory={decode64(tab.dirBase64)!}
457+
sessionId={tab.sessionId}
458+
onNavigate={() => {
459+
navigateTab(tab)
460+
461+
ref.scrollIntoView({ behavior: "instant" })
462+
}}
463+
onClose={() => tabsStoreActions.removeTab(i())}
464+
active={currentTab() === tab}
465+
activeServer={tab.server === server.key}
466+
forceTruncate={tabsAreOverflowing()}
467+
/>
468+
</>
469+
)
470+
}}
438471
</For>
439-
</div>
440-
<Show
441-
when={creating() && params.dir}
442-
fallback={
443-
<IconButtonV2
444-
type="button"
445-
variant="ghost-muted"
446-
size="large"
447-
class="shrink-0"
448-
icon={<IconV2 name="plus" />}
449-
as="a"
450-
href={newSessionHref()}
451-
aria-label={language.t("command.session.new")}
452-
/>
453-
}
454-
>
455-
<NewSessionTabItem
456-
href={`/${params.dir}/session`}
457-
title={language.t("command.session.new")}
458-
onClose={() => {
459-
const tab = tabsStore.at(-1)
460-
if (tab) navigateTab(tab)
461-
else navigate("/")
472+
<Show when={creating() && params.dir}>
473+
{(_) => {
474+
let ref!: HTMLDivElement
475+
476+
onMount(() => {
477+
ref.scrollIntoView({ behavior: "instant" })
478+
})
479+
480+
return (
481+
<>
482+
<div class="w-[1.5px] h-3 shrink-0 rounded-full bg-[var(--v2-background-bg-layer-02)]" />
483+
<NewSessionTabItem
484+
ref={ref}
485+
href={`/${params.dir}/session`}
486+
title={language.t("command.session.new")}
487+
onClose={() => {
488+
const tab = tabsStore.at(-1)
489+
if (tab) navigateTab(tab)
490+
else navigate("/")
491+
}}
492+
/>
493+
</>
494+
)
462495
}}
463-
/>
464-
</Show>
465-
<div class="min-w-0 flex-1" />
496+
</Show>
497+
</div>
466498
</div>
499+
<Show when={!(creating() && params.dir)}>
500+
<IconButtonV2
501+
type="button"
502+
variant="ghost-muted"
503+
size="large"
504+
class="shrink-0"
505+
icon={<IconV2 name="plus" />}
506+
as="a"
507+
href={newSessionHref()}
508+
aria-label={language.t("command.session.new")}
509+
/>
510+
</Show>
511+
<div class="flex-1" />
467512
<TitlebarV2Right state={v2RightState()} />
468513
<Show when={windows() && !electronWindows()}>
469514
<div data-tauri-decorum-tb class="flex flex-row" />
@@ -615,7 +660,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
615660
"flex items-center min-w-0 justify-end": true,
616661
"pr-2": !windows(),
617662
}}
618-
data-tauri-drag-region
663+
// data-tauri-drag-region
619664
onMouseDown={drag}
620665
>
621666
<div id="opencode-titlebar-right" class="flex items-center gap-1 shrink-0 justify-end" />
@@ -685,6 +730,7 @@ function TitlebarUpdateIconButton(props: { state: TitlebarUpdatePillState }) {
685730
}
686731

687732
function TabNavItem(props: {
733+
ref?: HTMLDivElement
688734
href: string
689735
server: ServerConnection.Key
690736
directory: string
@@ -694,6 +740,7 @@ function TabNavItem(props: {
694740
onNavigate: () => void
695741
active?: boolean
696742
activeServer: boolean
743+
forceTruncate?: boolean
697744
}) {
698745
const closeTab = (event: MouseEvent) => {
699746
event.preventDefault()
@@ -710,6 +757,7 @@ function TabNavItem(props: {
710757
const [session] = createResource(
711758
() => {
712759
const ctx = dirSyncCtx()
760+
console.log({ ctx, sessionId: props.sessionId })
713761
if (!ctx || !props.sessionId) return
714762
return [props.sessionId, ctx] as const
715763
},
@@ -722,6 +770,7 @@ function TabNavItem(props: {
722770

723771
return (
724772
<div
773+
ref={props.ref}
725774
class="group relative flex h-7 min-w-24 max-w-60 flex-row items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-[6px] bg-[var(--tab-bg)] px-1.5 [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)]"
726775
data-active={props.active}
727776
onMouseDown={(event) => {
@@ -731,6 +780,7 @@ function TabNavItem(props: {
731780
>
732781
<Show when={session.latest}>
733782
{(session) => {
783+
console.log({ session: session() })
734784
const project = createMemo(() => projectForSession(session(), serverCtx()?.projects.list() ?? []))
735785

736786
return (
@@ -756,7 +806,10 @@ function TabNavItem(props: {
756806
}}
757807
</Show>
758808

759-
<div class="absolute not-group-hover:not-group-data-[active=true]:left-52 group-hover:right-0 group-data-[active=true]:right-0 inset-y-0 flex flex-row items-center pr-1 py-1 w-8 pl-2">
809+
<div
810+
class="absolute not-group-hover:not-group-data-[active=true]:not-data-[truncate=true]:left-52 group-hover:right-0 group-data-[active=true]:right-0 data-[truncate=true]:right-0 inset-y-0 flex flex-row items-center pr-1 py-1 w-8 pl-2"
811+
data-truncate={props.forceTruncate}
812+
>
760813
<div
761814
class="absolute inset-0 rounded-r-[6px] bg-(image:--inactive-bg) group-hover:bg-(image:--active-bg) group-data-[active=true]:bg-(image:--active-bg)"
762815
style={{
@@ -796,15 +849,16 @@ function ProjectTabAvatar(props: {
796849
)
797850
}
798851

799-
function NewSessionTabItem(props: { href: string; title: string; onClose: () => void }) {
852+
function NewSessionTabItem(props: { ref?: HTMLDivElement; href: string; title: string; onClose: () => void }) {
800853
const closeTab = (event: MouseEvent) => {
801854
event.preventDefault()
802855
event.stopPropagation()
803856
props.onClose()
804857
}
805858
return (
806859
<div
807-
class="group relative flex h-7 max-w-60 flex-row items-center gap-1.5 overflow-hidden rounded-[6px] bg-[var(--v2-overlay-simple-overlay-pressed)] pl-1.5 pr-8 whitespace-nowrap focus-within:outline focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-[var(--v2-border-border-focus)]"
860+
ref={props.ref}
861+
class="group relative shrink-0 flex h-7 max-w-60 flex-row items-center gap-1.5 overflow-hidden rounded-[6px] bg-[var(--v2-overlay-simple-overlay-pressed)] pl-1.5 pr-8 whitespace-nowrap focus-within:outline focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-[var(--v2-border-border-focus)]"
808862
onMouseDown={(event) => {
809863
if (event.button !== 1) return
810864
closeTab(event)

0 commit comments

Comments
 (0)