Skip to content

feat: allow multiple apps to run concurrently with garbage collection#2825

Open
wwwillchen-bot wants to merge 3 commits intodyad-sh:mainfrom
wwwillchen-bot:agent-2819-1772233680
Open

feat: allow multiple apps to run concurrently with garbage collection#2825
wwwillchen-bot wants to merge 3 commits intodyad-sh:mainfrom
wwwillchen-bot:agent-2819-1772233680

Conversation

@wwwillchen-bot
Copy link
Collaborator

@wwwillchen-bot wwwillchen-bot commented Feb 27, 2026

Summary

  • Allow multiple Dyad apps to run simultaneously instead of stopping the previous app when switching
  • Add garbage collection that stops apps idle for 10+ minutes (unless currently selected)
  • Track lastViewedAt timestamp on running apps to determine eligibility for GC
  • Clean up all running apps when the Dyad application quits

Test plan

  1. Create two test apps in Dyad
  2. Run the first app and verify it starts
  3. Switch to the second app - verify the first app keeps running (check processes)
  4. Wait 10+ minutes without viewing the first app - verify it gets garbage collected
  5. Close Dyad completely - verify all running app processes are stopped

Closes #2819

🤖 Generated with Claude Code


Open with Devin

Previously, Dyad only kept one app running at a time - switching to a
different app would immediately stop the previous one. This caused
friction for users working on multiple apps.

Now multiple apps can run simultaneously. A garbage collector runs
every minute and stops apps that:
1. Haven't been viewed in the last 10 minutes
2. Are not the currently selected app

Changes:
- Add `lastViewedAt` timestamp to RunningAppInfo
- Create garbage collection system in process_manager.ts
- Add IPC handlers for tracking selected app and getting running apps
- Modify PreviewPanel to not stop previous app when switching
- Clean up all running apps on app quit

Closes dyad-sh#2819

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@wwwillchen
Copy link
Collaborator

@BugBot run

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the application's multitasking capabilities by allowing users to run multiple Dyad apps simultaneously without interruption when switching between them. It introduces a robust garbage collection system to manage resources efficiently by automatically terminating idle apps after a set period, while ensuring the currently active app remains untouched. Additionally, it improves application shutdown behavior by ensuring all background processes are properly terminated.

Highlights

  • Concurrent App Execution: Multiple Dyad applications can now run simultaneously, removing the previous limitation of stopping an app when switching to another.
  • Garbage Collection for Idle Apps: A new garbage collection mechanism automatically stops idle applications after 10 minutes, unless they are currently selected by the user.
  • Last Viewed Timestamp Tracking: Implemented tracking of lastViewedAt timestamps for running apps to determine their eligibility for garbage collection.
  • Graceful Application Shutdown: Ensured all running applications are gracefully stopped when the main Dyad application quits, preventing orphaned processes.
Changelog
  • src/components/preview_panel/PreviewPanel.tsx
    • Updated React imports to use useCallback instead of useRef.
    • Removed the local runningAppIdRef and stopApp logic, delegating app stopping to the backend's garbage collection.
    • Implemented a notifyAppSelected callback to inform the backend about the currently selected app for garbage collection tracking.
    • Modified the useEffect hook to start the selected app and notify the backend, explicitly stating that apps are no longer stopped on switch and cleanup is handled by GC or app quit.
  • src/ipc/handlers/app_handlers.ts
    • Imported new process manager functions: setCurrentlySelectedAppId, updateAppLastViewed, and startAppGarbageCollection.
    • Added lastViewedAt: Date.now() to RunningAppInfo when an app is started, initializing its last viewed timestamp.
    • Registered new IPC handlers: selectAppForPreview to update the selected app ID and its lastViewedAt timestamp, and getRunningApps to retrieve a list of active app IDs.
    • Initiated the app garbage collection process upon registration of app handlers.
  • src/ipc/types/app.ts
    • Defined RunningAppsResponseSchema for the getRunningApps IPC contract's output.
    • Added new IPC contracts: selectAppForPreview for notifying the backend of app selection, and getRunningApps for querying running app IDs.
  • src/ipc/utils/process_manager.ts
    • Extended the RunningAppInfo interface with a lastViewedAt timestamp to track app activity.
    • Implemented updateAppLastViewed to refresh an app's last viewed time when it's selected.
    • Introduced setCurrentlySelectedAppId and getCurrentlySelectedAppId to manage the active app, which is exempt from garbage collection.
    • Developed garbageCollectIdleApps logic to identify and stop apps that have been idle for 10 minutes, excluding the currently selected app.
    • Added startAppGarbageCollection and stopAppGarbageCollection to manage the periodic execution of the garbage collection process.
  • src/main.ts
    • Imported runningApps, stopAppByInfo, and stopAppGarbageCollection from the process manager.
    • Modified the main application's will-quit event handler to stop the garbage collection timer and then iterate through and stop all currently running applications before the main application exits.
Activity
  • The pull request introduces significant new features and includes a detailed test plan in the description.
  • The changes were generated by a bot, indicating an automated process for feature development or integration.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

gemini-code-assist[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 27, 2026

Greptile Summary

Enabled multiple Dyad apps to run concurrently by removing automatic app stopping on switch, implementing a garbage collector that stops apps idle for 10+ minutes (excluding currently selected app), and adding proper cleanup on application quit.

Key changes:

  • Frontend no longer stops apps when switching or unmounting, instead notifies backend via IPC to track which app is selected
  • Backend tracks lastViewedAt timestamps and runs GC every 1 minute to stop idle apps
  • Proxy URLs are cached and re-emitted when switching back to running apps for instant preview restoration
  • Synchronous cleanup on quit ensures all running apps are properly terminated

Confidence Score: 4/5

  • This PR is mostly safe to merge with one known race condition
  • Core garbage collection logic is well-implemented with proper safeguards (recursive setTimeout, currently-selected app protection, synchronous quit handling). The implementation correctly handles edge cases like re-emitting proxy URLs and cleaning up on quit. Score reduced by 1 due to the race condition in app selection IPC calls noted in previous review thread.
  • Pay close attention to PreviewPanel.tsx due to the race condition when rapidly switching apps

Important Files Changed

Filename Overview
src/components/preview_panel/PreviewPanel.tsx Removed app stopping logic when switching/unmounting, added IPC call to notify backend of selected app for GC tracking
src/ipc/handlers/app_handlers.ts Added GC tracking handlers, stores proxy URLs for re-emission when switching to already-running apps, starts garbage collection on handler registration
src/ipc/utils/process_manager.ts Implemented garbage collection system with proper safeguards: tracks lastViewedAt, uses recursive setTimeout, protects currently selected app, handles synchronous cleanup on quit

Sequence Diagram

sequenceDiagram
    participant User
    participant PreviewPanel
    participant Backend
    participant GC as Garbage Collector
    participant RunningApps as Running Apps Map

    Note over Backend,GC: App Startup
    Backend->>GC: startAppGarbageCollection()
    GC->>GC: Schedule next run in 1 min

    Note over User,RunningApps: User switches to App 1
    User->>PreviewPanel: Select App 1
    PreviewPanel->>Backend: selectAppForPreview(appId: 1)
    Backend->>RunningApps: setCurrentlySelectedAppId(1)
    Backend->>RunningApps: updateAppLastViewed(1)
    PreviewPanel->>Backend: runApp(1)
    Backend->>RunningApps: Add app 1 with lastViewedAt=now

    Note over User,RunningApps: User switches to App 2 (App 1 keeps running)
    User->>PreviewPanel: Select App 2
    PreviewPanel->>Backend: selectAppForPreview(appId: 2)
    Backend->>RunningApps: setCurrentlySelectedAppId(2)
    Backend->>RunningApps: updateAppLastViewed(2)
    PreviewPanel->>Backend: runApp(2)
    Backend->>RunningApps: Add app 2 with lastViewedAt=now
    
    Note over GC,RunningApps: After 10+ minutes of App 1 being idle
    GC->>GC: garbageCollectIdleApps()
    GC->>RunningApps: Check all apps
    RunningApps-->>GC: App 1: idle 10+ min, not selected
    RunningApps-->>GC: App 2: selected (protected)
    GC->>RunningApps: stopAppByInfo(app 1)
    GC->>GC: Schedule next run in 1 min

    Note over User,Backend: User quits application
    User->>Backend: Quit
    Backend->>GC: stopAppGarbageCollection()
    Backend->>RunningApps: stopAllAppsSync()
    RunningApps->>RunningApps: Send SIGTERM to all apps
Loading

Last reviewed commit: 06a9a08

greptile-apps[bot]

This comment was marked as resolved.

cubic-dev-ai[bot]

This comment was marked as resolved.

@github-actions
Copy link
Contributor

🎭 Playwright Test Results

❌ Some tests failed

OS Passed Failed Flaky Skipped
🍎 macOS 233 3 5 6

Summary: 233 passed, 3 failed, 5 flaky, 6 skipped

Failed Tests

🍎 macOS

  • fix_error.spec.ts > fix error with AI
    • Error: expect(locator).toMatchAriaSnapshot(expected) failed
  • refresh.spec.ts > refresh preserves current route
    • Error: expect(locator).toBeVisible() failed
  • select_component.spec.ts > select component next.js
    • Error: expect(locator).toBeVisible() failed

📋 Re-run Failing Tests (macOS)

Copy and paste to re-run all failing spec files locally:

npm run e2e \
  e2e-tests/fix_error.spec.ts \
  e2e-tests/refresh.spec.ts \
  e2e-tests/select_component.spec.ts

⚠️ Flaky Tests

🍎 macOS

  • logs_server.spec.ts > system messages UI shows server logs with correct type (passed after 1 retry)
  • partial_response.spec.ts > partial message is resumed (passed after 1 retry)
  • setup_flow.spec.ts > Setup Flow > setup banner shows correct state when node.js is installed (passed after 1 retry)
  • setup.spec.ts > setup ai provider (passed after 1 retry)
  • undo.spec.ts > undo (passed after 1 retry)

📊 View full report

github-actions bot and others added 2 commits February 27, 2026 23:36
- Fix async will-quit handler: make synchronous to prevent orphaned processes
- Switch GC from setInterval to recursive setTimeout to prevent overlapping runs
- Use electron-log instead of console.log in process_manager for consistency
- Store and re-emit proxy URL for already-running apps to preserve preview
- Add cancellation pattern to notifyAppSelected to prevent race conditions
- Add stopAllAppsSync for synchronous cleanup during app quit

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@github-actions
Copy link
Contributor

✅ Claude Code completed successfully

Summary

  • Fixed 6 review comments (all from trusted bot reviewers)
  • No new CI failures introduced; existing E2E flakiness and claude-review permissions issue are pre-existing

Review Comments Addressed

Thread File Author Action
setInterval → recursive setTimeout for GC process_manager.ts:267 gemini-code-assist Fixed + Resolved
Use electron-log instead of console.log process_manager.ts:234 gemini-code-assist Fixed + Resolved
Async will-quit handler not awaited main.ts:641 devin-ai-integration Fixed + Resolved
Preserve preview URL for running apps PreviewPanel.tsx:95 chatgpt-codex-connector Fixed + Resolved
Race condition with notifyAppSelected PreviewPanel.tsx:104 greptile-apps Fixed + Resolved
Async will-quit not guaranteed to complete main.ts:618 cubic-dev-ai Fixed (duplicate of devin thread) + Resolved

CI Checks

Check Status Notes
Build ✅ Pass
Lint ✅ Pass Verified locally
claude-review ❌ Pre-existing Bot lacks write permissions (not fixable by code)
E2E shard 1 ❌ Pre-existing flaky fix_error — also fails on main
E2E shard 2 ❌ Pre-existing flaky refresh, undo — also fails on main
Details of code changes

Critical fix — async will-quit handler (Threads 3 & 6):
Electron's EventEmitter does not await async callbacks. The will-quit handler was changed from async back to synchronous. A new stopAllAppsSync() function sends SIGTERM to all running app processes synchronously (fire-and-forget) instead of awaiting stopAppByInfo(). This ensures ALL running apps get kill signals before Electron exits.

GC timer fix (Thread 1):
Replaced setInterval with recursive setTimeout to prevent overlapping executions of the async garbageCollectIdleApps() function. Each GC run now completes before the next one is scheduled.

Logging consistency (Thread 2):
Replaced all console.log/console.error/console.warn in process_manager.ts with electron-log scoped logger for consistency with the rest of the application.

Preview URL preservation (Thread 4):
Added proxyUrl and originalUrl fields to RunningAppInfo. When the proxy server starts, the URL is stored. When runApp is called for an already-running app, the stored proxy URL is re-emitted so the frontend can restore the preview instead of showing "Starting your app server..."

Race condition fix (Thread 5):
Added a cancellation pattern to the useEffect in PreviewPanel.tsx that handles app selection. If the user rapidly switches apps, the cleanup function sets cancelled = true to prevent stale notifyAppSelected callbacks from triggering runApp for the wrong app.


Workflow run

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/ipc/utils/process_manager.ts">

<violation number="1" location="src/ipc/utils/process_manager.ts:301">
P1: `spawn("docker", ...)` is used without an `error` listener in `stopAllAppsSync`, which can raise an unhandled `error` event and crash the app on quit if Docker is unavailable.</violation>
</file>

<file name="src/ipc/handlers/app_handlers.ts">

<violation number="1" location="src/ipc/handlers/app_handlers.ts:1023">
P1: Re-emitting a cached proxy URL for already-running apps can emit stale/dead URLs after another app start terminates the previous global proxy worker.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

if (appInfo.isDocker) {
const containerName = appInfo.containerName || `dyad-app-${appId}`;
// Fire-and-forget: spawn docker stop without awaiting
spawn("docker", ["stop", containerName], { stdio: "ignore" });
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 27, 2026

Choose a reason for hiding this comment

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

P1: spawn("docker", ...) is used without an error listener in stopAllAppsSync, which can raise an unhandled error event and crash the app on quit if Docker is unavailable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/ipc/utils/process_manager.ts, line 301:

<comment>`spawn("docker", ...)` is used without an `error` listener in `stopAllAppsSync`, which can raise an unhandled `error` event and crash the app on quit if Docker is unavailable.</comment>

<file context>
@@ -217,51 +222,95 @@ export async function garbageCollectIdleApps(): Promise<void> {
+    if (appInfo.isDocker) {
+      const containerName = appInfo.containerName || `dyad-app-${appId}`;
+      // Fire-and-forget: spawn docker stop without awaiting
+      spawn("docker", ["stop", containerName], { stdio: "ignore" });
+      logger.info(`Sent docker stop for app ${appId} (${containerName})`);
+    } else if (appInfo.process.pid) {
</file context>
Suggested change
spawn("docker", ["stop", containerName], { stdio: "ignore" });
const stop = spawn("docker", ["stop", containerName], { stdio: "ignore" });
stop.on("error", (err) => {
logger.warn(`Failed to stop docker container for app ${appId} (${containerName}): ${err.message}`);
});
Fix with Cubic

logger.debug(`App ${appId} is already running.`);
// Re-emit the proxy URL so the frontend can restore the preview
const appInfo = runningApps.get(appId);
if (appInfo?.proxyUrl && appInfo?.originalUrl) {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 27, 2026

Choose a reason for hiding this comment

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

P1: Re-emitting a cached proxy URL for already-running apps can emit stale/dead URLs after another app start terminates the previous global proxy worker.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/ipc/handlers/app_handlers.ts, line 1023:

<comment>Re-emitting a cached proxy URL for already-running apps can emit stale/dead URLs after another app start terminates the previous global proxy worker.</comment>

<file context>
@@ -1012,6 +1018,15 @@ export function registerAppHandlers() {
         logger.debug(`App ${appId} is already running.`);
+        // Re-emit the proxy URL so the frontend can restore the preview
+        const appInfo = runningApps.get(appId);
+        if (appInfo?.proxyUrl && appInfo?.originalUrl) {
+          safeSend(event.sender, "app:output", {
+            type: "stdout",
</file context>
Fix with Cubic

Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

View 10 additional findings in Devin Review.

Open in Devin Review

Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 Singleton proxyWorker is terminated when a second app starts, breaking the first app's preview

With the new concurrent app model, multiple apps can run simultaneously. However, proxyWorker is a single module-level variable. When executeApp is called for any app, it unconditionally terminates the existing proxy worker at lines 177-179. This means starting App B kills App A's proxy, silently breaking App A's preview iframe.

Root cause and impact

The proxy worker forwards requests from the preview iframe to each app's local dev server. It is started per-app in listenToProcess at src/ipc/handlers/app_handlers.ts:349 when a localhost URL is detected in stdout:

proxyWorker = await startProxy(urlMatch[1], { ... });

But proxyWorker is a single let variable (src/ipc/handlers/app_handlers.ts:156). When executeApp is called for the second app, lines 177-179 terminate the previous worker:

if (proxyWorker) {
  proxyWorker.terminate();
  proxyWorker = null;
}

Previously this was fine because only one app ran at a time. Now with concurrent apps, this terminates App A's proxy when App B starts, making App A's preview unreachable. The proxyUrl/originalUrl stored in RunningAppInfo at src/ipc/handlers/app_handlers.ts:352-355 will reference a dead proxy, and re-emitting these URLs on app switch (src/ipc/handlers/app_handlers.ts:1023-1029) will point the iframe at a terminated worker.

Impact: The core feature of this PR (keeping multiple apps running) is undermined — only the most recently started app will have a working preview.

(Refers to lines 177-179)

Prompt for agents
The proxyWorker variable in src/ipc/handlers/app_handlers.ts (line 156) is a single module-level variable, but now multiple apps run concurrently and each needs its own proxy worker. To fix this:

1. Remove the singleton `let proxyWorker: Worker | null = null;` at line 156.
2. Remove the termination of proxyWorker in executeApp (lines 177-179).
3. Store the proxy Worker reference inside `RunningAppInfo` (in src/ipc/utils/process_manager.ts), alongside the existing `proxyUrl` and `originalUrl` fields.
4. When starting a new proxy in `listenToProcess` (line 349), store the worker in the app's `RunningAppInfo` entry rather than the module-level variable.
5. When stopping an app via `stopAppByInfo` in src/ipc/utils/process_manager.ts, also terminate that app's proxy worker if it exists.
6. Consider whether the proxy worker for an already-running app should be re-used (it currently would only be started once since the URL match only fires on process stdout).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +222 to +231
for (const appId of appsToStop) {
const appInfo = runningApps.get(appId);
if (appInfo) {
logger.info(`Garbage collecting idle app ${appId}`);
try {
await stopAppByInfo(appId, appInfo);
} catch (error) {
logger.error(`Failed to garbage collect app ${appId}:`, error);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 Garbage collector calls stopAppByInfo without withLock, racing with lock-protected app operations

The garbageCollectIdleApps function stops apps by calling stopAppByInfo directly, without acquiring the per-app lock via withLock. Meanwhile, runApp, stopApp, and restartApp handlers all protect their operations with withLock(appId, ...). This creates a race condition where GC and a user-initiated operation can operate on the same app concurrently.

Race condition scenario

Consider the following interleaving:

  1. GC reads appInfo from runningApps for app 1 and calls stopAppByInfo(1, appInfo) at src/ipc/utils/process_manager.ts:227
  2. stopAppByInfo calls await killProcess(appInfo.process) at src/ipc/utils/process_manager.ts:131, which awaits the process close/timeout
  3. During that await, the user triggers restartApp for app 1, which acquires the lock at src/ipc/handlers/app_handlers.ts:1122
  4. restartApp stops the old process, starts a new one, and adds the new entry to runningApps
  5. restartApp releases the lock
  6. GC's killProcess resolves; stopAppByInfo then executes runningApps.delete(appId) at src/ipc/utils/process_manager.ts:133
  7. This deletes the new app entry that restartApp just created

Impact: The freshly started app is removed from runningApps even though its process is still alive. The app becomes invisible to the system — it won't be tracked, can't be stopped via the UI, and its port won't be cleaned up properly. The orphaned process will continue running until the user quits Dyad.

Prompt for agents
In src/ipc/utils/process_manager.ts, the garbageCollectIdleApps function (lines 201-239) calls stopAppByInfo directly without acquiring the per-app lock. This races with lock-protected handlers (runApp, stopApp, restartApp) in src/ipc/handlers/app_handlers.ts.

To fix this, either:

Option A (recommended): Import and use withLock from src/ipc/utils/lock_utils.ts in the GC loop. Change the stop loop (lines 222-231) to acquire the lock for each app before stopping it:

  for (const appId of appsToStop) {
    try {
      await withLock(appId, async () => {
        const appInfo = runningApps.get(appId);
        if (appInfo) {
          // Re-check idle time under lock in case the app was viewed/restarted while waiting
          const now = Date.now();
          if (appId !== currentlySelectedAppId && (now - appInfo.lastViewedAt) >= IDLE_TIMEOUT_MS) {
            await stopAppByInfo(appId, appInfo);
          }
        }
      });
    } catch (error) {
      logger.error(`Failed to garbage collect app ${appId}:`, error);
    }
  }

Option B: Use removeAppIfCurrentProcess instead of the unconditional runningApps.delete in stopAppByInfo, so that if the process was replaced, the new entry won't be deleted. However, this requires changing stopAppByInfo's semantics which may affect other callers.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 06a9a081e2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +1023 to +1027
if (appInfo?.proxyUrl && appInfo?.originalUrl) {
safeSend(event.sender, "app:output", {
type: "stdout",
message: `[dyad-proxy-server]started=[${appInfo.proxyUrl}] original=[${appInfo.originalUrl}]`,
appId,

Choose a reason for hiding this comment

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

P1 Badge Keep proxy workers scoped per running app

This branch re-emits a cached proxy URL for already-running apps, but after enabling concurrent app processes that URL can be stale: executeApp still unconditionally terminates the singleton proxyWorker before starting another app (src/ipc/handlers/app_handlers.ts:177-179). In the flow “run A, run B, switch back to A”, app A stays in runningApps but its proxy worker has been killed, so this message restores a dead URL and the preview fails to load. Please track proxy workers per app (or stop terminating a global worker) before treating cached URLs as reusable.

Useful? React with 👍 / 👎.

Comment on lines +185 to +187
if (appId !== null) {
updateAppLastViewed(appId);
}

Choose a reason for hiding this comment

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

P2 Badge Update idle timestamp when changing selected app

setCurrentlySelectedAppId only refreshes lastViewedAt for the newly selected app, so the app being deselected keeps an old timestamp. Because garbage collection compares now - lastViewedAt, an app that was open for more than 10 minutes can be collected almost immediately after switching away (next GC tick) even though it was just viewed. Update the previous selection's lastViewedAt on selection change (or on deselection) so “idle for 10+ minutes” is measured from when viewing actually stopped.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

allow users to run multiple dyad apps

2 participants