diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc
index c3fd5c3c01..eb4f250c06 100644
--- a/CHANGELOG.adoc
+++ b/CHANGELOG.adoc
@@ -47,6 +47,7 @@ Release with new features and bugfixes:
* https://github.com/devonfw/IDEasy/issues/2292[#2292]: Questions appear twice
* https://github.com/devonfw/IDEasy/issues/825[#825]: Add commandlet for GC Log Analyzer
* https://github.com/devonfw/IDEasy/issues/1938[#1938]: GUI not launchable outside of project context
+* https://github.com/devonfw/IDEasy/issues/2206[#2206]: IDEasy title not properly shown on macOS
The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/48?closed=1[milestone 2026.08.001].
diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/gui/Gui.java b/cli/src/main/java/com/devonfw/tools/ide/tool/gui/Gui.java
index b0359c11ad..c22f3917a9 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/tool/gui/Gui.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/tool/gui/Gui.java
@@ -1,5 +1,6 @@
package com.devonfw.tools.ide.tool.gui;
+import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
@@ -88,7 +89,7 @@ protected void doRun() {
"-f", //use specified POM file
pomPath.toString(),
"org.codehaus.mojo:exec-maven-plugin:3.1.0:exec",
- "-Dexec.executable=java",
+ "-Dexec.executable=" + getGuiExecutable(javaInstallation),
"-Dexec.classpathScope=compile",
"-Dexec.args=-classpath %classpath com.devonfw.ide.gui.AppLauncher",
"-Dexec.async=true"
@@ -111,4 +112,59 @@ protected void doRun() {
"Failed to launch the GUI. If maven reports issues with dependency resolution, check whether the maven M2 repo is enabled in your project.", e);
}
}
+
+ private static final String GUI_APP_BUNDLE_ID = "com.devonfw.tools.ideasy.gui";
+
+ private static final String GUI_INFO_PLIST = """
+
+
+
+
+ CFBundleExecutable
+ IDEasy
+ CFBundleIdentifier
+ %s
+ CFBundleName
+ IDEasy
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.0
+
+
+ """.formatted(GUI_APP_BUNDLE_ID);
+
+ /**
+ * The GUI is launched as a plain {@code java} process without a native app bundle. On macOS this makes the Dock and menu bar show the executable's
+ * filename "java" instead of "IDEasy" (see issue #2206). Merely renaming/symlinking the bare executable is not reliable: without a real bundle,
+ * LaunchServices may still fall back to the underlying JDK's own identity. Instead, we wrap the launch in a minimal {@code .app} bundle: a proper
+ * {@code Info.plist} declaring our own {@code CFBundleName}/{@code CFBundleIdentifier}, with {@code Contents/MacOS/IDEasy} as a symlink pointing
+ * directly at the real java executable (not a wrapper script - an intermediate {@code exec} would replace the process image and lose the bundle
+ * identity again).
+ *
+ * @param javaInstallation the {@link ToolInstallation} of the java tool used to launch the GUI.
+ * @return the executable to pass to Maven's {@code exec:exec} goal in order to launch the GUI.
+ */
+ String getGuiExecutable(ToolInstallation javaInstallation) {
+
+ if (!this.context.getSystemInfo().isMac()) {
+ return "java";
+ }
+ Path javaExecutable = javaInstallation.binDir().resolve("java");
+ Path contentsDir = this.context.getTempPath().resolve("IDEasy.app").resolve("Contents");
+ Path launcher = contentsDir.resolve("MacOS").resolve("IDEasy");
+ this.context.getFileAccess().mkdirs(launcher.getParent());
+ writeFile(contentsDir.resolve("Info.plist"), GUI_INFO_PLIST);
+ this.context.getFileAccess().symlink(javaExecutable, launcher, false);
+ return launcher.toString();
+ }
+
+ private static void writeFile(Path file, String content) {
+
+ try {
+ Files.writeString(file, content);
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to write file: " + file, e);
+ }
+ }
}
diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/gui/GuiTest.java b/cli/src/test/java/com/devonfw/tools/ide/tool/gui/GuiTest.java
new file mode 100644
index 0000000000..7b6a9235b5
--- /dev/null
+++ b/cli/src/test/java/com/devonfw/tools/ide/tool/gui/GuiTest.java
@@ -0,0 +1,91 @@
+package com.devonfw.tools.ide.tool.gui;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.junit.jupiter.api.Test;
+
+import com.devonfw.tools.ide.context.AbstractIdeContextTest;
+import com.devonfw.tools.ide.context.AbstractIdeTestContext;
+import com.devonfw.tools.ide.context.IdeTestContext;
+import com.devonfw.tools.ide.io.WindowsSymlinkTestHelper;
+import com.devonfw.tools.ide.os.SystemInfoMock;
+import com.devonfw.tools.ide.tool.ToolInstallation;
+import com.devonfw.tools.ide.version.VersionIdentifier;
+
+/**
+ * Test of {@link Gui}.
+ */
+class GuiTest extends AbstractIdeContextTest {
+
+ private ToolInstallation newJavaInstallation(IdeTestContext context) {
+
+ Path binDir = context.getSoftwarePath().resolve("java/bin");
+ context.getFileAccess().mkdirs(binDir);
+ Path javaExecutable = binDir.resolve("java");
+ try {
+ Files.createFile(javaExecutable);
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+ return new ToolInstallation(binDir.getParent(), binDir.getParent(), binDir, VersionIdentifier.of("25.0.0"), false);
+ }
+
+ @Test
+ void testGetGuiExecutableOnMacCreatesAppBundleWithSymlinkNamedIDEasy() throws Exception {
+
+ // arrange
+ WindowsSymlinkTestHelper.assumeSymlinksSupported();
+ IdeTestContext context = newContext(PROJECT_BASIC, null, true);
+ ((AbstractIdeTestContext) context).setSystemInfo(SystemInfoMock.MAC_ARM64);
+ ToolInstallation javaInstallation = newJavaInstallation(context);
+ Gui gui = new Gui(context);
+
+ // act
+ String executable = gui.getGuiExecutable(javaInstallation);
+
+ // assert
+ Path contentsDir = context.getTempPath().resolve("IDEasy.app").resolve("Contents");
+ Path launcher = contentsDir.resolve("MacOS").resolve("IDEasy");
+ assertThat(executable).isEqualTo(launcher.toString());
+ assertThat(Files.isSymbolicLink(launcher)).isTrue();
+ assertThat(launcher.toRealPath()).isEqualTo(javaInstallation.binDir().resolve("java").toRealPath());
+ Path infoPlist = contentsDir.resolve("Info.plist");
+ assertThat(infoPlist).exists();
+ assertThat(Files.readString(infoPlist)).contains("CFBundleName", "IDEasy",
+ "CFBundleIdentifier");
+ }
+
+ @Test
+ void testGetGuiExecutableOnWindowsReturnsPlainJava() {
+
+ // arrange
+ IdeTestContext context = newContext(PROJECT_BASIC, null, true);
+ ((AbstractIdeTestContext) context).setSystemInfo(SystemInfoMock.WINDOWS_X64);
+ ToolInstallation javaInstallation = newJavaInstallation(context);
+ Gui gui = new Gui(context);
+
+ // act
+ String executable = gui.getGuiExecutable(javaInstallation);
+
+ // assert
+ assertThat(executable).isEqualTo("java");
+ }
+
+ @Test
+ void testGetGuiExecutableOnLinuxReturnsPlainJava() {
+
+ // arrange
+ IdeTestContext context = newContext(PROJECT_BASIC, null, true);
+ ((AbstractIdeTestContext) context).setSystemInfo(SystemInfoMock.LINUX_X64);
+ ToolInstallation javaInstallation = newJavaInstallation(context);
+ Gui gui = new Gui(context);
+
+ // act
+ String executable = gui.getGuiExecutable(javaInstallation);
+
+ // assert
+ assertThat(executable).isEqualTo("java");
+ }
+
+}