11import { PanelMessage } from "@components/ui/PanelMessage" ;
22import { isDiffTabActiveInTree , usePanelLayoutStore } from "@features/panels" ;
33import { useTaskData } from "@features/task-detail/hooks/useTaskData" ;
4- import { ArrowCounterClockwiseIcon , FileIcon } from "@phosphor-icons/react" ;
5- import { Badge , Box , Flex , IconButton , Text , Tooltip } from "@radix-ui/themes" ;
4+ import {
5+ ArrowCounterClockwiseIcon ,
6+ CodeIcon ,
7+ CopyIcon ,
8+ FileIcon ,
9+ FilePlus ,
10+ } from "@phosphor-icons/react" ;
11+ import {
12+ Badge ,
13+ Box ,
14+ DropdownMenu ,
15+ Flex ,
16+ IconButton ,
17+ Text ,
18+ Tooltip ,
19+ } from "@radix-ui/themes" ;
620import type { ChangedFile , GitFileStatus , Task } from "@shared/types" ;
21+ import { useExternalAppsStore } from "@stores/externalAppsStore" ;
722import { useQuery , useQueryClient } from "@tanstack/react-query" ;
823import { showMessageBox } from "@utils/dialog" ;
924import { handleExternalAppAction } from "@utils/handleExternalAppAction" ;
25+ import { useState } from "react" ;
1026import {
1127 selectWorktreePath ,
1228 useWorkspaceStore ,
@@ -92,7 +108,16 @@ function ChangedFileItem({
92108 ( state ) => state . closeDiffTabsForFile ,
93109 ) ;
94110 const queryClient = useQueryClient ( ) ;
111+ const { detectedApps } = useExternalAppsStore ( ) ;
112+
113+ const [ isDropdownOpen , setIsDropdownOpen ] = useState ( false ) ;
114+ const [ isHovered , setIsHovered ] = useState ( false ) ;
115+
116+ // show toolbar when hovered OR when dropdown is open
117+ const isToolbarVisible = isHovered || isDropdownOpen ;
118+
95119 const fileName = file . path . split ( "/" ) . pop ( ) || file . path ;
120+ const fullPath = `${ repoPath } /${ file . path } ` ;
96121 const indicator = getStatusIndicator ( file . status ) ;
97122
98123 const handleClick = ( ) => {
@@ -101,14 +126,30 @@ function ChangedFileItem({
101126
102127 const handleContextMenu = async ( e : React . MouseEvent ) => {
103128 e . preventDefault ( ) ;
104- const fullPath = `${ repoPath } /${ file . path } ` ;
105129 const result = await window . electronAPI . showFileContextMenu ( fullPath ) ;
106130
107131 if ( ! result . action ) return ;
108132
109133 await handleExternalAppAction ( result . action , fullPath , fileName ) ;
110134 } ;
111135
136+ const handleOpenWith = async ( appId : string ) => {
137+ await handleExternalAppAction (
138+ { type : "open-in-app" , appId } ,
139+ fullPath ,
140+ fileName ,
141+ ) ;
142+
143+ // blur active element to dismiss any open tooltip
144+ if ( document . activeElement instanceof HTMLElement ) {
145+ document . activeElement . blur ( ) ;
146+ }
147+ } ;
148+
149+ const handleCopyPath = async ( ) => {
150+ await handleExternalAppAction ( { type : "copy-path" } , fullPath , fileName ) ;
151+ } ;
152+
112153 const handleDiscard = async ( e : React . MouseEvent ) => {
113154 e . preventDefault ( ) ;
114155
@@ -147,7 +188,13 @@ function ChangedFileItem({
147188 gap = "1"
148189 onClick = { handleClick }
149190 onContextMenu = { handleContextMenu }
150- className = { `group ${ isActive ? "border-accent-8 border-y bg-accent-4" : "border-transparent border-y hover:bg-gray-3" } ` }
191+ onMouseEnter = { ( ) => setIsHovered ( true ) }
192+ onMouseLeave = { ( ) => setIsHovered ( false ) }
193+ className = {
194+ isActive
195+ ? "border-accent-8 border-y bg-accent-4"
196+ : "border-transparent border-y hover:bg-gray-3"
197+ }
151198 style = { {
152199 cursor : "pointer" ,
153200 whiteSpace : "nowrap" ,
@@ -187,11 +234,10 @@ function ChangedFileItem({
187234 { file . originalPath ? `${ file . originalPath } → ${ file . path } ` : file . path }
188235 </ Text >
189236
190- { hasLineStats && (
237+ { hasLineStats && ! isToolbarVisible && (
191238 < Flex
192239 align = "center"
193240 gap = "1"
194- className = "group-hover:hidden"
195241 style = { { flexShrink : 0 , fontSize : "10px" , fontFamily : "monospace" } }
196242 >
197243 { ( file . linesAdded ?? 0 ) > 0 && (
@@ -203,31 +249,84 @@ function ChangedFileItem({
203249 </ Flex >
204250 ) }
205251
206- < Flex
207- align = "center"
208- gap = "1"
209- className = "hidden group-hover:flex"
210- style = { { flexShrink : 0 } }
211- >
212- < Tooltip content = "Discard changes" >
213- < IconButton
214- size = "1"
215- variant = "ghost"
216- color = "gray"
217- onClick = { handleDiscard }
218- style = { {
219- flexShrink : 0 ,
220- width : "18px" ,
221- height : "18px" ,
222- padding : 0 ,
223- marginLeft : "2px" ,
224- marginRight : "2px" ,
225- } }
252+ { isToolbarVisible && (
253+ < Flex align = "center" gap = "1" style = { { flexShrink : 0 } } >
254+ < Tooltip content = "Discard changes" >
255+ < IconButton
256+ size = "1"
257+ variant = "ghost"
258+ color = "gray"
259+ onClick = { handleDiscard }
260+ style = { {
261+ flexShrink : 0 ,
262+ width : "18px" ,
263+ height : "18px" ,
264+ padding : 0 ,
265+ marginLeft : "2px" ,
266+ marginRight : "2px" ,
267+ } }
268+ >
269+ < ArrowCounterClockwiseIcon size = { 12 } />
270+ </ IconButton >
271+ </ Tooltip >
272+
273+ < DropdownMenu . Root
274+ open = { isDropdownOpen }
275+ onOpenChange = { setIsDropdownOpen }
226276 >
227- < ArrowCounterClockwiseIcon size = { 12 } />
228- </ IconButton >
229- </ Tooltip >
230- </ Flex >
277+ < Tooltip content = "Open file" >
278+ < DropdownMenu . Trigger >
279+ < IconButton
280+ size = "1"
281+ variant = "ghost"
282+ color = "gray"
283+ onClick = { ( e ) => e . stopPropagation ( ) }
284+ style = { {
285+ flexShrink : 0 ,
286+ width : "18px" ,
287+ height : "18px" ,
288+ padding : 0 ,
289+ } }
290+ >
291+ < FilePlus size = { 12 } weight = "regular" />
292+ </ IconButton >
293+ </ DropdownMenu . Trigger >
294+ </ Tooltip >
295+ < DropdownMenu . Content size = "1" align = "end" >
296+ { detectedApps
297+ . filter ( ( app ) => app . type !== "terminal" )
298+ . map ( ( app ) => (
299+ < DropdownMenu . Item
300+ key = { app . id }
301+ onSelect = { ( ) => handleOpenWith ( app . id ) }
302+ >
303+ < Flex align = "center" gap = "2" >
304+ { app . icon ? (
305+ < img
306+ src = { app . icon }
307+ width = { 16 }
308+ height = { 16 }
309+ alt = ""
310+ style = { { borderRadius : "2px" } }
311+ />
312+ ) : (
313+ < CodeIcon size = { 16 } weight = "regular" />
314+ ) }
315+ < Text size = "1" > { app . name } </ Text >
316+ </ Flex >
317+ </ DropdownMenu . Item >
318+ ) ) }
319+ < DropdownMenu . Separator />
320+ < DropdownMenu . Item onSelect = { handleCopyPath } >
321+ < Flex align = "center" gap = "2" >
322+ < CopyIcon size = { 16 } weight = "regular" />
323+ < Text size = "1" > Copy Path</ Text >
324+ </ Flex >
325+ </ DropdownMenu . Item >
326+ </ DropdownMenu . Content >
327+ </ DropdownMenu . Root >
328+ </ Flex >
329+ ) }
231330
232331 < Badge
233332 size = "1"
0 commit comments