@@ -40,10 +40,16 @@ function scrollTabsBy(delta: number) {
4040 tabsListRef .value ?.scrollBy ({ left: delta , behavior: ' smooth' })
4141}
4242
43- async function scrollActiveTabIntoView() {
43+ function scrollTabsToEnd() {
44+ const el = tabsListRef .value
45+ if (! el ) return
46+ el .scrollTo ({ left: el .scrollWidth , behavior: ' smooth' })
47+ }
48+
49+ async function scrollActiveTabIntoView(alignToEnd = false ) {
4450 await nextTick ()
4551 const el = tabsListRef .value ?.querySelector <HTMLElement >(' .tab-active' )
46- el ?.scrollIntoView ({ block: ' nearest' , inline: ' nearest' })
52+ el ?.scrollIntoView ({ block: ' nearest' , inline: alignToEnd ? ' end ' : ' nearest' })
4753}
4854
4955const enc = new TextEncoder ()
@@ -91,10 +97,12 @@ const tabs = shallowRef<TabState[]>([])
9197const activeTabId = ref (- 1 )
9298let nextTabId = 0
9399const termElMap = new Map <number , HTMLDivElement >()
100+ const tabWidth = ref (160 )
94101
95102const activeTab = computed (() => tabs .value .find (t => t .id === activeTabId .value ) ?? null )
96103
97104let keybarResizeObserver: ResizeObserver | null = null
105+ let tabsListResizeObserver: ResizeObserver | null = null
98106let panelResizeObserver: ResizeObserver | null = null
99107let previousBodyOverflow = ' '
100108let previousHtmlOverflow = ' '
@@ -146,6 +154,23 @@ function registerTermEl(id: number, el: unknown) {
146154 }
147155}
148156
157+ function updateTabsLayout() {
158+ const el = tabsListRef .value
159+ if (! el ) return
160+
161+ tabsScrollLeft .value = el .scrollLeft
162+ tabsScrollMax .value = Math .max (0 , el .scrollWidth - el .clientWidth )
163+
164+ const availableWidth = el .clientWidth
165+ const count = tabs .value .length
166+
167+ if (count > 1 && availableWidth > 0 ) {
168+ tabWidth .value = Math .max (80 , Math .min (160 , Math .floor (availableWidth / count )))
169+ } else {
170+ tabWidth .value = 160
171+ }
172+ }
173+
149174// The server only echoes a shell ID for the 2nd+ shell; the 1st shell's ID is the session ID.
150175function effectiveShellId(tab : TabState ): string | null {
151176 if (tab .shellId ) return tab .shellId
@@ -393,7 +418,8 @@ async function addTab() {
393418 activeTabId .value = tab .id
394419 await nextTick ()
395420 await new Promise <void >(resolve => requestAnimationFrame (() => resolve ()))
396- void scrollActiveTabIntoView ()
421+ updateTabsLayout ()
422+ scrollTabsToEnd ()
397423 await initTab (tab )
398424}
399425
@@ -443,11 +469,21 @@ const observePanelSize = () => {
443469 panelResizeObserver ?.disconnect ()
444470 panelResizeObserver = new ResizeObserver (() => {
445471 if (activeTab .value ) handleResizeTab (activeTab .value )
446- updateTabsScroll ()
472+ updateTabsLayout ()
447473 })
448474 panelResizeObserver .observe (panelRef .value )
449475}
450476
477+ const observeTabsListSize = () => {
478+ if (! tabsListRef .value || typeof ResizeObserver === ' undefined' ) return
479+
480+ tabsListResizeObserver ?.disconnect ()
481+ tabsListResizeObserver = new ResizeObserver (() => {
482+ updateTabsLayout ()
483+ })
484+ tabsListResizeObserver .observe (tabsListRef .value )
485+ }
486+
451487const observeKeybarHeight = () => {
452488 keybarHeight .value = keybarRef .value ?.offsetHeight ?? 0
453489
@@ -568,11 +604,13 @@ onMounted(async () => {
568604 window .visualViewport ?.addEventListener (' scroll' , updateKeybarPosition )
569605 await nextTick ()
570606 observeKeybarHeight ()
607+ observeTabsListSize ()
571608 observePanelSize ()
609+ updateTabsLayout ()
572610 updateKeybarPosition ()
573611
574612 await addTab ()
575- requestAnimationFrame (updateTabsScroll )
613+ requestAnimationFrame (updateTabsLayout )
576614})
577615
578616onUnmounted (() => {
@@ -582,6 +620,8 @@ onUnmounted(() => {
582620 window .visualViewport ?.removeEventListener (' scroll' , updateKeybarPosition )
583621 keybarResizeObserver ?.disconnect ()
584622 keybarResizeObserver = null
623+ tabsListResizeObserver ?.disconnect ()
624+ tabsListResizeObserver = null
585625 panelResizeObserver ?.disconnect ()
586626 panelResizeObserver = null
587627 clearTerminalTouchScroll ()
@@ -597,7 +637,7 @@ watch([terminalInsetBottom, panelViewportHeight], () => {
597637
598638watch (() => tabs .value .length , async () => {
599639 await nextTick ()
600- requestAnimationFrame (updateTabsScroll )
640+ requestAnimationFrame (updateTabsLayout )
601641})
602642
603643// Window focus doesn't move DOM focus, so refocus xterm ourselves.
@@ -616,24 +656,29 @@ watch(() => props.focused, (focused) => {
616656 :disabled =" tabsScrollLeft <= 0"
617657 @click =" scrollTabsBy(-160)"
618658 ><i class =" bi bi-chevron-left" ></i ></button >
619- <div ref =" tabsListRef" class =" tabs-list" @scroll =" updateTabsScroll" >
659+ <div
660+ ref =" tabsListRef"
661+ class =" tabs-list"
662+ :style =" { '--terminal-tab-width': `${tabWidth}px` }"
663+ @scroll =" updateTabsScroll"
664+ >
620665 <button
621666 v-for =" tab in tabs"
622667 :key =" tab.id"
623668 class =" tab-item"
624669 :class =" { 'tab-active': tab.id === activeTabId }"
625670 @mousedown.prevent =" switchTab(tab.id, $event)"
626- >
627- <span class =" tab-label" >{{ tab.label }}</span >
628- <span
629- class =" tab-close"
630- role =" button"
631- :title =" `Close ${tab.label}`"
632- @click.stop =" closeTab(tab.id)"
633- >× ; </span >
634- </button >
671+ >
672+ <span class =" tab-label" >{{ tab.label }}</span >
673+ <span
674+ class =" tab-close"
675+ role =" button"
676+ :title =" `Close ${tab.label}`"
677+ @click.stop =" closeTab(tab.id)"
678+ >× ; </span >
679+ </button >
680+ <button class =" tab-add" title =" New terminal" @click =" addTab" >+</button >
635681 </div >
636- <button class =" tab-add" title =" New terminal" @click =" addTab" >+</button >
637682 <button
638683 v-if =" tabsScrollMax > 0"
639684 class =" tab-scroll-btn"
@@ -820,14 +865,14 @@ watch(() => props.focused, (focused) => {
820865 align-items : stretch ;
821866 overflow : hidden ;
822867 margin : -0.25rem 0 -0.25rem -0.6rem ;
823- flex : 1 1 0 ;
868+ flex : 1 1 auto ;
824869 min-width : 0 ;
825870}
826871
827872.tabs-list {
828873 display : flex ;
829874 align-items : stretch ;
830- flex : 0 1 auto ;
875+ flex : 1 1 auto ;
831876 min-width : 0 ;
832877 overflow-x : auto ;
833878 scrollbar-width : none ;
@@ -840,7 +885,9 @@ watch(() => props.focused, (focused) => {
840885/* GNOME/Yaru style: inactive tabs blend into the bar, the active tab gets
841886 a lighter "pressed in" panel with an accent underline. */
842887.tab-item {
843- flex : 0 1 160px ;
888+ flex : 0 0 var (--terminal-tab-width , 160px );
889+ width : var (--terminal-tab-width , 160px );
890+ max-width : var (--terminal-tab-width , 160px );
844891 min-width : 80px ;
845892 display : flex ;
846893 align-items : center ;
0 commit comments