@@ -4,47 +4,175 @@ import { FileIcon } from "@opencode-ai/ui/file-icon"
44import { List } from "@opencode-ai/ui/list"
55import { getDirectory , getFilename } from "@opencode-ai/util/path"
66import { useParams } from "@solidjs/router"
7- import { createMemo } from "solid-js"
7+ import { createMemo , createSignal , onCleanup , Show } from "solid-js"
8+ import { formatKeybind , useCommand , type CommandOption } from "@/context/command"
89import { useLayout } from "@/context/layout"
910import { useFile } from "@/context/file"
1011
12+ type EntryType = "command" | "file"
13+
14+ type Entry = {
15+ id : string
16+ type : EntryType
17+ title : string
18+ description ?: string
19+ keybind ?: string
20+ category : "Commands" | "Files"
21+ option ?: CommandOption
22+ path ?: string
23+ }
24+
1125export function DialogSelectFile ( ) {
26+ const command = useCommand ( )
1227 const layout = useLayout ( )
1328 const file = useFile ( )
1429 const dialog = useDialog ( )
1530 const params = useParams ( )
1631 const sessionKey = createMemo ( ( ) => `${ params . dir } ${ params . id ? "/" + params . id : "" } ` )
1732 const tabs = createMemo ( ( ) => layout . tabs ( sessionKey ( ) ) )
1833 const view = createMemo ( ( ) => layout . view ( sessionKey ( ) ) )
34+ const state = { cleanup : undefined as ( ( ) => void ) | void , committed : false }
35+ const [ grouped , setGrouped ] = createSignal ( false )
36+ const common = [ "session.new" , "session.previous" , "session.next" , "terminal.toggle" , "review.toggle" ]
37+ const limit = 5
38+
39+ const allowed = createMemo ( ( ) =>
40+ command . options . filter (
41+ ( option ) => ! option . disabled && ! option . id . startsWith ( "suggested." ) && option . id !== "file.open" ,
42+ ) ,
43+ )
44+
45+ const commandItem = ( option : CommandOption ) : Entry => ( {
46+ id : "command:" + option . id ,
47+ type : "command" ,
48+ title : option . title ,
49+ description : option . description ,
50+ keybind : option . keybind ,
51+ category : "Commands" ,
52+ option,
53+ } )
54+
55+ const fileItem = ( path : string ) : Entry => ( {
56+ id : "file:" + path ,
57+ type : "file" ,
58+ title : path ,
59+ category : "Files" ,
60+ path,
61+ } )
62+
63+ const list = createMemo ( ( ) => allowed ( ) . map ( commandItem ) )
64+
65+ const picks = createMemo ( ( ) => {
66+ const all = allowed ( )
67+ const order = new Map ( common . map ( ( id , index ) => [ id , index ] ) )
68+ const picked = all . filter ( ( option ) => order . has ( option . id ) )
69+ const base = picked . length ? picked : all . slice ( 0 , limit )
70+ const sorted = picked . length ? [ ...base ] . sort ( ( a , b ) => ( order . get ( a . id ) ?? 0 ) - ( order . get ( b . id ) ?? 0 ) ) : base
71+ return sorted . map ( commandItem )
72+ } )
73+
74+ const recent = createMemo ( ( ) => {
75+ const all = tabs ( ) . all ( )
76+ const active = tabs ( ) . active ( )
77+ const order = active ? [ active , ...all . filter ( ( item ) => item !== active ) ] : all
78+ const seen = new Set < string > ( )
79+ const items : Entry [ ] = [ ]
80+
81+ for ( const item of order ) {
82+ const path = file . pathFromTab ( item )
83+ if ( ! path ) continue
84+ if ( seen . has ( path ) ) continue
85+ seen . add ( path )
86+ items . push ( fileItem ( path ) )
87+ }
88+
89+ return items . slice ( 0 , limit )
90+ } )
91+
92+ const items = async ( filter : string ) => {
93+ const query = filter . trim ( )
94+ setGrouped ( query . length > 0 )
95+ if ( ! query ) return [ ...picks ( ) , ...recent ( ) ]
96+ const files = await file . searchFiles ( query )
97+ const entries = files . map ( fileItem )
98+ return [ ...list ( ) , ...entries ]
99+ }
100+
101+ const handleMove = ( item : Entry | undefined ) => {
102+ state . cleanup ?.( )
103+ if ( ! item ) return
104+ if ( item . type !== "command" ) return
105+ state . cleanup = item . option ?. onHighlight ?.( )
106+ }
107+
108+ const open = ( path : string ) => {
109+ const value = file . tab ( path )
110+ tabs ( ) . open ( value )
111+ file . load ( path )
112+ view ( ) . reviewPanel . open ( )
113+ }
114+
115+ const handleSelect = ( item : Entry | undefined ) => {
116+ if ( ! item ) return
117+ state . committed = true
118+ state . cleanup = undefined
119+ dialog . close ( )
120+
121+ if ( item . type === "command" ) {
122+ item . option ?. onSelect ?.( "palette" )
123+ return
124+ }
125+
126+ if ( ! item . path ) return
127+ open ( item . path )
128+ }
129+
130+ onCleanup ( ( ) => {
131+ if ( state . committed ) return
132+ state . cleanup ?.( )
133+ } )
134+
19135 return (
20- < Dialog title = "Select file " >
136+ < Dialog title = "Search " >
21137 < List
22- search = { { placeholder : "Search files" , autofocus : true } }
23- emptyMessage = "No files found"
24- items = { file . searchFiles }
25- key = { ( x ) => x }
26- onSelect = { ( path ) => {
27- if ( path ) {
28- const value = file . tab ( path )
29- tabs ( ) . open ( value )
30- file . load ( path )
31- view ( ) . reviewPanel . open ( )
32- }
33- dialog . close ( )
34- } }
138+ search = { { placeholder : "Search files and commands" , autofocus : true } }
139+ emptyMessage = "No results found"
140+ items = { items }
141+ key = { ( item ) => item . id }
142+ filterKeys = { [ "title" , "description" , "category" ] }
143+ groupBy = { ( item ) => ( grouped ( ) ? item . category : "" ) }
144+ onMove = { handleMove }
145+ onSelect = { handleSelect }
35146 >
36- { ( i ) => (
37- < div class = "w-full flex items-center justify-between rounded-md" >
38- < div class = "flex items-center gap-x-3 grow min-w-0" >
39- < FileIcon node = { { path : i , type : "file" } } class = "shrink-0 size-4" />
40- < div class = "flex items-center text-14-regular" >
41- < span class = "text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0" >
42- { getDirectory ( i ) }
43- </ span >
44- < span class = "text-text-strong whitespace-nowrap" > { getFilename ( i ) } </ span >
147+ { ( item ) => (
148+ < Show
149+ when = { item . type === "command" }
150+ fallback = {
151+ < div class = "w-full flex items-center justify-between rounded-md" >
152+ < div class = "flex items-center gap-x-3 grow min-w-0" >
153+ < FileIcon node = { { path : item . path ?? "" , type : "file" } } class = "shrink-0 size-4" />
154+ < div class = "flex items-center text-14-regular" >
155+ < span class = "text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0" >
156+ { getDirectory ( item . path ?? "" ) }
157+ </ span >
158+ < span class = "text-text-strong whitespace-nowrap" > { getFilename ( item . path ?? "" ) } </ span >
159+ </ div >
160+ </ div >
161+ </ div >
162+ }
163+ >
164+ < div class = "w-full flex items-center justify-between gap-4" >
165+ < div class = "flex items-center gap-2 min-w-0" >
166+ < span class = "text-14-regular text-text-strong whitespace-nowrap" > { item . title } </ span >
167+ < Show when = { item . description } >
168+ < span class = "text-14-regular text-text-weak truncate" > { item . description } </ span >
169+ </ Show >
45170 </ div >
171+ < Show when = { item . keybind } >
172+ < span class = "text-12-regular text-text-subtle shrink-0" > { formatKeybind ( item . keybind ?? "" ) } </ span >
173+ </ Show >
46174 </ div >
47- </ div >
175+ </ Show >
48176 ) }
49177 </ List >
50178 </ Dialog >
0 commit comments