import React from 'react' import { Box, Text, stringWidth } from '@anthropic/ink' import { truncateToWidth } from '../utils/format.js' // Constants for width calculations - derived from actual rendered strings const ALL_TAB_LABEL = 'All' const TAB_PADDING = 2 // Space before and after tab text: " {tab} " const HASH_PREFIX_LENGTH = 1 // "#" prefix for non-All tabs const LEFT_ARROW_PREFIX = '← ' const RIGHT_HINT_WITH_COUNT_PREFIX = '→' const RIGHT_HINT_SUFFIX = ' (tab to cycle)' const RIGHT_HINT_NO_COUNT = '(tab to cycle)' const MAX_OVERFLOW_DIGITS = 2 // Assume max 99 hidden tabs for width calculation // Computed widths const LEFT_ARROW_WIDTH = LEFT_ARROW_PREFIX.length + MAX_OVERFLOW_DIGITS + 1 // "← NN " with gap const RIGHT_HINT_WIDTH_WITH_COUNT = RIGHT_HINT_WITH_COUNT_PREFIX.length + MAX_OVERFLOW_DIGITS + RIGHT_HINT_SUFFIX.length // "→NN (tab to cycle)" const RIGHT_HINT_WIDTH_NO_COUNT = RIGHT_HINT_NO_COUNT.length type Props = { tabs: string[] selectedIndex: number availableWidth: number showAllProjects?: boolean } /** * Calculate the display width of a tab */ function getTabWidth(tab: string, maxWidth?: number): number { if (tab === ALL_TAB_LABEL) { return ALL_TAB_LABEL.length + TAB_PADDING } // For non-All tabs: " #{tag} " but truncate tag if needed const tagWidth = stringWidth(tab) const effectiveTagWidth = maxWidth ? Math.min(tagWidth, maxWidth - TAB_PADDING - HASH_PREFIX_LENGTH) : tagWidth return Math.max(0, effectiveTagWidth) + TAB_PADDING + HASH_PREFIX_LENGTH } /** * Truncate a tag to fit within maxWidth, accounting for padding and hash prefix */ function truncateTag(tag: string, maxWidth: number): string { // Available space for the tag text itself: maxWidth - " #" - " " const availableForTag = maxWidth - TAB_PADDING - HASH_PREFIX_LENGTH if (stringWidth(tag) <= availableForTag) { return tag } if (availableForTag <= 1) { return tag.charAt(0) } return truncateToWidth(tag, availableForTag) } export function TagTabs({ tabs, selectedIndex, availableWidth, showAllProjects = false, }: Props): React.ReactNode { const resumeLabel = showAllProjects ? 'Resume (All Projects)' : 'Resume' const resumeLabelWidth = resumeLabel.length + 1 // +1 for gap // Calculate how much space we have for tabs (use worst-case hint width) const rightHintWidth = Math.max( RIGHT_HINT_WIDTH_WITH_COUNT, RIGHT_HINT_WIDTH_NO_COUNT, ) const maxTabsWidth = availableWidth - resumeLabelWidth - rightHintWidth - 2 // 2 for gaps // Clamp selectedIndex to valid range const safeSelectedIndex = Math.max( 0, Math.min(selectedIndex, tabs.length - 1), ) // Calculate width of each tab, with truncation for very long tags const maxSingleTabWidth = Math.max(20, Math.floor(maxTabsWidth / 2)) // At least show half the space for one tab const tabWidths = tabs.map(tab => getTabWidth(tab, maxSingleTabWidth)) // Find a window of tabs that fits, centered around selectedIndex let startIndex = 0 let endIndex = tabs.length // Calculate total width of all tabs const totalTabsWidth = tabWidths.reduce( (sum, w, i) => sum + w + (i < tabWidths.length - 1 ? 1 : 0), 0, ) // +1 for gaps between tabs if (totalTabsWidth > maxTabsWidth) { // Need to show a subset - account for left arrow when not at start const effectiveMaxWidth = maxTabsWidth - LEFT_ARROW_WIDTH // Start with the selected tab let windowWidth = tabWidths[safeSelectedIndex] ?? 0 startIndex = safeSelectedIndex endIndex = safeSelectedIndex + 1 // Expand window to include more tabs while (startIndex > 0 || endIndex < tabs.length) { const canExpandLeft = startIndex > 0 const canExpandRight = endIndex < tabs.length if (canExpandLeft) { const leftWidth = (tabWidths[startIndex - 1] ?? 0) + 1 // +1 for gap if (windowWidth + leftWidth <= effectiveMaxWidth) { startIndex-- windowWidth += leftWidth continue } } if (canExpandRight) { const rightWidth = (tabWidths[endIndex] ?? 0) + 1 // +1 for gap if (windowWidth + rightWidth <= effectiveMaxWidth) { endIndex++ windowWidth += rightWidth continue } } break } } const hiddenLeft = startIndex const hiddenRight = tabs.length - endIndex const visibleTabs = tabs.slice(startIndex, endIndex) const visibleIndices = visibleTabs.map((_, i) => startIndex + i) return ( {resumeLabel} {hiddenLeft > 0 && ( {LEFT_ARROW_PREFIX} {hiddenLeft} )} {visibleTabs.map((tab, i) => { const actualIndex = visibleIndices[i]! const isSelected = actualIndex === safeSelectedIndex const displayText = tab === ALL_TAB_LABEL ? tab : `#${truncateTag(tab, maxSingleTabWidth - TAB_PADDING)}` return ( {' '} {displayText}{' '} ) })} {hiddenRight > 0 ? ( {RIGHT_HINT_WITH_COUNT_PREFIX} {hiddenRight} {RIGHT_HINT_SUFFIX} ) : ( {RIGHT_HINT_NO_COUNT} )} ) }