Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 79 additions & 77 deletions apps/desktop/src/app/right-sidebar/files/tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { type NodeApi, type NodeRendererProps, type RowRendererProps, Tree, type

import { TreeSkeleton } from '@/components/chat/skeletons'
import { Codicon } from '@/components/ui/codicon'
import { Tip } from '@/components/ui/tooltip'
import { useResizeObserver } from '@/hooks/use-resize-observer'
import { cn } from '@/lib/utils'
import { $repoChangeByPath, type RepoChangeKind } from '@/store/coding-status'
Expand Down Expand Up @@ -271,85 +272,86 @@ function ProjectTreeRow({
const editing = !isPlaceholder && renamingPath === node.data.id

const row = (
<div
aria-expanded={isFolder ? node.isOpen : undefined}
aria-selected={node.isSelected}
className={cn(
'group/row flex h-full cursor-pointer select-none items-center gap-1 border border-transparent px-3 text-xs font-normal leading-(--file-tree-row-height) text-(--ui-text-secondary) transition-colors duration-100 ease-out hover:bg-(--ui-row-hover-background) hover:text-foreground hover:transition-none',
node.isSelected && 'bg-(--ui-row-active-background) text-foreground',
isPlaceholder && 'pointer-events-none italic text-muted-foreground/70'
)}
draggable={!isPlaceholder && !editing}
onClick={event => {
event.stopPropagation()

// Read the rename atom LIVE (not the render closure): the fall-through
// click from a context-menu close can fire before the editing re-render
// commits, so a stale closure would still select/activate and yank focus.
if (isPlaceholder || $renamingPath.get() === node.data.id) {
return
}

if (event.shiftKey) {
;(isFolder ? onAttachFolder : onAttachFile)(node.data.id)

return
}

if (isFolder) {
node.toggle()
} else {
node.select()
}
}}
onDoubleClick={event => {
event.stopPropagation()

if (!isFolder && !isPlaceholder && $renamingPath.get() !== node.data.id) {
onPreviewFile?.(node.data.id)
}
}}
onDragStart={event => {
if (isPlaceholder || $renamingPath.get() === node.data.id) {
event.preventDefault()

return
}

const payload = JSON.stringify([{ isDirectory: isFolder, path: node.data.id }])

event.dataTransfer.effectAllowed = 'copy'
event.dataTransfer.setData('application/x-hermes-paths', payload)
event.dataTransfer.setData('text/plain', node.data.id)
}}
ref={dragHandle}
style={{
...style,
paddingLeft: withTreeInset(style.paddingLeft)
}}
title={node.data.id}
>
{/* No chevron column β€” the folder icon (open/closed) already carries the
expand state, so the extra glyph was pure noise. */}
<span aria-hidden className="flex w-3.5 items-center justify-center text-(--ui-text-tertiary)">
{isPlaceholder && !isErrorPlaceholder ? (
<Codicon name="loading" size="0.75rem" spinning />
) : isErrorPlaceholder ? (
<Codicon name="warning" size="0.75rem" />
) : isFolder ? (
<Codicon name={node.isOpen ? 'folder-opened' : 'folder'} size="0.875rem" />
<Tip label={node.data.id} side="left">
<div
aria-expanded={isFolder ? node.isOpen : undefined}
aria-selected={node.isSelected}
className={cn(
'group/row flex h-full cursor-pointer select-none items-center gap-1 border border-transparent px-3 text-xs font-normal leading-(--file-tree-row-height) text-(--ui-text-secondary) transition-colors duration-100 ease-out hover:bg-(--ui-row-hover-background) hover:text-foreground hover:transition-none',
node.isSelected && 'bg-(--ui-row-active-background) text-foreground',
isPlaceholder && 'pointer-events-none italic text-muted-foreground/70'
)}
draggable={!isPlaceholder && !editing}
onClick={event => {
event.stopPropagation()

// Read the rename atom LIVE (not the render closure): the fall-through
// click from a context-menu close can fire before the editing re-render
// commits, so a stale closure would still select/activate and yank focus.
if (isPlaceholder || $renamingPath.get() === node.data.id) {
return
}

if (event.shiftKey) {
;(isFolder ? onAttachFolder : onAttachFile)(node.data.id)

return
}

if (isFolder) {
node.toggle()
} else {
node.select()
}
}}
onDoubleClick={event => {
event.stopPropagation()

if (!isFolder && !isPlaceholder && $renamingPath.get() !== node.data.id) {
onPreviewFile?.(node.data.id)
}
}}
onDragStart={event => {
if (isPlaceholder || $renamingPath.get() === node.data.id) {
event.preventDefault()

return
}

const payload = JSON.stringify([{ isDirectory: isFolder, path: node.data.id }])

event.dataTransfer.effectAllowed = 'copy'
event.dataTransfer.setData('application/x-hermes-paths', payload)
event.dataTransfer.setData('text/plain', node.data.id)
}}
ref={dragHandle}
style={{
...style,
paddingLeft: withTreeInset(style.paddingLeft)
}}
>
{/* No chevron column β€” the folder icon (open/closed) already carries the
expand state, so the extra glyph was pure noise. */}
<span aria-hidden className="flex w-3.5 items-center justify-center text-(--ui-text-tertiary)">
{isPlaceholder && !isErrorPlaceholder ? (
<Codicon name="loading" size="0.75rem" spinning />
) : isErrorPlaceholder ? (
<Codicon name="warning" size="0.75rem" />
) : isFolder ? (
<Codicon name={node.isOpen ? 'folder-opened' : 'folder'} size="0.875rem" />
) : (
<Codicon name="file" size="0.875rem" />
)}
</span>
{editing ? (
<InlineRenameInput name={node.data.name} path={node.data.id} />
) : (
<Codicon name="file" size="0.875rem" />
// Git decoration (VS Code-style): tint changed files; the explicit color
// wins over the row's hover/selected text color, so it persists.
<span className={cn('min-w-0 flex-1 truncate', changeKind && CHANGE_TINT[changeKind])}>{node.data.name}</span>
)}
</span>
{editing ? (
<InlineRenameInput name={node.data.name} path={node.data.id} />
) : (
// Git decoration (VS Code-style): tint changed files; the explicit color
// wins over the row's hover/selected text color, so it persists.
<span className={cn('min-w-0 flex-1 truncate', changeKind && CHANGE_TINT[changeKind])}>{node.data.name}</span>
)}
</div>
</div>
</Tip>
)

if (isPlaceholder) {
Expand Down
47 changes: 25 additions & 22 deletions apps/desktop/src/app/right-sidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { TreeSkeleton } from '@/components/chat/skeletons'
import { ErrorBoundary } from '@/components/error-boundary'
import { Button } from '@/components/ui/button'
import { Codicon } from '@/components/ui/codicon'
import { Tip } from '@/components/ui/tooltip'
import { useDelayedTrue } from '@/hooks/use-delayed-true'
import { useI18n } from '@/i18n'
import { normalizeOrLocalPreviewTarget } from '@/lib/local-preview'
Expand Down Expand Up @@ -151,28 +152,30 @@ function FilesystemTab({
<div className="flex min-w-0 flex-1">
<SidebarPanelLabel>{cwdName}</SidebarPanelLabel>
</div>
<Button
aria-label={r.refreshTree}
className={HEADER_ACTION_LABEL_REVEAL}
disabled={loading}
onClick={onRefresh}
size="icon-xs"
title={r.refreshTree}
variant="ghost"
>
<Codicon name="refresh" size="0.8125rem" spinning={loading} />
</Button>
<Button
aria-label={r.collapseAll}
className={cn(HEADER_ACTION_CLASS, !canCollapse && 'pointer-events-none opacity-0')}
disabled={!canCollapse}
onClick={onCollapseAll}
size="icon-xs"
title={r.collapseAll}
variant="ghost"
>
<Codicon name="collapse-all" size="0.8125rem" />
</Button>
<Tip label={r.refreshTree} side="left">
<Button
aria-label={r.refreshTree}
className={HEADER_ACTION_LABEL_REVEAL}
disabled={loading}
onClick={onRefresh}
size="icon-xs"
variant="ghost"
>
<Codicon name="refresh" size="0.8125rem" spinning={loading} />
</Button>
</Tip>
Comment on lines +155 to +166

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟒 Disabled refresh button loses tooltip after title->Tip migration (bug)

The refresh button in the right sidebar header (index.tsx:155-166) is wrapped in instead of using a native title attribute. When loading=true, the button is disabled. Radix UI's TooltipTrigger does not activate on disabled elements (documented Radix limitation). The button is still visually revealed via CSS group-hover (HEADER_ACTION_LABEL_REVEAL restores pointer-events-auto and opacity), creating a state where a visible disabled button shows no tooltip -- the old title={r.refreshTree} worked in this state. The collapse button at lines 167-178 has a similar pattern but is pointer-events-none opacity-0 when disabled, so the gap is moot there. Screen reader users are unaffected because aria-label is preserved.

πŸ’‘ Suggestion: Per Radix documentation, disabled elements need a wrapper for the tooltip trigger. Wrap the in a when loading is true and apply the to the span; when not loading, wrap the button directly as before. Alternatively, add the native title back as a fallback alongside .

πŸ“‹ Prompt for AI Agents

In apps/desktop/src/app/right-sidebar/index.tsx, lines 155-166, the refresh button is wrapped in a component. Radix UI TooltipTrigger does not activate on disabled elements. When the button is disabled (loading=true), the tooltip will not appear. Fix by conditionally wrapping the button in a when disabled so the tooltip trigger targets the span instead of the disabled button. For example: store the button JSX in a variable, and conditionally render {button} when loading, or {button} when not loading.

<Tip label={r.collapseAll} side="left">
<Button
aria-label={r.collapseAll}
className={cn(HEADER_ACTION_CLASS, !canCollapse && 'pointer-events-none opacity-0')}
disabled={!canCollapse}
onClick={onCollapseAll}
size="icon-xs"
variant="ghost"
>
<Codicon name="collapse-all" size="0.8125rem" />
</Button>
</Tip>
</RightSidebarSectionHeader>
<FileTreeBody
collapseNonce={collapseNonce}
Expand Down
Loading