Update titlebar app name controls (#1644)

This commit is contained in:
Neil
2026-05-09 18:48:53 -07:00
committed by GitHub
parent cfa99fd71d
commit 4789a3cd51
11 changed files with 69 additions and 331 deletions
@@ -66,7 +66,7 @@ function createSettings(overrides: Partial<GlobalSettings> = {}): GlobalSettings
terminalScrollbackBytes: 10_000_000,
openLinksInApp: false,
rightSidebarOpenByDefault: true,
showTitlebarAgentActivity: true,
showTitlebarAppName: true,
showTasksButton: true,
diffDefaultView: 'inline',
notifications: {
+1 -1
View File
@@ -60,7 +60,7 @@ function createSettings(overrides: Partial<GlobalSettings> = {}): GlobalSettings
terminalScrollbackBytes: 10_000_000,
openLinksInApp: false,
rightSidebarOpenByDefault: true,
showTitlebarAgentActivity: true,
showTitlebarAppName: true,
showTasksButton: true,
diffDefaultView: 'inline',
notifications: {
+1 -1
View File
@@ -550,7 +550,7 @@ app.whenReady().then(async () => {
const ui = store?.getUI()
return {
showTasksButton: settings?.showTasksButton !== false,
showTitlebarAgentActivity: settings?.showTitlebarAgentActivity !== false,
showTitlebarAppName: settings?.showTitlebarAppName !== false,
statusBarVisible: ui?.statusBarVisible !== false
}
}
+1 -1
View File
@@ -18,7 +18,7 @@ const SETTINGS_CHANGED_WHITELIST_SET = new Set<string>(SETTINGS_CHANGED_WHITELIS
// items when the backing state changes.
const APPEARANCE_MENU_KEYS: readonly (keyof GlobalSettings)[] = [
'showTasksButton',
'showTitlebarAgentActivity'
'showTitlebarAppName'
]
export function registerSettingsHandlers(store: Store): void {
+7 -5
View File
@@ -35,7 +35,7 @@ function buildMenuOptions() {
onToggleAppearance: vi.fn(),
getAppearanceState: vi.fn(() => ({
showTasksButton: true,
showTitlebarAgentActivity: true,
showTitlebarAppName: true,
statusBarVisible: true
}))
}
@@ -194,7 +194,7 @@ describe('registerAppMenu', () => {
const options = buildMenuOptions()
options.getAppearanceState.mockReturnValue({
showTasksButton: false,
showTitlebarAgentActivity: true,
showTitlebarAppName: true,
statusBarVisible: true
})
registerAppMenu(options)
@@ -209,9 +209,7 @@ describe('registerAppMenu', () => {
expect(tasksItem?.type).toBe('checkbox')
expect(tasksItem?.checked).toBe(false)
const titlebarItem = appearanceSubmenu.find(
(item) => item.label === 'Show Titlebar Agent Activity'
)
const titlebarItem = appearanceSubmenu.find((item) => item.label === 'Show Titlebar App Name')
expect(titlebarItem?.checked).toBe(true)
const statusBarItem = appearanceSubmenu.find((item) => item.label === 'Show Status Bar')
@@ -229,8 +227,12 @@ describe('registerAppMenu', () => {
appearanceSubmenu
.find((item) => item.label === 'Show Tasks Button')
?.click?.({} as never, {} as never, {} as never)
appearanceSubmenu
.find((item) => item.label === 'Show Titlebar App Name')
?.click?.({} as never, {} as never, {} as never)
expect(options.onToggleAppearance).toHaveBeenCalledWith('showTasksButton')
expect(options.onToggleAppearance).toHaveBeenCalledWith('showTitlebarAppName')
})
it('routes sidebar toggle items through their callbacks', () => {
+4 -4
View File
@@ -2,7 +2,7 @@ import { BrowserWindow, Menu, app } from 'electron'
export type AppearanceMenuState = {
showTasksButton: boolean
showTitlebarAgentActivity: boolean
showTitlebarAppName: boolean
statusBarVisible: boolean
}
@@ -181,10 +181,10 @@ function buildAndApplyMenu(options: RegisterAppMenuOptions): void {
click: () => onToggleAppearance('showTasksButton')
},
{
label: 'Show Titlebar Agent Activity',
label: 'Show Titlebar App Name',
type: 'checkbox',
checked: appearance.showTitlebarAgentActivity,
click: () => onToggleAppearance('showTitlebarAgentActivity')
checked: appearance.showTitlebarAppName,
click: () => onToggleAppearance('showTitlebarAppName')
}
]
}
+30 -123
View File
@@ -1,5 +1,5 @@
/* eslint-disable max-lines */
import { lazy, Suspense, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { lazy, Suspense, useEffect, useLayoutEffect, useRef, useState } from 'react'
import { getDefaultUIState } from '../../shared/constants'
import {
@@ -14,10 +14,14 @@ import logo from '../../../resources/logo.svg'
import { SYNC_FIT_PANES_EVENT, TOGGLE_TERMINAL_PANE_EXPAND_EVENT } from '@/constants/terminal'
import { syncZoomCSSVar } from '@/lib/ui-zoom'
import { buildAppFontFamily } from '@/lib/app-font-family'
import { activateTabAndFocusPane } from '@/lib/activate-tab-and-focus-pane'
import { toast } from 'sonner'
import { Toaster } from '@/components/ui/sonner'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger
} from '@/components/ui/context-menu'
import { useAppStore } from './store'
import { useShallow } from 'zustand/react/shallow'
import { useIpcEvents } from './hooks/useIpcEvents'
@@ -44,12 +48,8 @@ import {
import { useGlobalFileDrop } from './hooks/useGlobalFileDrop'
import { registerUpdaterBeforeUnloadBypass } from './lib/updater-beforeunload'
import { buildWorkspaceSessionPayload } from './lib/workspace-session'
import { countWorkingAgents, getWorkingAgentsPerWorktree } from './lib/agent-status'
import { activateAndRevealWorktree } from './lib/worktree-activation'
import { applyDocumentTheme } from './lib/document-theme'
import { isEditableTarget } from './lib/editable-target'
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
import { findWorktreeById, getRepoIdFromWorktreeId } from '@/store/slices/worktree-helpers'
import {
canGoBackWorktreeHistory,
canGoForwardWorktreeHistory
@@ -179,19 +179,6 @@ function App(): React.JSX.Element {
const activeWorktreeId = useAppStore((s) => s.activeWorktreeId)
const tabsByWorktree = useAppStore((s) => s.tabsByWorktree)
const activeTabId = useAppStore((s) => s.activeTabId)
const worktreesByRepo = useAppStore((s) => s.worktreesByRepo)
const agentInputs = useAppStore(
useShallow((s) => ({
tabsByWorktree: s.tabsByWorktree,
runtimePaneTitlesByTabId: s.runtimePaneTitlesByTabId,
worktreesByRepo: s.worktreesByRepo
}))
)
const activeAgentCount = useMemo(() => countWorkingAgents(agentInputs), [agentInputs])
const workingAgentsPerWorktree = useMemo(
() => getWorkingAgentsPerWorktree(agentInputs),
[agentInputs]
)
const expandedPaneByTabId = useAppStore((s) => s.expandedPaneByTabId)
const canExpandPaneByTabId = useAppStore((s) => s.canExpandPaneByTabId)
const workspaceSessionReady = useAppStore((s) => s.workspaceSessionReady)
@@ -739,14 +726,7 @@ function App(): React.JSX.Element {
})
observer.observe(controls)
return () => observer.disconnect()
}, [
activeAgentCount,
isFullScreen,
settings?.showTitlebarAgentActivity,
showSidebar,
workspaceActive,
sidebarOpen
])
}, [isFullScreen, settings?.showTitlebarAppName, showSidebar, workspaceActive, sidebarOpen])
useEffect(() => {
if (
@@ -807,6 +787,28 @@ function App(): React.JSX.Element {
) : (
<div className="pl-2" />
)}
{showSidebar && (
<>
{settings?.showTitlebarAppName !== false && (
<ContextMenu>
<ContextMenuTrigger asChild>
<div className="titlebar-app-name" aria-label="Orca">
Orca
</div>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem
onSelect={() => {
void actions.updateSettings({ showTitlebarAppName: false })
}}
>
Hide App Name
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
)}
</>
)}
{showSidebar && (
<Tooltip>
<TooltipTrigger asChild>
@@ -823,101 +825,6 @@ function App(): React.JSX.Element {
</TooltipContent>
</Tooltip>
)}
{settings?.showTitlebarAgentActivity !== false ? (
<Popover>
<PopoverTrigger asChild>
<button
className={`titlebar-agent-badge${activeAgentCount === 0 ? ' titlebar-agent-badge-idle' : ''}`}
aria-label={`${activeAgentCount} ${activeAgentCount === 1 ? 'agent' : 'agents'} active`}
>
<span
className={`titlebar-agent-badge-dot${activeAgentCount === 0 ? ' titlebar-agent-badge-dot-idle' : ''}`}
aria-hidden
/>
<span className="titlebar-agent-badge-count">{activeAgentCount}</span>
</button>
</PopoverTrigger>
<PopoverContent side="bottom" sideOffset={6} className="titlebar-agent-hovercard">
<div
className={`titlebar-agent-hovercard-header${activeAgentCount > 0 ? ' titlebar-agent-hovercard-header-with-list' : ''}`}
>
{activeAgentCount === 0
? 'No agents active'
: `${activeAgentCount} ${activeAgentCount === 1 ? 'agent' : 'agents'} active`}
</div>
{activeAgentCount > 0 && (
<div className="titlebar-agent-hovercard-list">
{Object.entries(workingAgentsPerWorktree).map(([worktreeId, { agents }]) => {
const wt = findWorktreeById(worktreesByRepo, worktreeId)
// Why: when a transient git error causes worktreesByRepo to
// lose a worktree, the raw worktreeId (uuid::path) is not
// useful. Extract a cross-platform path basename as a
// readable fallback.
const sepIdx = worktreeId.indexOf('::')
const pathPart = sepIdx !== -1 ? worktreeId.slice(sepIdx + 2) : worktreeId
const fallbackName = pathPart.split(/[\\/]/).pop() || pathPart
return (
<div key={worktreeId}>
<button
className="titlebar-agent-hovercard-worktree"
onClick={() => {
// Why: if the worktree is missing from worktreesByRepo
// (transient git error cleared the list), refresh the
// repo's worktrees before navigating so the activation
// lookup succeeds instead of silently failing.
if (!wt) {
const repoId = getRepoIdFromWorktreeId(worktreeId)
void useAppStore
.getState()
.fetchWorktrees(repoId)
.then(() => {
activateAndRevealWorktree(worktreeId)
})
return
}
activateAndRevealWorktree(worktreeId)
}}
>
<span className="titlebar-agent-hovercard-name">
{wt?.displayName ?? fallbackName}
</span>
</button>
{agents.map((agent) => (
<button
key={`${agent.tabId}:${agent.paneId ?? 'none'}:${agent.label}`}
className="titlebar-agent-hovercard-agent"
onClick={() => {
activateAndRevealWorktree(worktreeId)
activateTabAndFocusPane(agent.tabId, agent.paneId)
}}
>
<span className="titlebar-agent-hovercard-agent-label">
{agent.label}
</span>
<span className="titlebar-agent-hovercard-agent-dot" />
</button>
))}
</div>
)
})}
</div>
)}
<button
className="titlebar-agent-hovercard-hide"
onClick={() => {
void actions.updateSettings({ showTitlebarAgentActivity: false })
toast('Agent activity badge hidden', {
description: 'You can turn it back on in Settings → Appearance.',
duration: Infinity,
dismissible: true
})
}}
>
Hide from titlebar
</button>
</PopoverContent>
</Popover>
) : null}
</div>
{/* Why: Back/Forward traverse mixed worktree + Tasks history, so the
cluster is shown wherever the history shortcut is live (terminal or
+9 -178
View File
@@ -528,192 +528,23 @@
color: var(--muted-foreground);
}
.titlebar-agent-badge {
.titlebar-app-name {
-webkit-app-region: no-drag;
display: inline-flex;
align-items: center;
gap: 6px;
margin-left: 4px;
margin-right: 8px;
padding: 3px 8px;
border-radius: 999px;
border: 1px solid color-mix(in srgb, var(--border) 78%, transparent);
background: color-mix(in srgb, var(--accent) 80%, transparent);
color: var(--foreground);
font-size: 11px;
font-weight: 600;
line-height: 1;
white-space: nowrap;
cursor: pointer;
}
/* Why: the titlebar is a drag region, and descendant elements inherit that
region unless they opt out. Without this, hovering the dot/count spans
inside the badge showed the default cursor instead of pointer because the
drag region overrode the button's cursor. pointer-events: none forwards
hover/click hit-testing to the button, which already has no-drag set. */
.titlebar-agent-badge > * {
pointer-events: none;
}
.titlebar-agent-badge:hover {
background: var(--accent);
border-color: var(--border);
}
.titlebar-agent-badge-idle {
opacity: 0.55;
}
.titlebar-agent-badge-dot {
width: 7px;
height: 7px;
border-radius: 999px;
flex-shrink: 0;
background: #16a34a;
box-shadow: 0 0 0 3px color-mix(in srgb, #16a34a 18%, transparent);
}
.titlebar-agent-badge-dot-idle {
background: var(--muted-foreground);
box-shadow: none;
}
.titlebar-agent-badge-count {
min-width: 1ch;
}
.titlebar-agent-badge-label {
height: 24px;
margin-left: 2px;
margin-right: 4px;
padding: 0 6px;
border-radius: 4px;
color: var(--muted-foreground);
font-weight: 500;
}
/* ─── Agent hover card (titlebar badge popover) ───────────────────── */
.titlebar-agent-hovercard {
width: auto;
min-width: 180px;
max-width: 280px;
padding: 8px 0 0;
}
.titlebar-agent-hovercard-header {
padding: 0 12px 6px;
font-size: 11px;
font-weight: 500;
color: var(--muted-foreground);
}
.titlebar-agent-hovercard-header-with-list {
border-bottom: 1px solid var(--border);
margin-bottom: 4px;
}
.titlebar-agent-hovercard-hide {
display: flex;
align-items: center;
width: 100%;
padding: 5px 12px 8px;
margin-top: 4px;
border-top: 1px solid var(--border);
background: none;
border-left: none;
border-right: none;
border-bottom: none;
color: var(--muted-foreground);
font-size: 11px;
cursor: pointer;
border-radius: 0;
}
.titlebar-agent-hovercard-hide:hover {
color: var(--foreground);
background: var(--accent);
}
.titlebar-agent-hovercard-list {
display: flex;
flex-direction: column;
}
.titlebar-agent-hovercard-worktree {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 12px;
background: none;
border: none;
color: var(--foreground);
font-size: 13px;
font-weight: 500;
text-align: left;
cursor: pointer;
border-radius: 0;
width: 100%;
}
.titlebar-agent-hovercard-worktree:hover {
background: var(--accent);
}
.titlebar-agent-hovercard-agent {
display: flex;
align-items: center;
justify-content: space-between;
padding: 3px 12px 3px 24px;
font-size: 12px;
color: var(--muted-foreground);
background: none;
border: none;
text-align: left;
width: 100%;
font-weight: 600;
line-height: 24px;
white-space: nowrap;
cursor: pointer;
}
.titlebar-agent-hovercard-agent:hover {
background: var(--accent);
color: var(--foreground);
}
.titlebar-agent-hovercard-agent-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.titlebar-agent-hovercard-agent-dot {
width: 6px;
height: 6px;
border-radius: 999px;
flex-shrink: 0;
background: #16a34a;
}
.titlebar-agent-hovercard-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
}
.titlebar-agent-hovercard-count {
flex-shrink: 0;
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: var(--muted-foreground);
font-variant-numeric: tabular-nums;
}
.titlebar-agent-hovercard-dot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 999px;
background: #16a34a;
}
.titlebar-icon-button {
-webkit-app-region: no-drag;
background: none;
@@ -114,9 +114,9 @@ const LAYOUT_ENTRIES: SettingsSearchEntry[] = [
const TITLEBAR_ENTRIES: SettingsSearchEntry[] = [
{
title: 'Titlebar Agent Activity',
description: 'Show the number of active agents in the titlebar.',
keywords: ['titlebar', 'agent', 'badge', 'active', 'count', 'status']
title: 'Titlebar App Name',
description: 'Show Orca in the titlebar.',
keywords: ['titlebar', 'orca', 'app', 'name', 'brand']
}
]
@@ -289,32 +289,30 @@ export function AppearancePane({
</div>
<SearchableSetting
title="Titlebar Agent Activity"
description="Show the number of active agents in the titlebar."
keywords={['titlebar', 'agent', 'badge', 'active', 'count', 'status']}
title="Titlebar App Name"
description="Show Orca in the titlebar."
keywords={['titlebar', 'orca', 'app', 'name', 'brand']}
className="flex items-center justify-between gap-4 px-1 py-2"
>
<div className="space-y-0.5">
<Label>Titlebar Agent Activity</Label>
<p className="text-xs text-muted-foreground">
Show the number of active agents in the titlebar.
</p>
<Label>Titlebar App Name</Label>
<p className="text-xs text-muted-foreground">Show Orca in the titlebar.</p>
</div>
<button
role="switch"
aria-checked={settings.showTitlebarAgentActivity}
aria-checked={settings.showTitlebarAppName}
onClick={() =>
updateSettings({
showTitlebarAgentActivity: !settings.showTitlebarAgentActivity
showTitlebarAppName: !settings.showTitlebarAppName
})
}
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors ${
settings.showTitlebarAgentActivity ? 'bg-foreground' : 'bg-muted-foreground/30'
settings.showTitlebarAppName ? 'bg-foreground' : 'bg-muted-foreground/30'
}`}
>
<span
className={`pointer-events-none block size-3.5 rounded-full bg-background shadow-sm transition-transform ${
settings.showTitlebarAgentActivity ? 'translate-x-4' : 'translate-x-0.5'
settings.showTitlebarAppName ? 'translate-x-4' : 'translate-x-0.5'
}`}
/>
</button>
+1 -1
View File
@@ -196,7 +196,7 @@ export function getDefaultSettings(homedir: string): GlobalSettings {
terminalScrollbackBytes: 10_000_000,
openLinksInApp: true,
rightSidebarOpenByDefault: true,
showTitlebarAgentActivity: true,
showTitlebarAppName: true,
showTasksButton: true,
notifications: getDefaultNotificationSettings(),
diffDefaultView: 'inline',
+2 -2
View File
@@ -1145,8 +1145,8 @@ export type GlobalSettings = {
* until the user explicitly wants worktree-scoped in-app browsing. */
openLinksInApp: boolean
rightSidebarOpenByDefault: boolean
/** Whether to show the live agent activity count badge in the titlebar. */
showTitlebarAgentActivity: boolean
/** Whether to show the Orca app name in the titlebar. */
showTitlebarAppName: boolean
/** Why: some users do not use the Tasks feature and prefer to keep the
* left sidebar free of its button entirely. Hiding the button here also
* removes it from keyboard navigation. */