File size: 5,124 Bytes
9b906ea | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | import { ListTree } from "lucide-react";
import type { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import CloseIcon from "#/icons/u-close.svg?react";
import { I18nKey } from "#/i18n/declaration";
import { cn } from "#/utils/utils";
interface FileQuickRowProps {
/** Open file tabs only (paths the user or agent has opened). */
openPaths: string[];
selectedPath: string | null;
onSelectFile: (path: string) => void;
onCloseFile: (path: string) => void;
/** Whether the left-hand file tree is currently visible. */
isTreeVisible: boolean;
/** Toggle the visibility of the left-hand file tree. */
onToggleTree: () => void;
/** Trailing actions (e.g. refresh), pinned to the right. */
actions?: ReactNode;
}
const HIDDEN_SCROLLBAR_CLASSNAME =
"[scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden";
/**
* Horizontal strip of open-file tabs. A path only appears after the user or
* agent opens it. Overflow scrolls horizontally without a visible scrollbar.
*/
export function FileQuickRow({
openPaths,
selectedPath,
onSelectFile,
onCloseFile,
isTreeVisible,
onToggleTree,
actions,
}: FileQuickRowProps) {
const { t } = useTranslation("openhands");
return (
<div
className="flex h-[34px] shrink-0 items-stretch gap-1.5 overflow-y-hidden border-b border-[var(--oh-border)] px-2"
data-testid="file-quick-row"
>
<button
type="button"
onClick={onToggleTree}
data-testid="file-quick-row-tree-toggle"
aria-pressed={isTreeVisible}
aria-label={t(
isTreeVisible
? I18nKey.FILES$HIDE_FILE_TREE
: I18nKey.FILES$SHOW_FILE_TREE,
)}
title={t(
isTreeVisible
? I18nKey.FILES$HIDE_FILE_TREE
: I18nKey.FILES$SHOW_FILE_TREE,
)}
className={cn(
"shrink-0 self-center inline-flex items-center justify-center w-6 h-6 rounded-md cursor-pointer",
"text-[var(--oh-text-tertiary)] hover:bg-tertiary",
isTreeVisible && "bg-[var(--oh-surface-raised)]",
)}
>
<ListTree className="w-3 h-3" aria-hidden strokeWidth={2} />
</button>
{openPaths.length > 0 ? (
<div
role="tablist"
aria-label={t(I18nKey.COMMON$FILES)}
className={cn(
"flex min-h-0 min-w-0 flex-1 flex-nowrap items-stretch overflow-x-auto overflow-y-hidden overscroll-y-none",
HIDDEN_SCROLLBAR_CLASSNAME,
)}
>
{openPaths.map((path, index) => {
const isSelected = selectedPath === path;
const fileName = path.split("/").pop() || path;
return (
<div
key={path}
className={cn(
"group/file-tab relative flex shrink-0 items-stretch",
// Vertical edges on every tab (left on the first, right on all).
"border-r border-r-[var(--oh-border)]",
index === 0 && "border-l border-l-[var(--oh-border)]",
"border-b-2 -mb-px transition-colors",
isSelected
? "border-b-white text-white"
: "border-b-transparent text-[var(--oh-muted)] hover:text-white hover:border-b-white/25",
)}
>
<button
type="button"
role="tab"
aria-selected={isSelected}
onClick={() => onSelectFile(path)}
title={path}
data-testid={`file-quick-row-item-${path}`}
className="flex min-w-0 max-w-[160px] items-center pl-2.5 pr-1 text-xs cursor-pointer text-inherit"
>
<span className="truncate">{fileName}</span>
</button>
<button
type="button"
data-testid={`file-quick-row-close-${path}`}
aria-label={t(I18nKey.FILES$CLOSE_TAB, { path })}
title={t(I18nKey.FILES$CLOSE_TAB, { path })}
className={cn(
"inline-flex items-center justify-center size-5 self-center mr-1 rounded-sm shrink-0 cursor-pointer",
"text-inherit hover:bg-white/10",
// Always visible on small / touch-first viewports; hover to
// reveal on fine pointers (desktop).
"opacity-100 transition-opacity",
"md:opacity-0 md:group-hover/file-tab:opacity-100 md:group-focus-within/file-tab:opacity-100",
)}
onClick={() => onCloseFile(path)}
>
<CloseIcon width={10} height={10} aria-hidden />
</button>
</div>
);
})}
</div>
) : (
<div className="flex-1 min-w-0" />
)}
{actions ? (
<div className="ml-auto shrink-0 self-center flex items-center gap-1">
{actions}
</div>
) : null}
</div>
);
}
|