@@ -117,9 +117,80 @@ export function useEditor() {
117117 const saveFile = useCallback (
118118 async ( filePath ?: string ) => {
119119 const path = filePath ?? activeTab . filePath ;
120- if ( ! path ) return ;
121- await Files . write ( path , activeTab . content ) ;
122- updateTab ( activeTabId , { dirty : false , filePath : path , label : path . split ( / [ / \\ ] / ) . pop ( ) ?? path } ) ;
120+ if ( path ) {
121+ await Files . write ( path , activeTab . content ) ;
122+ updateTab ( activeTabId , { dirty : false , filePath : path , label : path . split ( / [ / \\ ] / ) . pop ( ) ?? path } ) ;
123+ return ;
124+ }
125+
126+ // Save As flow: ask user for a destination when the tab has no path yet.
127+ const rawLabel = activeTab . label . trim ( ) || "Untitled" ;
128+ const suggestedName = / \. [ A - Z a - z 0 - 9 ] + $ / . test ( rawLabel ) ? rawLabel : `${ rawLabel } .md` ;
129+
130+ // In Tauri desktop mode, use native save dialog and persist via backend API.
131+ const isTauri = typeof window !== "undefined" && "__TAURI__" in window ;
132+ if ( isTauri ) {
133+ try {
134+ const { save } = await import ( "@tauri-apps/plugin-dialog" ) ;
135+ const target = await save ( {
136+ defaultPath : suggestedName ,
137+ filters : [ { name : "Markdown" , extensions : [ "md" , "markdown" , "txt" ] } ] ,
138+ } ) ;
139+
140+ if ( ! target ) return ;
141+
142+ const resolvedPath = Array . isArray ( target ) ? target [ 0 ] : target ;
143+ await Files . write ( resolvedPath , activeTab . content ) ;
144+ updateTab ( activeTabId , {
145+ dirty : false ,
146+ filePath : resolvedPath ,
147+ label : resolvedPath . split ( / [ / \\ ] / ) . pop ( ) ?? resolvedPath ,
148+ } ) ;
149+ Files . addRecent ( resolvedPath )
150+ . then ( ( { entries } ) => setRecentFiles ( entries ) )
151+ . catch ( console . error ) ;
152+ } catch {
153+ // No dialog capability in runtime: silently no-op.
154+ }
155+ return ;
156+ }
157+
158+ // In browser mode, use the native file save picker when available.
159+ const maybePicker = ( window as Window & {
160+ showSaveFilePicker ?: ( options ?: {
161+ suggestedName ?: string ;
162+ types ?: Array < { description ?: string ; accept : Record < string , string [ ] > } > ;
163+ } ) => Promise < {
164+ createWritable : ( ) => Promise < {
165+ write : ( data : string ) => Promise < void > ;
166+ close : ( ) => Promise < void > ;
167+ } > ;
168+ name ?: string ;
169+ } > ;
170+ } ) . showSaveFilePicker ;
171+
172+ if ( ! maybePicker ) return ;
173+
174+ try {
175+ const handle = await maybePicker ( {
176+ suggestedName,
177+ types : [
178+ {
179+ description : "Markdown files" ,
180+ accept : {
181+ "text/markdown" : [ ".md" , ".markdown" ] ,
182+ "text/plain" : [ ".txt" ] ,
183+ } ,
184+ } ,
185+ ] ,
186+ } ) ;
187+ const writable = await handle . createWritable ( ) ;
188+ await writable . write ( activeTab . content ) ;
189+ await writable . close ( ) ;
190+ updateTab ( activeTabId , { dirty : false , label : handle . name || suggestedName } ) ;
191+ } catch {
192+ // No picker capability (or picker cancelled): silently no-op.
193+ }
123194 } ,
124195 [ activeTab , activeTabId , updateTab ]
125196 ) ;
0 commit comments