authorgravatar for 71498452+StiensWout@users.noreply.github.comWout Stiens <71498452+StiensWout@users.noreply.github.com> 2026-08-24 09:39:54+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-24 03:39:54-04:00
log10626c537886b41b6d9fc404521512f4f3702157
tree4cb5266bdb21361f1e2f4a5756da4c4b89ca5abe
parentb60a2c0b99c8d5d8f02b6705171da694d12e77e3
signature Signed by PGP key B5690EEEBB952194

fix(web): clarify terminal sidebar grouping (#7967)


3 files changed, 98 insertions(+), 70 deletions(-)

apps/web/src/components/RightPanelTabs.tsx+16-21
......@@ -10,7 +10,6 @@ import {
1010 TerminalSquare,
1111 Volume2,
1212 VolumeOff,
13 X,
1413} from "lucide-react";
1514import {
1615 type KeyboardEvent as ReactKeyboardEvent,
......@@ -33,6 +32,7 @@ import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip";
3332import { Kbd } from "~/components/ui/kbd";
3433import { Menu, MenuItem, MenuPopup, MenuShortcut, MenuTrigger } from "~/components/ui/menu";
3534import { ScrollArea } from "~/components/ui/scroll-area";
35import { PanelTabCloseButton } from "~/components/ui/panel-tab-close-button";
3636import { faviconUrlForOrigin } from "~/lib/favicon";
3737import { useTheme } from "~/hooks/useTheme";
3838import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "~/workspaceTitlebar";
......@@ -823,29 +823,24 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
823823 : "text-muted-foreground hover:bg-accent/60 hover:text-foreground",
824824 )}
825825 >
826 <button
827 type="button"
828 className="cursor-pointer group/close relative flex size-4 shrink-0 items-center justify-center rounded-sm hover:bg-muted"
829 aria-label={`Close ${title}`}
826 <PanelTabCloseButton
827 label={`Close ${title}`}
830828 onClick={() => props.onCloseSurface(surface)}
831829 >
832 <span className="relative flex size-3 items-center justify-center group-hover/tab:hidden group-focus-visible/close:hidden">
833 <SurfaceIcon
834 surface={surface}
835 sessions={props.previewSessions}
836 desktopByTabId={props.desktopByTabId}
837 theme={resolvedTheme}
838 pullRequestStatuses={props.pullRequestStatuses}
830 <SurfaceIcon
831 surface={surface}
832 sessions={props.previewSessions}
833 desktopByTabId={props.desktopByTabId}
834 theme={resolvedTheme}
835 pullRequestStatuses={props.pullRequestStatuses}
836 />
837 {pending ? (
838 <span
839 className="absolute -right-0.5 -bottom-0.5 size-1.5 rounded-full bg-current"
840 aria-hidden
839841 />
840 {pending ? (
841 <span
842 className="absolute -right-0.5 -bottom-0.5 size-1.5 rounded-full bg-current"
843 aria-hidden
844 />
845 ) : null}
846 </span>
847 <X className="hidden size-3 group-hover/tab:block group-focus-visible/close:block" />
848 </button>
842 ) : null}
843 </PanelTabCloseButton>
849844 {audio === "none" || !audioRuntimeTabId ? null : (
850845 <Tooltip>
851846 <TooltipTrigger
apps/web/src/components/ThreadTerminalDrawer.tsx+41-49
......@@ -6,11 +6,11 @@ import {
66import { type TerminalSessionState } from "@t3tools/client-runtime/state/terminal";
77import {
88 Plus,
9 Square,
910 SquareSplitHorizontal,
1011 SquareSplitVertical,
1112 TerminalSquare,
1213 Trash2,
13 XIcon,
1414} from "lucide-react";
1515import {
1616 type ContextMenuItem,
......@@ -33,6 +33,7 @@ import {
3333} from "react";
3434import { Popover, PopoverPopup, PopoverTrigger } from "~/components/ui/popover";
3535import { Button } from "~/components/ui/button";
36import { PanelTabCloseButton } from "~/components/ui/panel-tab-close-button";
3637import { readTextFromClipboard, writeTextToClipboard } from "~/hooks/useCopyToClipboard";
3738import { cn } from "~/lib/utils";
3839import { type TerminalContextSelection } from "~/lib/terminalContext";
......@@ -1622,85 +1623,76 @@ export default function ThreadTerminalDrawer({
16221623 </div>
16231624
16241625 <div className="min-h-0 flex-1 overflow-y-auto px-1 py-1">
1625 {resolvedTerminalGroups.map((terminalGroup, groupIndex) => {
1626 {resolvedTerminalGroups.map((terminalGroup) => {
16261627 const isGroupActive =
16271628 terminalGroup.terminalIds.includes(resolvedActiveTerminalId);
16281629 const groupActiveTerminalId = isGroupActive
16291630 ? resolvedActiveTerminalId
16301631 : (terminalGroup.terminalIds[0] ?? resolvedActiveTerminalId);
1632 const terminalCount = terminalGroup.terminalIds.length;
1633 const isSplitGroup = terminalCount > 1;
1634 const groupLabel = !isSplitGroup
1635 ? "Single"
1636 : terminalGroup.splitDirection === "vertical"
1637 ? "Stacked"
1638 : "Side by side";
1639 const GroupIcon = !isSplitGroup
1640 ? Square
1641 : terminalGroup.splitDirection === "vertical"
1642 ? SquareSplitVertical
1643 : SquareSplitHorizontal;
16311644
16321645 return (
16331646 <div key={terminalGroup.id} className="pb-0.5">
16341647 {showGroupHeaders && (
16351648 <button
16361649 type="button"
1637 className={`flex w-full items-center rounded px-1 py-0.5 text-[10px] uppercase tracking-[0.08em] ${
1650 className={`flex h-[22px] w-full cursor-pointer items-center gap-1 rounded px-1.5 text-[11px] ${
16381651 isGroupActive
1639 ? "bg-accent/70 text-foreground"
1640 : "text-muted-foreground hover:bg-accent/50 hover:text-foreground"
1652 ? "bg-accent/50 text-foreground"
1653 : "text-muted-foreground hover:bg-accent/40 hover:text-foreground"
16411654 }`}
16421655 onClick={() => onActiveTerminalChange(groupActiveTerminalId)}
16431656 >
1644 Group {groupIndex + 1}
1657 <GroupIcon className="size-3 shrink-0" />
1658 <span className="min-w-0 flex-1 truncate text-left">{groupLabel}</span>
1659 <span className="text-muted-foreground/70 text-[10px] tabular-nums">
1660 {terminalCount}
1661 </span>
16451662 </button>
16461663 )}
16471664
1648 <div
1649 className={showGroupHeaders ? "ml-1 border-l border-border/60 pl-1.5" : ""}
1650 >
1665 <div className="flex flex-col gap-0.5">
16511666 {terminalGroup.terminalIds.map((terminalId) => {
16521667 const isActive = terminalId === resolvedActiveTerminalId;
1653 const closeTerminalLabel = `Close ${
1654 terminalLabelById.get(terminalId) ?? "terminal"
1655 }${isActive && closeShortcutLabel ? ` (${closeShortcutLabel})` : ""}`;
1668 const terminalLabel = terminalLabelById.get(terminalId) ?? "Terminal";
1669 const closeTerminalLabel = `Close ${terminalLabel}${
1670 isActive && closeShortcutLabel ? ` (${closeShortcutLabel})` : ""
1671 }`;
16561672 return (
16571673 <div
16581674 key={terminalId}
1659 className={`group flex items-center gap-1 rounded px-1 py-0.5 text-[11px] ${
1675 className={cn(
1676 "group/tab flex h-6 w-full items-center gap-0.5 rounded-md pr-2 pl-1.5 text-xs",
16601677 isActive
16611678 ? "bg-accent text-foreground"
1662 : "text-muted-foreground hover:bg-accent/50 hover:text-foreground"
1663 }`}
1664 >
1665 {showGroupHeaders && (
1666 <span className="text-[10px] text-muted-foreground/80">└</span>
1679 : "text-muted-foreground hover:bg-accent/60 hover:text-foreground",
16671680 )}
1681 >
1682 <PanelTabCloseButton
1683 label={closeTerminalLabel}
1684 onClick={() => confirmCloseTerminal(terminalId)}
1685 tooltip={closeTerminalLabel}
1686 >
1687 <TerminalSquare className="size-3 shrink-0" />
1688 </PanelTabCloseButton>
16681689 <button
16691690 type="button"
1670 className="flex min-w-0 flex-1 items-center gap-1 text-left"
1691 className="flex min-w-0 flex-1 cursor-pointer items-center gap-1 text-left"
16711692 onClick={() => onActiveTerminalChange(terminalId)}
16721693 >
1673 <TerminalSquare className="size-3 shrink-0" />
1674 <span className="truncate">
1675 {terminalLabelById.get(terminalId) ?? "Terminal"}
1676 </span>
1694 <span className="truncate">{terminalLabel}</span>
16771695 </button>
1678 {normalizedTerminalIds.length > 1 && (
1679 <Popover>
1680 <PopoverTrigger
1681 openOnHover
1682 render={
1683 <button
1684 type="button"
1685 className="inline-flex size-3.5 items-center justify-center rounded text-xs font-medium leading-none text-muted-foreground opacity-0 transition hover:bg-accent hover:text-foreground group-hover:opacity-100"
1686 onClick={() => confirmCloseTerminal(terminalId)}
1687 aria-label={closeTerminalLabel}
1688 />
1689 }
1690 >
1691 <XIcon className="size-2.5" />
1692 </PopoverTrigger>
1693 <PopoverPopup
1694 tooltipStyle
1695 side="bottom"
1696 sideOffset={6}
1697 align="center"
1698 className="pointer-events-none select-none"
1699 >
1700 {closeTerminalLabel}
1701 </PopoverPopup>
1702 </Popover>
1703 )}
17041696 </div>
17051697 );
17061698 })}
apps/web/src/components/ui/panel-tab-close-button.tsx created+41
......@@ -0,0 +1,41 @@
1import { X } from "lucide-react";
2import type { ReactNode } from "react";
3import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip";
4
5interface PanelTabCloseButtonProps {
6 children: ReactNode;
7 label: string;
8 onClick: () => void;
9 tooltip?: string;
10}
11
12/** Inside a `group/tab` row, swaps the tab identity for its close action on hover or focus. */
13export function PanelTabCloseButton({
14 children,
15 label,
16 onClick,
17 tooltip,
18}: PanelTabCloseButtonProps) {
19 const button = (
20 <button
21 type="button"
22 className="cursor-pointer group/close relative flex size-4 shrink-0 items-center justify-center rounded-sm hover:bg-muted"
23 aria-label={label}
24 onClick={onClick}
25 >
26 <span className="relative flex size-3 items-center justify-center group-hover/tab:hidden group-focus-visible/close:hidden">
27 {children}
28 </span>
29 <X className="hidden size-3 group-hover/tab:block group-focus-visible/close:block" />
30 </button>
31 );
32
33 if (!tooltip) return button;
34
35 return (
36 <Tooltip>
37 <TooltipTrigger render={button} />
38 <TooltipPopup>{tooltip}</TooltipPopup>
39 </Tooltip>
40 );
41}