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>
  );
}