Skip to content

Fix Docker Compose CORS issues with nginx API proxying#793

Merged
gsxdsm merged 8 commits intoAutoMaker-Org:v0.15.0rcfrom
gsxdsm:fix/docker-compose-cors-error
Feb 21, 2026
Merged

Fix Docker Compose CORS issues with nginx API proxying#793
gsxdsm merged 8 commits intoAutoMaker-Org:v0.15.0rcfrom
gsxdsm:fix/docker-compose-cors-error

Conversation

@gsxdsm
Copy link
Copy Markdown
Collaborator

@gsxdsm gsxdsm commented Feb 21, 2026

Summary

  • Fixes [Bug]: TypeScript compilation error when running docker compose setup after fresh git clone #786
  • Removes hardcoded VITE_SERVER_URL to enable relative URLs, allowing nginx to proxy API requests without CORS issues
  • Adds nginx reverse proxy configuration to route API and WebSocket requests to the backend server container
  • Refactors CORS validation logic to support local/private network addresses and improve code organization
  • Changes default CORS_ORIGIN environment variable to be unset, relying on automatic local network detection

Changes

  • Dockerfile: Changed VITE_SERVER_URL default from http://localhost:3008 to empty string, enabling relative URLs that nginx can proxy. Updated comments to explain Docker Compose CORS avoidance strategy.

  • apps/ui/nginx.conf: Added reverse proxy configuration to route /api/ requests to the backend server container (http://server:3008). Includes proper handling of WebSocket upgrades via the $connection_upgrade map for features like /api/events and /api/terminal/ws. Forwards necessary headers (Host, X-Real-IP, X-Forwarded-For, X-Forwarded-Proto) for correct origin detection.

  • apps/server/src/index.ts:

    • Extracted local origin validation into reusable isLocalOrigin() function to reduce code duplication and improve maintainability
    • Improved CORS origin handling to filter empty strings from comma-separated CORS_ORIGIN values
    • Refactored logic to fall through to local network check when explicit CORS origins don't match, allowing both whitelisted and local origins simultaneously
    • Simplified error handling by removing unnecessary try-catch and relying on the extracted function
  • docker-compose.yml: Changed CORS_ORIGIN default from http://localhost:3007 to empty string. Updated comments to clarify that with nginx proxying, CORS is not needed for same-origin API access and should only be set when accessing from a different domain.


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **Chores**
  * Updated container defaults and nginx proxying (including WebSocket support) to reduce cross-origin issues in container setups.

* **New Features**
  * Bot badges for PR review comments and dialogs; comment author detection improved.
  * Sessions scoped/persisted per worktree with working-directory info surfaced.
  * Dev server start now opens Dev Server Logs automatically; Start/Stop actions use split-button menus.
  * Branch badge shown in agent header.
  * Crash-loop detection: option to clear UI cache & reload.

* **Bug Fixes**
  * Worktree deletion now falls back to main to avoid stale views.
  * UI cache restore sanitized to avoid restoring invalid worktree selections.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 21, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changed Docker/compose defaults to empty so the UI uses relative URLs proxied by nginx; nginx now proxies /api/ (including WebSocket upgrades) to the server; server CORS handling refactored with a new isLocalOrigin helper to permit local/private origins.

Changes

Cohort / File(s) Summary
Docker & Compose config
Dockerfile, docker-compose.yml
Default VITE_SERVER_URL/CORS_ORIGIN changed from explicit localhost to empty; comments explain empty causes UI to use relative URLs proxied by nginx; values remain overridable.
UI nginx proxy
apps/ui/nginx.conf
Added $http_upgrade$connection_upgrade map and new /api/ proxy to http://server:3008 with HTTP/1.1, Upgrade/Connection, Host, X-Real-IP, and X-Forwarded-* headers to support API and WebSocket upgrades.
Server CORS handling
apps/server/src/index.ts
Added isLocalOrigin(origin: string) helper; tightened allowedOrigins parsing; changed flow to consult local/private origin detection before rejecting CORS.
PR review comments & GraphQL/REST pagination
apps/server/src/services/pr-review-comments.service.ts
Added isBot?: boolean; introduced executeGraphQL helper, capped pagination (MAX_PAGINATION_PAGES), switched some comment fetches to REST (preserve bot/app authors), merged multiple comment sources, and added truncation/logging.
Session/worktree scoping & types
libs/types/src/session.ts, apps/ui/src/types/electron.d.ts, apps/ui/src/lib/electron.ts, apps/ui/src/components/session-manager.tsx, apps/ui/src/components/views/agent-view/..., apps/ui/src/components/views/agent-view/hooks/use-agent-session.ts
Added optional workingDirectory to session types and threaded workingDirectory through SessionManager, useAgentSession, and AgentView for per-worktree persistence and scoping (persistenceKey, filtering, create/delete flows).
UI behavior & controls
apps/ui/src/components/dialogs/pr-comment-resolution-dialog.tsx, apps/ui/src/components/ui/app-error-boundary.tsx, apps/ui/src/store/ui-cache-store.ts, apps/ui/src/components/views/board-view*.tsx, apps/ui/src/components/views/board-view/hooks/use-board-actions.ts, apps/ui/src/components/views/board-view/worktree-panel/*
UI tweaks: bot badges, consolidated disclosure/expand controls, crash-loop detection with Clear Cache flow, sanitizing cached worktree restores, fallback to main worktree on missing selection, omit transient fields on duplications, dev-server split-button + open-logs-before-start behavior, and minor error/toast improvements.
Dev server logs / actions
apps/ui/src/components/views/board-view/worktree-panel/worktree-actions-dropdown.tsx, apps/ui/src/components/views/board-view/worktree-panel/hooks/use-dev-servers.ts, apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx
Introduced View Dev Server Logs action in submenus, converted Start/Stop to split-buttons with logs submenu, and show logs panel before starting dev server; improved toast error description handling.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Nginx as UI/nginx
    participant Server as API:server:3008

    Client->>Nginx: HTTP /api/... request
    alt WebSocket upgrade
        Client->>Nginx: Upgrade: websocket + Connection: upgrade
        Nginx->>Server: proxy (HTTP/1.1, Upgrade/Connection, X-Forwarded-*)
        Server-->>Nginx: accept WS upgrade
        Nginx-->>Client: WS established
    else Regular HTTP
        Client->>Nginx: HTTP request (same-origin)
        Nginx->>Server: proxy HTTP request (X-Forwarded-*)
        Server->>Server: check CORS (allowedOrigins / isLocalOrigin)
        Server-->>Nginx: HTTP response
        Nginx-->>Client: HTTP response
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

Bug, scope: ui, Refactor

Poem

🐰 I hopped through configs, neat and spry,
nginx bridges APIs under sky,
WebSockets wiggled, CORS learned to bend,
Sessions scoped to trees — a tidy blend,
A carrot-cheer for builds that fly!

🚥 Pre-merge checks | ✅ 1 | ❌ 4

❌ Failed checks (4 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning Title describes Docker Compose and nginx API proxying but linked issue #786 is about TypeScript compilation error in dev-server-service.ts, not CORS. Update title to address the actual root cause (TypeScript compilation error resolution) or clarify the connection between CORS fixes and the compilation issue.
Linked Issues check ⚠️ Warning Issue #786 requires resolving TS2345 error in dev-server-service.ts for EventType, but changeset makes CORS/nginx changes without addressing the TypeScript compilation error. Add changes to fix the TS2345 compilation error in apps/server/src/services/dev-server-service.ts by updating EventType or the emission site.
Out of Scope Changes check ⚠️ Warning Multiple out-of-scope changes detected: PR review comments service refactoring, session manager worktree scoping, UI dialog layout changes, app error boundary crash-loop detection, and board view worktree handling are unrelated to the linked issue #786. Remove out-of-scope changes unrelated to fixing the TypeScript compilation error, or split into separate focused PRs.
Docstring Coverage ⚠️ Warning Docstring coverage is 35.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @gsxdsm, 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 addresses and resolves Cross-Origin Resource Sharing (CORS) issues within Docker Compose environments by introducing an Nginx reverse proxy for API requests. The changes streamline the interaction between the frontend UI and the backend API by allowing relative URLs, thereby eliminating the need for explicit CORS configurations in many common deployment scenarios. Additionally, the backend's CORS validation logic has been enhanced for better maintainability and flexibility in handling local and private network origins.

Highlights

  • CORS Resolution Strategy: Implemented a new strategy to resolve CORS issues in Docker Compose setups by leveraging Nginx as an API proxy.
  • Frontend Configuration: Modified the VITE_SERVER_URL in the Dockerfile to be empty by default, enabling the UI to use relative URLs that Nginx can then proxy.
  • Nginx Proxy Setup: Added Nginx configuration to apps/ui/nginx.conf to reverse proxy /api/ requests to the backend server, including proper handling for WebSocket upgrades.
  • Backend CORS Logic Refinement: Refactored the backend's CORS validation in apps/server/src/index.ts to include a dedicated isLocalOrigin function, support filtering empty CORS_ORIGIN values, and allow simultaneous whitelisted and local network origins.
  • Environment Variable Defaults: Updated docker-compose.yml to set the default CORS_ORIGIN to an empty string, promoting automatic local network detection over explicit configuration when using the Nginx proxy.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • Dockerfile
    • Changed the default VITE_SERVER_URL argument from "http://localhost:3008" to an empty string.
    • Updated comments to explain that an empty VITE_SERVER_URL enables relative URLs for Nginx proxying, avoiding CORS issues.
  • apps/server/src/index.ts
    • Introduced a new isLocalOrigin function to encapsulate logic for identifying local/private network origins.
    • Modified the CORS origin handling to filter out empty strings from the CORS_ORIGIN environment variable.
    • Refactored the CORS callback logic to first check explicit CORS_ORIGIN values, then fall through to the isLocalOrigin check, allowing both to be active.
    • Removed a try-catch block around URL parsing, relying on the new isLocalOrigin function for error handling.
  • apps/ui/nginx.conf
    • Added a map directive for $http_upgrade to $connection_upgrade to handle WebSocket connections.
    • Introduced a location /api/ block to proxy requests to http://server:3008.
    • Configured proxy_http_version, Upgrade, Connection, Host, X-Real-IP, X-Forwarded-For, and X-Forwarded-Proto headers for proper proxying, including WebSocket upgrades.
  • docker-compose.yml
    • Changed the default value for the CORS_ORIGIN environment variable from "http://localhost:3007" to an empty string.
    • Updated comments to clarify that CORS is generally not needed with Nginx proxying for same-origin access and should only be set for different domains.
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.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request effectively resolves CORS issues in the Docker Compose environment by introducing an nginx reverse proxy. The changes are well-thought-out, including updates to the Dockerfile for relative URL usage in the UI, a solid nginx proxy configuration, and refined server-side CORS logic. I have one suggestion to improve the precision of the private IP address validation in the CORS handling logic for better security and correctness.

Comment thread apps/server/src/index.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
apps/ui/nginx.conf (1)

15-24: Consider adding timeouts and disabling buffering for WebSocket/streaming connections.

Nginx's default proxy_read_timeout is 60s, which will silently close idle WebSocket connections (e.g., /api/events) if no data is sent within that window. The terminal WebSocket has a ping/pong mechanism, but the events WebSocket may go quiet for extended periods. Also, proxy_buffering off is recommended for streaming/WebSocket endpoints to avoid latency.

Proposed fix
 location /api/ {
     proxy_pass http://server:3008;
     proxy_http_version 1.1;
     proxy_set_header Upgrade $http_upgrade;
     proxy_set_header Connection $connection_upgrade;
     proxy_set_header Host $host;
     proxy_set_header X-Real-IP $remote_addr;
     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
     proxy_set_header X-Forwarded-Proto $scheme;
+    proxy_read_timeout 86400s;
+    proxy_send_timeout 86400s;
+    proxy_buffering off;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/nginx.conf` around lines 15 - 24, The /api/ location block currently
risks silent WebSocket disconnects and buffering-induced latency; update the
location /api/ block to disable buffering and increase timeouts for
long-lived/streaming connections by adding directives such as proxy_buffering
off and raising proxy_read_timeout (e.g., several minutes), plus sensible
proxy_send_timeout and proxy_connect_timeout values; keep the existing WebSocket
headers (proxy_set_header Upgrade and Connection) intact so the changes apply
only to timeouts/buffering within the same location /api/ block.
apps/server/src/index.ts (1)

300-315: Verify the intended CORS behavior: explicit origins + local origins are now always allowed simultaneously.

Previously, when CORS_ORIGIN was set and the request origin didn't match, the request was immediately rejected. Now it falls through to the isLocalOrigin check, meaning local/private network origins are always allowed regardless of CORS_ORIGIN configuration. This is fine for the Docker Compose use case, but be aware that operators cannot use CORS_ORIGIN as an exhaustive allowlist to lock out local origins — which may matter in shared or multi-tenant hosting environments.

A brief inline comment above line 303 documenting this design choice (explicit origins + local origins are both always allowed) would help future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/index.ts` around lines 300 - 315, The change allows both
explicit CORS_ORIGIN entries and any local/private origins via
isLocalOrigin(origin), which means local origins are always permitted even if
they aren’t in allowedOrigins; add a concise inline comment just above the block
(near the allowedOrigins / isLocalOrigin checks) stating this design decision so
future maintainers know that CORS_ORIGIN is not an exhaustive deny-list and
local/loopback/private origins are intentionally always allowed (reference
variables/functions: allowedOrigins, process.env.CORS_ORIGIN, isLocalOrigin,
origin, callback).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/server/src/index.ts`:
- Around line 272-288: The isLocalOrigin function currently treats any hostname
starting with "172." as local; change that check to only accept 172.16.0.0/12 —
implement a precise check in isLocalOrigin by parsing the hostname when it looks
like an IPv4 address and either (a) split on dots and ensure the first octet ===
172 and the second octet is between 16 and 31 inclusive, or (b) convert the IPv4
to an integer and test it falls within the 172.16.0.0–172.31.255.255 range;
update the hostname.startsWith('172.') branch to use this stricter test so only
RFC1918 172/12 addresses are considered local.

---

Nitpick comments:
In `@apps/server/src/index.ts`:
- Around line 300-315: The change allows both explicit CORS_ORIGIN entries and
any local/private origins via isLocalOrigin(origin), which means local origins
are always permitted even if they aren’t in allowedOrigins; add a concise inline
comment just above the block (near the allowedOrigins / isLocalOrigin checks)
stating this design decision so future maintainers know that CORS_ORIGIN is not
an exhaustive deny-list and local/loopback/private origins are intentionally
always allowed (reference variables/functions: allowedOrigins,
process.env.CORS_ORIGIN, isLocalOrigin, origin, callback).

In `@apps/ui/nginx.conf`:
- Around line 15-24: The /api/ location block currently risks silent WebSocket
disconnects and buffering-induced latency; update the location /api/ block to
disable buffering and increase timeouts for long-lived/streaming connections by
adding directives such as proxy_buffering off and raising proxy_read_timeout
(e.g., several minutes), plus sensible proxy_send_timeout and
proxy_connect_timeout values; keep the existing WebSocket headers
(proxy_set_header Upgrade and Connection) intact so the changes apply only to
timeouts/buffering within the same location /api/ block.

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/server/src/index.ts`:
- Around line 299-315: The CORS origin handling incorrectly checks only
allowedOrigins[0] for '*' and thus skips wildcard handling; update the logic in
the origin-checking block (where allowedOrigins is derived from
process.env.CORS_ORIGIN and isLocalOrigin is used) to: detect wildcard with
allowedOrigins.includes('*') and handle it explicitly by calling callback(null,
true) and returning; otherwise, if allowedOrigins includes the incoming origin
call callback(null, origin) and return; if neither, fall through to the existing
isLocalOrigin check as before.

Comment thread apps/server/src/index.ts
gsxdsm and others added 2 commits February 21, 2026 11:10
…AutoMaker-Org#792)

* Changes from fix/delete-worktree-hotifx

* fix: Improve bot detection and prevent UI overflow issues

- Include GitHub app-initiated comments in bot detection
- Wrap handleQuickCreateSession with useCallback to fix dependency issues
- Truncate long branch names in agent header to prevent layout overflow

* feat: Support GitHub App comments in PR review and fix session filtering

* feat: Return invalidation result from delete session handler
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/server/src/services/pr-review-comments.service.ts (1)

271-283: ⚠️ Potential issue | 🔴 Critical

Fix gh api --paginate JSON parsing failure on paginated results.

The code uses gh api --paginate without --slurp on three REST endpoints that return JSON arrays (lines 271–283, 314–326, and 373–385). With gh CLI 2.63.2 (pinned in Dockerfile.dev), --paginate outputs multiple separate JSON arrays sequentially (e.g., [...][...]), which is not valid JSON and will cause JSON.parse() to throw a SyntaxError when a PR exceeds the page size (typically ~30 items).

Add --slurp to each gh api call and flatten the result:

Example fix for lines 271–283
const { stdout: commentsOutput } = await execFileAsync(
  'gh',
  ['api', issueCommentsEndpoint, '--paginate', '--slurp'],
  { ... }
);

const pagesData = JSON.parse(commentsOutput);
const commentsData = pagesData.flat();  // Flatten [[...page1...], [...page2...]] → [...]

Apply the same pattern to the inline review comments (line 314) and review body comments (line 373) fetches.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/services/pr-review-comments.service.ts` around lines 271 -
283, The gh CLI --paginate calls that fetch arrays must add the --slurp flag and
flatten the parsed result to avoid JSON parse errors when multiple pages are
output; update the three execFileAsync calls that call 'gh api' for issue
comments, inline review comments, and review body comments (look for variables
issueCommentsEndpoint, the inline review comments fetch, and the review body
comments fetch) to include '--slurp' in the args, parse the stdout into
pagesData (JSON.parse) and then set commentsData (or the equivalent variable) to
pagesData.flat() before further use.
🧹 Nitpick comments (6)
apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx (1)

402-452: LGTM — new split-button pattern is consistent with the rest of the file.

Both the Stop (lines 402-420) and Start (lines 425-452) dev server blocks follow the established DropdownMenuSub + flex-div + DropdownMenuItem/DropdownMenuSubTrigger split-button convention already used throughout this component (editor, terminal, pull/push, commits, stash, PR). The disabled/opacity-50/cursor-not-allowed handling on the Start sub-trigger correctly mirrors the primary item's loading state.

One minor point: the "View Dev Server Logs" DropdownMenuItem is duplicated verbatim in both DropdownMenuSubContent blocks. If this action's label, icon, or callback ever changes, both copies need to stay in sync.

♻️ Extract the shared log item to eliminate the duplication
+        {/* Shared submenu item — usable in both running and not-running states */}
+        {(() => {
+          const viewLogsItem = (
+            <DropdownMenuItem onClick={() => onViewDevServerLogs(worktree)} className="text-xs">
+              <ScrollText className="w-3.5 h-3.5 mr-2" />
+              View Dev Server Logs
+            </DropdownMenuItem>
+          );

Or, more idiomatically, hoist it as a local constant above the return:

+  const devServerLogsItem = (
+    <DropdownMenuItem onClick={() => onViewDevServerLogs(worktree)} className="text-xs">
+      <ScrollText className="w-3.5 h-3.5 mr-2" />
+      View Dev Server Logs
+    </DropdownMenuItem>
+  );

Then replace both DropdownMenuSubContent bodies:

-              <DropdownMenuSubContent>
-                <DropdownMenuItem onClick={() => onViewDevServerLogs(worktree)} className="text-xs">
-                  <ScrollText className="w-3.5 h-3.5 mr-2" />
-                  View Dev Server Logs
-                </DropdownMenuItem>
-              </DropdownMenuSubContent>
+              <DropdownMenuSubContent>{devServerLogsItem}</DropdownMenuSubContent>

(applied to both the running and not-running branches)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx`
around lines 402 - 452, Duplicate "View Dev Server Logs" DropdownMenuItem in the
two DropdownMenuSubContent blocks should be hoisted to a single reusable
constant to avoid drift; create a local constant (e.g., viewDevServerLogsItem)
above the component return that returns the DropdownMenuItem using
onViewDevServerLogs(worktree), the ScrollText icon and the same className, then
replace both DropdownMenuSubContent bodies with that constant (references:
DropdownMenuSubContent, DropdownMenuItem, onViewDevServerLogs, ScrollText).
apps/ui/src/components/views/board-view/worktree-panel/hooks/use-dev-servers.ts (1)

459-465: Toast descriptions assume the parent opens the log panel

The descriptions 'Logs are now visible in the dev server panel.' (success) and 'Check the dev server logs panel for details.' (failure) embed an assumption that the caller has already opened the log panel. This is only true when handleStartDevServer is called through the handleStartDevServerAndShowLogs wrapper in worktree-panel.tsx. Any future consumer that calls handleStartDevServer directly will show an inaccurate message.

Consider either removing the panel-specific phrasing from the hook and letting the wrapper add it, or accepting the coupling as intentional and documenting it:

♻️ Option: neutral descriptions in the hook, wrapper adds the panel hint

In use-dev-servers.ts:

- toast.success('Dev server started, detecting port...', {
-   description: 'Logs are now visible in the dev server panel.',
- });
+ toast.success('Dev server started, detecting port...');
- toast.error(result.error || 'Failed to start dev server', {
-   description: 'Check the dev server logs panel for details.',
- });
+ toast.error(result.error || 'Failed to start dev server');

In worktree-panel.tsx (inside the wrapper):

  setLogPanelWorktree(worktree);
  setLogPanelOpen(true);
+ // toast shown by handleStartDevServer; description is accurate because the panel is now open
  await handleStartDevServer(worktree);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/ui/src/components/views/board-view/worktree-panel/hooks/use-dev-servers.ts`
around lines 459 - 465, The toast descriptions in handleStartDevServer currently
reference the dev server logs panel, which assumes callers opened that panel;
remove panel-specific phrasing from use-dev-servers.ts (update the success and
error toast descriptions to neutral messages like "Dev server started" and
"Failed to start dev server, check logs") and move the panel-specific messages
into the wrapper function handleStartDevServerAndShowLogs in worktree-panel.tsx
(have that wrapper call handleStartDevServer and then show an additional toast
or description that mentions the dev server logs panel), or alternatively add a
clear comment/docs on handleStartDevServer stating it requires the caller to
open the logs panel if they rely on panel-specific descriptions.
apps/ui/src/components/views/board-view.tsx (1)

489-504: Transient isCurrent: false on the fallback main-worktree object

When the fallback fires (line 491-493), found becomes the main worktree but currentWorktreePath is still non-null (pointing to the deleted path). The isCurrent computation at lines 499-502 is:

found.isCurrent ?? (currentWorktreePath !== null
  ? pathsEqual(found.path, currentWorktreePath)  // → false (main ≠ deleted path)
  : found.isMain)

If the main worktree's stored isCurrent field is null/undefined, the fallback expression evaluates to false, so selectedWorktree is returned with isCurrent: false during the window between the deletion event and the onDeleted reset (lines 1961-1969). This is passed to useAutoMode(selectedWorktree) at line 508.

In practice this is a single-render transient state before setCurrentWorktree(…, null, mainBranch) fires, so it's unlikely to be user-visible. A minimal guard would make the intent explicit and remove the subtle dependency on the store having isCurrent pre-populated for the main worktree:

🔧 Suggested clarification
-      if (!found) {
-        found = worktrees.find((w) => w.isMain);
-      }
+      if (!found) {
+        // Deleted worktree – fall back to main for this render cycle.
+        // onDeleted will call setCurrentWorktree(…, null) to reset properly.
+        found = worktrees.find((w) => w.isMain);
+      }

And in the return block, explicitly mark it current when we know we're falling back:

     return {
       ...found,
       isCurrent:
-        found.isCurrent ??
-        (currentWorktreePath !== null ? pathsEqual(found.path, currentWorktreePath) : found.isMain),
+        found.isCurrent ??
+        (currentWorktreePath !== null
+          ? pathsEqual(found.path, currentWorktreePath) || found.isMain  // treat main as current during fallback
+          : found.isMain),
       hasWorktree: found.hasWorktree ?? true,
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/board-view.tsx` around lines 489 - 504, The
selectedWorktree fallback can briefly return the main worktree with isCurrent
=== false when currentWorktreePath still points at the deleted path; adjust the
memo to detect when you used the fallback main (the branch that sets found =
worktrees.find(w => w.isMain)) and explicitly set isCurrent: true for that
returned object (instead of relying on the pathsEqual expression). Update the
return construction (the object built from found) to compute isCurrent using the
fallback condition (using found.isMain && /* fallback-detected */) so
useAutoMode receives the main as current during that transient render; reference
symbols: found, worktrees.find(...), currentWorktreePath, pathsEqual, isMain,
isCurrent, and the memo that produces selectedWorktree.
apps/ui/src/components/views/board-view/hooks/use-board-actions.ts (1)

220-227: childDependencies leaks into newFeatureData via ...restFeatureData

childDependencies is a transient creation parameter (same category as initialStatus and workMode) but is not excluded from the destructure. It therefore ends up inside newFeatureData, gets passed to addFeature, and is potentially stored in the Zustand state and forwarded to the server via persistFeatureCreate. If the store spreads the object as-is, future duplications of that feature would propagate the stale childDependencies array further.

Destructure it out alongside the other transient fields and reference it explicitly in the child-dependency loop below (line 247):

♻️ Proposed fix
      const {
        initialStatus: requestedStatus,
        workMode: _workMode,
+       childDependencies,
        ...restFeatureData
      } = featureData;
      const initialStatus = requestedStatus || 'backlog';

Then update the child-dependency block to use the locally destructured binding instead of featureData.childDependencies:

-     if (featureData.childDependencies && featureData.childDependencies.length > 0) {
-       for (const childId of featureData.childDependencies) {
+     if (childDependencies && childDependencies.length > 0) {
+       for (const childId of childDependencies) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/board-view/hooks/use-board-actions.ts` around
lines 220 - 227, The transient childDependencies field is not being removed and
is leaking into newFeatureData via the rest spread; update the destructuring of
featureData to pull out childDependencies alongside initialStatus and workMode
(so restFeatureData no longer contains it), ensure newFeatureData is built from
...restFeatureData (excluding childDependencies), and modify the
child-dependency loop to reference the locally destructured childDependencies
variable instead of featureData.childDependencies when calling
addFeature/persistFeatureCreate.
apps/server/src/services/pr-review-comments.service.ts (2)

106-154: Well-structured GraphQL execution helper.

The timeout handling, stdin error suppression, and process lifecycle management are all handled correctly. A couple of minor observations:

  • Line 150: Only the first GraphQL error message is thrown; if the API returns multiple errors, the rest are silently dropped. Consider joining all error messages or at least logging them.
🔧 Optional: surface all GraphQL errors
   if (response.errors && response.errors.length > 0) {
-    throw new Error(response.errors[0].message);
+    const messages = response.errors.map((e) => e.message).join('; ');
+    throw new Error(messages);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/services/pr-review-comments.service.ts` around lines 106 -
154, The executeGraphQL function currently throws only the first GraphQL error
(response.errors[0].message), losing additional error details; update the error
handling after the gh process completes so that when response.errors exists you
aggregate all messages (e.g., map over response.errors and join them into a
single string) and throw a new Error containing the combined message (and
optionally log or include the full errors array for debugging) instead of
discarding all but the first entry.

270-283: Consider extracting a shared REST API fetch helper.

The three execFileAsync('gh', ['api', endpoint, '--paginate'], { cwd, env, maxBuffer, timeout }) blocks are nearly identical. A small helper (e.g., fetchGitHubRest(projectPath, endpoint)) would reduce duplication and centralize the maxBuffer/timeout configuration.

Also applies to: 313-324, 372-383

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/server/src/index.ts`:
- Around line 272-288: The IPv6 loopback check in isLocalOrigin uses hostname
=== '::1' but URL.hostname returns bracketed IPv6 like '[::1]', so change the
comparison to check for '[::1]' (or normalize hostname by trimming surrounding
brackets) in the isLocalOrigin function so IPv6 loopback addresses are correctly
recognized; update the hostname === '::1' check to hostname === '[::1]' (or
implement hostname.replace(/^\[|\]$/g, '') === '::1') within isLocalOrigin.

In `@apps/server/src/services/pr-review-comments.service.ts`:
- Around line 236-243: The pagination warning uses MAX_PAGINATION_PAGES * 100
but the loop actually allows one initial page plus MAX_PAGINATION_PAGES
additional pages, so update the logger.warn message in the block referencing
logger.warn (and symbols prNumber, owner, repo, cursor, pageCount,
MAX_PAGINATION_PAGES) to report the correct ceiling as (MAX_PAGINATION_PAGES +
1) * 100 (e.g., 2100) or rephrase to "more than (MAX_PAGINATION_PAGES + 1) * 100
review threads" so the logged count matches the implemented pagination
behaviour.

In `@apps/ui/src/components/session-manager.tsx`:
- Around line 301-306: The post-delete fallback picks any active session from
freshSessions and can jump to another worktree; after getting refetchResult in
invalidateSessions() and computing freshSessions/activeSessionsList (used when
currentSessionId === sessionId), further filter activeSessionsList to only
include sessions whose worktree/effective directory matches the current
worktree/effective directory variable in this component (e.g., compare
session.worktree or session.effectiveDirectory to the component's
effectiveDirectory/currentWorktree variable), then pick the fallback from that
filtered list so the new session remains within the current worktree scope.

In `@apps/ui/src/components/views/agent-view/hooks/use-agent-session.ts`:
- Around line 24-66: The reset effect that sets initialSessionLoadedRef.current
= false and setCurrentSessionId(null) runs unconditionally on mount and clears
whatever the restore effect (which reads getLastSelectedSession(persistenceKey)
and sets setCurrentSessionId) just restored; modify the reset effect to only run
when persistenceKey actually changes after initial mount (i.e., guard it with a
check comparing previous persistenceKey or track a didMount ref) so it doesn't
run immediately on first render, or reorder so the restore effect runs after the
guarded reset; update references to initialSessionLoadedRef, persistenceKey,
getLastSelectedSession, and setCurrentSessionId accordingly to ensure restored
session persists.

---

Outside diff comments:
In `@apps/server/src/services/pr-review-comments.service.ts`:
- Around line 271-283: The gh CLI --paginate calls that fetch arrays must add
the --slurp flag and flatten the parsed result to avoid JSON parse errors when
multiple pages are output; update the three execFileAsync calls that call 'gh
api' for issue comments, inline review comments, and review body comments (look
for variables issueCommentsEndpoint, the inline review comments fetch, and the
review body comments fetch) to include '--slurp' in the args, parse the stdout
into pagesData (JSON.parse) and then set commentsData (or the equivalent
variable) to pagesData.flat() before further use.

---

Nitpick comments:
In `@apps/server/src/services/pr-review-comments.service.ts`:
- Around line 106-154: The executeGraphQL function currently throws only the
first GraphQL error (response.errors[0].message), losing additional error
details; update the error handling after the gh process completes so that when
response.errors exists you aggregate all messages (e.g., map over
response.errors and join them into a single string) and throw a new Error
containing the combined message (and optionally log or include the full errors
array for debugging) instead of discarding all but the first entry.

In `@apps/ui/src/components/views/board-view.tsx`:
- Around line 489-504: The selectedWorktree fallback can briefly return the main
worktree with isCurrent === false when currentWorktreePath still points at the
deleted path; adjust the memo to detect when you used the fallback main (the
branch that sets found = worktrees.find(w => w.isMain)) and explicitly set
isCurrent: true for that returned object (instead of relying on the pathsEqual
expression). Update the return construction (the object built from found) to
compute isCurrent using the fallback condition (using found.isMain && /*
fallback-detected */) so useAutoMode receives the main as current during that
transient render; reference symbols: found, worktrees.find(...),
currentWorktreePath, pathsEqual, isMain, isCurrent, and the memo that produces
selectedWorktree.

In `@apps/ui/src/components/views/board-view/hooks/use-board-actions.ts`:
- Around line 220-227: The transient childDependencies field is not being
removed and is leaking into newFeatureData via the rest spread; update the
destructuring of featureData to pull out childDependencies alongside
initialStatus and workMode (so restFeatureData no longer contains it), ensure
newFeatureData is built from ...restFeatureData (excluding childDependencies),
and modify the child-dependency loop to reference the locally destructured
childDependencies variable instead of featureData.childDependencies when calling
addFeature/persistFeatureCreate.

In
`@apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx`:
- Around line 402-452: Duplicate "View Dev Server Logs" DropdownMenuItem in the
two DropdownMenuSubContent blocks should be hoisted to a single reusable
constant to avoid drift; create a local constant (e.g., viewDevServerLogsItem)
above the component return that returns the DropdownMenuItem using
onViewDevServerLogs(worktree), the ScrollText icon and the same className, then
replace both DropdownMenuSubContent bodies with that constant (references:
DropdownMenuSubContent, DropdownMenuItem, onViewDevServerLogs, ScrollText).

In
`@apps/ui/src/components/views/board-view/worktree-panel/hooks/use-dev-servers.ts`:
- Around line 459-465: The toast descriptions in handleStartDevServer currently
reference the dev server logs panel, which assumes callers opened that panel;
remove panel-specific phrasing from use-dev-servers.ts (update the success and
error toast descriptions to neutral messages like "Dev server started" and
"Failed to start dev server, check logs") and move the panel-specific messages
into the wrapper function handleStartDevServerAndShowLogs in worktree-panel.tsx
(have that wrapper call handleStartDevServer and then show an additional toast
or description that mentions the dev server logs panel), or alternatively add a
clear comment/docs on handleStartDevServer stating it requires the caller to
open the logs panel if they rely on panel-specific descriptions.

Comment thread apps/server/src/index.ts
Comment on lines +236 to 243
} while (cursor && pageCount < MAX_PAGINATION_PAGES);

// Check if reviewThreads data was truncated (more than 100 threads)
const pageInfo = response.data?.repository?.pullRequest?.reviewThreads?.pageInfo;
if (pageInfo?.hasNextPage) {
if (pageCount >= MAX_PAGINATION_PAGES) {
logger.warn(
`PR #${prNumber} in ${owner}/${repo} has more than 100 review threads — ` +
'results are truncated. Some comments may be missing resolved status.'
`PR #${prNumber} in ${owner}/${repo} has more than ${MAX_PAGINATION_PAGES * 100} review threads — ` +
'pagination limit reached. Some comments may be missing resolved status.'
);
// TODO: Implement cursor-based pagination by iterating with
// reviewThreads.nodes pageInfo.endCursor across spawn calls.
}

const threads = response.data?.repository?.pullRequest?.reviewThreads?.nodes ?? [];
for (const thread of threads) {
if (thread.comments.pageInfo?.hasNextPage) {
logger.warn(
`Review thread ${thread.id} in PR #${prNumber} has more than 100 comments — ` +
'comment list is truncated. Some comments may be missing resolved status.'
);
}
const info: ReviewThreadInfo = { isResolved: thread.isResolved, threadId: thread.id };
for (const comment of thread.comments.nodes) {
resolvedMap.set(String(comment.databaseId), info);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Off-by-one in pagination warning message.

The loop fetches one initial page plus up to MAX_PAGINATION_PAGES additional pages (21 total). The warning on line 240 reports MAX_PAGINATION_PAGES * 100 (2000), but the actual ceiling is (MAX_PAGINATION_PAGES + 1) * 100 (2100). Very minor, but could be confusing during debugging.

🔧 Suggested fix
     if (pageCount >= MAX_PAGINATION_PAGES) {
       logger.warn(
-        `PR #${prNumber} in ${owner}/${repo} has more than ${MAX_PAGINATION_PAGES * 100} review threads — ` +
+        `PR #${prNumber} in ${owner}/${repo} has more than ${(MAX_PAGINATION_PAGES + 1) * 100} review threads — ` +
           'pagination limit reached. Some comments may be missing resolved status.'
       );
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} while (cursor && pageCount < MAX_PAGINATION_PAGES);
// Check if reviewThreads data was truncated (more than 100 threads)
const pageInfo = response.data?.repository?.pullRequest?.reviewThreads?.pageInfo;
if (pageInfo?.hasNextPage) {
if (pageCount >= MAX_PAGINATION_PAGES) {
logger.warn(
`PR #${prNumber} in ${owner}/${repo} has more than 100 review threads — ` +
'results are truncated. Some comments may be missing resolved status.'
`PR #${prNumber} in ${owner}/${repo} has more than ${MAX_PAGINATION_PAGES * 100} review threads — ` +
'pagination limit reached. Some comments may be missing resolved status.'
);
// TODO: Implement cursor-based pagination by iterating with
// reviewThreads.nodes pageInfo.endCursor across spawn calls.
}
const threads = response.data?.repository?.pullRequest?.reviewThreads?.nodes ?? [];
for (const thread of threads) {
if (thread.comments.pageInfo?.hasNextPage) {
logger.warn(
`Review thread ${thread.id} in PR #${prNumber} has more than 100 comments — ` +
'comment list is truncated. Some comments may be missing resolved status.'
);
}
const info: ReviewThreadInfo = { isResolved: thread.isResolved, threadId: thread.id };
for (const comment of thread.comments.nodes) {
resolvedMap.set(String(comment.databaseId), info);
}
}
} while (cursor && pageCount < MAX_PAGINATION_PAGES);
if (pageCount >= MAX_PAGINATION_PAGES) {
logger.warn(
`PR #${prNumber} in ${owner}/${repo} has more than ${(MAX_PAGINATION_PAGES + 1) * 100} review threads — ` +
'pagination limit reached. Some comments may be missing resolved status.'
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/services/pr-review-comments.service.ts` around lines 236 -
243, The pagination warning uses MAX_PAGINATION_PAGES * 100 but the loop
actually allows one initial page plus MAX_PAGINATION_PAGES additional pages, so
update the logger.warn message in the block referencing logger.warn (and symbols
prNumber, owner, repo, cursor, pageCount, MAX_PAGINATION_PAGES) to report the
correct ceiling as (MAX_PAGINATION_PAGES + 1) * 100 (e.g., 2100) or rephrase to
"more than (MAX_PAGINATION_PAGES + 1) * 100 review threads" so the logged count
matches the implemented pagination behaviour.

Comment thread apps/ui/src/components/session-manager.tsx
Comment on lines 24 to +66
// Track if initial session has been loaded
const initialSessionLoadedRef = useRef(false);

// Use workingDirectory as the persistence key so sessions are scoped per worktree
const persistenceKey = workingDirectory || projectPath;

// Handle session selection with persistence
const handleSelectSession = useCallback(
(sessionId: string | null) => {
setCurrentSessionId(sessionId);
// Persist the selection for this project
if (projectPath) {
setLastSelectedSession(projectPath, sessionId);
// Persist the selection for this worktree/project
if (persistenceKey) {
setLastSelectedSession(persistenceKey, sessionId);
}
},
[projectPath, setLastSelectedSession]
[persistenceKey, setLastSelectedSession]
);

// Restore last selected session when switching to Agent view or when project changes
// Restore last selected session when switching to Agent view or when worktree changes
useEffect(() => {
if (!projectPath) {
if (!persistenceKey) {
// No project, reset
setCurrentSessionId(null);
initialSessionLoadedRef.current = false;
return;
}

// Only restore once per project
// Only restore once per persistence key
if (initialSessionLoadedRef.current) return;
initialSessionLoadedRef.current = true;

const lastSessionId = getLastSelectedSession(projectPath);
const lastSessionId = getLastSelectedSession(persistenceKey);
if (lastSessionId) {
logger.info('Restoring last selected session:', lastSessionId);
setCurrentSessionId(lastSessionId);
}
}, [projectPath, getLastSelectedSession]);
}, [persistenceKey, getLastSelectedSession]);

// Reset initialSessionLoadedRef when project changes
// Reset when worktree/project changes - clear current session and allow restore
useEffect(() => {
initialSessionLoadedRef.current = false;
}, [projectPath]);
setCurrentSessionId(null);
}, [persistenceKey]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reset effect clears restored session on initial load.
The reset effect runs on mount after the restore effect, so the restored session is immediately cleared and persistence never sticks. Guard the reset to only run when the key actually changes (after initial mount), or reorder the effects.

🛠️ Suggested fix (guard reset + reorder)
   // Track if initial session has been loaded
   const initialSessionLoadedRef = useRef(false);
+  const previousKeyRef = useRef<string | undefined>(undefined);

-  // Restore last selected session when switching to Agent view or when worktree changes
-  useEffect(() => {
-    if (!persistenceKey) {
-      // No project, reset
-      setCurrentSessionId(null);
-      initialSessionLoadedRef.current = false;
-      return;
-    }
-
-    // Only restore once per persistence key
-    if (initialSessionLoadedRef.current) return;
-    initialSessionLoadedRef.current = true;
-
-    const lastSessionId = getLastSelectedSession(persistenceKey);
-    if (lastSessionId) {
-      logger.info('Restoring last selected session:', lastSessionId);
-      setCurrentSessionId(lastSessionId);
-    }
-  }, [persistenceKey, getLastSelectedSession]);
-
   // Reset when worktree/project changes - clear current session and allow restore
   useEffect(() => {
-    initialSessionLoadedRef.current = false;
-    setCurrentSessionId(null);
+    if (previousKeyRef.current && previousKeyRef.current !== persistenceKey) {
+      initialSessionLoadedRef.current = false;
+      setCurrentSessionId(null);
+    }
+    previousKeyRef.current = persistenceKey;
   }, [persistenceKey]);

+  // Restore last selected session when switching to Agent view or when worktree changes
+  useEffect(() => {
+    if (!persistenceKey) {
+      // No project, reset
+      setCurrentSessionId(null);
+      initialSessionLoadedRef.current = false;
+      return;
+    }
+
+    // Only restore once per persistence key
+    if (initialSessionLoadedRef.current) return;
+    initialSessionLoadedRef.current = true;
+
+    const lastSessionId = getLastSelectedSession(persistenceKey);
+    if (lastSessionId) {
+      logger.info('Restoring last selected session:', lastSessionId);
+      setCurrentSessionId(lastSessionId);
+    }
+  }, [persistenceKey, getLastSelectedSession]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/agent-view/hooks/use-agent-session.ts` around
lines 24 - 66, The reset effect that sets initialSessionLoadedRef.current =
false and setCurrentSessionId(null) runs unconditionally on mount and clears
whatever the restore effect (which reads getLastSelectedSession(persistenceKey)
and sets setCurrentSessionId) just restored; modify the reset effect to only run
when persistenceKey actually changes after initial mount (i.e., guard it with a
check comparing previous persistenceKey or track a didMount ref) so it doesn't
run immediately on first render, or reorder so the restore effect runs after the
guarded reset; update references to initialSessionLoadedRef, persistenceKey,
getLastSelectedSession, and setCurrentSessionId accordingly to ensure restored
session persists.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx (1)

427-449: Consider keeping the logs submenu accessible while the dev server is starting.

The subtrigger is disabled during startup (line 445), which prevents the user from viewing dev server logs while the process is spinning up. Startup logs can be useful for diagnosing slow or failed starts. The Stop branch (lines 410–422) keeps the subtrigger always enabled.

If this is intentional (e.g., no log session exists yet), feel free to disregard.

🔧 Suggested change to keep the subtrigger enabled
                <DropdownMenuSubTrigger
-                  className={cn(
-                    'text-xs px-1 rounded-l-none border-l border-border/30 h-8',
-                    isStartingDevServer && 'opacity-50 cursor-not-allowed'
-                  )}
-                  disabled={isStartingDevServer}
+                  className="text-xs px-1 rounded-l-none border-l border-border/30 h-8"
                />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx`
around lines 427 - 449, The DropdownMenuSubTrigger is being disabled when
isStartingDevServer is true, preventing access to viewDevServerLogsItem while
the dev server starts; modify the worktree-actions-dropdown so the
DropdownMenuSubTrigger remains enabled regardless of isStartingDevServer (remove
or conditionally avoid setting disabled={isStartingDevServer} on
DropdownMenuSubTrigger) while keeping the start button (DropdownMenuItem
onStartDevServer) disabled during startup, so users can open the logs submenu
even when the server is starting.
apps/server/src/index.ts (1)

280-280: Nit: 0.0.0.0 is dead code in an origin check.

Browsers never send 0.0.0.0 as an Origin header value (it's a bind-all/unspecified address). This condition can never be true and can be safely removed.

🧹 Proposed cleanup
     return (
       hostname === 'localhost' ||
       hostname === '127.0.0.1' ||
       hostname === '[::1]' ||
-      hostname === '0.0.0.0' ||
       hostname.startsWith('192.168.') ||
       hostname.startsWith('10.') ||
       /^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(hostname)
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/index.ts` at line 280, Remove the dead origin check comparing
hostname === '0.0.0.0' in the origin validation logic (the condition shown as
"hostname === '0.0.0.0' ||"); browsers never send 0.0.0.0 as an Origin so delete
that clause and adjust the surrounding boolean expression accordingly to keep
the remaining valid checks and operator precedence intact.
apps/ui/src/components/views/board-view/hooks/use-board-actions.ts (1)

421-428: workMode still leaks into the persisted feature record in the update path.

handleAddFeature was fixed to strip workMode (and initialStatus) from the feature data before persisting. handleUpdateFeature only strips childDependencies, so workMode flows through restUpdatesfinalUpdatespersistFeatureUpdate. For consistency with the add path, consider stripping it here too.

♻️ Suggested fix
-const { childDependencies, ...restUpdates } = updates;
+const { childDependencies, workMode: _workMode, ...restUpdates } = updates;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/board-view/hooks/use-board-actions.ts` around
lines 421 - 428, handleUpdateFeature currently only removes childDependencies
but lets workMode (and initialStatus) flow into restUpdates and then
finalUpdates before calling persistFeatureUpdate; update handleUpdateFeature to
strip workMode and initialStatus from updates (same as handleAddFeature does) so
they are not persisted: when you destructure updates, remove childDependencies,
workMode, and initialStatus (or explicitly build restUpdates without those keys)
and then construct finalUpdates (used by persistFeatureUpdate) from that cleaned
object along with title/branchName so persisted records match the add path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/server/src/index.ts`:
- Around line 303-319: The current CORS check lets isLocalOrigin(...) bypass an
explicit allowedOrigins list; change the flow in the origin handling so that
when allowedOrigins is configured and non-empty you only allow '*' or items
present in allowedOrigins (using the existing allowedOrigins.includes(origin)
branch) and if it does not match you reject the request immediately (invoke the
CORS callback with a rejection) instead of falling through to isLocalOrigin;
only call isLocalOrigin(origin) when there is no configured allowedOrigins
(empty/undefined). Reference the existing symbols: allowedOrigins, origin,
isLocalOrigin, and the callback used for CORS decision.

In `@apps/ui/src/components/session-manager.tsx`:
- Around line 302-313: The deletion branch leaves the parent's currentSessionId
pointing at a removed session when no replacement exists; update the logic in
the block that computes freshSessions/refetchResult and activeSessionsList so
that if activeSessionsList is empty you call onSelectSession(null) to clear the
selection (i.e., add an else that invokes onSelectSession(null)); make sure this
change touches the same function that references currentSessionId, sessionId,
refetchResult, activeSessionsList, onSelectSession, and
effectiveWorkingDirectory so the UI doesn't retain a stale session id.

In `@apps/ui/src/components/views/board-view.tsx`:
- Around line 502-508: The isCurrent expression incorrectly uses the
nullish-coalescing on found.isCurrent before checking usedFallback, so when
found.isCurrent is false the usedFallback branch never runs; change the
expression to evaluate usedFallback first (so when usedFallback is true return
found.isMain) and otherwise fall back to found.isCurrent ?? (currentWorktreePath
!== null ? pathsEqual(found.path, currentWorktreePath) : found.isMain); update
the isCurrent assignment referencing found, usedFallback, found.isMain,
found.isCurrent, currentWorktreePath, and pathsEqual accordingly.

---

Nitpick comments:
In `@apps/server/src/index.ts`:
- Line 280: Remove the dead origin check comparing hostname === '0.0.0.0' in the
origin validation logic (the condition shown as "hostname === '0.0.0.0' ||");
browsers never send 0.0.0.0 as an Origin so delete that clause and adjust the
surrounding boolean expression accordingly to keep the remaining valid checks
and operator precedence intact.

In `@apps/ui/src/components/views/board-view/hooks/use-board-actions.ts`:
- Around line 421-428: handleUpdateFeature currently only removes
childDependencies but lets workMode (and initialStatus) flow into restUpdates
and then finalUpdates before calling persistFeatureUpdate; update
handleUpdateFeature to strip workMode and initialStatus from updates (same as
handleAddFeature does) so they are not persisted: when you destructure updates,
remove childDependencies, workMode, and initialStatus (or explicitly build
restUpdates without those keys) and then construct finalUpdates (used by
persistFeatureUpdate) from that cleaned object along with title/branchName so
persisted records match the add path.

In
`@apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx`:
- Around line 427-449: The DropdownMenuSubTrigger is being disabled when
isStartingDevServer is true, preventing access to viewDevServerLogsItem while
the dev server starts; modify the worktree-actions-dropdown so the
DropdownMenuSubTrigger remains enabled regardless of isStartingDevServer (remove
or conditionally avoid setting disabled={isStartingDevServer} on
DropdownMenuSubTrigger) while keeping the start button (DropdownMenuItem
onStartDevServer) disabled during startup, so users can open the logs submenu
even when the server is starting.

Comment thread apps/server/src/index.ts
Comment on lines +303 to 319
if (allowedOrigins && allowedOrigins.length > 0) {
if (allowedOrigins.includes('*')) {
callback(null, true);
return;
}
if (allowedOrigins.includes(origin)) {
callback(null, origin);
} else {
callback(new Error('Not allowed by CORS'));
return;
}
return;
// Fall through to local network check below
}

// For local development, allow all localhost/loopback origins (any port)
try {
const url = new URL(origin);
const hostname = url.hostname;

if (
hostname === 'localhost' ||
hostname === '127.0.0.1' ||
hostname === '::1' ||
hostname === '0.0.0.0' ||
hostname.startsWith('192.168.') ||
hostname.startsWith('10.') ||
hostname.startsWith('172.')
) {
callback(null, origin);
return;
}
} catch {
// Ignore URL parsing errors
// Allow all localhost/loopback/private network origins (any port)
if (isLocalOrigin(origin)) {
callback(null, origin);
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

CORS_ORIGIN allowlist no longer acts as an exclusive gate — local network origins silently bypass it.

When CORS_ORIGIN is explicitly set (non-empty, non-wildcard) and an incoming origin is not in the list, the code now falls through to isLocalOrigin() and permits the request. Before this change, a non-matching origin was rejected. The result is that CORS_ORIGIN is now additive (explicit origins plus all RFC 1918/loopback addresses) rather than a replacement allowlist.

In a Docker Compose or corporate LAN deployment, any machine on a shared private subnet (e.g. 192.168.x.x) can reach the API even when the operator intended CORS_ORIGIN to restrict access to a specific external domain only.

The isLocalOrigin fallback should only apply when no explicit CORS origin list is configured:

🐛 Proposed fix
       const allowedOrigins = process.env.CORS_ORIGIN?.split(',')
         .map((o) => o.trim())
         .filter(Boolean);
       if (allowedOrigins && allowedOrigins.length > 0) {
         if (allowedOrigins.includes('*')) {
           callback(null, true);
           return;
         }
         if (allowedOrigins.includes(origin)) {
           callback(null, origin);
           return;
         }
-        // Fall through to local network check below
+        // Explicit list is set but origin didn't match — reject immediately.
+        callback(new Error('Not allowed by CORS'));
+        return;
       }

-      // Allow all localhost/loopback/private network origins (any port)
+      // No explicit list configured — allow all localhost/loopback/private network origins
       if (isLocalOrigin(origin)) {
         callback(null, origin);
         return;
       }

If the intent is to always permit local origins regardless of CORS_ORIGIN, that invariant should be explicitly documented in a comment so it isn't treated as a bug in the future.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (allowedOrigins && allowedOrigins.length > 0) {
if (allowedOrigins.includes('*')) {
callback(null, true);
return;
}
if (allowedOrigins.includes(origin)) {
callback(null, origin);
} else {
callback(new Error('Not allowed by CORS'));
return;
}
return;
// Fall through to local network check below
}
// For local development, allow all localhost/loopback origins (any port)
try {
const url = new URL(origin);
const hostname = url.hostname;
if (
hostname === 'localhost' ||
hostname === '127.0.0.1' ||
hostname === '::1' ||
hostname === '0.0.0.0' ||
hostname.startsWith('192.168.') ||
hostname.startsWith('10.') ||
hostname.startsWith('172.')
) {
callback(null, origin);
return;
}
} catch {
// Ignore URL parsing errors
// Allow all localhost/loopback/private network origins (any port)
if (isLocalOrigin(origin)) {
callback(null, origin);
return;
}
if (allowedOrigins && allowedOrigins.length > 0) {
if (allowedOrigins.includes('*')) {
callback(null, true);
return;
}
if (allowedOrigins.includes(origin)) {
callback(null, origin);
return;
}
// Explicit list is set but origin didn't match — reject immediately.
callback(new Error('Not allowed by CORS'));
return;
}
// No explicit list configured — allow all localhost/loopback/private network origins
if (isLocalOrigin(origin)) {
callback(null, origin);
return;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/index.ts` around lines 303 - 319, The current CORS check lets
isLocalOrigin(...) bypass an explicit allowedOrigins list; change the flow in
the origin handling so that when allowedOrigins is configured and non-empty you
only allow '*' or items present in allowedOrigins (using the existing
allowedOrigins.includes(origin) branch) and if it does not match you reject the
request immediately (invoke the CORS callback with a rejection) instead of
falling through to isLocalOrigin; only call isLocalOrigin(origin) when there is
no configured allowedOrigins (empty/undefined). Reference the existing symbols:
allowedOrigins, origin, isLocalOrigin, and the callback used for CORS decision.

Comment on lines 302 to 313
if (currentSessionId === sessionId) {
// Switch to another session or create a new one
const activeSessionsList = sessions.filter((s) => !s.isArchived);
// Switch to another session using fresh data, excluding the deleted session
// Filter to sessions within the same worktree to avoid jumping to a different worktree
const freshSessions = refetchResult?.data ?? [];
const activeSessionsList = freshSessions.filter((s) => {
if (s.isArchived || s.id === sessionId) return false;
const sessionDir = s.workingDirectory || s.projectPath;
return pathsEqual(sessionDir, effectiveWorkingDirectory);
});
if (activeSessionsList.length > 0) {
onSelectSession(activeSessionsList[0].id);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing onSelectSession(null) when the deleted session has no in-scope replacement.

When activeSessionsList is empty (no other active sessions in the current worktree), the parent's currentSessionId remains pointing to the now-deleted session — a stale reference. Add an else branch to clear it.

🛠️ Suggested fix
        if (activeSessionsList.length > 0) {
          onSelectSession(activeSessionsList[0].id);
+       } else {
+         onSelectSession(null);
        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/session-manager.tsx` around lines 302 - 313, The
deletion branch leaves the parent's currentSessionId pointing at a removed
session when no replacement exists; update the logic in the block that computes
freshSessions/refetchResult and activeSessionsList so that if activeSessionsList
is empty you call onSelectSession(null) to clear the selection (i.e., add an
else that invokes onSelectSession(null)); make sure this change touches the same
function that references currentSessionId, sessionId, refetchResult,
activeSessionsList, onSelectSession, and effectiveWorkingDirectory so the UI
doesn't retain a stale session id.

Comment on lines 502 to +508
isCurrent:
found.isCurrent ??
(currentWorktreePath !== null ? pathsEqual(found.path, currentWorktreePath) : found.isMain),
(usedFallback
? found.isMain // treat main as current during the transient fallback render
: currentWorktreePath !== null
? pathsEqual(found.path, currentWorktreePath)
: found.isMain),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

usedFallback branch is unreachable when isCurrent is false.

The ?? (nullish-coalescing) operator only falls through when the left-hand side is null or undefined. When usedFallback = true, found is the main worktree — and in the store the main worktree typically has isCurrent: false (an explicit boolean) when a non-main worktree was previously active. This means false ?? (usedFallback ? found.isMain : ...) short-circuits to false, so the "treat main as current during the transient fallback render" intent stated in the comment is never applied.

The usedFallback check should move before the ?? so it always wins:

🐛 Proposed fix
-    isCurrent:
-      found.isCurrent ??
-      (usedFallback
-        ? found.isMain // treat main as current during the transient fallback render
-        : currentWorktreePath !== null
-          ? pathsEqual(found.path, currentWorktreePath)
-          : found.isMain),
+    isCurrent: usedFallback
+      ? found.isMain // treat main as current during the transient fallback render
+      : (found.isCurrent ??
+          (currentWorktreePath !== null
+            ? pathsEqual(found.path, currentWorktreePath)
+            : found.isMain)),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
isCurrent:
found.isCurrent ??
(currentWorktreePath !== null ? pathsEqual(found.path, currentWorktreePath) : found.isMain),
(usedFallback
? found.isMain // treat main as current during the transient fallback render
: currentWorktreePath !== null
? pathsEqual(found.path, currentWorktreePath)
: found.isMain),
isCurrent: usedFallback
? found.isMain // treat main as current during the transient fallback render
: (found.isCurrent ??
(currentWorktreePath !== null
? pathsEqual(found.path, currentWorktreePath)
: found.isMain)),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/board-view.tsx` around lines 502 - 508, The
isCurrent expression incorrectly uses the nullish-coalescing on found.isCurrent
before checking usedFallback, so when found.isCurrent is false the usedFallback
branch never runs; change the expression to evaluate usedFallback first (so when
usedFallback is true return found.isMain) and otherwise fall back to
found.isCurrent ?? (currentWorktreePath !== null ? pathsEqual(found.path,
currentWorktreePath) : found.isMain); update the isCurrent assignment
referencing found, usedFallback, found.isMain, found.isCurrent,
currentWorktreePath, and pathsEqual accordingly.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
apps/ui/src/store/ui-cache-store.ts (1)

163-187: Solid defensive sanitization for worktree cache restoration.

The rationale for only restoring path === null entries is well-documented and the crash-loop prevention story ties nicely with the error boundary changes.

One minor robustness thought: if localStorage data is ever corrupted (e.g., an entry value is null or missing .path), worktree.path on line 177 would throw a TypeError during startup — before the error boundary's crash-loop detection can intervene meaningfully. Consider a defensive guard:

🛡️ Optional: guard against corrupted cache entries
     for (const [projectPath, worktree] of Object.entries(cache.cachedCurrentWorktreeByProject)) {
-      if (worktree.path === null) {
+      if (worktree && worktree.path === null) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/store/ui-cache-store.ts` around lines 163 - 187, The loop that
restores cache from cachedCurrentWorktreeByProject assumes each entry is an
object with a .path property and can throw if a cached value is null or
malformed; update the for-loop over
Object.entries(cache.cachedCurrentWorktreeByProject) to defensively check that
worktree is an object (e.g., typeof worktree === "object" && worktree !== null)
and that 'path' in worktree before accessing worktree.path, only assigning to
sanitized[projectPath] when those guards pass and worktree.path === null; keep
using stateUpdate.currentWorktreeByProject for the sanitized result.
apps/ui/src/components/ui/app-error-boundary.tsx (1)

66-73: Hardcoded 'automaker-ui-cache' key is duplicated from ui-cache-store.ts.

This key duplicates STORE_NAME in ui-cache-store.ts. If it ever changes in one place but not the other, the cache-clear won't work. That said, for an error boundary you probably want minimal imports to avoid pulling in code that might itself be the crash source — so a shared constant could be risky here.

Consider extracting the key to a tiny, dependency-free constants file (e.g., constants/storage-keys.ts) that both modules import, or at minimum add a comment cross-referencing the source of truth:

-        localStorage.removeItem('automaker-ui-cache');
+        // Must match STORE_NAME in store/ui-cache-store.ts
+        localStorage.removeItem('automaker-ui-cache');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/ui/app-error-boundary.tsx` around lines 66 - 73,
Replace the hardcoded 'automaker-ui-cache' string in app-error-boundary.ts with
a shared, minimal dependency constant to avoid drift with ui-cache-store.ts's
STORE_NAME; either create a tiny dependency-free constants module (e.g.,
constants/storage-keys.ts) exporting AUTOMAKER_UI_CACHE and import it into both
app-error-boundary.ts and ui-cache-store.ts, or if you want to avoid any import
from the error boundary, add a clear cross-reference comment above the
localStorage.removeItem('automaker-ui-cache') call pointing to
ui-cache-store.ts's STORE_NAME to ensure maintainers update both places when the
key changes (update the localStorage.removeItem call and the sessionStorage key
usage to use the new constant or documented reference).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/ui/src/components/ui/app-error-boundary.tsx`:
- Around line 66-73: Replace the hardcoded 'automaker-ui-cache' string in
app-error-boundary.ts with a shared, minimal dependency constant to avoid drift
with ui-cache-store.ts's STORE_NAME; either create a tiny dependency-free
constants module (e.g., constants/storage-keys.ts) exporting AUTOMAKER_UI_CACHE
and import it into both app-error-boundary.ts and ui-cache-store.ts, or if you
want to avoid any import from the error boundary, add a clear cross-reference
comment above the localStorage.removeItem('automaker-ui-cache') call pointing to
ui-cache-store.ts's STORE_NAME to ensure maintainers update both places when the
key changes (update the localStorage.removeItem call and the sessionStorage key
usage to use the new constant or documented reference).

In `@apps/ui/src/store/ui-cache-store.ts`:
- Around line 163-187: The loop that restores cache from
cachedCurrentWorktreeByProject assumes each entry is an object with a .path
property and can throw if a cached value is null or malformed; update the
for-loop over Object.entries(cache.cachedCurrentWorktreeByProject) to
defensively check that worktree is an object (e.g., typeof worktree === "object"
&& worktree !== null) and that 'path' in worktree before accessing
worktree.path, only assigning to sanitized[projectPath] when those guards pass
and worktree.path === null; keep using stateUpdate.currentWorktreeByProject for
the sanitized result.

@gsxdsm gsxdsm merged commit 28becb1 into AutoMaker-Org:v0.15.0rc Feb 21, 2026
6 checks passed
@gsxdsm gsxdsm deleted the fix/docker-compose-cors-error branch February 21, 2026 21:56
gsxdsm added a commit to gsxdsm/automaker that referenced this pull request Feb 23, 2026
…#793)

* Changes from fix/docker-compose-cors-error

* Update apps/server/src/index.ts

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* Fix: Delete Worktree Crash + PR Comments + Dev Server UX Improvements (AutoMaker-Org#792)

* Changes from fix/delete-worktree-hotifx

* fix: Improve bot detection and prevent UI overflow issues

- Include GitHub app-initiated comments in bot detection
- Wrap handleQuickCreateSession with useCallback to fix dependency issues
- Truncate long branch names in agent header to prevent layout overflow

* feat: Support GitHub App comments in PR review and fix session filtering

* feat: Return invalidation result from delete session handler

* fix: Improve CORS origin validation to handle wildcard correctly

* fix: Correct IPv6 localhost parsing and improve responsive UI layouts

* Changes from fix/pwa-cache-fix (AutoMaker-Org#794)

* fix: Add type checking to prevent crashes from malformed cache entries

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant