diff --git a/apps/desktop/DESIGN.md b/apps/desktop/DESIGN.md
index 485073200d..8d4512e65f 100644
--- a/apps/desktop/DESIGN.md
+++ b/apps/desktop/DESIGN.md
@@ -151,6 +151,12 @@ Notes:
- SVGs inherit `size-3.5` (`size-3` at `xs`). Don't re-set icon size.
- Polymorph with `asChild` when the button must render as a link/Slot.
+## Badges — one component
+
+`src/components/ui/badge.tsx`. Variants: `default` (tinted primary), `muted`,
+`warn`, `destructive`, `outline`, `solid` (primary fill — icon-corner counts).
+Sizes: `default`, `xs`, `overlay` (titlebar glyph counts).
+
## Form controls
- **`controlVariants`** (`src/components/ui/control.ts`) is the shared shape for
diff --git a/apps/desktop/src/app/shell/titlebar-controls.tsx b/apps/desktop/src/app/shell/titlebar-controls.tsx
index 72e297d9ac..33d8d4e235 100644
--- a/apps/desktop/src/app/shell/titlebar-controls.tsx
+++ b/apps/desktop/src/app/shell/titlebar-controls.tsx
@@ -5,9 +5,11 @@ import { useLocation, useNavigate } from 'react-router'
import { hudTargetSessionId } from '@/app/hud/handoff'
import { toggleLayoutEditMode } from '@/components/pane-shell/edit-mode'
import { resetLayoutTree } from '@/components/pane-shell/tree/store'
+import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Tip, TipKeybindLabel } from '@/components/ui/tooltip'
import { useI18n } from '@/i18n'
+import { compactNumber } from '@/lib/format'
import { triggerHaptic } from '@/lib/haptics'
import { formatModifierToken } from '@/lib/keybinds/combo'
import { cn } from '@/lib/utils'
@@ -15,11 +17,13 @@ import { $hapticsMuted, toggleHapticsMuted } from '@/store/haptics'
import { toggleHud } from '@/store/hud'
import {
$fileBrowserOpen,
+ $panesFlipped,
$sidebarOpen,
toggleFileBrowserOpen,
togglePanesFlipped,
toggleSidebarOpen
} from '@/store/layout'
+import { $unreadSessionCount } from '@/store/session-dot-state'
import { appViewForPath, isOverlayView } from '../routes'
@@ -43,6 +47,8 @@ export interface TitlebarTool {
onSelect?: (event?: MouseEvent) => void
/** Keybind action id — when set, the tooltip shows the label + keybind hint. */
actionId?: string
+ /** Overlay count on the glyph (unread sessions). Hidden when 0/undefined. */
+ badge?: number
title?: string
to?: string
}
@@ -79,6 +85,24 @@ function LayoutGlyph({ modHeld }: { modHeld: boolean }) {
)
}
+/** Overlay count on a titlebar glyph. Hidden when count is 0/undefined. */
+function withCountBadge(icon: ReactNode, count: number | undefined): ReactNode {
+ if (!count) {
+ return icon
+ }
+
+ return (
+
+ {icon}
+
+
+ {compactNumber(count)}
+
+
+
+ )
+}
+
/** Live ⌘/Ctrl tracking — mod-click affordances telegraph themselves (the
* layout button morphs into its reset form while the modifier is down). */
function useModifierHeld(): boolean {
@@ -109,7 +133,11 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }:
const modHeld = useModifierHeld()
const hapticsMuted = useStore($hapticsMuted)
const fileBrowserOpen = useStore($fileBrowserOpen)
+ const panesFlipped = useStore($panesFlipped)
const sidebarOpen = useStore($sidebarOpen)
+ const unreadCount = useStore($unreadSessionCount)
+ const unreadBadge = unreadCount > 0 ? unreadCount : undefined
+ const unreadHint = unreadBadge ? ` · ${t.titlebar.unreadSessions(unreadBadge)}` : ''
const toggleHaptics = () => {
if (!hapticsMuted) {
@@ -130,13 +158,16 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }:
// show/hide affordances.
const leftEdge = { open: sidebarOpen, toggle: toggleSidebarOpen }
const rightEdge = { open: fileBrowserOpen, toggle: toggleFileBrowserOpen }
+ const leftLabel = leftEdge.open ? t.titlebar.hideSidebar : t.titlebar.showSidebar
+ const rightLabel = rightEdge.open ? t.titlebar.hideRightSidebar : t.titlebar.showRightSidebar
const leftToolbarTools: TitlebarTool[] = [
{
actionId: 'view.toggleSidebar',
+ badge: panesFlipped ? undefined : unreadBadge,
icon: ,
id: 'sidebar',
- label: leftEdge.open ? t.titlebar.hideSidebar : t.titlebar.showSidebar,
+ label: `${leftLabel}${panesFlipped ? '' : unreadHint}`,
onSelect: () => {
triggerHaptic('tap')
leftEdge.toggle()
@@ -157,9 +188,10 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }:
const rightSidebarTool: TitlebarTool = {
actionId: 'view.toggleRightSidebar',
+ badge: panesFlipped ? unreadBadge : undefined,
icon: ,
id: 'right-sidebar',
- label: rightEdge.open ? t.titlebar.hideRightSidebar : t.titlebar.showRightSidebar,
+ label: `${rightLabel}${panesFlipped ? unreadHint : ''}`,
onSelect: () => {
triggerHaptic('tap')
rightEdge.toggle()
@@ -306,7 +338,7 @@ function TitlebarToolButton({ navigate, tool }: { navigate: ReturnType
- {tool.icon}
+ {withCountBadge(tool.icon, tool.badge)}
@@ -332,7 +364,7 @@ function TitlebarToolButton({ navigate, tool }: { navigate: ReturnType
- {tool.icon}
+ {withCountBadge(tool.icon, tool.badge)}
)
diff --git a/apps/desktop/src/components/ui/badge.tsx b/apps/desktop/src/components/ui/badge.tsx
index c4e46e5236..d3a8a50449 100644
--- a/apps/desktop/src/components/ui/badge.tsx
+++ b/apps/desktop/src/components/ui/badge.tsx
@@ -15,11 +15,14 @@ const badgeVariants = cva(
muted: 'bg-muted text-muted-foreground',
warn: 'bg-amber-500/10 text-amber-600 dark:text-amber-300',
destructive: 'bg-destructive/10 text-destructive',
- outline: 'border border-(--ui-stroke-secondary) text-muted-foreground'
+ outline: 'border border-(--ui-stroke-secondary) text-muted-foreground',
+ // Solid fill — icon-corner counts (titlebar unread, etc.).
+ solid: 'bg-primary text-primary-foreground'
},
size: {
default: 'px-1.5 py-0.5 text-[0.65rem] [&_svg]:size-3',
- xs: 'px-1 py-px text-[0.6rem] [&_svg]:size-2.5'
+ xs: 'px-1 py-px text-[0.6rem] [&_svg]:size-2.5',
+ overlay: 'h-2 min-w-2 justify-center rounded-[2px] px-px text-[7px] font-semibold tabular-nums'
}
},
defaultVariants: { variant: 'default', size: 'default' }
diff --git a/apps/desktop/src/i18n/ar.ts b/apps/desktop/src/i18n/ar.ts
index 5ed5db5686..2280ab4a4c 100644
--- a/apps/desktop/src/i18n/ar.ts
+++ b/apps/desktop/src/i18n/ar.ts
@@ -181,6 +181,7 @@ export const ar = defineLocale({
swapSidebarSides: 'تبديل جانبي الأشرطة',
hideRightSidebar: 'إخفاء الشريط الأيمن',
showRightSidebar: 'إظهار الشريط الأيمن',
+ unreadSessions: count => (count === 1 ? 'جلسة واحدة غير مقروءة' : `${count} جلسات غير مقروءة`),
muteHaptics: 'كتم الاهتزازات',
unmuteHaptics: 'تفعيل الاهتزازات',
openSettings: 'فتح الإعدادات',
diff --git a/apps/desktop/src/i18n/en.ts b/apps/desktop/src/i18n/en.ts
index 5e94e4c4f0..f5f0664aa4 100644
--- a/apps/desktop/src/i18n/en.ts
+++ b/apps/desktop/src/i18n/en.ts
@@ -211,6 +211,7 @@ export const en: Translations = {
swapSidebarSides: 'Swap sidebar sides',
hideRightSidebar: 'Hide right sidebar',
showRightSidebar: 'Show right sidebar',
+ unreadSessions: count => (count === 1 ? '1 unread session' : `${count} unread sessions`),
muteHaptics: 'Mute haptics',
unmuteHaptics: 'Unmute haptics',
openSettings: 'Open settings',
diff --git a/apps/desktop/src/i18n/ja.ts b/apps/desktop/src/i18n/ja.ts
index 7864578edc..034dc6a631 100644
--- a/apps/desktop/src/i18n/ja.ts
+++ b/apps/desktop/src/i18n/ja.ts
@@ -212,6 +212,7 @@ export const ja = defineLocale({
swapSidebarSides: 'サイドバーの向きを切り替え',
hideRightSidebar: '右サイドバーを非表示',
showRightSidebar: '右サイドバーを表示',
+ unreadSessions: count => (count === 1 ? '未読セッション 1 件' : `未読セッション ${count} 件`),
muteHaptics: '触覚フィードバックをオフ',
unmuteHaptics: '触覚フィードバックをオン',
openSettings: '設定を開く',
diff --git a/apps/desktop/src/i18n/types.ts b/apps/desktop/src/i18n/types.ts
index afd3fda150..dcba755c64 100644
--- a/apps/desktop/src/i18n/types.ts
+++ b/apps/desktop/src/i18n/types.ts
@@ -253,6 +253,7 @@ export interface Translations {
swapSidebarSides: string
hideRightSidebar: string
showRightSidebar: string
+ unreadSessions: (count: number) => string
muteHaptics: string
unmuteHaptics: string
openSettings: string
diff --git a/apps/desktop/src/i18n/zh-hant.ts b/apps/desktop/src/i18n/zh-hant.ts
index 23a3232625..c68b3e2f73 100644
--- a/apps/desktop/src/i18n/zh-hant.ts
+++ b/apps/desktop/src/i18n/zh-hant.ts
@@ -206,6 +206,7 @@ export const zhHant = defineLocale({
swapSidebarSides: '交換側邊欄位置',
hideRightSidebar: '隱藏右側邊欄',
showRightSidebar: '顯示右側邊欄',
+ unreadSessions: count => (count === 1 ? '1 個未讀工作階段' : `${count} 個未讀工作階段`),
muteHaptics: '靜音觸感回饋',
unmuteHaptics: '開啟觸感回饋',
openSettings: '開啟設定',
diff --git a/apps/desktop/src/i18n/zh.ts b/apps/desktop/src/i18n/zh.ts
index 613679cd72..44620ac17e 100644
--- a/apps/desktop/src/i18n/zh.ts
+++ b/apps/desktop/src/i18n/zh.ts
@@ -206,6 +206,7 @@ export const zh: Translations = {
swapSidebarSides: '交换侧边栏位置',
hideRightSidebar: '隐藏右侧栏',
showRightSidebar: '显示右侧栏',
+ unreadSessions: count => (count === 1 ? '1 个未读会话' : `${count} 个未读会话`),
muteHaptics: '关闭触感反馈',
unmuteHaptics: '开启触感反馈',
openSettings: '打开设置',