@@ -4,6 +4,7 @@ import { IPC } from '../../electron/ipc/channels';
44import { theme } from '../lib/theme' ;
55import { sf } from '../lib/fontScale' ;
66import { getStatusColor } from '../lib/status-colors' ;
7+ import { openFileInEditor } from '../lib/shell' ;
78import { buildFileTree , flattenVisibleTree } from '../lib/file-tree' ;
89import {
910 type CommitSelection ,
@@ -147,9 +148,42 @@ function FileCoverageBadge(props: {
147148 ) ;
148149}
149150
151+ function OpenInEditorButton ( props : { worktreePath : string ; filePath : string } ) {
152+ return (
153+ < button
154+ class = "changed-files-open-editor-btn"
155+ onClick = { ( e ) => {
156+ e . stopPropagation ( ) ;
157+ void openFileInEditor ( props . worktreePath , props . filePath ) ;
158+ } }
159+ onKeyDown = { ( e ) => e . stopPropagation ( ) }
160+ tabIndex = { - 1 }
161+ disabled = { ! props . worktreePath }
162+ style = { {
163+ background : `color-mix(in srgb, ${ theme . bgElevated } 92%, transparent)` ,
164+ border : 'none' ,
165+ color : theme . fgMuted ,
166+ cursor : props . worktreePath ? 'pointer' : 'default' ,
167+ padding : '4px' ,
168+ display : 'flex' ,
169+ 'align-items' : 'center' ,
170+ 'justify-content' : 'center' ,
171+ 'border-radius' : '4px' ,
172+ } }
173+ title = "Open in editor"
174+ aria-label = { `Open ${ props . filePath } in editor` }
175+ >
176+ < svg width = "16" height = "16" viewBox = "0 0 16 16" fill = "currentColor" >
177+ < path d = "M3.5 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-3a.75.75 0 0 1 1.5 0v3A3 3 0 0 1 12.5 16h-9A3 3 0 0 1 0 12.5v-9A3 3 0 0 1 3.5 0h3a.75.75 0 0 1 0 1.5h-3ZM10 .75a.75.75 0 0 1 .75-.75h4.5a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-1.5 0V2.56L8.53 8.53a.75.75 0 0 1-1.06-1.06L13.44 1.5H10.75A.75.75 0 0 1 10 .75Z" />
178+ </ svg >
179+ </ button >
180+ ) ;
181+ }
182+
150183export function ChangedFilesList ( props : ChangedFilesListProps ) {
151184 const [ files , setFiles ] = createSignal < ChangedFile [ ] > ( [ ] ) ;
152185 const [ coverage , setCoverage ] = createSignal < CoverageSummary | null > ( null ) ;
186+ const [ canOpenFilesInEditor , setCanOpenFilesInEditor ] = createSignal ( false ) ;
153187 const [ selectedIndex , setSelectedIndex ] = createSignal ( - 1 ) ;
154188 const [ collapsed , setCollapsed ] = createSignal < Set < string > > ( new Set ( ) ) ;
155189 const rowRefs : HTMLDivElement [ ] = [ ] ;
@@ -301,6 +335,7 @@ export function ChangedFilesList(props: ChangedFilesListProps) {
301335 let cancelled = false ;
302336 let inFlight = false ;
303337 let usingBranchFallback = false ;
338+ setCanOpenFilesInEditor ( false ) ;
304339
305340 async function refresh ( ) {
306341 if ( inFlight ) return ;
@@ -313,9 +348,15 @@ export function ChangedFilesList(props: ChangedFilesListProps) {
313348 worktreePath : path ,
314349 commitHash : singleCommitHash ,
315350 } ) ;
316- if ( ! cancelled ) setFiles ( result ) ;
351+ if ( ! cancelled ) {
352+ setFiles ( result ) ;
353+ setCanOpenFilesInEditor ( true ) ;
354+ }
317355 } catch {
318- if ( ! cancelled ) setFiles ( [ ] ) ;
356+ if ( ! cancelled ) {
357+ setFiles ( [ ] ) ;
358+ setCanOpenFilesInEditor ( false ) ;
359+ }
319360 }
320361 return ;
321362 }
@@ -325,9 +366,15 @@ export function ChangedFilesList(props: ChangedFilesListProps) {
325366 const result = await invoke < ChangedFile [ ] > ( IPC . GetUncommittedChangedFiles , {
326367 worktreePath : path ,
327368 } ) ;
328- if ( ! cancelled ) setFiles ( result ) ;
369+ if ( ! cancelled ) {
370+ setFiles ( result ) ;
371+ setCanOpenFilesInEditor ( true ) ;
372+ }
329373 } catch {
330- if ( ! cancelled ) setFiles ( [ ] ) ;
374+ if ( ! cancelled ) {
375+ setFiles ( [ ] ) ;
376+ setCanOpenFilesInEditor ( false ) ;
377+ }
331378 }
332379 return ;
333380 }
@@ -339,9 +386,13 @@ export function ChangedFilesList(props: ChangedFilesListProps) {
339386 worktreePath : path ,
340387 baseBranch,
341388 } ) ;
342- if ( ! cancelled ) setFiles ( result ) ;
389+ if ( ! cancelled ) {
390+ setFiles ( result ) ;
391+ setCanOpenFilesInEditor ( true ) ;
392+ }
343393 return ;
344394 } catch {
395+ if ( ! cancelled ) setCanOpenFilesInEditor ( false ) ;
345396 // Worktree may not exist — try branch fallback below
346397 }
347398 }
@@ -357,8 +408,10 @@ export function ChangedFilesList(props: ChangedFilesListProps) {
357408 } ) ;
358409 if ( ! cancelled ) {
359410 setFiles ( uncommittedOnly ? result . filter ( ( f ) => ! f . committed ) : result ) ;
411+ setCanOpenFilesInEditor ( false ) ;
360412 }
361413 } catch {
414+ if ( ! cancelled ) setCanOpenFilesInEditor ( false ) ;
362415 // Branch may no longer exist
363416 }
364417 }
@@ -445,6 +498,7 @@ export function ChangedFilesList(props: ChangedFilesListProps) {
445498 ref = { ( el ) => ( rowRefs [ i ] = el ) }
446499 class = "file-row"
447500 style = { {
501+ position : 'relative' ,
448502 display : 'flex' ,
449503 'align-items' : 'center' ,
450504 gap : '6px' ,
@@ -566,6 +620,12 @@ export function ChangedFilesList(props: ChangedFilesListProps) {
566620 -{ row ( ) . node . file ?. lines_removed }
567621 </ span >
568622 </ Show >
623+ < Show when = { canOpenFilesInEditor ( ) } >
624+ < OpenInEditorButton
625+ worktreePath = { props . worktreePath }
626+ filePath = { row ( ) . node . file ?. path ?? row ( ) . node . path }
627+ />
628+ </ Show >
569629 </ >
570630 ) }
571631 </ div >
0 commit comments