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,25 @@ 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+
144+ const handleCopyPath = async ( ) => {
145+ await handleExternalAppAction ( { type : "copy-path" } , fullPath , fileName ) ;
146+ } ;
147+
112148 const handleDiscard = async ( e : React . MouseEvent ) => {
113149 e . preventDefault ( ) ;
114150
@@ -147,7 +183,13 @@ function ChangedFileItem({
147183 gap = "1"
148184 onClick = { handleClick }
149185 onContextMenu = { handleContextMenu }
150- className = { `group ${ isActive ? "border-accent-8 border-y bg-accent-4" : "border-transparent border-y hover:bg-gray-3" } ` }
186+ onMouseEnter = { ( ) => setIsHovered ( true ) }
187+ onMouseLeave = { ( ) => setIsHovered ( false ) }
188+ className = {
189+ isActive
190+ ? "border-accent-8 border-y bg-accent-4"
191+ : "border-transparent border-y hover:bg-gray-3"
192+ }
151193 style = { {
152194 cursor : "pointer" ,
153195 whiteSpace : "nowrap" ,
@@ -187,11 +229,10 @@ function ChangedFileItem({
187229 { file . originalPath ? `${ file . originalPath } → ${ file . path } ` : file . path }
188230 </ Text >
189231
190- { hasLineStats && (
232+ { hasLineStats && ! isToolbarVisible && (
191233 < Flex
192234 align = "center"
193235 gap = "1"
194- className = "group-hover:hidden"
195236 style = { { flexShrink : 0 , fontSize : "10px" , fontFamily : "monospace" } }
196237 >
197238 { ( file . linesAdded ?? 0 ) > 0 && (
@@ -203,31 +244,82 @@ function ChangedFileItem({
203244 </ Flex >
204245 ) }
205246
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- } }
247+ { isToolbarVisible && (
248+ < Flex align = "center" gap = "1" style = { { flexShrink : 0 } } >
249+ < Tooltip content = "Discard changes" >
250+ < IconButton
251+ size = "1"
252+ variant = "ghost"
253+ color = "gray"
254+ onClick = { handleDiscard }
255+ style = { {
256+ flexShrink : 0 ,
257+ width : "18px" ,
258+ height : "18px" ,
259+ padding : 0 ,
260+ marginLeft : "2px" ,
261+ marginRight : "2px" ,
262+ } }
263+ >
264+ < ArrowCounterClockwiseIcon size = { 12 } />
265+ </ IconButton >
266+ </ Tooltip >
267+
268+ < DropdownMenu . Root
269+ open = { isDropdownOpen }
270+ onOpenChange = { setIsDropdownOpen }
226271 >
227- < ArrowCounterClockwiseIcon size = { 12 } />
228- </ IconButton >
229- </ Tooltip >
230- </ Flex >
272+ < Tooltip content = "Open file" >
273+ < DropdownMenu . Trigger >
274+ < IconButton
275+ size = "1"
276+ variant = "ghost"
277+ color = "gray"
278+ onClick = { ( e ) => e . stopPropagation ( ) }
279+ style = { {
280+ flexShrink : 0 ,
281+ width : "18px" ,
282+ height : "18px" ,
283+ padding : 0 ,
284+ } }
285+ >
286+ < FilePlus size = { 12 } weight = "regular" />
287+ </ IconButton >
288+ </ DropdownMenu . Trigger >
289+ </ Tooltip >
290+ < DropdownMenu . Content size = "1" align = "end" >
291+ { detectedApps . map ( ( app ) => (
292+ < DropdownMenu . Item
293+ key = { app . id }
294+ onSelect = { ( ) => handleOpenWith ( app . id ) }
295+ >
296+ < Flex align = "center" gap = "2" >
297+ { app . icon ? (
298+ < img
299+ src = { app . icon }
300+ width = { 16 }
301+ height = { 16 }
302+ alt = ""
303+ style = { { borderRadius : "2px" } }
304+ />
305+ ) : (
306+ < CodeIcon size = { 16 } weight = "regular" />
307+ ) }
308+ < Text size = "1" > { app . name } </ Text >
309+ </ Flex >
310+ </ DropdownMenu . Item >
311+ ) ) }
312+ < DropdownMenu . Separator />
313+ < DropdownMenu . Item onSelect = { handleCopyPath } >
314+ < Flex align = "center" gap = "2" >
315+ < CopyIcon size = { 16 } weight = "regular" />
316+ < Text size = "1" > Copy Path</ Text >
317+ </ Flex >
318+ </ DropdownMenu . Item >
319+ </ DropdownMenu . Content >
320+ </ DropdownMenu . Root >
321+ </ Flex >
322+ ) }
231323
232324 < Badge
233325 size = "1"
0 commit comments