refactor(desktop): tighten right-rail tab close API

Promote closeRightRailTab/closeActiveRightRailTab as the single
public entry point. Drops the activeTabRef + handleCloseDocument
indirection in ChatPreviewRail, the unused $rightRailHasContent
atom, and the legacy dismissFilePreviewTarget alias. -70 LOC.
This commit is contained in:
Brooklyn Nicholson 2026-05-05 13:27:05 -05:00
parent dda3894523
commit c9987f1e22
4 changed files with 23 additions and 93 deletions

View file

@ -1,5 +1,5 @@
import { useStore } from '@nanostores/react'
import { type MouseEvent, useCallback, useEffect, useMemo, useRef } from 'react'
import { useEffect, useMemo } from 'react'
import type { SetTitlebarToolGroup } from '@/app/shell/titlebar-controls'
import { X } from '@/lib/icons'
@ -14,9 +14,8 @@ import {
$filePreviewTabs,
$previewReloadRequest,
$previewTarget,
closeFilePreviewTab,
dismissPreviewTarget,
type FilePreviewTab,
closeActiveRightRailTab,
closeRightRailTab,
type PreviewTarget
} from '@/store/preview'
@ -39,21 +38,16 @@ interface ChatPreviewRailProps {
}
interface RailTab {
closeLabel: string
id: RightRailTabId
label: string
target: PreviewTarget
}
function previewTabLabel(target: PreviewTarget): string {
function tabLabelFor(target: PreviewTarget): string {
const value = target.label || target.path || target.source || target.url
const parts = value.split(/[\\/]/).filter(Boolean)
const tail = value.split(/[\\/]/).filter(Boolean).at(-1)
return parts.at(-1) || value || 'Preview'
}
function tabLabel(tab: FilePreviewTab): string {
return previewTabLabel(tab.target)
return tail || value || 'Preview'
}
export function ChatPreviewRail({ onRestartServer, setTitlebarToolGroup }: ChatPreviewRailProps) {
@ -64,30 +58,13 @@ export function ChatPreviewRail({ onRestartServer, setTitlebarToolGroup }: ChatP
const tabs = useMemo<readonly RailTab[]>(
() => [
...(previewTarget
? [
{
closeLabel: 'Close preview',
id: RIGHT_RAIL_PREVIEW_TAB_ID,
label: 'Preview',
target: previewTarget
} satisfies RailTab
]
: []),
...filePreviewTabs.map(tab => ({
closeLabel: `Close ${tabLabel(tab)}`,
id: tab.id,
label: tabLabel(tab),
target: tab.target
}))
...(previewTarget ? [{ id: RIGHT_RAIL_PREVIEW_TAB_ID, label: 'Preview', target: previewTarget } as RailTab] : []),
...filePreviewTabs.map(({ id, target }) => ({ id, label: tabLabelFor(target), target }) as RailTab)
],
[filePreviewTabs, previewTarget]
)
const activeTab = tabs.find(tab => tab.id === activeTabId) ?? tabs[0]
// Read-by-ref so close handlers stay reference-stable across renders.
const activeTabRef = useRef<RailTab | undefined>(activeTab)
activeTabRef.current = activeTab
useEffect(() => {
if (activeTab && activeTab.id !== activeTabId) {
@ -95,32 +72,6 @@ export function ChatPreviewRail({ onRestartServer, setTitlebarToolGroup }: ChatP
}
}, [activeTab, activeTabId])
const closeRailTab = useCallback((tab: RailTab) => {
if (tab.id === RIGHT_RAIL_PREVIEW_TAB_ID) {
dismissPreviewTarget()
return
}
closeFilePreviewTab(tab.id)
}, [])
// Stable: PreviewPane lists onClose in a useEffect dep array that pushes
// titlebar tools. A fresh closure every render → setTitlebarToolGroup every
// render → DesktopController setState → re-render → ∞.
const handleCloseDocument = useCallback(() => {
const tab = activeTabRef.current
if (tab) {
closeRailTab(tab)
}
}, [closeRailTab])
const closeTab = (event: MouseEvent, tab: RailTab) => {
event.stopPropagation()
closeRailTab(tab)
}
if (!activeTab) {
return null
}
@ -158,13 +109,13 @@ export function ChatPreviewRail({ onRestartServer, setTitlebarToolGroup }: ChatP
{tab.label}
</button>
<button
aria-label={tab.closeLabel}
aria-label={`Close ${tab.label}`}
className={cn(
'mr-1.5 hidden size-4 shrink-0 place-items-center rounded-sm text-muted-foreground/55 transition-colors hover:bg-accent hover:text-foreground focus-visible:grid group-hover/tab:grid',
active && 'grid'
)}
onClick={event => closeTab(event, tab)}
title={tab.closeLabel}
onClick={() => closeRightRailTab(tab.id)}
title={`Close ${tab.label}`}
type="button"
>
<X className="size-3" />
@ -177,7 +128,7 @@ export function ChatPreviewRail({ onRestartServer, setTitlebarToolGroup }: ChatP
<div className="min-h-0 flex-1 overflow-hidden">
<PreviewPane
embedded
onClose={handleCloseDocument}
onClose={closeActiveRightRailTab}
onRestartServer={isPreview ? onRestartServer : undefined}
reloadRequest={previewReloadRequest}
setTitlebarToolGroup={setTitlebarToolGroup}

View file

@ -19,7 +19,7 @@ import {
SIDEBAR_MAX_WIDTH,
unpinSession
} from '../store/layout'
import { $filePreviewTarget, $previewTarget, dismissFilePreviewTarget, dismissPreviewTarget } from '../store/preview'
import { $filePreviewTarget, $previewTarget, closeActiveRightRailTab } from '../store/preview'
import {
$activeSessionId,
$currentCwd,
@ -138,18 +138,6 @@ export function DesktopController() {
}, [chatOpen, filePreviewTarget, previewTarget])
useEffect(() => {
const closePreview = () => {
if ($filePreviewTarget.get()) {
dismissFilePreviewTarget()
return
}
if ($previewTarget.get()) {
dismissPreviewTarget()
}
}
const onKeyDown = (event: KeyboardEvent) => {
if (!$filePreviewTarget.get() && !$previewTarget.get()) {
return
@ -158,11 +146,11 @@ export function DesktopController() {
if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === 'w') {
event.preventDefault()
event.stopPropagation()
closePreview()
closeActiveRightRailTab()
}
}
const unsubscribe = window.hermesDesktop?.onClosePreviewRequested?.(closePreview)
const unsubscribe = window.hermesDesktop?.onClosePreviewRequested?.(closeActiveRightRailTab)
window.addEventListener('keydown', onKeyDown, { capture: true })

View file

@ -10,7 +10,7 @@ import {
$sessionPreviewRegistry,
beginPreviewServerRestart,
clearSessionPreviewRegistry,
dismissFilePreviewTarget,
closeActiveRightRailTab,
dismissPreviewTarget,
getSessionPreviewRecord,
type PreviewTarget,
@ -114,7 +114,7 @@ describe('preview store', () => {
expect($previewTarget.get()).toEqual(withRenderMode(preview, 'preview'))
expect(getSessionPreviewRecord('session-1')?.normalized).toEqual(withRenderMode(preview, 'preview'))
dismissFilePreviewTarget()
closeActiveRightRailTab()
expect($filePreviewTarget.get()).toBeNull()
expect($previewTarget.get()).toEqual(withRenderMode(preview, 'preview'))

View file

@ -58,9 +58,6 @@ export const $filePreviewTarget = computed([$filePreviewTabs, $rightRailActiveTa
return tabs.find(tab => tab.id === activeTabId)?.target ?? null
})
export const $rightRailHasContent = computed([$previewTarget, $filePreviewTabs], (target, tabs) =>
Boolean(target || tabs.length)
)
export const $previewReloadRequest = atom(0)
export const $previewServerRestart = atom<PreviewServerRestart | null>(null)
export const $previewServerRestartStatus = computed($previewServerRestart, restart => restart?.status ?? 'idle')
@ -368,7 +365,7 @@ export function dismissPreviewTarget() {
}
}
export function closeFilePreviewTab(tabId: RightRailTabId) {
function closeFilePreviewTab(tabId: RightRailTabId) {
if (!tabId.startsWith('file:')) {
return
}
@ -389,14 +386,8 @@ export function closeFilePreviewTab(tabId: RightRailTabId) {
}
}
export function dismissFilePreviewTarget() {
closeFilePreviewTab($rightRailActiveTabId.get())
}
export function closeActiveRightRailTab() {
const activeTabId = $rightRailActiveTabId.get()
if (activeTabId === RIGHT_RAIL_PREVIEW_TAB_ID) {
export function closeRightRailTab(tabId: RightRailTabId) {
if (tabId === RIGHT_RAIL_PREVIEW_TAB_ID) {
if ($previewTarget.get()) {
dismissPreviewTarget()
}
@ -404,11 +395,11 @@ export function closeActiveRightRailTab() {
return
}
if (activeTabId.startsWith('file:')) {
closeFilePreviewTab(activeTabId)
}
closeFilePreviewTab(tabId)
}
export const closeActiveRightRailTab = () => closeRightRailTab($rightRailActiveTabId.get())
export function clearSessionPreviewRegistry() {
$sessionPreviewRegistry.set({})
setPreviewTarget(null)