Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
afcb970
fix: On a large monitor, when commands run in CodexMontior their outp…
aolin480 Mar 13, 2026
3861093
feat: Added PHPStorm IDE as an IDE option
aolin480 Mar 17, 2026
f6c9b1b
fix: Running code lines should stretch 100% of the window content
aolin480 Mar 17, 2026
41d8bb3
Fix: Commands not stretching 100% width of content area and on wide m…
aolin480 Mar 18, 2026
387f0b9
Chore: Let's not overdo it on checking if the phpstorm binary exists …
aolin480 Mar 18, 2026
c1b4a01
fix: add PHPStorm as a built-in IDE target and preserve macOS editor …
aolin480 Mar 18, 2026
1d84967
fix: use the Windows PHPStorm launcher name in open app targets
aolin480 Mar 18, 2026
3f18662
fix: use the macOS app launcher fallback for PHPStorm
aolin480 Mar 18, 2026
0732034
fix: migrate persisted open app targets to include PHPStorm
aolin480 Mar 18, 2026
026ffc9
fix: only migrate the PHPStorm open app target
aolin480 Mar 18, 2026
97969f4
fix: restore missing HashMap import in workspace IO
aolin480 Mar 18, 2026
5035fde
fix: add PHPStorm to the frontend open app defaults
aolin480 Mar 18, 2026
d372eed
fix: normalize Windows namespace paths for PHPStorm launches
aolin480 Mar 18, 2026
742857c
fix: use the Linux PHPStorm launcher in open app defaults
aolin480 Mar 18, 2026
363d2fa
fix: preserve line-aware args in the macOS PHPStorm fallback
aolin480 Mar 19, 2026
44bae44
fix: use a fresh open instance for the macOS PHPStorm fallback
aolin480 Mar 19, 2026
18b2755
fix: recognize PhpStorm app bundle names in macOS launch mapping
aolin480 Mar 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 97 additions & 17 deletions src-tauri/src/shared/workspaces_core/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use super::helpers::resolve_workspace_root;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum LineAwareLaunchStrategy {
GotoFlag,
JetBrainsLineColumnFlags,
PathWithLineColumn,
}

Expand Down Expand Up @@ -53,6 +54,9 @@ fn command_launch_strategy(command: &str) -> Option<LineAwareLaunchStrategy> {
{
return Some(LineAwareLaunchStrategy::GotoFlag);
}
if identifier == "phpstorm" || identifier == "phpstorm64" {
return Some(LineAwareLaunchStrategy::JetBrainsLineColumnFlags);
}
if identifier == "zed" || identifier == "zed-preview" {
return Some(LineAwareLaunchStrategy::PathWithLineColumn);
}
Expand All @@ -64,6 +68,9 @@ fn app_launch_strategy(app: &str) -> Option<LineAwareLaunchStrategy> {
if normalized.contains("visual studio code") || normalized.starts_with("cursor") {
return Some(LineAwareLaunchStrategy::GotoFlag);
}
if is_phpstorm_app_identifier(&normalized) {
return Some(LineAwareLaunchStrategy::JetBrainsLineColumnFlags);
}
if normalized == "zed" || normalized.starts_with("zed ") {
return Some(LineAwareLaunchStrategy::PathWithLineColumn);
}
Expand All @@ -81,26 +88,19 @@ fn app_cli_command(app: &str) -> Option<&'static str> {
if normalized.starts_with("cursor") {
return Some("cursor");
}
if is_phpstorm_app_identifier(&normalized) {
return Some("phpstorm");
Comment on lines +91 to +92

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Respect the configured PhpStorm bundle on macOS

For macOS file links with a line/column, every recognized PhpStorm app name/path is collapsed to the global phpstorm launcher here, and open_workspace_in_core() then prefers that CLI (io.rs:265-273) over open -a <configured app>. Because the app name field is user-editable in Settings (src/features/settings/components/sections/SettingsOpenAppsSection.tsx:111-126), users who point it at a specific PhpStorm.app bundle (for example a non-default install or a second build) will still open whichever installation the PATH launcher targets, not the one they selected.

Useful? React with 👍 / 👎.

}
if normalized == "zed" || normalized.starts_with("zed ") {
return Some("zed");
}
None
}

fn normalize_app_identifier(app: &str) -> String {
app.trim()
.chars()
.map(|value| {
if value.is_ascii_alphanumeric() {
value.to_ascii_lowercase()
} else {
' '
}
})
.collect::<String>()
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
fn is_phpstorm_app_identifier(normalized: &str) -> bool {
normalized == "phpstorm"
|| normalized == "phpstorm app"
|| normalized.ends_with(" phpstorm app")
}

fn find_executable_in_path(program: &str) -> Option<PathBuf> {
Expand All @@ -125,6 +125,22 @@ fn find_executable_in_path(program: &str) -> Option<PathBuf> {
None
}

fn normalize_app_identifier(app: &str) -> String {
app.trim()
.chars()
.map(|value| {
if value.is_ascii_alphanumeric() {
value.to_ascii_lowercase()
} else {
' '
}
})
.collect::<String>()
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
}

fn build_launch_args(
path: &str,
args: &[String],
Expand All @@ -141,6 +157,16 @@ fn build_launch_args(
launch_args.push("--goto".to_string());
launch_args.push(located_path);
}
Some(LineAwareLaunchStrategy::JetBrainsLineColumnFlags) => {
let sanitized_path = normalize_windows_namespace_path(path);
launch_args.push("--line".to_string());
launch_args.push(line.to_string());
if let Some(column) = column {
launch_args.push("--column".to_string());
launch_args.push(column.to_string());
}
launch_args.push(sanitized_path);
}
Some(LineAwareLaunchStrategy::PathWithLineColumn) => {
let sanitized_path = normalize_windows_namespace_path(path);
let located_path = format_path_with_location(&sanitized_path, line, column);
Expand Down Expand Up @@ -249,10 +275,25 @@ pub(crate) async fn open_workspace_in_core(
.await
.map_err(|error| format!("Failed to open app ({target_label}): {error}"))?
} else {
let fallback_app_args = if matches!(
app_strategy,
Some(LineAwareLaunchStrategy::JetBrainsLineColumnFlags)
) && normalize_open_location(line, column).is_some()
{
build_launch_args(&path, &args, line, column, app_strategy)
} else {
Vec::new()
};
let mut cmd = tokio_command("open");
cmd.arg("-a").arg(trimmed).arg(&path);
if !args.is_empty() {
cmd.arg("--args").args(&args);
cmd.arg("-a").arg(trimmed);
if fallback_app_args.is_empty() {
cmd.arg(&path);
if !args.is_empty() {
cmd.arg("--args").args(&args);
}
} else {
cmd.arg("-n");
cmd.arg("--args").args(&fallback_app_args);
}
cmd.output()
.await
Expand Down Expand Up @@ -323,6 +364,10 @@ mod tests {
command_launch_strategy("zed"),
Some(LineAwareLaunchStrategy::PathWithLineColumn)
);
assert_eq!(
command_launch_strategy("phpstorm64.exe"),
Some(LineAwareLaunchStrategy::JetBrainsLineColumnFlags)
);
assert_eq!(command_launch_strategy("vim"), None);
}

Expand All @@ -340,6 +385,14 @@ mod tests {
app_launch_strategy("Zed Preview"),
Some(LineAwareLaunchStrategy::PathWithLineColumn)
);
assert_eq!(
app_launch_strategy("PhpStorm.app"),
Some(LineAwareLaunchStrategy::JetBrainsLineColumnFlags)
);
assert_eq!(
app_launch_strategy("/Applications/PhpStorm.app"),
Some(LineAwareLaunchStrategy::JetBrainsLineColumnFlags)
);
assert_eq!(app_launch_strategy("Ghostty"), None);
}

Expand All @@ -355,6 +408,11 @@ mod tests {
Some("code-insiders")
);
assert_eq!(app_cli_command("Cursor"), Some("cursor"));
assert_eq!(app_cli_command("PhpStorm.app"), Some("phpstorm"));
assert_eq!(
app_cli_command("/Applications/PhpStorm.app"),
Some("phpstorm")
);
assert_eq!(app_cli_command("Zed Preview"), Some("zed"));
assert_eq!(app_cli_command("Ghostty"), None);
}
Expand Down Expand Up @@ -490,6 +548,28 @@ mod tests {
assert_eq!(args, vec!["/tmp/project/src/App.tsx:33".to_string()]);
}

#[test]
fn builds_line_and_column_flags_for_phpstorm_targets() {
let args = build_launch_args(
"/tmp/project/src/App.tsx",
&[],
Some(33),
Some(7),
Some(LineAwareLaunchStrategy::JetBrainsLineColumnFlags),
);

assert_eq!(
args,
vec![
"--line".to_string(),
"33".to_string(),
"--column".to_string(),
"7".to_string(),
"/tmp/project/src/App.tsx".to_string(),
]
);
}

#[test]
fn falls_back_to_plain_path_for_unknown_targets() {
let args = build_launch_args(
Expand Down
79 changes: 79 additions & 0 deletions src-tauri/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ pub(crate) fn read_settings(path: &PathBuf) -> Result<AppSettings, String> {
let data = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
let mut value: Value = serde_json::from_str(&data).map_err(|e| e.to_string())?;
migrate_follow_up_message_behavior(&mut value);
migrate_open_app_targets(&mut value);
match serde_json::from_value(value.clone()) {
Ok(settings) => Ok(settings),
Err(_) => {
sanitize_remote_settings_for_tcp_only(&mut value);
migrate_follow_up_message_behavior(&mut value);
migrate_open_app_targets(&mut value);
serde_json::from_value(value).map_err(|e| e.to_string())
}
}
Expand Down Expand Up @@ -92,6 +94,39 @@ fn migrate_follow_up_message_behavior(value: &mut Value) {
);
}

fn migrate_open_app_targets(value: &mut Value) {
let Value::Object(root) = value else {
return;
};
let Some(Value::Array(existing_targets)) = root.get_mut("openAppTargets") else {
return;
};

let has_phpstorm = existing_targets
.iter()
.any(|target| target.get("id").and_then(Value::as_str) == Some("phpstorm"));
if has_phpstorm {
return;
}

let phpstorm_target = match serde_json::to_value(AppSettings::default().open_app_targets) {
Ok(Value::Array(targets)) => targets
.into_iter()
.find(|target| target.get("id").and_then(Value::as_str) == Some("phpstorm")),
_ => None,
};
let Some(phpstorm_target) = phpstorm_target else {
return;
};

let insert_at = existing_targets
.iter()
.position(|target| target.get("id").and_then(Value::as_str) == Some("finder"))
.unwrap_or(existing_targets.len());

existing_targets.insert(insert_at, phpstorm_target);
Comment on lines +123 to +127

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Version the PhpStorm target migration

read_settings() runs migrate_open_app_targets() on every launch, and this insertion fires whenever the saved openAppTargets array lacks phpstorm. Removing built-in targets is already a supported/persisted user action in src/features/settings/hooks/useSettingsOpenAppDrafts.ts:103-115, so after upgrading, anyone who deletes PhpStorm from Settings will have it silently re-added the next time the app restarts. This needs a one-time/versioned migration; otherwise the new target can never stay removed.

Useful? React with 👍 / 👎.

}

#[cfg(test)]
mod tests {
use super::{read_settings, read_workspaces, write_workspaces};
Expand Down Expand Up @@ -251,4 +286,48 @@ mod tests {
let settings = read_settings(&path).expect("read settings");
assert_eq!(settings.follow_up_message_behavior, "queue");
}

#[test]
fn read_settings_migrates_missing_open_app_targets() {
let temp_dir = std::env::temp_dir().join(format!("codex-monitor-test-{}", Uuid::new_v4()));
std::fs::create_dir_all(&temp_dir).expect("create temp dir");
let path = temp_dir.join("settings.json");

std::fs::write(
&path,
r#"{
"theme": "dark",
"selectedOpenAppId": "vscode",
"openAppTargets": [
{ "id": "vscode", "label": "VS Code", "kind": "command", "appName": null, "command": "code", "args": [] },
{ "id": "cursor", "label": "Cursor", "kind": "command", "appName": null, "command": "cursor", "args": [] },
{ "id": "zed", "label": "Zed", "kind": "command", "appName": null, "command": "zed", "args": [] },
{ "id": "ghostty", "label": "Ghostty", "kind": "command", "appName": null, "command": "ghostty", "args": [] },
{ "id": "antigravity", "label": "Antigravity", "kind": "command", "appName": null, "command": "antigravity", "args": [] },
{ "id": "finder", "label": "File Manager", "kind": "finder", "appName": null, "command": null, "args": [] }
]
}"#,
)
.expect("write settings");

let settings = read_settings(&path).expect("read settings");
let ids: Vec<&str> = settings
.open_app_targets
.iter()
.map(|target| target.id.as_str())
.collect();

assert_eq!(
ids,
vec![
"vscode",
"cursor",
"zed",
"ghostty",
"antigravity",
"phpstorm",
"finder"
]
);
}
}
26 changes: 25 additions & 1 deletion src-tauri/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,14 @@ fn default_workspace_groups() -> Vec<WorkspaceGroup> {
}

fn default_open_app_targets() -> Vec<OpenAppTarget> {
let phpstorm_command = if cfg!(target_os = "windows") {
"phpstorm64.exe"
} else if cfg!(target_os = "linux") {
"phpstorm.sh"
} else {
"phpstorm"
};

if cfg!(target_os = "macos") {
return vec![
OpenAppTarget {
Expand Down Expand Up @@ -1043,6 +1051,14 @@ fn default_open_app_targets() -> Vec<OpenAppTarget> {
command: None,
args: Vec::new(),
},
OpenAppTarget {
id: "phpstorm".to_string(),
label: "PHPStorm".to_string(),
kind: "app".to_string(),
app_name: Some("PhpStorm".to_string()),
command: None,
args: Vec::new(),
},
OpenAppTarget {
id: "finder".to_string(),
label: "Finder".to_string(),
Expand Down Expand Up @@ -1101,6 +1117,14 @@ fn default_open_app_targets() -> Vec<OpenAppTarget> {
command: Some("antigravity".to_string()),
args: Vec::new(),
},
OpenAppTarget {
id: "phpstorm".to_string(),
label: "PHPStorm".to_string(),
kind: "command".to_string(),
app_name: None,
command: Some(phpstorm_command.to_string()),
args: Vec::new(),
},
OpenAppTarget {
id: "finder".to_string(),
label: file_manager_label.to_string(),
Expand Down Expand Up @@ -1364,7 +1388,7 @@ mod tests {
"vscode"
};
assert_eq!(settings.selected_open_app_id, expected_open_id);
assert_eq!(settings.open_app_targets.len(), 6);
assert_eq!(settings.open_app_targets.len(), 7);
assert_eq!(settings.open_app_targets[0].id, "vscode");
}

Expand Down
Binary file added src/assets/app-icons/phpstorm.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions src/features/app/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import {

export const OPEN_APP_STORAGE_KEY = "open-workspace-app";
export const DEFAULT_OPEN_APP_ID = isWindowsPlatform() ? "finder" : "vscode";
const PHPSTORM_COMMAND = isWindowsPlatform()
? "phpstorm64.exe"
: isMacPlatform()
? "phpstorm"
: "phpstorm.sh";

export type OpenAppId = string;

Expand Down Expand Up @@ -47,6 +52,13 @@ export const DEFAULT_OPEN_APP_TARGETS: OpenAppTarget[] = isMacPlatform()
appName: "Antigravity",
args: [],
},
{
id: "phpstorm",
label: "PHPStorm",
kind: "app",
appName: "PhpStorm",
args: [],
},
{
id: "finder",
label: fileManagerName(),
Expand Down Expand Up @@ -90,6 +102,13 @@ export const DEFAULT_OPEN_APP_TARGETS: OpenAppTarget[] = isMacPlatform()
command: "antigravity",
args: [],
},
{
id: "phpstorm",
label: "PHPStorm",
kind: "command",
command: PHPSTORM_COMMAND,
args: [],
},
{
id: "finder",
label: fileManagerName(),
Expand Down
Loading
Loading