Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2d8cce5
feat(agent-file-preview): show previews in files overlay
AtomsH4 Jul 3, 2026
46089c1
fix(agent-file-preview): gate file tree model by pane state
AtomsH4 Jul 4, 2026
2685558
fix(agent-file-preview): clear stale overlay selections
AtomsH4 Jul 4, 2026
1da9eae
fix(file-tree): allow context menu bubbling
AtomsH4 Jul 4, 2026
18565f8
fix(agent-file-preview): focus overlay previews
AtomsH4 Jul 4, 2026
41b87d8
Merge remote-tracking branch 'origin/main' into feat/file-preview-ove…
AtomsH4 Jul 4, 2026
29ca171
Merge branch 'main' into feat/file-preview-overlay
AtomsH4 Jul 4, 2026
97a260a
Merge branch 'main' into feat/file-preview-overlay
AtomsH4 Jul 4, 2026
24af41c
fix(agent-file-preview): clear stale standalone overlays
AtomsH4 Jul 5, 2026
d693f62
fix(agent-file-preview): gate file tree by active tab
AtomsH4 Jul 5, 2026
2e186c3
refactor(artifact-pane): simplify file preview overlay
AtomsH4 Jul 5, 2026
287f5f9
Merge remote-tracking branch 'origin/main' into feat/file-preview-ove…
AtomsH4 Jul 6, 2026
988773a
docs(file-docs): revert unrelated reference link updates
AtomsH4 Jul 6, 2026
81eb5d4
fix(command-menu): stop nested context menu bubbling
AtomsH4 Jul 6, 2026
74d64c3
docs(agent-file-preview): update overlay docs
AtomsH4 Jul 6, 2026
764a42c
Merge branch 'main' into feat/file-preview-overlay
AtomsH4 Jul 6, 2026
633c7f1
chore(file-preview-overlay): merge main
AtomsH4 Jul 6, 2026
28d38dc
Merge branch 'main' into feat/file-preview-overlay
AtomsH4 Jul 6, 2026
0ff2151
Merge branch 'main' into feat/file-preview-overlay
AtomsH4 Jul 6, 2026
5082ce5
Merge branch 'main' into feat/file-preview-overlay
0xfullex Jul 6, 2026
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
49 changes: 27 additions & 22 deletions src/renderer/components/FileTree/FileTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export function FileTree(props: FileTreeProps) {
searchKeyword = '',
onSearchKeywordChange,
searchPlaceholder,
searchToolbar,
searchClearLabel,
emptyState
} = props

Expand Down Expand Up @@ -114,28 +116,31 @@ export function FileTree(props: FileTreeProps) {

return (
<div className="flex h-full min-h-0 flex-col">
<div className="relative px-2 py-2">
<Search
size={14}
className="-translate-y-1/2 pointer-events-none absolute top-1/2 left-4 text-muted-foreground"
/>
<Input
type="text"
value={searchKeyword}
onChange={(e) => onSearchKeywordChange?.(e.target.value)}
placeholder={searchPlaceholder}
className="h-8 pr-7 pl-7 text-sm"
data-testid="file-tree-search-input"
/>
{searchKeyword && (
<button
type="button"
aria-label="Clear search"
onClick={() => onSearchKeywordChange?.('')}
className="-translate-y-1/2 absolute top-1/2 right-3 flex size-5 cursor-pointer items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground">
<X size={13} />
</button>
)}
<div className="flex items-center gap-2 px-2 py-2">
<div className="relative min-w-0 flex-1">
<Search
size={14}
className="-translate-y-1/2 pointer-events-none absolute top-1/2 left-2 text-muted-foreground"
/>
<Input
type="text"
value={searchKeyword}
onChange={(e) => onSearchKeywordChange?.(e.target.value)}
placeholder={searchPlaceholder}
className="h-8 min-w-0 flex-1 pr-7 pl-7 text-sm"
data-testid="file-tree-search-input"
/>
{searchKeyword && (
<button
type="button"
aria-label={searchClearLabel ?? 'Clear search'}
onClick={() => onSearchKeywordChange?.('')}
className="-translate-y-1/2 absolute top-1/2 right-1 flex size-5 cursor-pointer items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground">
<X size={13} />
</button>
)}
</div>
{searchToolbar}
</div>
<div className="min-h-0 flex-1">{tree}</div>
</div>
Expand Down
1 change: 0 additions & 1 deletion src/renderer/components/FileTree/FileTreeRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ export function FileTreeRow(props: FileTreeRowProps) {
data-node-id={node.id}
data-kind={node.kind}
onClick={handleRowClick}
onContextMenu={(e) => e.stopPropagation()}
title={node.name}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[B7/B3] Removing the unconditional onContextMenu stopPropagation regresses NotesSidebar's context menu

This deletes onContextMenu={(e) => e.stopPropagation()} from the shared FileTreeRow to fix the native-menu case in ArtifactPane (which has no outer menu). But FileTreeRow is also used by NotesSidebar (NotesSidebar.tsx:375), where the whole FileTree is wrapped in an outer CommandContextMenu (the non-empty "New note / New folder / Upload…" empty-area menu) while each row also has its own CommandContextMenu.

In the default cherry presentation mode, CommandContextMenu uses Radix ContextMenuTrigger, whose onContextMenu calls handleOpen + preventDefault but not stopPropagation. The row's stopPropagation was what kept a row right-click from also bubbling to the outer empty-area trigger. With it removed, right-clicking a note row now opens both the row menu and the empty-area menu at once (the empty-area one ends up on top), so users can no longer reliably reach the row actions.

Native mode is unaffected because handleNativeContextMenu calls stopPropagation itself — only the default cherry mode regresses. The added test (allows row context-menu events to reach the menu wrapper) only asserts the bubbling that causes this; it doesn't cover the nested-wrapper case.

Suggested direction (author's call): fix it upstream so nested cherry-mode menus don't double-open — e.g. have CommandContextMenu's cherry-mode trigger stop propagation of the contextmenu event once it handles it — or reintroduce a scoped stop on the row that still lets the row's own wrapper receive the event.

@AtomsH4 AtomsH4 Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This review comment was translated automatically.

Verified current head 74d64c35dc5d1f2dbbde63836deecad0dcfe6e56, this nested cherry-mode context menu issue has been fixed.

The fix is in 81eb5d48d5: CommandContextMenu's cherry-mode trigger now calls event.stopPropagation() after handling contextmenu, so the inner row menu won't bubble up to the NotesSidebar outer empty-area menu after handling right-click; native mode still uses stopPropagation() in handleNativeContextMenu.

Also added a regression test stops cherry context-menu events after an inner menu handles them, covering that the outer onOpenChange doesn't trigger when the inner menu opens. The latest 74d64c35dc is just documentation updates, not affecting this behavior.


Original Content

已核对当前 head 74d64c35dc5d1f2dbbde63836deecad0dcfe6e56,这个 nested cherry-mode context menu 问题已经修复。

修复在 81eb5d48d5CommandContextMenu 的 cherry-mode trigger 现在在处理 contextmenu 后调用 event.stopPropagation(),所以内层 row menu 处理右键后不会再冒泡到 NotesSidebar 外层 empty-area menu;native mode 仍沿用 handleNativeContextMenu 里的 stopPropagation()

也补了回归测试 stops cherry context-menu events after an inner menu handles them,覆盖内层 menu 打开时外层 onOpenChange 不触发。最新的 74d64c35dc 只是文档更新,没有影响这部分行为。

style={indent}
className={cn(
Expand Down
54 changes: 53 additions & 1 deletion src/renderer/components/FileTree/__tests__/FileTree.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,27 @@ describe('FileTree - editable form (all callbacks)', () => {

expect(screen.getByText('Menu for Root')).toBeInTheDocument()
})

it('stops row context-menu events after opening the row menu', () => {
const onWrapperContextMenu = vi.fn()

render(
<div onContextMenu={onWrapperContextMenu}>
<FileTree
nodes={nodes}
defaultExpandedIds={new Set(['root'])}
getMenuItems={(n) => [{ type: 'item', id: `menu-${n.id}`, label: `Menu for ${n.name}`, onSelect: () => {} }]}
renderList={passthroughRenderList}
/>
</div>
)

const rootRow = screen.getByText('Root').closest('[data-node-id="root"]')!
fireEvent.contextMenu(rootRow)

expect(screen.getByText('Menu for Root')).toBeInTheDocument()
expect(onWrapperContextMenu).not.toHaveBeenCalled()
})
})

describe('FileTree - icon behaviour', () => {
Expand Down Expand Up @@ -243,8 +264,15 @@ describe('FileTree - icon behaviour', () => {

describe('FileTree - search box', () => {
it('does not render the search input when showSearch is omitted', () => {
render(<FileTree nodes={nodes} renderList={passthroughRenderList} />)
render(
<FileTree
nodes={nodes}
searchToolbar={<span data-testid="search-toolbar">Tools</span>}
renderList={passthroughRenderList}
/>
)
expect(screen.queryByTestId('file-tree-search-input')).toBeNull()
expect(screen.queryByTestId('search-toolbar')).toBeNull()
})

it('renders the search input when showSearch is true', () => {
Expand Down Expand Up @@ -290,6 +318,30 @@ describe('FileTree - search box', () => {
expect(onSearchKeywordChange).toHaveBeenCalledWith('a')
})

it('renders the search toolbar and keeps the clear button usable', async () => {
const onSearchKeywordChange = vi.fn()
const user = userEvent.setup()
render(
<FileTree
nodes={nodes}
showSearch
searchKeyword="abc"
searchClearLabel="Clear files search"
searchToolbar={
<button type="button" data-testid="search-toolbar">
Refresh
</button>
}
onSearchKeywordChange={onSearchKeywordChange}
renderList={passthroughRenderList}
/>
)

expect(screen.getByTestId('search-toolbar')).toBeInTheDocument()
await user.click(screen.getByLabelText('Clear files search'))
expect(onSearchKeywordChange).toHaveBeenCalledWith('')
})

it('clears the keyword via the clear button', async () => {
const onSearchKeywordChange = vi.fn()
const user = userEvent.setup()
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/components/FileTree/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ export interface FileTreeProps {
onSearchKeywordChange?: (keyword: string) => void
/** Placeholder for the search input. */
searchPlaceholder?: string
/** Optional trailing toolbar rendered on the search row. */
searchToolbar?: React.ReactNode
/** Accessible label for the search clear button. */
searchClearLabel?: string

emptyState?: React.ReactNode
}
Loading
Loading