diff --git a/src/main/java/de/php_perfect/intellij/ddev/actions/DdevAddAddonAction.java b/src/main/java/de/php_perfect/intellij/ddev/actions/DdevAddAddonAction.java
new file mode 100644
index 00000000..e09cc506
--- /dev/null
+++ b/src/main/java/de/php_perfect/intellij/ddev/actions/DdevAddAddonAction.java
@@ -0,0 +1,23 @@
+package de.php_perfect.intellij.ddev.actions;
+
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.project.Project;
+import de.php_perfect.intellij.ddev.ui.AddonListPopup;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Action to add an add-on to a DDEV project.
+ */
+public final class DdevAddAddonAction extends DdevAddonAction {
+
+    @Override
+    public void actionPerformed(@NotNull AnActionEvent e) {
+        Project project = e.getProject();
+
+        if (project == null) {
+            return;
+        }
+
+        new AddonListPopup(project).show();
+    }
+}
diff --git a/src/main/java/de/php_perfect/intellij/ddev/actions/DdevAddonAction.java b/src/main/java/de/php_perfect/intellij/ddev/actions/DdevAddonAction.java
new file mode 100644
index 00000000..6a9867fa
--- /dev/null
+++ b/src/main/java/de/php_perfect/intellij/ddev/actions/DdevAddonAction.java
@@ -0,0 +1,48 @@
+package de.php_perfect.intellij.ddev.actions;
+
+import com.intellij.openapi.actionSystem.ActionUpdateThread;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.project.Project;
+import de.php_perfect.intellij.ddev.state.DdevStateManager;
+import de.php_perfect.intellij.ddev.state.State;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Base class for DDEV add-on related actions.
+ */
+public abstract class DdevAddonAction extends DdevAwareAction {
+
+    @Override
+    public void update(@NotNull AnActionEvent e) {
+        super.update(e);
+
+        Project project = e.getProject();
+
+        if (project == null) {
+            e.getPresentation().setEnabled(false);
+            return;
+        }
+
+        e.getPresentation().setEnabled(isActive(project));
+    }
+
+    @Override
+    protected boolean isActive(@NotNull Project project) {
+        final State state = DdevStateManager.getInstance(project).getState();
+    
+        if (!state.isAvailable() || !state.isConfigured()) {
+            return false;
+        }
+    
+        if (state.getDescription() == null) {
+            return false;
+        }
+    
+        return state.getDescription().getStatus() == de.php_perfect.intellij.ddev.cmd.Description.Status.RUNNING;
+    }
+
+    @Override
+    public @NotNull ActionUpdateThread getActionUpdateThread() {
+        return ActionUpdateThread.BGT;
+    }
+}
diff --git a/src/main/java/de/php_perfect/intellij/ddev/actions/DdevDeleteAddonAction.java b/src/main/java/de/php_perfect/intellij/ddev/actions/DdevDeleteAddonAction.java
new file mode 100644
index 00000000..ee86219b
--- /dev/null
+++ b/src/main/java/de/php_perfect/intellij/ddev/actions/DdevDeleteAddonAction.java
@@ -0,0 +1,40 @@
+package de.php_perfect.intellij.ddev.actions;
+
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.Messages;
+import de.php_perfect.intellij.ddev.cmd.DdevAddon;
+import de.php_perfect.intellij.ddev.cmd.DdevRunner;
+import de.php_perfect.intellij.ddev.ui.AddonDeletePopup;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * Action to remove an add-on from a DDEV project.
+ */
+public final class DdevDeleteAddonAction extends DdevAddonAction {
+
+    @Override
+    public void actionPerformed(@NotNull AnActionEvent e) {
+        Project project = e.getProject();
+
+        if (project == null) {
+            return;
+        }
+
+        List<DdevAddon> installedAddons = DdevRunner.getInstance().getInstalledAddons(project);
+
+        if (installedAddons.isEmpty()) {
+            Messages.showInfoMessage(
+                    project,
+                    "No add-ons installed that can be removed.",
+                    "DDEV Add-Ons"
+            );
+            return;
+        }
+
+        // Show the custom add-on delete popup
+        new AddonDeletePopup(project, installedAddons).show();
+    }
+}
diff --git a/src/main/java/de/php_perfect/intellij/ddev/addon/AddonCache.java b/src/main/java/de/php_perfect/intellij/ddev/addon/AddonCache.java
new file mode 100644
index 00000000..77008dbc
--- /dev/null
+++ b/src/main/java/de/php_perfect/intellij/ddev/addon/AddonCache.java
@@ -0,0 +1,125 @@
+package de.php_perfect.intellij.ddev.addon;
+
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.project.ProjectManager;
+import com.intellij.openapi.project.ProjectManagerListener;
+import de.php_perfect.intellij.ddev.cmd.DdevAddon;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Cache for DDEV addons to avoid repeated calls to the DDEV CLI.
+ */
+public class AddonCache {
+    private static final Logger LOG = Logger.getInstance(AddonCache.class);
+    private static final ConcurrentHashMap<Project, AddonCache> INSTANCES = new ConcurrentHashMap<>();
+    // Cache expiration time: 24 hours in milliseconds
+    private static final long CACHE_EXPIRATION_TIME = 24L * 60 * 60 * 1000;
+
+    private final Project project;
+    private final AtomicBoolean isRefreshing = new AtomicBoolean(false);
+    private List<DdevAddon> availableAddons = Collections.synchronizedList(Collections.emptyList());
+    private final AtomicLong lastRefreshTime = new AtomicLong(0);
+
+    /**
+     * Gets the AddonCache instance for the given project.
+     *
+     * @param project The project to get the cache for
+     * @return The AddonCache instance
+     */
+    public static @NotNull AddonCache getInstance(@NotNull Project project) {
+        return INSTANCES.computeIfAbsent(project, AddonCache::new);
+    }
+
+    private AddonCache(@NotNull Project project) {
+        this.project = project;
+
+        // Initialize the cache in the background
+        refreshCacheAsync();
+
+        // Register a project dispose listener to remove the cache when the project is closed
+        ProjectManager.getInstance().addProjectManagerListener(project, new ProjectManagerListener() {
+            @Override
+            public void projectClosed(@NotNull Project closedProject) {
+                if (project.equals(closedProject)) {
+                    INSTANCES.remove(project);
+                }
+            }
+        });
+    }
+
+    /**
+     * Gets the list of available addons from the cache.
+     * If the cache is empty or expired, it will be refreshed asynchronously.
+     *
+     * @return The list of available addons
+     */
+    public @NotNull List<DdevAddon> getAvailableAddons() {
+        // If the cache is empty or expired, refresh it asynchronously
+        if (availableAddons.isEmpty() || isCacheExpired()) {
+            refreshCacheAsync();
+        }
+        return availableAddons;
+    }
+
+    /**
+     * Checks if the cache is expired.
+     *
+     * @return true if the cache is expired, false otherwise
+     */
+    private boolean isCacheExpired() {
+        return System.currentTimeMillis() - lastRefreshTime.get() > CACHE_EXPIRATION_TIME;
+    }
+
+    /**
+     * Refreshes the cache asynchronously.
+     */
+    public void refreshCacheAsync() {
+        // If already refreshing, don't start another refresh
+        if (isRefreshing.compareAndSet(false, true)) {
+            LOG.debug("Starting async refresh of addon cache for project: " + project.getName());
+
+            ApplicationManager.getApplication().executeOnPooledThread(() -> {
+                try {
+                    List<DdevAddon> addons = fetchAvailableAddons();
+                    if (addons != null) {
+                        availableAddons = addons;
+                        lastRefreshTime.set(System.currentTimeMillis());
+                        LOG.info("Refreshed addon cache for project: " + project.getName() + ", found " + addons.size() + " addons");
+                    }
+                } catch (Exception e) {
+                    LOG.error("Failed to refresh addon cache for project: " + project.getName(), e);
+                } finally {
+                    isRefreshing.set(false);
+                }
+            });
+        }
+    }
+
+    /**
+     * Fetches the list of available addons from the DDEV CLI.
+     *
+     * @return The list of available addons, or null if the operation failed
+     */
+    @Nullable
+    private List<DdevAddon> fetchAvailableAddons() {
+        // Execute the DDEV command to get all available add-ons
+        AddonUtilsService addonUtilsService = AddonUtilsService.getInstance();
+        Map<String, Object> jsonObject = addonUtilsService.executeAddonCommand(project, "list", "--all", "--json-output");
+
+        // Parse the JSON response into a list of DdevAddon objects
+        List<DdevAddon> addons = addonUtilsService.parseAvailableAddons(jsonObject);
+
+        // Return null if no addons were found (to trigger appropriate logging)
+        return addons.isEmpty() ? null : addons;
+    }
+}
diff --git a/src/main/java/de/php_perfect/intellij/ddev/addon/AddonCacheStartupActivity.java b/src/main/java/de/php_perfect/intellij/ddev/addon/AddonCacheStartupActivity.java
new file mode 100644
index 00000000..f2ccd8df
--- /dev/null
+++ b/src/main/java/de/php_perfect/intellij/ddev/addon/AddonCacheStartupActivity.java
@@ -0,0 +1,39 @@
+package de.php_perfect.intellij.ddev.addon;
+
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.startup.ProjectActivity;
+import de.php_perfect.intellij.ddev.state.DdevStateManager;
+import de.php_perfect.intellij.ddev.state.State;
+import kotlin.Unit;
+import kotlin.coroutines.Continuation;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Startup activity to initialize the AddonCache when the project is opened.
+ */
+public class AddonCacheStartupActivity implements ProjectActivity {
+    private static final Logger LOG = Logger.getInstance(AddonCacheStartupActivity.class);
+
+    @Nullable
+    @Override
+    public Object execute(@NotNull Project project, @NotNull Continuation<? super Unit> continuation) {
+        LOG.debug("Initializing AddonCache for project: " + project.getName());
+
+        // Only initialize the cache if DDEV is available and configured
+        State state = DdevStateManager.getInstance(project).getState();
+        if (state.isAvailable() && state.isConfigured()) {
+            // This will create the cache instance and trigger an async refresh in the background
+            ApplicationManager.getApplication().executeOnPooledThread(() -> {
+                AddonCache.getInstance(project);
+                LOG.info("AddonCache initialized for project: " + project.getName());
+            });
+        } else {
+            LOG.debug("Skipping AddonCache initialization for project: " + project.getName() + " because DDEV is not available or not configured");
+        }
+
+        return Unit.INSTANCE;
+    }
+}
diff --git a/src/main/java/de/php_perfect/intellij/ddev/addon/AddonUtilsService.java b/src/main/java/de/php_perfect/intellij/ddev/addon/AddonUtilsService.java
new file mode 100644
index 00000000..d0f47f0a
--- /dev/null
+++ b/src/main/java/de/php_perfect/intellij/ddev/addon/AddonUtilsService.java
@@ -0,0 +1,78 @@
+package de.php_perfect.intellij.ddev.addon;
+
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.project.Project;
+import de.php_perfect.intellij.ddev.cmd.DdevAddon;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Service interface for DDEV addon-related operations.
+ */
+public interface AddonUtilsService {
+    /**
+     * Executes a DDEV addon command and returns the JSON output.
+     *
+     * @param project    The project to execute the command for
+     * @param parameters The command parameters
+     * @return The parsed JSON object, or null if the command failed
+     */
+    @Nullable
+    Map<String, Object> executeAddonCommand(@NotNull Project project, @NotNull String... parameters);
+
+    /**
+     * Processes an addon name to remove the "ddev-" prefix if present.
+     *
+     * @param name The addon name
+     * @return The processed addon name
+     */
+    @NotNull
+    String processAddonName(@NotNull String name);
+
+    /**
+     * Determines if an addon is official based on the vendor name.
+     *
+     * @param vendorName The vendor name
+     * @return true if the addon is official, false otherwise
+     */
+    boolean isOfficialAddon(@Nullable String vendorName);
+
+    /**
+     * Gets the addon type based on whether it's official or not.
+     *
+     * @param vendorName The vendor name
+     * @return "official" if the addon is official, "community" otherwise
+     */
+    @NotNull
+    String getAddonType(@Nullable String vendorName);
+
+    /**
+     * Parses available addons from a JSON object.
+     *
+     * @param jsonObject The JSON object to parse
+     * @return A list of available addons, or an empty list if parsing failed
+     */
+    @NotNull
+    List<DdevAddon> parseAvailableAddons(@Nullable Map<String, Object> jsonObject);
+
+    /**
+     * Parses installed addons from a JSON object.
+     *
+     * @param jsonObject The JSON object to parse
+     * @return A list of installed addons, or an empty list if parsing failed
+     */
+    @NotNull
+    List<DdevAddon> parseInstalledAddons(@Nullable Map<String, Object> jsonObject);
+
+    /**
+     * Gets the AddonUtilsService instance.
+     *
+     * @return The AddonUtilsService instance
+     */
+    static AddonUtilsService getInstance() {
+        return ApplicationManager.getApplication().getService(AddonUtilsService.class);
+    }
+}
diff --git a/src/main/java/de/php_perfect/intellij/ddev/addon/AddonUtilsServiceImpl.java b/src/main/java/de/php_perfect/intellij/ddev/addon/AddonUtilsServiceImpl.java
new file mode 100644
index 00000000..26bb1c78
--- /dev/null
+++ b/src/main/java/de/php_perfect/intellij/ddev/addon/AddonUtilsServiceImpl.java
@@ -0,0 +1,230 @@
+package de.php_perfect.intellij.ddev.addon;
+
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.configurations.GeneralCommandLine;
+import com.intellij.execution.process.ProcessOutput;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import de.php_perfect.intellij.ddev.cmd.DdevAddon;
+import de.php_perfect.intellij.ddev.cmd.DdevRunnerImpl;
+import de.php_perfect.intellij.ddev.cmd.ProcessExecutor;
+import de.php_perfect.intellij.ddev.cmd.parser.JsonUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+/**
+ * Implementation of the AddonUtilsService interface.
+ */
+public class AddonUtilsServiceImpl implements AddonUtilsService {
+    private static final Logger LOG = Logger.getInstance(AddonUtilsServiceImpl.class);
+    private static final int COMMAND_TIMEOUT = 60000; // 60 seconds
+
+    @Override
+    @Nullable
+    public Map<String, Object> executeAddonCommand(@NotNull Project project, @NotNull String... parameters) {
+        try {
+            // Build the command line
+            GeneralCommandLine commandLine = DdevRunnerImpl.createCommandLine("add-on", project);
+            for (String param : parameters) {
+                commandLine.addParameter(param);
+            }
+
+            // Execute the command
+            ProcessOutput output = executeCommand(commandLine);
+            if (output == null) {
+                return null;
+            }
+
+            // Check the exit code
+            if (output.getExitCode() != 0) {
+                LOG.warn("DDEV command failed: " + output.getStderr());
+                return null;
+            }
+
+            // Parse the JSON output
+            String stdout = output.getStdout();
+            if (stdout.isEmpty()) {
+                LOG.warn("Empty output from DDEV command");
+                return null;
+            }
+
+            // Parse the JSON
+            return JsonUtil.parseJson(stdout);
+        } catch (Exception e) {
+            LOG.error("Error executing DDEV addon command", e);
+            return null;
+        }
+    }
+
+    /**
+     * Executes a command line and returns the process output.
+     *
+     * @param commandLine The command line to execute
+     * @return The process output, or null if the command failed
+     */
+    @Nullable
+    private ProcessOutput executeCommand(@NotNull GeneralCommandLine commandLine) {
+        try {
+            return ProcessExecutor.getInstance().executeCommandLine(commandLine, COMMAND_TIMEOUT, false);
+        } catch (ExecutionException e) {
+            LOG.error("Failed to execute DDEV command: " + e.getMessage(), e);
+            return null;
+        }
+    }
+
+    @Override
+    @NotNull
+    public String processAddonName(@NotNull String name) {
+        if (name.startsWith("ddev-")) {
+            return name.substring(5);
+        }
+        return name;
+    }
+
+    @Override
+    public boolean isOfficialAddon(@Nullable String vendorName) {
+        return "ddev".equals(vendorName);
+    }
+
+    @Override
+    @NotNull
+    public String getAddonType(@Nullable String vendorName) {
+        return isOfficialAddon(vendorName) ? "official" : "community";
+    }
+
+    /**
+     * Generic method to parse a list of add-ons from a JSON object.
+     *
+     * @param jsonObject The JSON object to parse
+     * @param parser The function to parse each add-on JSON object
+     * @param errorMessage The error message to log if parsing fails
+     * @return A list of add-ons, or an empty list if parsing failed
+     */
+    @NotNull
+    private List<DdevAddon> parseAddons(
+            @Nullable Map<String, Object> jsonObject,
+            Function<Map<String, Object>, DdevAddon> parser,
+            String errorMessage) {
+
+        if (jsonObject == null) {
+            return Collections.emptyList();
+        }
+
+        try {
+            List<Map<String, Object>> rawAddons = JsonUtil.getList(jsonObject, "raw");
+            List<DdevAddon> addons = new ArrayList<>();
+
+            for (Map<String, Object> addonJson : rawAddons) {
+                parseAddon(parser, addonJson, addons);
+            }
+
+            return addons;
+        } catch (Exception e) {
+            LOG.error(errorMessage, e);
+            return Collections.emptyList();
+        }
+    }
+
+    /**
+     * Parses a single addon and adds it to the list if successful.
+     *
+     * @param parser    The function to parse the addon JSON object
+     * @param addonJson The JSON object to parse
+     * @param addons    The list to add the parsed addon to
+     */
+    private void parseAddon(
+            Function<Map<String, Object>, DdevAddon> parser,
+            Map<String, Object> addonJson,
+            List<DdevAddon> addons) {
+        try {
+            DdevAddon addon = parser.apply(addonJson);
+            if (addon != null) {
+                addons.add(addon);
+            }
+        } catch (Exception e) {
+            // Skip this addon if there's an issue with it
+            LOG.warn("Skipping addon due to error: " + e.getMessage());
+        }
+    }
+
+    @Override
+    @NotNull
+    public List<DdevAddon> parseAvailableAddons(@Nullable Map<String, Object> jsonObject) {
+        return parseAddons(
+                jsonObject,
+                addonJson -> {
+                    String name = JsonUtil.getString(addonJson, "name");
+                    String fullName = JsonUtil.getString(addonJson, "full_name");
+                    String description = JsonUtil.getString(addonJson, "description");
+                    int stars = JsonUtil.getInt(addonJson, "stargazers_count");
+
+                    // Extract owner information
+                    Map<String, Object> ownerJson = JsonUtil.getMap(addonJson, "owner");
+                    String vendorName = ownerJson != null ? JsonUtil.getString(ownerJson, "login") : null;
+
+                    // Process the addon name and determine its type
+                    String addonName = processAddonName(name);
+                    String type = getAddonType(vendorName);
+
+                    return new DdevAddon(
+                            addonName,
+                            description,
+                            null,
+                            type,
+                            stars,
+                            vendorName,
+                            fullName
+                    );
+                },
+                "Failed to parse available addons"
+        );
+    }
+
+    @Override
+    @NotNull
+    public List<DdevAddon> parseInstalledAddons(@Nullable Map<String, Object> jsonObject) {
+        return parseAddons(
+                jsonObject,
+                addonJson -> {
+                    // Get the addon name and repository
+                    String name = JsonUtil.getString(addonJson, "Name");
+                    String repository = JsonUtil.getString(addonJson, "Repository");
+                    String version = JsonUtil.getString(addonJson, "Version");
+
+                    // Process the addon name
+                    String addonName = processAddonName(name);
+
+                    // Extract vendor name from repository
+                    String vendorName = null;
+                    String fullName = addonName;
+                    if (repository.contains("/")) {
+                        String[] parts = repository.split("/");
+                        if (parts.length > 1) {
+                            vendorName = parts[0];
+                            fullName = repository;
+                        }
+                    }
+
+                    // Determine the type
+                    String type = getAddonType(vendorName);
+
+                    return new DdevAddon(
+                            addonName,
+                            repository,
+                            version,
+                            type,
+                            0,
+                            vendorName,
+                            fullName
+                    );
+                },
+                "Failed to parse installed addons"
+        );
+    }
+}
diff --git a/src/main/java/de/php_perfect/intellij/ddev/cmd/DdevAddon.java b/src/main/java/de/php_perfect/intellij/ddev/cmd/DdevAddon.java
new file mode 100644
index 00000000..831a486f
--- /dev/null
+++ b/src/main/java/de/php_perfect/intellij/ddev/cmd/DdevAddon.java
@@ -0,0 +1,135 @@
+package de.php_perfect.intellij.ddev.cmd;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Represents a DDEV add-on that can be added to or removed from a DDEV project.
+ */
+public class DdevAddon {
+    private final @NotNull String name;
+    private final @NotNull String description;
+    private final @Nullable String version;
+    private final @Nullable String type;
+    private final int stars;
+    private final @Nullable String vendorName;
+    private final @Nullable String fullName;
+
+    public DdevAddon(@NotNull String name, @NotNull String description, @Nullable String version,
+                      @Nullable String type, int stars, @Nullable String vendorName, @Nullable String fullName) {
+        this.name = name;
+        this.description = description;
+        this.version = version;
+        this.type = type;
+        this.stars = stars;
+        this.vendorName = vendorName;
+        this.fullName = fullName;
+    }
+
+    /**
+     * Gets the name of the add-on.
+     *
+     * @return The add-on name
+     */
+    public @NotNull String getName() {
+        return name;
+    }
+
+    /**
+     * Gets the description of the add-on.
+     *
+     * @return The add-on description
+     */
+    public @NotNull String getDescription() {
+        return description;
+    }
+
+    /**
+     * Gets the version of the add-on, if available.
+     *
+     * @return The add-on version, or null if not specified
+     */
+    public @Nullable String getVersion() {
+        return version;
+    }
+
+    /**
+     * Gets the type of the add-on, if available.
+     *
+     * @return The add-on type, or null if not specified
+     */
+    public @Nullable String getType() {
+        return type;
+    }
+
+    /**
+     * Gets the number of stars of the add-on.
+     *
+     * @return The add-on stars
+     */
+    public int getStars() {
+        return stars;
+    }
+
+    /**
+     * Gets the vendor name of the add-on, if available.
+     *
+     * @return The add-on vendor name, or null if not specified
+     */
+    public @Nullable String getVendorName() {
+        return vendorName;
+    }
+
+    /**
+     * Gets the full name of the add-on (with vendor prefix), if available.
+     * This is the name that should be used for installation.
+     *
+     * @return The full add-on name, or null if not specified
+     */
+    public @Nullable String getFullName() {
+        return fullName;
+    }
+
+    /**
+     * Gets the name to use for installation.
+     * If fullName is available, it will be used; otherwise, name will be used.
+     *
+     * @return The name to use for installation
+     */
+    public @NotNull String getInstallName() {
+        if (fullName != null && !fullName.isEmpty()) {
+            return fullName;
+        }
+        return name;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+
+        // Add name
+        sb.append(name);
+
+        // Add vendor name as a comment if available
+        if (vendorName != null && !vendorName.isEmpty()) {
+            sb.append(" (").append(vendorName).append(")");
+        }
+
+        // Add version if available
+        if (version != null) {
+            sb.append(" v").append(version);
+        }
+
+        // Add type if available
+        if (type != null) {
+            sb.append(" [").append(type).append("]");
+        }
+
+        // Add stars if available
+        if (stars > 0) {
+            sb.append(" ★").append(stars);
+        }
+
+        return sb.toString();
+    }
+}
diff --git a/src/main/java/de/php_perfect/intellij/ddev/cmd/DdevRunner.java b/src/main/java/de/php_perfect/intellij/ddev/cmd/DdevRunner.java
index ff9b0f7d..49a40e86 100644
--- a/src/main/java/de/php_perfect/intellij/ddev/cmd/DdevRunner.java
+++ b/src/main/java/de/php_perfect/intellij/ddev/cmd/DdevRunner.java
@@ -4,6 +4,8 @@
 import com.intellij.openapi.project.Project;
 import org.jetbrains.annotations.NotNull;
 
+import java.util.List;
+
 public interface DdevRunner {
 
     void start(@NotNull Project project);
@@ -20,6 +22,38 @@ public interface DdevRunner {
 
     void config(@NotNull Project project);
 
+    /**
+     * Gets a list of available add-ons that can be added to a DDEV project.
+     *
+     * @param project The project to get available add-ons for
+     * @return A list of available add-ons
+     */
+    @NotNull List<DdevAddon> getAvailableAddons(@NotNull Project project);
+
+    /**
+     * Gets a list of add-ons currently installed in a DDEV project.
+     *
+     * @param project The project to get installed add-ons for
+     * @return A list of installed add-ons
+     */
+    @NotNull List<DdevAddon> getInstalledAddons(@NotNull Project project);
+
+    /**
+     * Adds an add-on to a DDEV project.
+     *
+     * @param project The project to add the add-on to
+     * @param addon   The add-on to add
+     */
+    void addAddon(@NotNull Project project, @NotNull DdevAddon addon);
+
+    /**
+     * Removes an add-on from a DDEV project.
+     *
+     * @param project The project to remove the add-on from
+     * @param addon   The add-on to remove
+     */
+    void deleteAddon(@NotNull Project project, @NotNull DdevAddon addon);
+
     static DdevRunner getInstance() {
         return ApplicationManager.getApplication().getService(DdevRunner.class);
     }
diff --git a/src/main/java/de/php_perfect/intellij/ddev/cmd/DdevRunnerImpl.java b/src/main/java/de/php_perfect/intellij/ddev/cmd/DdevRunnerImpl.java
index b3469978..e3707066 100644
--- a/src/main/java/de/php_perfect/intellij/ddev/cmd/DdevRunnerImpl.java
+++ b/src/main/java/de/php_perfect/intellij/ddev/cmd/DdevRunnerImpl.java
@@ -3,64 +3,71 @@
 import com.intellij.execution.configurations.GeneralCommandLine;
 import com.intellij.execution.configurations.PtyCommandLine;
 import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.extensions.ExtensionPointName;
 import com.intellij.openapi.fileEditor.FileEditorManager;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.vfs.VirtualFile;
 import de.php_perfect.intellij.ddev.DdevConfigArgumentProvider;
 import de.php_perfect.intellij.ddev.DdevIntegrationBundle;
+import de.php_perfect.intellij.ddev.addon.AddonCache;
+import de.php_perfect.intellij.ddev.addon.AddonUtilsService;
 import de.php_perfect.intellij.ddev.state.DdevConfigLoader;
 import de.php_perfect.intellij.ddev.state.DdevStateManager;
 import de.php_perfect.intellij.ddev.state.State;
 import org.jetbrains.annotations.NotNull;
 
 import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 
 public final class DdevRunnerImpl implements DdevRunner {
+    private static final Logger LOG = Logger.getInstance(DdevRunnerImpl.class);
     private static final ExtensionPointName<DdevConfigArgumentProvider> CONFIG_ARGUMENT_PROVIDER_EP = ExtensionPointName.create("de.php_perfect.intellij.ddev.ddevConfigArgumentProvider");
 
     @Override
     public void start(@NotNull Project project) {
         final String title = DdevIntegrationBundle.message("ddev.run.start");
         final Runner runner = Runner.getInstance(project);
-        runner.run(this.createCommandLine("start", project), title, () -> this.updateDescription(project));
+        runner.run(DdevRunnerImpl.createCommandLine("start", project), title, () -> this.updateDescription(project));
     }
 
     @Override
     public void restart(@NotNull Project project) {
         final String title = DdevIntegrationBundle.message("ddev.run.restart");
         final Runner runner = Runner.getInstance(project);
-        runner.run(this.createCommandLine("restart", project), title, () -> this.updateDescription(project));
+        runner.run(DdevRunnerImpl.createCommandLine("restart", project), title, () -> this.updateDescription(project));
     }
 
     @Override
     public void stop(@NotNull Project project) {
         final String title = DdevIntegrationBundle.message("ddev.run.stop");
         final Runner runner = Runner.getInstance(project);
-        runner.run(this.createCommandLine("stop", project), title, () -> this.updateDescription(project));
+        runner.run(DdevRunnerImpl.createCommandLine("stop", project), title, () -> this.updateDescription(project));
     }
 
     @Override
     public void powerOff(@NotNull Project project) {
         final String title = DdevIntegrationBundle.message("ddev.run.powerOff");
         final Runner runner = Runner.getInstance(project);
-        runner.run(this.createCommandLine("poweroff", project), title, () -> this.updateDescription(project));
+        runner.run(DdevRunnerImpl.createCommandLine("poweroff", project), title, () -> this.updateDescription(project));
     }
 
     @Override
     public void delete(@NotNull Project project) {
         final String title = DdevIntegrationBundle.message("ddev.run.delete");
         final Runner runner = Runner.getInstance(project);
-        runner.run(this.createCommandLine("delete", project), title, () -> this.updateDescription(project));
+        runner.run(DdevRunnerImpl.createCommandLine("delete", project), title, () -> this.updateDescription(project));
     }
 
     @Override
     public void share(@NotNull Project project) {
         final String title = DdevIntegrationBundle.message("ddev.run.share");
         final Runner runner = Runner.getInstance(project);
-        runner.run(this.createCommandLine("share", project), title);
+        runner.run(DdevRunnerImpl.createCommandLine("share", project), title, () -> this.updateDescription(project));
     }
 
     @Override
@@ -82,15 +89,21 @@ private void openConfig(@NotNull Project project) {
     }
 
     private void updateDescription(Project project) {
-        ApplicationManager.getApplication().executeOnPooledThread(() -> DdevStateManager.getInstance(project).updateDescription());
+        ApplicationManager.getApplication().executeOnPooledThread(() ->
+            DdevStateManager.getInstance(project).updateDescription()
+            // No need to refresh the addon cache here, it will be refreshed when needed
+        );
     }
 
     private void updateConfiguration(Project project) {
-        ApplicationManager.getApplication().executeOnPooledThread(() -> DdevStateManager.getInstance(project).updateConfiguration());
+        ApplicationManager.getApplication().executeOnPooledThread(() ->
+            DdevStateManager.getInstance(project).updateConfiguration()
+            // No need to refresh the addon cache here, it will be refreshed when needed
+        );
     }
 
     private @NotNull GeneralCommandLine buildConfigCommandLine(@NotNull Project project) {
-        final GeneralCommandLine commandLine = this.createCommandLine("config", project)
+        final GeneralCommandLine commandLine = DdevRunnerImpl.createCommandLine("config", project)
                 .withParameters("--auto");
 
         for (final DdevConfigArgumentProvider ddevConfigArgumentProvider : CONFIG_ARGUMENT_PROVIDER_EP.getExtensionList()) {
@@ -100,7 +113,7 @@ private void updateConfiguration(Project project) {
         return commandLine;
     }
 
-    private @NotNull GeneralCommandLine createCommandLine(@NotNull String ddevAction, @NotNull Project project) {
+    public static @NotNull GeneralCommandLine createCommandLine(@NotNull String ddevAction, @NotNull Project project) {
         State state = DdevStateManager.getInstance(project).getState();
 
         return new PtyCommandLine(List.of(Objects.requireNonNull(state.getDdevBinary()), ddevAction))
@@ -110,4 +123,81 @@ private void updateConfiguration(Project project) {
                 .withCharset(StandardCharsets.UTF_8)
                 .withEnvironment("DDEV_NONINTERACTIVE", "true");
     }
+
+    @Override
+    public @NotNull List<DdevAddon> getAvailableAddons(@NotNull Project project) {
+        LOG.debug("Getting available add-ons for project: " + project.getName());
+
+        State state = DdevStateManager.getInstance(project).getState();
+
+        if (!state.isAvailable() || !state.isConfigured()) {
+            LOG.warn("DDEV is not available or not configured for project: " + project.getName());
+            return new ArrayList<>();
+        }
+
+        // Get the list of installed add-ons to exclude them from the available add-ons
+        List<String> installedAddonNames = getInstalledAddons(project).stream()
+                .map(DdevAddon::getName)
+                .toList();
+
+        // Get the available addons from the cache
+        // The getAvailableAddons method will handle refreshing if needed
+        List<DdevAddon> allAddons = AddonCache.getInstance(project).getAvailableAddons();
+
+        // Filter out installed add-ons
+        return allAddons.stream()
+                .filter(addon -> !installedAddonNames.contains(addon.getName()))
+                .toList();
+    }
+
+
+
+    @Override
+    public @NotNull List<DdevAddon> getInstalledAddons(@NotNull Project project) {
+        LOG.debug("Getting installed add-ons for project: " + project.getName());
+        State state = DdevStateManager.getInstance(project).getState();
+
+        if (!state.isAvailable() || !state.isConfigured()) {
+            LOG.warn("DDEV is not available or not configured for project: " + project.getName());
+            return Collections.emptyList();
+        }
+
+        // Execute the DDEV command to get installed add-ons
+        AddonUtilsService addonUtilsService = AddonUtilsService.getInstance();
+        Map<String, Object> jsonObject = addonUtilsService.executeAddonCommand(project, "list", "--installed", "--json-output");
+
+        // Parse the JSON response into a list of DdevAddon objects
+        return addonUtilsService.parseInstalledAddons(jsonObject);
+    }
+
+    @Override
+    public void addAddon(@NotNull Project project, @NotNull DdevAddon addon) {
+        String installName = addon.getInstallName();
+        LOG.debug("Adding add-on: " + installName + " to project: " + project.getName());
+        final String title = DdevIntegrationBundle.message("ddev.run.addAddon", addon.getName());
+        final Runner runner = Runner.getInstance(project);
+
+        // Use the DDEV add-on get command to properly add the add-on
+        GeneralCommandLine commandLine = DdevRunnerImpl.createCommandLine("add-on", project)
+                .withParameters("get", installName);
+
+        runner.run(commandLine, title, () -> this.updateDescription(project));
+    }
+
+    @Override
+    public void deleteAddon(@NotNull Project project, @NotNull DdevAddon addon) {
+        // For the installed add-ons list, we need to use the original name from the list
+        // This is the name without any modifications
+        String addonName = addon.getName();
+
+        LOG.debug("Removing add-on: " + addonName + " from project: " + project.getName());
+        final String title = DdevIntegrationBundle.message("ddev.run.deleteAddon", addon.getName());
+        final Runner runner = Runner.getInstance(project);
+
+        // Use the DDEV add-on remove command to properly remove the add-on
+        GeneralCommandLine commandLine = DdevRunnerImpl.createCommandLine("add-on", project)
+                .withParameters("remove", addonName);
+
+        runner.run(commandLine, title, () -> this.updateDescription(project));
+    }
 }
diff --git a/src/main/java/de/php_perfect/intellij/ddev/cmd/Description.java b/src/main/java/de/php_perfect/intellij/ddev/cmd/Description.java
index e520ece1..9c09f2e1 100644
--- a/src/main/java/de/php_perfect/intellij/ddev/cmd/Description.java
+++ b/src/main/java/de/php_perfect/intellij/ddev/cmd/Description.java
@@ -36,7 +36,6 @@ public enum Status {
 
     private final @Nullable String mailpitHttpUrl;
 
-
     private final @Nullable Map<String, Service> services;
 
     @SerializedName("dbinfo")
diff --git a/src/main/java/de/php_perfect/intellij/ddev/cmd/ProcessExecutorImpl.java b/src/main/java/de/php_perfect/intellij/ddev/cmd/ProcessExecutorImpl.java
index 41a41f52..8aef68e9 100644
--- a/src/main/java/de/php_perfect/intellij/ddev/cmd/ProcessExecutorImpl.java
+++ b/src/main/java/de/php_perfect/intellij/ddev/cmd/ProcessExecutorImpl.java
@@ -5,33 +5,93 @@
 import com.intellij.execution.configurations.GeneralCommandLine;
 import com.intellij.execution.process.CapturingProcessHandler;
 import com.intellij.execution.process.ProcessOutput;
+import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.diagnostic.Logger;
-import com.intellij.openapi.progress.EmptyProgressIndicator;
-import com.intellij.openapi.progress.ProgressManager;
 import de.php_perfect.intellij.ddev.cmd.wsl.WslAware;
 import org.jetbrains.annotations.NotNull;
 
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicReference;
 
 public class ProcessExecutorImpl implements ProcessExecutor {
     public static final Logger LOG = Logger.getInstance(ProcessExecutorImpl.class);
+    private static final String FAILED_EXECUTE_COMMAND_MSG = "Failed to execute command: ";
 
     public @NotNull ProcessOutput executeCommandLine(GeneralCommandLine commandLine, int timeout, boolean loginShell) throws ExecutionException {
+        // Check if we're on the EDT
+        if (ApplicationManager.getApplication().isDispatchThread()) {
+            // If we're on the EDT, run the command in a background thread
+            return executeCommandLineInBackground(commandLine, timeout, loginShell);
+        } else {
+            // If we're already on a background thread, run the command directly
+            return executeCommandLineDirectly(commandLine, timeout, loginShell);
+        }
+    }
+
+    private @NotNull ProcessOutput executeCommandLineDirectly(GeneralCommandLine commandLine, int timeout, boolean loginShell) throws ExecutionException {
+        final GeneralCommandLine patchedCommandLine = WslAware.patchCommandLine(commandLine, loginShell);
+
+        try {
+            CapturingProcessHandler processHandler = new CapturingProcessHandler(patchedCommandLine);
+            ProcessOutput output = processHandler.runProcess(timeout);
+
+            LOG.debug("command: " + processHandler.getCommandLine() + " returned: " + output);
+            return output;
+        } catch (ExecutionException e) {
+            LOG.error(FAILED_EXECUTE_COMMAND_MSG + patchedCommandLine.getCommandLineString(), e);
+            throw e;
+        }
+    }
+
+    private @NotNull ProcessOutput executeCommandLineInBackground(GeneralCommandLine commandLine, int timeout, boolean loginShell) throws ExecutionException {
         final GeneralCommandLine patchedCommandLine = WslAware.patchCommandLine(commandLine, loginShell);
         final AtomicReference<ProcessOutput> outputReference = new AtomicReference<>();
+        final AtomicReference<ExecutionException> exceptionReference = new AtomicReference<>();
 
-        ProgressManager.getInstance().runProcess(() -> {
+        // Create a future that will be completed when the task is done
+        Future<ProcessOutput> future = ApplicationManager.getApplication().executeOnPooledThread(() -> {
             try {
                 CapturingProcessHandler processHandler = new CapturingProcessHandler(patchedCommandLine);
                 ProcessOutput output = processHandler.runProcess(timeout);
                 outputReference.set(output);
 
                 LOG.debug("command: " + processHandler.getCommandLine() + " returned: " + output);
+                return output;
             } catch (ExecutionException e) {
+                LOG.error(FAILED_EXECUTE_COMMAND_MSG + patchedCommandLine.getCommandLineString(), e);
+                exceptionReference.set(e);
                 throw new UncheckedExecutionException(e);
             }
-        }, new EmptyProgressIndicator());
+        });
+
+        try {
+            // Wait for the future to complete with a timeout
+            future.get(timeout, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException e) {
+            LOG.error(FAILED_EXECUTE_COMMAND_MSG + patchedCommandLine.getCommandLineString(), e);
+            future.cancel(true);
+            // Restore the interrupted status
+            Thread.currentThread().interrupt();
+            throw new ExecutionException(FAILED_EXECUTE_COMMAND_MSG + e.getMessage(), e);
+        } catch (java.util.concurrent.ExecutionException | TimeoutException e) {
+            LOG.error(FAILED_EXECUTE_COMMAND_MSG + patchedCommandLine.getCommandLineString(), e);
+            future.cancel(true);
+            throw new ExecutionException(FAILED_EXECUTE_COMMAND_MSG + e.getMessage(), e);
+        }
+
+        // Check if an exception was thrown
+        if (exceptionReference.get() != null) {
+            throw exceptionReference.get();
+        }
+
+        // Return the output
+        ProcessOutput output = outputReference.get();
+        if (output == null) {
+            throw new ExecutionException(FAILED_EXECUTE_COMMAND_MSG + "No output returned");
+        }
 
-        return outputReference.get();
+        return output;
     }
 }
diff --git a/src/main/java/de/php_perfect/intellij/ddev/cmd/parser/JsonUtil.java b/src/main/java/de/php_perfect/intellij/ddev/cmd/parser/JsonUtil.java
new file mode 100644
index 00000000..88d6cdc6
--- /dev/null
+++ b/src/main/java/de/php_perfect/intellij/ddev/cmd/parser/JsonUtil.java
@@ -0,0 +1,211 @@
+package de.php_perfect.intellij.ddev.cmd.parser;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSyntaxException;
+import com.intellij.openapi.diagnostic.Logger;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * Utility class for parsing JSON.
+ */
+public final class JsonUtil {
+    private static final Logger LOG = Logger.getInstance(JsonUtil.class);
+
+    private JsonUtil() {
+        // Utility class
+    }
+
+    /**
+     * Parses a JSON string into a Map.
+     *
+     * @param json The JSON string to parse
+     * @return A Map representing the JSON object, or null if parsing fails
+     */
+    @Nullable
+    public static Map<String, Object> parseJson(@NotNull String json) {
+        try {
+            JsonElement jsonElement = JsonParser.parseString(json);
+            if (jsonElement.isJsonObject()) {
+                return convertJsonObjectToMap(jsonElement.getAsJsonObject());
+            } else {
+                LOG.warn("JSON is not an object: " + json);
+                return null;
+            }
+        } catch (JsonSyntaxException e) {
+            LOG.error("Failed to parse JSON: " + json, e);
+            return null;
+        }
+    }
+
+    /**
+     * Converts a JsonObject to a Map.
+     *
+     * @param jsonObject The JsonObject to convert
+     * @return A Map representing the JsonObject
+     */
+    private static Map<String, Object> convertJsonObjectToMap(JsonObject jsonObject) {
+        Map<String, Object> map = new HashMap<>();
+        for (Entry<String, JsonElement> entry : jsonObject.entrySet()) {
+            String key = entry.getKey();
+            JsonElement value = entry.getValue();
+            map.put(key, convertJsonElementToObject(value));
+        }
+        return map;
+    }
+
+    /**
+     * Converts a JsonElement to a Java object.
+     *
+     * @param jsonElement The JsonElement to convert
+     * @return A Java object representing the JsonElement
+     */
+    private static Object convertJsonElementToObject(JsonElement jsonElement) {
+        if (jsonElement.isJsonNull()) {
+            return null;
+        } else if (jsonElement.isJsonPrimitive()) {
+            JsonPrimitive primitive = jsonElement.getAsJsonPrimitive();
+            if (primitive.isBoolean()) {
+                return primitive.getAsBoolean();
+            } else if (primitive.isNumber()) {
+                return primitive.getAsNumber();
+            } else {
+                return primitive.getAsString();
+            }
+        } else if (jsonElement.isJsonArray()) {
+            List<Object> list = new ArrayList<>();
+            JsonArray jsonArray = jsonElement.getAsJsonArray();
+            for (JsonElement element : jsonArray) {
+                list.add(convertJsonElementToObject(element));
+            }
+            return list;
+        } else if (jsonElement.isJsonObject()) {
+            return convertJsonObjectToMap(jsonElement.getAsJsonObject());
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Gets a list of maps from a JSON object.
+     *
+     * @param json The JSON object as a Map
+     * @param key  The key of the array in the JSON object
+     * @return A list of maps representing the JSON array, or an empty list if the key is not found
+     */
+    @NotNull
+    public static List<Map<String, Object>> getList(@Nullable Map<String, Object> json, @NotNull String key) {
+        if (json == null) {
+            return new ArrayList<>();
+        }
+
+        Object value = json.get(key);
+        if (value instanceof List<?> list) {
+            List<Map<String, Object>> result = new ArrayList<>();
+            for (Object item : list) {
+                if (item instanceof Map<?, ?> map) {
+                    @SuppressWarnings("unchecked")
+                    Map<String, Object> typedMap = (Map<String, Object>) map;
+                    result.add(typedMap);
+                }
+            }
+            return result;
+        }
+
+        return new ArrayList<>();
+    }
+
+    /**
+     * Gets a string value from a JSON object.
+     *
+     * @param json The JSON object as a Map
+     * @param key  The key of the string in the JSON object
+     * @return The string value, or an empty string if the key is not found
+     */
+    @NotNull
+    public static String getString(@Nullable Map<String, Object> json, @NotNull String key) {
+        if (json == null) {
+            return "";
+        }
+
+        Object value = json.get(key);
+        if (value instanceof String string) {
+            return string;
+        }
+
+        return value != null ? value.toString() : "";
+    }
+
+    /**
+     * Gets a boolean value from a JSON object.
+     *
+     * @param json The JSON object as a Map
+     * @param key  The key of the boolean in the JSON object
+     * @return The boolean value, or false if the key is not found
+     */
+    public static boolean getBoolean(@Nullable Map<String, Object> json, @NotNull String key) {
+        if (json == null) {
+            return false;
+        }
+
+        Object value = json.get(key);
+        if (value instanceof Boolean boolValue) {
+            return boolValue;
+        }
+
+        return false;
+    }
+
+    /**
+     * Gets an integer value from a JSON object.
+     *
+     * @param json The JSON object as a Map
+     * @param key  The key of the integer in the JSON object
+     * @return The integer value, or 0 if the key is not found
+     */
+    public static int getInt(@Nullable Map<String, Object> json, @NotNull String key) {
+        if (json == null) {
+            return 0;
+        }
+
+        Object value = json.get(key);
+        if (value instanceof Number number) {
+            return number.intValue();
+        }
+
+        return 0;
+    }
+
+    /**
+     * Gets a map value from a JSON object.
+     *
+     * @param json The JSON object as a Map
+     * @param key  The key of the map in the JSON object
+     * @return The map value, or null if the key is not found
+     */
+    @Nullable
+    public static Map<String, Object> getMap(@Nullable Map<String, Object> json, @NotNull String key) {
+        if (json == null) {
+            return null;
+        }
+
+        Object value = json.get(key);
+        if (value instanceof Map<?, ?> map) {
+            @SuppressWarnings("unchecked")
+            Map<String, Object> typedMap = (Map<String, Object>) map;
+            return typedMap;
+        }
+
+        return null;
+    }
+}
diff --git a/src/main/java/de/php_perfect/intellij/ddev/ui/AbstractAddonPopup.java b/src/main/java/de/php_perfect/intellij/ddev/ui/AbstractAddonPopup.java
new file mode 100644
index 00000000..72263403
--- /dev/null
+++ b/src/main/java/de/php_perfect/intellij/ddev/ui/AbstractAddonPopup.java
@@ -0,0 +1,400 @@
+package de.php_perfect.intellij.ddev.ui;
+
+import com.intellij.icons.AllIcons;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.popup.ComponentPopupBuilder;
+import com.intellij.openapi.ui.popup.JBPopup;
+import com.intellij.openapi.ui.popup.JBPopupFactory;
+import com.intellij.openapi.ui.popup.JBPopupListener;
+import com.intellij.openapi.ui.popup.LightweightWindowEvent;
+import com.intellij.ui.ColoredListCellRenderer;
+import com.intellij.ui.SimpleTextAttributes;
+import com.intellij.ui.components.JBList;
+import com.intellij.ui.components.JBScrollPane;
+import com.intellij.ui.components.JBTextField;
+import com.intellij.util.ui.JBUI;
+import de.php_perfect.intellij.ddev.cmd.DdevAddon;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import javax.swing.event.ListSelectionEvent;
+import java.awt.*;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Base class for DDEV addon popups with common functionality.
+ */
+public abstract class AbstractAddonPopup {
+    protected static final Logger LOG = Logger.getInstance(AbstractAddonPopup.class);
+
+    protected final Project project;
+    protected final List<DdevAddon> allAddons = new ArrayList<>();
+    protected final JBList<DdevAddon> addonList;
+    protected final DefaultListModel<DdevAddon> listModel;
+    protected final JBTextField searchField;
+    protected final JPanel panel;
+    protected final JButton refreshButton;
+    protected JBPopup popup;
+    protected Timer refreshTimer;
+
+    /**
+     * Creates a new addon popup.
+     *
+     * @param project The project context
+     * @param searchPlaceholder Text to show in the search field when empty
+     * @param initialAddons Initial list of addons to display (can be empty)
+     * @param dimensions Initial dimensions for the popup
+     */
+    protected AbstractAddonPopup(@NotNull Project project, @NotNull String searchPlaceholder,
+                               @Nullable List<DdevAddon> initialAddons, @NotNull Dimension dimensions) {
+        this.project = project;
+
+        // Initialize components
+        this.listModel = new DefaultListModel<>();
+        this.addonList = new JBList<>(listModel);
+        this.searchField = new JBTextField();
+        this.refreshButton = new JButton("Refresh", AllIcons.Actions.Refresh);
+        this.panel = new JPanel(new BorderLayout());
+
+        // Add initial addons if provided
+        if (initialAddons != null) {
+            // Sort add-ons by name
+            this.allAddons.addAll(initialAddons.stream()
+                    .sorted((a1, a2) -> a1.getName().compareToIgnoreCase(a2.getName()))
+                    .toList());
+        }
+
+        // Initialize UI
+        initializeUI(searchPlaceholder, dimensions);
+
+        // Update the list initially
+        updateList("");
+    }
+
+    /**
+     * Sets up the UI components.
+     *
+     * @param searchPlaceholder Text to show in search field when empty
+     * @param dimensions Initial dimensions for the popup
+     */
+    protected void initializeUI(@NotNull String searchPlaceholder, @NotNull Dimension dimensions) {
+        // Set up the search field
+        searchField.getEmptyText().setText(searchPlaceholder);
+        searchField.getDocument().addDocumentListener(new DocumentListener() {
+            @Override
+            public void insertUpdate(DocumentEvent e) {
+                updateList(searchField.getText());
+            }
+
+            @Override
+            public void removeUpdate(DocumentEvent e) {
+                updateList(searchField.getText());
+            }
+
+            @Override
+            public void changedUpdate(DocumentEvent e) {
+                updateList(searchField.getText());
+            }
+        });
+
+        // Set up the add-on list
+        addonList.setCellRenderer(createListCellRenderer());
+        addonList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+        addonList.addListSelectionListener(this::handleListSelection);
+
+        // Set up the refresh button
+        refreshButton.setToolTipText(getRefreshButtonTooltip());
+        refreshButton.addActionListener(e -> handleRefreshAction());
+
+        // Create the panel
+        JPanel searchPanel = new JPanel(new BorderLayout());
+        searchPanel.add(searchField, BorderLayout.CENTER);
+        searchPanel.add(refreshButton, BorderLayout.EAST);
+        searchPanel.setBorder(JBUI.Borders.empty(5));
+
+        panel.add(searchPanel, BorderLayout.NORTH);
+        panel.add(new JBScrollPane(addonList), BorderLayout.CENTER);
+        panel.setPreferredSize(dimensions);
+    }
+
+    /**
+     * Updates the list based on the search text.
+     *
+     * @param searchText The text to filter by
+     */
+    protected void updateList(String searchText) {
+        listModel.clear();
+
+        List<DdevAddon> filteredAddons = filterAddons(searchText);
+        for (DdevAddon addon : filteredAddons) {
+            listModel.addElement(addon);
+        }
+
+        if (filteredAddons.isEmpty()) {
+            handleEmptyList(searchText);
+        }
+    }
+
+    /**
+     * Filters addons based on search text.
+     *
+     * @param searchText The text to filter by
+     * @return Filtered list of addons
+     */
+    protected List<DdevAddon> filterAddons(String searchText) {
+        if (searchText == null || searchText.trim().isEmpty()) {
+            return new ArrayList<>(allAddons);
+        }
+
+        String lowerCaseSearchText = searchText.toLowerCase();
+
+        return allAddons.stream()
+                .filter(addon -> {
+                    // Check if the add-on name contains the search text
+                    if (addon.getName().toLowerCase().contains(lowerCaseSearchText)) {
+                        return true;
+                    }
+
+                    // Check if the description contains the search text
+                    if (addon.getDescription().toLowerCase().contains(lowerCaseSearchText)) {
+                        return true;
+                    }
+
+                    // Check if the vendor name contains the search text
+                    if (addon.getVendorName() != null &&
+                        addon.getVendorName().toLowerCase().contains(lowerCaseSearchText)) {
+                        return true;
+                    }
+
+                    // Check if the type contains the search text
+                    return addon.getType() != null &&
+                            addon.getType().toLowerCase().contains(lowerCaseSearchText);
+                })
+                .toList();
+    }
+
+    /**
+     * Shows the popup.
+     */
+    public void show() {
+        ComponentPopupBuilder builder = JBPopupFactory.getInstance()
+                .createComponentPopupBuilder(panel, searchField)
+                .setTitle(getPopupTitle())
+                .setResizable(true)
+                .setMovable(true)
+                .setRequestFocus(true);
+
+        popup = builder.createPopup();
+
+        // Add listeners
+        popup.addListener(createPopupListener());
+
+        // Show the popup
+        popup.showCenteredInCurrentWindow(project);
+    }
+
+    /**
+     * Creates a cell renderer for the addon list.
+     *
+     * @return The cell renderer
+     */
+    protected ListCellRenderer<DdevAddon> createListCellRenderer() {
+        return new AddonListCellRenderer(getAddonIcon(), shouldShowDescription());
+    }
+
+    /**
+     * Creates a popup listener.
+     *
+     * @return The popup listener
+     */
+    protected JBPopupListener createPopupListener() {
+        return new JBPopupListener() {
+            @Override
+            public void onClosed(@NotNull LightweightWindowEvent event) {
+                // Hook for cleanup actions
+                handlePopupClosed();
+            }
+        };
+    }
+
+    // Abstract methods to be implemented by subclasses
+
+    /**
+     * Gets the title for the popup.
+     *
+     * @return The popup title
+     */
+    protected abstract @NotNull String getPopupTitle();
+
+    /**
+     * Gets the tooltip text for the refresh button.
+     *
+     * @return The tooltip text
+     */
+    protected abstract @NotNull String getRefreshButtonTooltip();
+
+    /**
+     * Handles the list selection event.
+     *
+     * @param e The list selection event
+     */
+    protected abstract void handleListSelection(ListSelectionEvent e);
+
+    /**
+     * Handles the refresh button action.
+     * This is a template method that calls the specific implementation methods.
+     */
+    protected void handleRefreshAction() {
+        // Show a loading indicator
+        addonList.setPaintBusy(true);
+        addonList.setEmptyText("Refreshing add-ons...");
+
+        // Stop any existing timer
+        stopRefreshTimer();
+
+        // Create a new timer to refresh the add-ons
+        refreshTimer = new Timer(getRefreshDelayMillis(), event -> {
+            // Get the refreshed add-ons
+            List<DdevAddon> refreshedAddons = getRefreshedAddons();
+
+            // Sort add-ons by name
+            List<DdevAddon> sortedAddons = refreshedAddons.stream()
+                    .sorted((a1, a2) -> a1.getName().compareToIgnoreCase(a2.getName()))
+                    .toList();
+
+            // Update the allAddons list
+            allAddons.clear();
+            allAddons.addAll(sortedAddons);
+
+            // Update the list
+            updateList(searchField.getText());
+
+            // Stop the loading indicator
+            addonList.setPaintBusy(false);
+
+            // Log the refresh
+            LOG.debug("Add-ons refreshed successfully: " + refreshedAddons.size() + " add-ons found");
+        });
+
+        refreshTimer.setRepeats(false);
+        refreshTimer.start();
+    }
+
+    /**
+     * Gets the delay in milliseconds to wait before refreshing the add-ons.
+     * Default is 1000ms (1 second).
+     *
+     * @return The delay in milliseconds
+     */
+    protected int getRefreshDelayMillis() {
+        return 1000;
+    }
+
+    /**
+     * Gets the refreshed add-ons.
+     * This method should be implemented by subclasses to provide the specific add-ons to display.
+     *
+     * @return The refreshed add-ons
+     */
+    protected abstract List<DdevAddon> getRefreshedAddons();
+
+    /**
+     * Stops the refresh timer if it's running.
+     */
+    protected void stopRefreshTimer() {
+        if (refreshTimer != null && refreshTimer.isRunning()) {
+            refreshTimer.stop();
+        }
+    }
+
+    /**
+     * Handles what to show when the filtered list is empty.
+     *
+     * @param searchText The current search text
+     */
+    protected abstract void handleEmptyList(String searchText);
+
+    /**
+     * Gets the icon to use for addons in the list.
+     *
+     * @return The icon
+     */
+    protected abstract @NotNull Icon getAddonIcon();
+
+    /**
+     * Determines whether to show the description in the list.
+     *
+     * @return true to show the description, false otherwise
+     */
+    protected abstract boolean shouldShowDescription();
+
+    /**
+     * Called when the popup is closed.
+     * Default implementation stops the refresh timer.
+     */
+    protected void handlePopupClosed() {
+        stopRefreshTimer();
+    }
+
+    /**
+     * Cell renderer for addon items.
+     */
+    protected static class AddonListCellRenderer extends ColoredListCellRenderer<DdevAddon> {
+        private final transient Icon addonIcon;
+        private final boolean showDescription;
+
+        /**
+         * Creates a new addon list cell renderer.
+         *
+         * @param addonIcon The icon to use for addons
+         * @param showDescription Whether to show the description
+         */
+        public AddonListCellRenderer(@NotNull Icon addonIcon, boolean showDescription) {
+            this.addonIcon = addonIcon;
+            this.showDescription = showDescription;
+        }
+
+        @Override
+        protected void customizeCellRenderer(@NotNull JList<? extends DdevAddon> list, DdevAddon addon,
+                                          int index, boolean selected, boolean hasFocus) {
+            // Set the add-on name
+            append(addon.getName(), SimpleTextAttributes.REGULAR_ATTRIBUTES);
+
+            // Add vendor name as a comment if available
+            if (addon.getVendorName() != null && !addon.getVendorName().isEmpty()) {
+                append(" (" + addon.getVendorName() + ")", SimpleTextAttributes.GRAYED_ATTRIBUTES);
+            }
+
+            // Add version if available
+            if (addon.getVersion() != null) {
+                append(" v" + addon.getVersion(), SimpleTextAttributes.GRAYED_ATTRIBUTES);
+            }
+
+            // Add type if available
+            if (addon.getType() != null) {
+                SimpleTextAttributes typeAttributes = "official".equals(addon.getType())
+                        ? new SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, JBUI.CurrentTheme.Link.Foreground.ENABLED)
+                        : SimpleTextAttributes.GRAY_ATTRIBUTES;
+                append(" [" + addon.getType() + "]", typeAttributes);
+            }
+
+            // Add stars if available
+            if (addon.getStars() > 0) {
+                append(" ★" + addon.getStars(), SimpleTextAttributes.GRAY_ATTRIBUTES);
+            }
+
+            // Add description if needed
+            if (showDescription && !addon.getDescription().isEmpty()) {
+                append(" - " + addon.getDescription(), SimpleTextAttributes.GRAY_ATTRIBUTES);
+            }
+
+            // Set icon
+            setIcon(addonIcon);
+        }
+    }
+}
diff --git a/src/main/java/de/php_perfect/intellij/ddev/ui/AddonDeletePopup.java b/src/main/java/de/php_perfect/intellij/ddev/ui/AddonDeletePopup.java
new file mode 100644
index 00000000..33a8e82a
--- /dev/null
+++ b/src/main/java/de/php_perfect/intellij/ddev/ui/AddonDeletePopup.java
@@ -0,0 +1,73 @@
+package de.php_perfect.intellij.ddev.ui;
+
+import com.intellij.icons.AllIcons;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.Messages;
+import de.php_perfect.intellij.ddev.cmd.DdevAddon;
+import de.php_perfect.intellij.ddev.cmd.DdevRunner;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+import javax.swing.event.ListSelectionEvent;
+import java.awt.*;
+import java.util.List;
+
+/**
+ * Custom popup for displaying and filtering installed DDEV add-ons for deletion.
+ */
+public class AddonDeletePopup extends AbstractAddonPopup {
+    public AddonDeletePopup(@NotNull Project project, @NotNull List<DdevAddon> addons) {
+        super(project, "Search installed add-ons...", addons, new Dimension(500, 300));
+    }
+
+    @Override
+    protected @NotNull String getPopupTitle() {
+        return "Select a DDEV Add-on to Remove";
+    }
+
+    @Override
+    protected @NotNull String getRefreshButtonTooltip() {
+        return "Refresh the list of installed add-ons";
+    }
+
+    @Override
+    protected void handleListSelection(ListSelectionEvent e) {
+        if (!e.getValueIsAdjusting() && addonList.getSelectedValue() != null) {
+            DdevAddon selectedAddon = addonList.getSelectedValue();
+            popup.closeOk(null);
+
+            int result = Messages.showYesNoDialog(
+                    project,
+                    "Are you sure you want to remove the add-on '" + selectedAddon.getName() + "'?",
+                    "Confirm Add-on Removal",
+                    Messages.getQuestionIcon()
+            );
+
+            if (result == Messages.YES) {
+                DdevRunner.getInstance().deleteAddon(project, selectedAddon);
+            }
+        }
+    }
+
+    @Override
+    protected List<DdevAddon> getRefreshedAddons() {
+        return DdevRunner.getInstance().getInstalledAddons(project);
+    }
+
+    @Override
+    protected void handleEmptyList(String searchText) {
+        // If no add-ons match the search, show a message
+        addonList.setEmptyText("No add-ons found matching '" + searchText + "'");
+    }
+
+    @Override
+    protected @NotNull Icon getAddonIcon() {
+        return AllIcons.Actions.GC;
+    }
+
+    @Override
+    protected boolean shouldShowDescription() {
+        // Skip the description as it's redundant (just says "Installed add-on: X")
+        return false;
+    }
+}
diff --git a/src/main/java/de/php_perfect/intellij/ddev/ui/AddonListPopup.java b/src/main/java/de/php_perfect/intellij/ddev/ui/AddonListPopup.java
new file mode 100644
index 00000000..085c495d
--- /dev/null
+++ b/src/main/java/de/php_perfect/intellij/ddev/ui/AddonListPopup.java
@@ -0,0 +1,160 @@
+package de.php_perfect.intellij.ddev.ui;
+
+import com.intellij.icons.AllIcons;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import de.php_perfect.intellij.ddev.addon.AddonCache;
+import de.php_perfect.intellij.ddev.cmd.DdevAddon;
+import de.php_perfect.intellij.ddev.cmd.DdevRunner;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+import javax.swing.event.ListSelectionEvent;
+import java.awt.*;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Custom popup for displaying and filtering DDEV add-ons.
+ */
+public class AddonListPopup extends AbstractAddonPopup {
+    private static final Logger LOG = Logger.getInstance(AddonListPopup.class);
+    private final AtomicBoolean isLoading = new AtomicBoolean(false);
+    private Timer loadingCheckTimer;
+
+    /**
+     * Creates a new AddonListPopup with an empty list that will be populated asynchronously.
+     *
+     * @param project The project to get add-ons for
+     */
+    public AddonListPopup(@NotNull Project project) {
+        super(project, "Search add-ons...", null, new Dimension(600, 400));
+
+        // Show loading indicator
+        addonList.setPaintBusy(true);
+        addonList.setEmptyText("Loading add-ons...");
+
+        // Start loading add-ons in the background
+        loadAddonsAsync();
+    }
+
+    /**
+     * Loads add-ons asynchronously and updates the UI when they are available.
+     */
+    private void loadAddonsAsync() {
+        if (isLoading.compareAndSet(false, true)) {
+            // Trigger a refresh of the cache to make sure we get the latest data
+            AddonCache.getInstance(project).refreshCacheAsync();
+
+            // Create a timer to check for add-ons periodically
+            loadingCheckTimer = new Timer(500, e -> {
+                List<DdevAddon> availableAddons = getRefreshedAddons();
+
+                if (!availableAddons.isEmpty()) {
+                    // We have add-ons, stop the timer and update the UI
+                    loadingCheckTimer.stop();
+
+                    // Sort and update the list using the same logic as in handleRefreshAction
+                    List<DdevAddon> sortedAddons = availableAddons.stream()
+                            .sorted((a1, a2) -> a1.getName().compareToIgnoreCase(a2.getName()))
+                            .toList();
+
+                    allAddons.clear();
+                    allAddons.addAll(sortedAddons);
+                    updateList(searchField.getText());
+
+                    // Stop the loading indicator
+                    addonList.setPaintBusy(false);
+
+                    LOG.debug("Add-ons loaded successfully: " + availableAddons.size() + " add-ons found");
+                }
+            });
+
+            loadingCheckTimer.setRepeats(true);
+            loadingCheckTimer.start();
+        }
+    }
+
+    @Override
+    protected @NotNull String getPopupTitle() {
+        return "Select a DDEV Add-on to Add";
+    }
+
+    @Override
+    protected @NotNull String getRefreshButtonTooltip() {
+        return "Refresh the list of available add-ons";
+    }
+
+    @Override
+    protected void handleListSelection(ListSelectionEvent e) {
+        if (!e.getValueIsAdjusting() && addonList.getSelectedValue() != null) {
+            DdevAddon selectedAddon = addonList.getSelectedValue();
+            popup.closeOk(null);
+            DdevRunner.getInstance().addAddon(project, selectedAddon);
+        }
+    }
+
+    @Override
+    protected void handleRefreshAction() {
+        // Show a loading indicator
+        addonList.setPaintBusy(true);
+        addonList.setEmptyText("Refreshing add-ons...");
+        isLoading.set(true);
+
+        // Clear the current list
+        allAddons.clear();
+        updateList(searchField.getText());
+
+        // Refresh the cache in the background
+        AddonCache.getInstance(project).refreshCacheAsync();
+
+        // Use the base class refresh logic
+        super.handleRefreshAction();
+    }
+
+    @Override
+    protected List<DdevAddon> getRefreshedAddons() {
+        List<DdevAddon> availableAddons = DdevRunner.getInstance().getAvailableAddons(project);
+        if (!availableAddons.isEmpty()) {
+            isLoading.set(false);
+        }
+        return availableAddons;
+    }
+
+    @Override
+    protected int getRefreshDelayMillis() {
+        return 500; // Use a shorter delay for available add-ons
+    }
+
+    @Override
+    protected void handleEmptyList(String searchText) {
+        if (isLoading.get()) {
+            // If we're still loading, show a loading message
+            addonList.setEmptyText("Loading add-ons...");
+        } else if (allAddons.isEmpty()) {
+            // If there are no add-ons at all, show a message
+            addonList.setEmptyText("No add-ons available");
+        } else {
+            // If no add-ons match the search, show a message
+            addonList.setEmptyText("No add-ons found matching '" + searchText + "'");
+        }
+    }
+
+    @Override
+    protected @NotNull Icon getAddonIcon() {
+        return AllIcons.Nodes.Plugin;
+    }
+
+    @Override
+    protected boolean shouldShowDescription() {
+        return true;
+    }
+
+    @Override
+    protected void handlePopupClosed() {
+        super.handlePopupClosed();
+        if (loadingCheckTimer != null && loadingCheckTimer.isRunning()) {
+            loadingCheckTimer.stop();
+        }
+    }
+}
diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml
index 685b7867..3653b4ee 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -74,6 +74,8 @@
                             serviceInterface="de.php_perfect.intellij.ddev.cmd.parser.JsonParser"/>
         <applicationService serviceImplementation="de.php_perfect.intellij.ddev.cmd.DockerImpl"
                             serviceInterface="de.php_perfect.intellij.ddev.cmd.Docker"/>
+        <applicationService serviceImplementation="de.php_perfect.intellij.ddev.addon.AddonUtilsServiceImpl"
+                            serviceInterface="de.php_perfect.intellij.ddev.addon.AddonUtilsService"/>
 
         <projectService serviceImplementation="de.php_perfect.intellij.ddev.index.ManagedConfigurationIndexImpl"
                         serviceInterface="de.php_perfect.intellij.ddev.index.ManagedConfigurationIndex"/>
@@ -98,6 +100,7 @@
                              key="settings.title"/>
 
         <postStartupActivity implementation="de.php_perfect.intellij.ddev.InitPluginActivity"/>
+        <postStartupActivity implementation="de.php_perfect.intellij.ddev.addon.AddonCacheStartupActivity"/>
 
         <statusBarWidgetFactory order="before light.edit.large.file.encoding.widget"
                                 implementation="de.php_perfect.intellij.ddev.statusBar.DdevStatusBarWidgetFactoryImpl"
@@ -147,6 +150,18 @@
                 <override-text place="MainMenu"/>
                 <override-text place="EditorPopup" use-text-of-place="MainMenu"/>
             </action>
+            <separator/>
+            <action id="DdevIntegration.Run.AddAddon" icon="AllIcons.General.Add"
+                    class="de.php_perfect.intellij.ddev.actions.DdevAddAddonAction">
+                <override-text place="MainMenu"/>
+                <override-text place="EditorPopup" use-text-of-place="MainMenu"/>
+            </action>
+            <action id="DdevIntegration.Run.DeleteAddon" icon="AllIcons.General.Remove"
+                    class="de.php_perfect.intellij.ddev.actions.DdevDeleteAddonAction">
+                <override-text place="MainMenu"/>
+                <override-text place="EditorPopup" use-text-of-place="MainMenu"/>
+            </action>
+
         </group>
         <group id="DdevIntegration.Services" class="de.php_perfect.intellij.ddev.actions.ServicesActionGroup"/>
         <action id="DdevIntegration.CheckVersion" class="de.php_perfect.intellij.ddev.actions.CheckVersionAction"/>
diff --git a/src/main/resources/messages/DdevIntegrationBundle.properties b/src/main/resources/messages/DdevIntegrationBundle.properties
index 6dacb929..76d216ba 100644
--- a/src/main/resources/messages/DdevIntegrationBundle.properties
+++ b/src/main/resources/messages/DdevIntegrationBundle.properties
@@ -45,6 +45,12 @@ action.DdevIntegration.Run.Share.description=Share project on the internet via n
 action.DdevIntegration.Run.Config.text=Configure DDEV Project
 action.DdevIntegration.Run.Config.MainMenu.text=Configure
 action.DdevIntegration.Run.Config.description=Create a DDEV configuration in the current project
+action.DdevIntegration.Run.AddAddon.text=Add Add-On to DDEV Project
+action.DdevIntegration.Run.AddAddon.MainMenu.text=Add Add-On
+action.DdevIntegration.Run.AddAddon.description=Add an add-on to your DDEV project
+action.DdevIntegration.Run.DeleteAddon.text=Remove Add-On from DDEV Project
+action.DdevIntegration.Run.DeleteAddon.MainMenu.text=Remove Add-On
+action.DdevIntegration.Run.DeleteAddon.description=Remove an add-on from your DDEV project
 action.DdevIntegration.CheckVersion.text=Check for DDEV Updates
 action.DdevIntegration.CheckVersion.description=Check whether a new DDEV version is available for download
 action.DdevIntegration.Terminal.text=DDEV Web Container
@@ -85,6 +91,9 @@ ddev.run.powerOff=DDEV Power Off
 ddev.run.delete=DDEV Delete
 ddev.run.share=DDEV Share
 ddev.run.config=DDEV Config
+
+ddev.run.addAddon=DDEV Add Add-on: {0}
+ddev.run.deleteAddon=DDEV Delete Add-on: {0}
 # Status Bar
 statusBar.displayName=DDEV Status
 statusBar.toolTip=DDEV Status and Services