@@ -31,11 +31,13 @@ pub(crate) fn read_settings(path: &PathBuf) -> Result<AppSettings, String> {
3131 let data = std:: fs:: read_to_string ( path) . map_err ( |e| e. to_string ( ) ) ?;
3232 let mut value: Value = serde_json:: from_str ( & data) . map_err ( |e| e. to_string ( ) ) ?;
3333 migrate_follow_up_message_behavior ( & mut value) ;
34+ migrate_open_app_targets ( & mut value) ;
3435 match serde_json:: from_value ( value. clone ( ) ) {
3536 Ok ( settings) => Ok ( settings) ,
3637 Err ( _) => {
3738 sanitize_remote_settings_for_tcp_only ( & mut value) ;
3839 migrate_follow_up_message_behavior ( & mut value) ;
40+ migrate_open_app_targets ( & mut value) ;
3941 serde_json:: from_value ( value) . map_err ( |e| e. to_string ( ) )
4042 }
4143 }
@@ -92,6 +94,47 @@ fn migrate_follow_up_message_behavior(value: &mut Value) {
9294 ) ;
9395}
9496
97+ fn migrate_open_app_targets ( value : & mut Value ) {
98+ let Value :: Object ( root) = value else {
99+ return ;
100+ } ;
101+ let Some ( Value :: Array ( existing_targets) ) = root. get_mut ( "openAppTargets" ) else {
102+ return ;
103+ } ;
104+
105+ let default_targets = match serde_json:: to_value ( AppSettings :: default ( ) . open_app_targets ) {
106+ Ok ( Value :: Array ( targets) ) => targets,
107+ _ => return ,
108+ } ;
109+
110+ let existing_ids = existing_targets
111+ . iter ( )
112+ . filter_map ( |target| target. get ( "id" ) . and_then ( Value :: as_str) )
113+ . collect :: < std:: collections:: HashSet < _ > > ( ) ;
114+
115+ let missing_targets: Vec < Value > = default_targets
116+ . into_iter ( )
117+ . filter ( |target| {
118+ target
119+ . get ( "id" )
120+ . and_then ( Value :: as_str)
121+ . map ( |id| !existing_ids. contains ( id) )
122+ . unwrap_or ( false )
123+ } )
124+ . collect ( ) ;
125+
126+ if missing_targets. is_empty ( ) {
127+ return ;
128+ }
129+
130+ let insert_at = existing_targets
131+ . iter ( )
132+ . position ( |target| target. get ( "id" ) . and_then ( Value :: as_str) == Some ( "finder" ) )
133+ . unwrap_or ( existing_targets. len ( ) ) ;
134+
135+ existing_targets. splice ( insert_at..insert_at, missing_targets) ;
136+ }
137+
95138#[ cfg( test) ]
96139mod tests {
97140 use super :: { read_settings, read_workspaces, write_workspaces} ;
@@ -251,4 +294,48 @@ mod tests {
251294 let settings = read_settings ( & path) . expect ( "read settings" ) ;
252295 assert_eq ! ( settings. follow_up_message_behavior, "queue" ) ;
253296 }
297+
298+ #[ test]
299+ fn read_settings_migrates_missing_open_app_targets ( ) {
300+ let temp_dir = std:: env:: temp_dir ( ) . join ( format ! ( "codex-monitor-test-{}" , Uuid :: new_v4( ) ) ) ;
301+ std:: fs:: create_dir_all ( & temp_dir) . expect ( "create temp dir" ) ;
302+ let path = temp_dir. join ( "settings.json" ) ;
303+
304+ std:: fs:: write (
305+ & path,
306+ r#"{
307+ "theme": "dark",
308+ "selectedOpenAppId": "vscode",
309+ "openAppTargets": [
310+ { "id": "vscode", "label": "VS Code", "kind": "command", "appName": null, "command": "code", "args": [] },
311+ { "id": "cursor", "label": "Cursor", "kind": "command", "appName": null, "command": "cursor", "args": [] },
312+ { "id": "zed", "label": "Zed", "kind": "command", "appName": null, "command": "zed", "args": [] },
313+ { "id": "ghostty", "label": "Ghostty", "kind": "command", "appName": null, "command": "ghostty", "args": [] },
314+ { "id": "antigravity", "label": "Antigravity", "kind": "command", "appName": null, "command": "antigravity", "args": [] },
315+ { "id": "finder", "label": "File Manager", "kind": "finder", "appName": null, "command": null, "args": [] }
316+ ]
317+ }"# ,
318+ )
319+ . expect ( "write settings" ) ;
320+
321+ let settings = read_settings ( & path) . expect ( "read settings" ) ;
322+ let ids: Vec < & str > = settings
323+ . open_app_targets
324+ . iter ( )
325+ . map ( |target| target. id . as_str ( ) )
326+ . collect ( ) ;
327+
328+ assert_eq ! (
329+ ids,
330+ vec![
331+ "vscode" ,
332+ "cursor" ,
333+ "zed" ,
334+ "ghostty" ,
335+ "antigravity" ,
336+ "phpstorm" ,
337+ "finder"
338+ ]
339+ ) ;
340+ }
254341}
0 commit comments