Fix Docker Compose CORS issues with nginx API proxying#793
Fix Docker Compose CORS issues with nginx API proxying#793gsxdsm merged 8 commits intoAutoMaker-Org:v0.15.0rcfrom
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughChanged Docker/compose defaults to empty so the UI uses relative URLs proxied by nginx; nginx now proxies Changes
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 4❌ Failed checks (4 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Summary of ChangesHello @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
🧠 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
Using Gemini Code AssistThe 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
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 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
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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_timeoutis 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 offis 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_ORIGINwas set and the request origin didn't match, the request was immediately rejected. Now it falls through to theisLocalOrigincheck, meaning local/private network origins are always allowed regardless ofCORS_ORIGINconfiguration. This is fine for the Docker Compose use case, but be aware that operators cannot useCORS_ORIGINas 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>
There was a problem hiding this comment.
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.
…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
There was a problem hiding this comment.
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 | 🔴 CriticalFix
gh api --paginateJSON parsing failure on paginated results.The code uses
gh api --paginatewithout--slurpon 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),--paginateoutputs multiple separate JSON arrays sequentially (e.g.,[...][...]), which is not valid JSON and will causeJSON.parse()to throw aSyntaxErrorwhen a PR exceeds the page size (typically ~30 items).Add
--slurpto eachgh apicall 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/DropdownMenuSubTriggersplit-button convention already used throughout this component (editor, terminal, pull/push, commits, stash, PR). Thedisabled/opacity-50/cursor-not-allowedhandling on the Start sub-trigger correctly mirrors the primary item's loading state.One minor point: the "View Dev Server Logs"
DropdownMenuItemis duplicated verbatim in bothDropdownMenuSubContentblocks. 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
DropdownMenuSubContentbodies:- <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 panelThe 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 whenhandleStartDevServeris called through thehandleStartDevServerAndShowLogswrapper inworktree-panel.tsx. Any future consumer that callshandleStartDevServerdirectly 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: TransientisCurrent: falseon the fallback main-worktree objectWhen the fallback fires (line 491-493),
foundbecomes the main worktree butcurrentWorktreePathis still non-null (pointing to the deleted path). TheisCurrentcomputation 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
isCurrentfield isnull/undefined, the fallback expression evaluates tofalse, soselectedWorktreeis returned withisCurrent: falseduring the window between the deletion event and theonDeletedreset (lines 1961-1969). This is passed touseAutoMode(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 havingisCurrentpre-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:childDependenciesleaks intonewFeatureDatavia...restFeatureData
childDependenciesis a transient creation parameter (same category asinitialStatusandworkMode) but is not excluded from the destructure. It therefore ends up insidenewFeatureData, gets passed toaddFeature, and is potentially stored in the Zustand state and forwarded to the server viapersistFeatureCreate. If the store spreads the object as-is, future duplications of that feature would propagate the stalechildDependenciesarray 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 themaxBuffer/timeoutconfiguration.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.
| } 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); | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| } 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.
| // 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]); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.0is dead code in an origin check.Browsers never send
0.0.0.0as anOriginheader 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:workModestill leaks into the persisted feature record in the update path.
handleAddFeaturewas fixed to stripworkMode(andinitialStatus) from the feature data before persisting.handleUpdateFeatureonly stripschildDependencies, soworkModeflows throughrestUpdates→finalUpdates→persistFeatureUpdate. 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.
| 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; | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| 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); | ||
| } |
There was a problem hiding this comment.
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.
| 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), |
There was a problem hiding this comment.
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.
| 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.
There was a problem hiding this comment.
🧹 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 === nullentries 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
nullor missing.path),worktree.pathon line 177 would throw aTypeErrorduring 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 fromui-cache-store.ts.This key duplicates
STORE_NAMEinui-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.
…#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>
Summary
VITE_SERVER_URLto enable relative URLs, allowing nginx to proxy API requests without CORS issuesCORS_ORIGINenvironment variable to be unset, relying on automatic local network detectionChanges
Dockerfile: Changed
VITE_SERVER_URLdefault fromhttp://localhost:3008to 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_upgrademap for features like/api/eventsand/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:
isLocalOrigin()function to reduce code duplication and improve maintainabilityCORS_ORIGINvaluesdocker-compose.yml: Changed
CORS_ORIGINdefault fromhttp://localhost:3007to 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.