feat(sidebar): bold unread workspaces instead of bell icon (#1330)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Brennan Benson
2026-05-01 16:20:58 -07:00
committed by GitHub
co-authored by Orca
parent c167e4e6e9
commit f12c3005ed
5 changed files with 47 additions and 66 deletions
@@ -26,7 +26,7 @@ const GROUP_BY_OPTIONS = [
const PROPERTY_OPTIONS: { id: WorktreeCardProperty; label: string }[] = [
{ id: 'status', label: 'Terminal status' },
{ id: 'unread', label: 'Unread indicator' },
{ id: 'unread', label: 'Bold unread workspaces' },
{ id: 'ci', label: 'CI checks' },
{ id: 'issue', label: 'Linked issue' },
{ id: 'pr', label: 'Linked PR' },
@@ -4,7 +4,7 @@ import { useShallow } from 'zustand/react/shallow'
import { useAppStore } from '@/store'
import { Badge } from '@/components/ui/badge'
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
import { Bell, GitMerge, LoaderCircle, CircleCheck, CircleX, Server, ServerOff } from 'lucide-react'
import { GitMerge, LoaderCircle, CircleCheck, CircleX, Server, ServerOff } from 'lucide-react'
import StatusIndicator from './StatusIndicator'
import CacheTimer from './CacheTimer'
import WorktreeContextMenu from './WorktreeContextMenu'
@@ -26,8 +26,7 @@ import {
checksLabel,
CONFLICT_OPERATION_LABELS,
EMPTY_TABS,
EMPTY_BROWSER_TABS,
FilledBellIcon
EMPTY_BROWSER_TABS
} from './WorktreeCardHelpers'
import { IssueSection, PrSection, CommentSection } from './WorktreeCardMeta'
@@ -53,7 +52,6 @@ const WorktreeCard = React.memo(function WorktreeCard({
hintNumber
}: WorktreeCardProps) {
const openModal = useAppStore((s) => s.openModal)
const updateWorktreeMeta = useAppStore((s) => s.updateWorktreeMeta)
const fetchPRForBranch = useAppStore((s) => s.fetchPRForBranch)
const fetchIssue = useAppStore((s) => s.fetchIssue)
const cardProps = useAppStore((s) => s.worktreeCardProperties)
@@ -317,16 +315,11 @@ const WorktreeCard = React.memo(function WorktreeCard({
})
}, [worktree.id, worktree.displayName, worktree.linkedIssue, worktree.comment, openModal])
const handleToggleUnreadQuick = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault()
event.stopPropagation()
updateWorktreeMeta(worktree.id, { isUnread: !worktree.isUnread })
},
[worktree.id, worktree.isUnread, updateWorktreeMeta]
)
const unreadTooltip = worktree.isUnread ? 'Mark read' : 'Mark unread'
// Why: the 'unread' card property is the user's opt-out. When off, we render
// as if the workspace is read so bold emphasis never appears — matching the
// old "hide the bell" behavior exactly. The persisted `worktree.isUnread`
// flag is unchanged; only the rendering changes.
const showUnreadEmphasis = cardProps.includes('unread') && worktree.isUnread
const cardBody = (
<div
@@ -367,41 +360,14 @@ const WorktreeCard = React.memo(function WorktreeCard({
</div>
)}
{/* Status indicator on the left */}
{(cardProps.includes('status') || cardProps.includes('unread')) && (
<div className="flex flex-col items-center justify-start pt-[2px] gap-2 shrink-0">
{cardProps.includes('status') && (
<>
<StatusIndicator status={status} aria-hidden="true" />
<span className="sr-only">{getWorktreeStatusLabel(status)}</span>
</>
)}
{cardProps.includes('unread') && (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={handleToggleUnreadQuick}
className={cn(
'group/unread flex size-4 cursor-pointer items-center justify-center rounded transition-all',
'hover:bg-accent/80 active:scale-95',
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring'
)}
aria-label={worktree.isUnread ? 'Mark as read' : 'Mark as unread'}
>
{worktree.isUnread ? (
<FilledBellIcon className="size-[13px] text-amber-500 drop-shadow-sm" />
) : (
<Bell className="size-3 text-muted-foreground/40 opacity-0 group-hover:opacity-100 group-hover/unread:opacity-100 transition-opacity" />
)}
</button>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={8}>
<span>{unreadTooltip}</span>
</TooltipContent>
</Tooltip>
)}
{/* Status indicator on the left.
Why: bold-for-unread carries the attention signal in the title row
now, so the rail only needs to render when the status dot is
enabled — the 'unread' property is intentionally excluded here. */}
{cardProps.includes('status') && (
<div className="flex items-center justify-start pt-[2px] shrink-0">
<StatusIndicator status={status} aria-hidden="true" />
<span className="sr-only">{getWorktreeStatusLabel(status)}</span>
</div>
)}
@@ -427,7 +393,21 @@ const WorktreeCard = React.memo(function WorktreeCard({
</Tooltip>
)}
<div className="text-[12px] font-semibold text-foreground truncate leading-tight">
{/* Why: weight alone carries the unread signal; color stays
at text-foreground in both states so the title keeps
hierarchy against the muted branch row below (muting the
title as well flattened the card — same reasoning as the
repo chip comment below). */}
<div
className={cn(
'text-[12px] truncate leading-tight text-foreground',
showUnreadEmphasis ? 'font-semibold' : 'font-normal'
)}
>
{/* Why: the card root is a non-interactive <div>, so aria-label
on it is announced inconsistently across screen readers.
A visible-text prefix inside the accessible name is reliable. */}
{showUnreadEmphasis && <span className="sr-only">Unread: </span>}
{worktree.displayName}
</div>
@@ -505,7 +485,17 @@ const WorktreeCard = React.memo(function WorktreeCard({
{repo && !hideRepoBadge && (
<div className="flex items-center gap-1.5 shrink-0 px-1.5 py-0.5 rounded-[4px] bg-accent border border-border dark:bg-accent/50 dark:border-border/60">
<div className="size-1.5 rounded-full" style={{ backgroundColor: repo.badgeColor }} />
<span className="text-[10px] font-semibold text-foreground truncate max-w-[6rem] leading-none lowercase">
{/* Why: repo label tracks the title's weight. Keeps
text-foreground on the read state (not muted) because this
label sits on a tinted bg-accent chip — muting it would leave
it nearly invisible; the weight change alone restores
hierarchy against the title. */}
<span
className={cn(
'text-[10px] truncate max-w-[6rem] leading-none lowercase text-foreground',
showUnreadEmphasis ? 'font-semibold' : 'font-normal'
)}
>
{repo.displayName}
</span>
</div>
@@ -97,7 +97,7 @@ export function PrSection({ pr, onClick }: PrSectionProps): React.JSX.Element {
href={pr.url}
target="_blank"
rel="noreferrer"
className="text-foreground opacity-80 font-medium shrink-0 hover:text-foreground hover:underline"
className="text-foreground opacity-80 shrink-0 hover:text-foreground hover:underline"
onClick={(e) => e.stopPropagation()}
>
PR #{pr.number}
@@ -10,8 +10,6 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
import {
FolderOpen,
Copy,
Bell,
BellOff,
Link,
MessageSquare,
Moon,
@@ -64,10 +62,6 @@ const WorktreeContextMenu = React.memo(function WorktreeContextMenu({
window.api.ui.writeClipboardText(worktree.path)
}, [worktree.path])
const handleToggleRead = useCallback(() => {
updateWorktreeMeta(worktree.id, { isUnread: !worktree.isUnread })
}, [worktree.id, worktree.isUnread, updateWorktreeMeta])
const handleTogglePin = useCallback(() => {
updateWorktreeMeta(worktree.id, { isPinned: !worktree.isPinned })
}, [worktree.id, worktree.isPinned, updateWorktreeMeta])
@@ -169,10 +163,6 @@ const WorktreeContextMenu = React.memo(function WorktreeContextMenu({
<Pencil className="size-3.5" />
Rename
</DropdownMenuItem>
<DropdownMenuItem onSelect={handleToggleRead} disabled={isDeleting}>
{worktree.isUnread ? <BellOff className="size-3.5" /> : <Bell className="size-3.5" />}
{worktree.isUnread ? 'Mark Read' : 'Mark Unread'}
</DropdownMenuItem>
<DropdownMenuItem onSelect={handleLinkIssue} disabled={isDeleting}>
<Link className="size-3.5" />
{worktree.linkedIssue ? 'Edit GH Issue' : 'Link GH Issue'}
@@ -259,9 +259,10 @@ export default function SortableTab({
)}
{showActivityAffordance ? (
// Why: the activity marker sits to the LEFT of the tab title using
// Orca's filled bell glyph (amber-500 with a subtle drop shadow)
// so it matches the worktree-level bell in the sidebar — keeping
// every "needs your attention" surface in Orca consistent.
// Orca's filled bell glyph (amber-500 with a subtle drop shadow).
// The sidebar WorktreeCard used to render the same bell; it now
// uses a bold-title treatment. The tab bar keeps the bell for
// now — aligning the two surfaces is a deliberate follow-up.
<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>