Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 99 additions & 33 deletions frontend/src/components/tool/ResultsTabs.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useMemo } from 'react';
import { useMemo, useRef, useState, useEffect, useCallback } from 'react';
import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
import { cn } from '../../lib/utils';
import { ResultsTable } from './ResultsTable';
import type { ResultTab } from './types';
Expand Down Expand Up @@ -28,11 +29,41 @@ export function ResultsTabs({
onTabClose,
isLoading,
}: ResultsTabsProps) {
const tabsContainerRef = useRef<HTMLDivElement>(null);
const [canScrollLeft, setCanScrollLeft] = useState(false);
const [canScrollRight, setCanScrollRight] = useState(false);

const activeTab = useMemo(
() => tabs.find((tab) => tab.id === activeTabId),
[tabs, activeTabId]
);

const updateScrollButtons = useCallback(() => {
const container = tabsContainerRef.current;
if (!container) return;

const { scrollLeft, scrollWidth, clientWidth } = container;
setCanScrollLeft(scrollLeft > 0);
setCanScrollRight(scrollLeft + clientWidth < scrollWidth - 1);
}, []);

useEffect(() => {
updateScrollButtons();
window.addEventListener('resize', updateScrollButtons);
return () => window.removeEventListener('resize', updateScrollButtons);
}, [updateScrollButtons, tabs]);

const scroll = (direction: 'left' | 'right') => {
const container = tabsContainerRef.current;
if (!container) return;

const scrollAmount = 150;
Copy link

Copilot AI Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The magic number 150 for scroll amount lacks context and makes the scrolling behavior difficult to adjust. Consider extracting this to a named constant (e.g., SCROLL_AMOUNT or TAB_SCROLL_DISTANCE) at the top of the component or file to improve code maintainability and make the purpose clearer.

Copilot uses AI. Check for mistakes.
container.scrollBy({
left: direction === 'left' ? -scrollAmount : scrollAmount,
behavior: 'smooth',
});
};
Comment on lines +56 to +65
Copy link

Copilot AI Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scroll callback does not trigger updateScrollButtons after scrolling completes. When using smooth scrolling behavior, the scroll position updates asynchronously, but updateScrollButtons is only called during onScroll events. This can cause the scroll buttons to not update their visibility state immediately after clicking them, leading to a poor user experience where buttons remain visible when they shouldn't be or vice versa.

Copilot uses AI. Check for mistakes.

// Loading state (no tabs yet)
if (isLoading && tabs.length === 0) {
return (
Expand All @@ -56,44 +87,79 @@ export function ResultsTabs({
return (
<div className="space-y-2">
{/* Tab bar */}
<div className="flex items-center gap-1 border-b border-border overflow-x-auto overflow-y-hidden">
{tabs.map((tab) => (
<div className="relative flex items-center border-b border-border">
{/* Left scroll button */}
{canScrollLeft && (
<button
key={tab.id}
type="button"
onClick={() => onTabSelect(tab.id)}
className={cn(
'group flex items-center gap-1.5 px-3 py-1.5 text-sm whitespace-nowrap',
'border-b-2 -mb-px transition-colors cursor-pointer',
tab.id === activeTabId
? 'border-primary text-foreground'
: 'border-transparent text-muted-foreground hover:text-foreground'
)}
onClick={() => scroll('left')}
className="absolute left-0 z-10 flex items-center justify-center w-6 h-full bg-background hover:bg-muted cursor-pointer"
aria-label="Scroll tabs left"
>
<span>{formatTimestamp(tab.timestamp)}</span>
{tab.error && (
<span className="w-1.5 h-1.5 rounded-full bg-destructive" aria-label="Error" />
)}
<span
role="button"
tabIndex={0}
aria-label="Close tab"
onClick={(e) => {
e.stopPropagation();
onTabClose(tab.id);
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
<ChevronLeftIcon className="w-4 h-4 text-muted-foreground" />
</button>
Comment on lines 61 to +100
Copy link

Copilot AI Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scroll buttons lack keyboard accessibility. The buttons can only be activated by mouse clicks, but users navigating with a keyboard cannot use them effectively. While the buttons have type="button" and aria-label attributes, they should also be properly focusable and respond to keyboard events (Enter and Space keys) to ensure full accessibility compliance.

Copilot uses AI. Check for mistakes.
)}

{/* Tabs container */}
<div
ref={tabsContainerRef}
onScroll={updateScrollButtons}
className={cn(
"flex items-center gap-1 overflow-hidden",
Copy link

Copilot AI Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tabs container has overflow-hidden which prevents the container from being scrollable. For the scroll function to work (which calls container.scrollBy()), the element must have a scrollable overflow context. The container should use overflow-x-auto or overflow-x-scroll with scrollbar-hidden styling (e.g., via CSS or Tailwind classes like scrollbar-hide) to enable scrolling while hiding the scrollbar visually.

Suggested change
"flex items-center gap-1 overflow-hidden",
"flex items-center gap-1 overflow-x-auto overflow-y-hidden",

Copilot uses AI. Check for mistakes.
canScrollLeft && "pl-6",
canScrollRight && "pr-6"
)}
>
{tabs.map((tab) => (
<button
key={tab.id}
type="button"
onClick={() => onTabSelect(tab.id)}
className={cn(
'group flex items-center gap-1.5 px-3 py-1.5 text-sm whitespace-nowrap',
'border-b-2 -mb-px transition-colors cursor-pointer',
tab.id === activeTabId
? 'border-primary text-foreground'
: 'border-transparent text-muted-foreground hover:text-foreground'
)}
>
<span>{formatTimestamp(tab.timestamp)}</span>
{tab.error && (
<span className="w-1.5 h-1.5 rounded-full bg-destructive" aria-label="Error" />
)}
<span
role="button"
tabIndex={0}
aria-label="Close tab"
onClick={(e) => {
e.stopPropagation();
onTabClose(tab.id);
}
}}
className="opacity-0 group-hover:opacity-100 hover:bg-muted rounded p-0.5 transition-opacity"
>
<XIcon className="w-3 h-3" />
</span>
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.stopPropagation();
onTabClose(tab.id);
}
}}
className="opacity-0 group-hover:opacity-100 hover:bg-muted rounded p-0.5 transition-opacity"
>
<XIcon className="w-3 h-3" />
</span>
</button>
))}
</div>

{/* Right scroll button */}
{canScrollRight && (
<button
type="button"
onClick={() => scroll('right')}
className="absolute right-0 z-10 flex items-center justify-center w-6 h-full bg-background hover:bg-muted cursor-pointer"
aria-label="Scroll tabs right"
>
<ChevronRightIcon className="w-4 h-4 text-muted-foreground" />
</button>
Comment on lines +154 to 161
Copy link

Copilot AI Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scroll buttons lack keyboard accessibility. The buttons can only be activated by mouse clicks, but users navigating with a keyboard cannot use them effectively. While the buttons have type="button" and aria-label attributes, they should also be properly focusable and respond to keyboard events (Enter and Space keys) to ensure full accessibility compliance.

Copilot uses AI. Check for mistakes.
))}
)}
</div>

{/* Active tab content */}
Expand Down
Loading