mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
feat(sidebar): bold unread workspaces instead of bell icon (#1330)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user