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 {...@@ -10,7 +10,6 @@ import {
10 TerminalSquare,10 TerminalSquare,
11 Volume2,11 Volume2,
12 VolumeOff,12 VolumeOff,
13 X,
14} from "lucide-react";13} from "lucide-react";
15import {14import {
16 type KeyboardEvent as ReactKeyboardEvent,15 type KeyboardEvent as ReactKeyboardEvent,
...@@ -33,6 +32,7 @@ import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip";...@@ -33,6 +32,7 @@ import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip";
33import { Kbd } from "~/components/ui/kbd";32import { Kbd } from "~/components/ui/kbd";
34import { Menu, MenuItem, MenuPopup, MenuShortcut, MenuTrigger } from "~/components/ui/menu";33import { Menu, MenuItem, MenuPopup, MenuShortcut, MenuTrigger } from "~/components/ui/menu";
35import { ScrollArea } from "~/components/ui/scroll-area";34import { ScrollArea } from "~/components/ui/scroll-area";
35import { PanelTabCloseButton } from "~/components/ui/panel-tab-close-button";
36import { faviconUrlForOrigin } from "~/lib/favicon";36import { faviconUrlForOrigin } from "~/lib/favicon";
37import { useTheme } from "~/hooks/useTheme";37import { useTheme } from "~/hooks/useTheme";
38import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "~/workspaceTitlebar";38import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "~/workspaceTitlebar";
...@@ -823,29 +823,24 @@ export function RightPanelTabs(props: RightPanelTabsProps) {...@@ -823,29 +823,24 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
823 : "text-muted-foreground hover:bg-accent/60 hover:text-foreground",823 : "text-muted-foreground hover:bg-accent/60 hover:text-foreground",
824 )}824 )}
825 >825 >
826 <button826 <PanelTabCloseButton
827 type="button"827 label={`Close ${title}`}
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}`}
830 onClick={() => props.onCloseSurface(surface)}828 onClick={() => props.onCloseSurface(surface)}
831 >829 >
832 <span className="relative flex size-3 items-center justify-center group-hover/tab:hidden group-focus-visible/close:hidden">830 <SurfaceIcon
833 <SurfaceIcon831 surface={surface}
834 surface={surface}832 sessions={props.previewSessions}
835 sessions={props.previewSessions}833 desktopByTabId={props.desktopByTabId}
836 desktopByTabId={props.desktopByTabId}834 theme={resolvedTheme}
837 theme={resolvedTheme}835 pullRequestStatuses={props.pullRequestStatuses}
838 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
839 />841 />
840 {pending ? (842 ) : null}
841 <span843 </PanelTabCloseButton>
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>
849 {audio === "none" || !audioRuntimeTabId ? null : (844 {audio === "none" || !audioRuntimeTabId ? null : (
850 <Tooltip>845 <Tooltip>
851 <TooltipTrigger846 <TooltipTrigger
apps/web/src/components/ThreadTerminalDrawer.tsx+41-49
...@@ -6,11 +6,11 @@ import {...@@ -6,11 +6,11 @@ import {
6import { type TerminalSessionState } from "@t3tools/client-runtime/state/terminal";6import { type TerminalSessionState } from "@t3tools/client-runtime/state/terminal";
7import {7import {
8 Plus,8 Plus,
9 Square,
9 SquareSplitHorizontal,10 SquareSplitHorizontal,
10 SquareSplitVertical,11 SquareSplitVertical,
11 TerminalSquare,12 TerminalSquare,
12 Trash2,13 Trash2,
13 XIcon,
14} from "lucide-react";14} from "lucide-react";
15import {15import {
16 type ContextMenuItem,16 type ContextMenuItem,
...@@ -33,6 +33,7 @@ import {...@@ -33,6 +33,7 @@ import {
33} from "react";33} from "react";
34import { Popover, PopoverPopup, PopoverTrigger } from "~/components/ui/popover";34import { Popover, PopoverPopup, PopoverTrigger } from "~/components/ui/popover";
35import { Button } from "~/components/ui/button";35import { Button } from "~/components/ui/button";
36import { PanelTabCloseButton } from "~/components/ui/panel-tab-close-button";
36import { readTextFromClipboard, writeTextToClipboard } from "~/hooks/useCopyToClipboard";37import { readTextFromClipboard, writeTextToClipboard } from "~/hooks/useCopyToClipboard";
37import { cn } from "~/lib/utils";38import { cn } from "~/lib/utils";
38import { type TerminalContextSelection } from "~/lib/terminalContext";39import { type TerminalContextSelection } from "~/lib/terminalContext";
...@@ -1622,85 +1623,76 @@ export default function ThreadTerminalDrawer({...@@ -1622,85 +1623,76 @@ export default function ThreadTerminalDrawer({
1622 </div>1623 </div>
16231624
1624 <div className="min-h-0 flex-1 overflow-y-auto px-1 py-1">1625 <div className="min-h-0 flex-1 overflow-y-auto px-1 py-1">
1625 {resolvedTerminalGroups.map((terminalGroup, groupIndex) => {1626 {resolvedTerminalGroups.map((terminalGroup) => {
1626 const isGroupActive =1627 const isGroupActive =
1627 terminalGroup.terminalIds.includes(resolvedActiveTerminalId);1628 terminalGroup.terminalIds.includes(resolvedActiveTerminalId);
1628 const groupActiveTerminalId = isGroupActive1629 const groupActiveTerminalId = isGroupActive
1629 ? resolvedActiveTerminalId1630 ? resolvedActiveTerminalId
1630 : (terminalGroup.terminalIds[0] ?? resolvedActiveTerminalId);1631 : (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
1632 return (1645 return (
1633 <div key={terminalGroup.id} className="pb-0.5">1646 <div key={terminalGroup.id} className="pb-0.5">
1634 {showGroupHeaders && (1647 {showGroupHeaders && (
1635 <button1648 <button
1636 type="button"1649 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] ${
1638 isGroupActive1651 isGroupActive
1639 ? "bg-accent/70 text-foreground"1652 ? "bg-accent/50 text-foreground"
1640 : "text-muted-foreground hover:bg-accent/50 hover:text-foreground"1653 : "text-muted-foreground hover:bg-accent/40 hover:text-foreground"
1641 }`}1654 }`}
1642 onClick={() => onActiveTerminalChange(groupActiveTerminalId)}1655 onClick={() => onActiveTerminalChange(groupActiveTerminalId)}
1643 >1656 >
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>
1645 </button>1662 </button>
1646 )}1663 )}
16471664
1648 <div1665 <div className="flex flex-col gap-0.5">
1649 className={showGroupHeaders ? "ml-1 border-l border-border/60 pl-1.5" : ""}
1650 >
1651 {terminalGroup.terminalIds.map((terminalId) => {1666 {terminalGroup.terminalIds.map((terminalId) => {
1652 const isActive = terminalId === resolvedActiveTerminalId;1667 const isActive = terminalId === resolvedActiveTerminalId;
1653 const closeTerminalLabel = `Close ${1668 const terminalLabel = terminalLabelById.get(terminalId) ?? "Terminal";
1654 terminalLabelById.get(terminalId) ?? "terminal"1669 const closeTerminalLabel = `Close ${terminalLabel}${
1655 }${isActive && closeShortcutLabel ? ` (${closeShortcutLabel})` : ""}`;1670 isActive && closeShortcutLabel ? ` (${closeShortcutLabel})` : ""
1671 }`;
1656 return (1672 return (
1657 <div1673 <div
1658 key={terminalId}1674 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",
1660 isActive1677 isActive
1661 ? "bg-accent text-foreground"1678 ? "bg-accent text-foreground"
1662 : "text-muted-foreground hover:bg-accent/50 hover:text-foreground"1679 : "text-muted-foreground hover:bg-accent/60 hover:text-foreground",
1663 }`}
1664 >
1665 {showGroupHeaders && (
1666 <span className="text-[10px] text-muted-foreground/80">└</span>
1667 )}1680 )}
1681 >
1682 <PanelTabCloseButton
1683 label={closeTerminalLabel}
1684 onClick={() => confirmCloseTerminal(terminalId)}
1685 tooltip={closeTerminalLabel}
1686 >
1687 <TerminalSquare className="size-3 shrink-0" />
1688 </PanelTabCloseButton>
1668 <button1689 <button
1669 type="button"1690 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"
1671 onClick={() => onActiveTerminalChange(terminalId)}1692 onClick={() => onActiveTerminalChange(terminalId)}
1672 >1693 >
1673 <TerminalSquare className="size-3 shrink-0" />1694 <span className="truncate">{terminalLabel}</span>
1674 <span className="truncate">
1675 {terminalLabelById.get(terminalId) ?? "Terminal"}
1676 </span>
1677 </button>1695 </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 )}
1704 </div>1696 </div>
1705 );1697 );
1706 })}1698 })}
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}