feat(tabs): stronger tab icons — blue browser globe, bold terminal tile (#1305)

- BrowserTab: Globe icon uses text-blue-500 on both active and inactive tabs
  for a distinct, recognizable anchor in the tab strip.
- shell-icons: GenericTerminalIcon now renders a pitch-black tile with a
  thick stroked >_ (chevron at 3.2px, underscore at 2.6px, taller chevron)
  so mac/linux/default terminal tabs match the colored-badge treatment of
  the PowerShell/CMD/WSL icons instead of a flat lucide chevron.
- SortableTab: all terminal tabs use ShellIcon. On Windows, tabs without a
  per-tab shellOverride fall back to the user's configured default
  Windows shell so the tab-strip icon reflects what's actually running.

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil 2026-05-01 00:33:46 -07:00 committed by GitHub
parent ff22944fd7
commit b8ded7e9fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 69 additions and 24 deletions

View File

@ -149,9 +149,14 @@ export default function BrowserTab({
}}
>
{isActive && <span className={ACTIVE_TAB_INDICATOR_CLASSES} aria-hidden />}
<Globe
className={`w-3 h-3 mr-1 shrink-0 ${isActive ? 'text-foreground' : 'text-muted-foreground'}`}
/>
{/* Why: the browser tab icon is the only non-terminal, non-editor
surface in the tab strip. Coloring the Globe blue (matching the
in-app browser's identity and the default tab insertion bar)
gives it a distinct, recognizable anchor so users can spot
browser tabs at a glance even when the strip is saturated. We
keep full color on both active and inactive tabs dimming to
muted-foreground made the icon read as "disabled" in practice. */}
<Globe className="w-3 h-3 mr-1 shrink-0 text-blue-500" />
<span className="truncate max-w-[100px] mr-1">{getBrowserTabLabel(tab)}</span>
{tab.loading && !tab.loadError && !isBlankBrowserTab(tab) && (
<span className="mr-1 size-1.5 rounded-full bg-sky-500/80 shrink-0" />

View File

@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { useSortable } from '@dnd-kit/sortable'
import { X, Terminal as TerminalIcon, Minimize2, Columns2, Rows2 } from 'lucide-react'
import { X, Minimize2, Columns2, Rows2 } from 'lucide-react'
import { ShellIcon } from './shell-icons'
import {
DropdownMenu,
@ -81,6 +81,16 @@ export default function SortableTab({
// because the slice returns a fresh object reference on each mark/clear.
const hasUnreadActivity = useAppStore((s) => s.unreadTerminalTabs[tab.id] === true)
// Why: on Windows, tabs created before the per-tab shell override landed (or
// created via the default Ctrl+T path without picking a specific shell)
// don't carry a shellOverride. We still want the tab-strip icon to reflect
// the shell actually running, so fall back to the user's configured default
// Windows shell. On mac/linux this resolves to undefined and the ShellIcon
// generic-terminal fallback renders.
const defaultWindowsShell = useAppStore((s) => s.settings?.terminalWindowsShell)
const isWindows = navigator.userAgent.includes('Windows')
const shellForIcon = tab.shellOverride ?? (isWindows ? defaultWindowsShell : undefined)
// Why: intentionally no transform/transition/opacity here. The PR's
// design is that tabs stay visually anchored during a drag — only the
// blue insertion bar moves. Siblings also don't shift (see
@ -255,25 +265,21 @@ export default function SortableTab({
<span data-testid="tab-activity-bell" className="inline-flex shrink-0">
<FilledBellIcon className="w-3 h-3 mr-1 text-amber-500 drop-shadow-sm" />
</span>
) : tab.shellOverride ? (
// Why: when the tab was explicitly opened with a specific Windows
// shell (PowerShell / CMD / WSL via the "+" menu), render the
// matching brand-style glyph so the strip shows at a glance which
// shell this tab is running. Falls back to the generic lucide
// TerminalIcon below for mac/linux shells and for Windows tabs
// that were spawned before the per-tab shell override landed
// (shellOverride absent → same neutral glyph as before, no visual
// regression for existing sessions).
) : (
// Why: ShellIcon renders a colored brand-style tile for PowerShell,
// CMD, and WSL so Windows users can distinguish shells at a glance.
// On mac/linux (or Windows tabs without a resolved shell) it falls
// back to a matching colored generic-terminal tile — keeping every
// tab's leading glyph in the same visual idiom instead of mixing a
// flat lucide chevron with the brand tiles. Opacity dims the icon
// on inactive tabs to match the existing text treatment without
// desaturating the brand colors beyond recognition.
<span
className={`mr-1 inline-flex shrink-0 ${isActive ? 'text-foreground' : 'text-muted-foreground'}`}
className={`mr-1 inline-flex shrink-0 ${isActive ? '' : 'opacity-70'}`}
aria-hidden
>
<ShellIcon shell={tab.shellOverride} size={12} />
<ShellIcon shell={shellForIcon} size={12} />
</span>
) : (
<TerminalIcon
className={`w-3 h-3 mr-1 shrink-0 ${isActive ? 'text-foreground' : 'text-muted-foreground'}`}
/>
)}
{isEditing ? (
<Input

View File

@ -1,5 +1,4 @@
import React from 'react'
import { Terminal as TerminalIcon } from 'lucide-react'
export type WindowsShell = 'powershell.exe' | 'cmd.exe' | 'wsl.exe'
@ -9,7 +8,10 @@ export type WindowsShell = 'powershell.exe' | 'cmd.exe' | 'wsl.exe'
// hand-crafted icons (derived from the official brand marks and redrawn as
// small currentColor-aware paths so they inherit the tab's text color) make
// each shell identifiable at a glance without shipping a heavier brand-asset
// package like simple-icons.
// package like simple-icons. The generic (macOS/Linux) terminal fallback uses
// the same colored-tile treatment so the tab strip reads as a consistent set
// of badges rather than a monochrome lucide glyph next to colorful brand
// marks.
function PowerShellIcon({ size = 14 }: { size?: number }): React.JSX.Element {
return (
@ -75,6 +77,40 @@ function WslIcon({ size = 14 }: { size?: number }): React.JSX.Element {
)
}
function GenericTerminalIcon({ size = 14 }: { size?: number }): React.JSX.Element {
// Why: matches the tile treatment of PowerShell/CMD/WSL so the tab strip
// reads as a consistent set of badges instead of a flat monochrome chevron.
// Uses black/white (same palette as the CmdIcon) so generic mac/linux
// sessions stay visually neutral — the colorful brand tiles are reserved
// for shells that actually have a brand identity.
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden
>
<rect x="1.5" y="3" width="21" height="18" rx="2.5" fill="#000000" />
<path
d="M6 7.5 L11.5 12 L6 16.5"
stroke="#ffffff"
strokeWidth="3.2"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
/>
<path
d="M12.5 16.5 L18 16.5"
stroke="#ffffff"
strokeWidth="2.6"
strokeLinecap="round"
fill="none"
/>
</svg>
)
}
export function ShellIcon({
shell,
size = 14
@ -92,7 +128,5 @@ export function ShellIcon({
if (normalized === 'wsl.exe' || normalized.startsWith('wsl')) {
return <WslIcon size={size} />
}
// Fallback: generic terminal glyph for mac/linux shells and anything
// unrecognized. Inherits currentColor so it matches the surrounding tab.
return <TerminalIcon width={size} height={size} aria-hidden />
return <GenericTerminalIcon size={size} />
}