Feature: worktree view customization and stability fixes#805
Feature: worktree view customization and stability fixes#805gsxdsm merged 12 commits intoAutoMaker-Org:v0.15.0rcfrom
Conversation
* Changes from feature/quick-add * feat: Clarify system prompt and improve error handling across services. Address PR Feedback * feat: Improve PR description parsing and refactor event handling * feat: Add context options to pipeline orchestrator initialization * fix: Deduplicate React and handle CJS interop for use-sync-external-store Resolve "Cannot read properties of null (reading 'useState')" errors by deduplicating React/react-dom and ensuring use-sync-external-store is bundled together with React to prevent CJS packages from resolving to different React instances.
- Update DEFAULT_MAX_TURNS from 1000 to 10000 in settings-helpers.ts and agent-executor.ts - Update MAX_ALLOWED_TURNS from 2000 to 10000 in settings-helpers.ts - Update UI clamping logic from 2000 to 10000 in app-store.ts - Update fallback values from 1000 to 10000 in use-settings-sync.ts - Update default value from 1000 to 10000 in DEFAULT_GLOBAL_SETTINGS - Update documentation to reflect new range: 1-10000 Allows agents to perform up to 10000 turns for complex feature execution. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughBumps versions and adds UI/server features: expanded tool presets and max-turns, richer auto-mode/feature-completed events, CodeMirror unified diff + inline diffs, pinned worktree UI with PR-number update route and Electron API, additive enhancement modes, and multiple settings/state migrations. Changes
Sequence Diagram(s)sequenceDiagram
participant UI as Client(UI/Dialog)
participant Electron as Electron API
participant Server as Server (/api)
participant Git as Local Git
participant GH as GitHub CLI (gh)
UI->>Electron: updatePRNumber(worktreePath, prNumber, projectPath?)
Electron->>Server: POST /api/worktree/update-pr-number {worktreePath, prNumber, projectPath}
Server->>Git: resolve branch for worktreePath
alt gh CLI available
Server->>GH: gh pr view --repo? <prNumber>
GH-->>Server: pr metadata (number, url, title, state, createdAt)
else gh CLI missing / fetch fails
Server-->>Server: synthesize minimal prInfo
end
Server->>Server: persist worktree metadata (branch -> prInfo)
Server-->>Electron: { success, result: { branch, prInfo, ghCliUnavailable? } }
Electron-->>UI: result -> close dialog / show toast
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 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 user experience by introducing customizable worktree views, improving the file editor with inline diffs and direct feature creation from code, and upgrading the diff viewer to a more robust CodeMirror-based solution. It also refines the AI enhancement workflow with distinct additive and rewrite modes, expands agent capabilities with new tools and increased max turns, and improves server-side event handling for better feature lifecycle management. These changes collectively aim to provide a more flexible, powerful, and stable development environment. Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Changelog
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces several significant improvements to the worktree management and file editor. Key features include customizable pinned worktree tabs with branch swapping, a richer CodeMirror-based diff viewer, and inline diff decorations in the file editor. The AI enhancement workflow has been refined with a distinction between 'Rewrite' and 'Append Details' modes, supported by updated prompts that prevent redundant content generation. I have identified a few minor issues in the file tree status rollup logic and tooltip formatting that should be addressed to ensure consistent visual feedback for all git statuses.
| const STATUS_PRIORITY: Record<string, number> = { | ||
| U: 6, // Conflicted - highest priority | ||
| D: 5, // Deleted | ||
| A: 4, // Added | ||
| M: 3, // Modified | ||
| R: 2, // Renamed | ||
| S: 1, // Staged | ||
| '?': 0, // Untracked | ||
| }; |
There was a problem hiding this comment.
The STATUS_PRIORITY map is missing the C (Copied) status code, which is a valid git status handled elsewhere in this file (e.g., in getGitStatusColor). Without it, folders containing only copied files will not show a dominant status indicator in the tree.
| const STATUS_PRIORITY: Record<string, number> = { | |
| U: 6, // Conflicted - highest priority | |
| D: 5, // Deleted | |
| A: 4, // Added | |
| M: 3, // Modified | |
| R: 2, // Renamed | |
| S: 1, // Staged | |
| '?': 0, // Untracked | |
| }; | |
| const STATUS_PRIORITY: Record<string, number> = { | |
| U: 6, // Conflicted - highest priority | |
| D: 5, // Deleted | |
| A: 4, // Added | |
| M: 3, // Modified | |
| R: 2, // Renamed | |
| C: 2, // Copied | |
| S: 1, // Staged | |
| '?': 0, // Untracked | |
| }; |
| 'bg-cyan-500': folderRollup.dominantStatus === 'C', | ||
| 'bg-orange-500': folderRollup.dominantStatus === 'U', | ||
| })} | ||
| title={`${getGitStatusLabel(folderRollup.dominantStatus ?? undefined)} (${folderRollup.count})`} |
There was a problem hiding this comment.
If folderRollup.dominantStatus is null (which can happen if all changed files in a folder have statuses not present in STATUS_PRIORITY), getGitStatusLabel(undefined) will return an empty string, resulting in a tooltip title like (5). Providing a fallback label like 'Changed' ensures the tooltip remains informative.
| title={`${getGitStatusLabel(folderRollup.dominantStatus ?? undefined)} (${folderRollup.count})`} | |
| title="${folderRollup.dominantStatus ? getGitStatusLabel(folderRollup.dominantStatus) : 'Changed'} (${folderRollup.count})" |
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 (6)
libs/prompts/tests/enhancement.test.ts (1)
334-348:⚠️ Potential issue | 🟡 Minor
ux-revieweris missing from theall modestest array.The typed
modesarray on line 334 omits'ux-reviewer', so a breakage in prompt building for that mode won't be surfaced here. While the integration test at line 444–464 iterates overgetAvailableEnhancementModes()(which does includeux-reviewer), the dedicatedall modesblock should be complete.💡 Proposed fix
- const modes: Array<'improve' | 'technical' | 'simplify' | 'acceptance'> = [ + const modes: Array<'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer'> = [ 'improve', 'technical', 'simplify', 'acceptance', + 'ux-reviewer', ];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/prompts/tests/enhancement.test.ts` around lines 334 - 348, The test's hard-coded modes array in enhancement.test.ts is missing 'ux-reviewer', so update the all-modes test to cover that mode: either add 'ux-reviewer' to the modes array or replace the manual array with getAvailableEnhancementModes() and iterate over its results, calling buildUserPrompt(mode, testText) and asserting as before; ensure the test references buildUserPrompt and uses the same expectations so ux-reviewer prompt generation is validated.apps/server/tests/unit/lib/enhancement-prompts.test.ts (1)
194-200:⚠️ Potential issue | 🟡 MinorMissing assertion coverage for additive-mode instruction strings.
The
should work with all enhancement modestest iterates over all five modes but only verifies that the prompt containstestTextand has length > 100. A regression in the additive instruction ('Generate ONLY the additional details section...') or additive examples intro ('Here are examples of the additional details section...') fortechnical,acceptance, orux-reviewermodes would not be caught.💡 Suggested additional assertions
it('should work with all enhancement modes', () => { + const ADDITIVE_MODES = ['technical', 'acceptance', 'ux-reviewer']; ENHANCEMENT_MODES.forEach((mode) => { const prompt = buildUserPrompt(mode, testText); expect(prompt).toContain(testText); expect(prompt.length).toBeGreaterThan(100); + if (ADDITIVE_MODES.includes(mode)) { + expect(prompt).toContain('Generate ONLY the additional details'); + } else { + expect(prompt).toContain('Please enhance the following task description:'); + } }); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/tests/unit/lib/enhancement-prompts.test.ts` around lines 194 - 200, The test iterating ENHANCEMENT_MODES using buildUserPrompt lacks assertions that verify the additive-mode instruction and examples strings; update the test in enhancement-prompts.test.ts to, for the additive-related modes (e.g., 'technical', 'acceptance', 'ux-reviewer'), assert that the generated prompt includes the exact additive instruction phrase and the additive examples intro phrase (the strings that begin with "Generate ONLY the additional details section..." and "Here are examples of the additional details section...") in addition to the existing contains and length checks so regressions in those specific strings are caught.apps/server/src/services/worktree-service.ts (1)
137-162:⚠️ Potential issue | 🟡 MinorENOENT from
mkdir/cp/copyFileis misclassified as "File not found".The single
catchblock coversstat,mkdir, andcopyFile/cp. IfmkdirorcpthrowsENOENT(e.g., a deeply nested destination path disappears mid-operation), the error is silently treated as a skipped source file rather than a real failure — the item never reachesfailures[]andCopyFilesErroris not thrown.Separating the
statcall from the copy operations closes the gap:🐛 Proposed fix
try { - // Check if source exists - const stat = await fs.stat(sourcePath); - - // Ensure destination directory exists - const destDir = path.dirname(destPath); - await fs.mkdir(destDir, { recursive: true }); - - if (stat.isDirectory()) { - await fs.cp(sourcePath, destPath, { recursive: true, force: true }); - } else { - await fs.copyFile(sourcePath, destPath); - } - - emitter.emit('worktree:copy-files:copied', { - path: normalized, - type: stat.isDirectory() ? 'directory' : 'file', - }); + // Check if source exists (ENOENT here means "source missing") + let stat: Awaited<ReturnType<typeof fs.stat>>; + try { + stat = await fs.stat(sourcePath); + } catch (statErr) { + if ((statErr as NodeJS.ErrnoException).code === 'ENOENT') { + emitter.emit('worktree:copy-files:skipped', { + path: normalized, + reason: 'File not found in project root', + }); + } else { + throw statErr; // propagate unexpected stat errors to outer catch + } + continue; + } + + // Ensure destination directory exists, then copy + const destDir = path.dirname(destPath); + await fs.mkdir(destDir, { recursive: true }); + if (stat.isDirectory()) { + await fs.cp(sourcePath, destPath, { recursive: true, force: true }); + } else { + await fs.copyFile(sourcePath, destPath); + } + emitter.emit('worktree:copy-files:copied', { + path: normalized, + type: stat.isDirectory() ? 'directory' : 'file', + }); } catch (err) { - if ((err as NodeJS.ErrnoException).code === 'ENOENT') { - emitter.emit('worktree:copy-files:skipped', { - path: normalized, - reason: 'File not found in project root', - }); - } else { - const errorMessage = err instanceof Error ? err.message : String(err); - emitter.emit('worktree:copy-files:failed', { - path: normalized, - error: errorMessage, - }); - failures.push({ path: normalized, error: errorMessage }); - } + const errorMessage = err instanceof Error ? err.message : String(err); + emitter.emit('worktree:copy-files:failed', { + path: normalized, + error: errorMessage, + }); + failures.push({ path: normalized, error: errorMessage }); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/worktree-service.ts` around lines 137 - 162, Split the try/catch so fs.stat(sourcePath) is awaited in its own try block and only ENOENT from that call emits the 'worktree:copy-files:skipped' event (using normalized) while returning/continuing; perform fs.mkdir/fs.cp/fs.copyFile in a separate try/catch so ENOENT or other errors from mkdir/cp/copyFile are treated as real failures (push into failures[] and eventually raise CopyFilesError). Locate the logic around fs.stat, fs.mkdir, fs.cp/fs.copyFile, emitter.emit('worktree:copy-files:skipped', ...) and ensure only the stat-related ENOENT is mapped to "File not found", other errors fall through to the failures handling that constructs/throws CopyFilesError for sourcePath/destPath.libs/git-utils/src/status.ts (2)
199-202:⚠️ Potential issue | 🟡 Minor
rebase-applydirectory is shared betweengit rebase --applyandgit am— causes false-positive rebase detection.Both
git amandgit rebase --applyuse the.git/rebase-apply/directory. Whengit amis interrupted, the directory exists but only contains anapplyingsentinel;git rebase --applywrites arebasingsentinel instead. The current code will incorrectly reportmergeOperationType: 'rebase'for a stuckgit amsession.Check for
rebase-apply/rebasinginstead to distinguish an actual rebase from a baregit amin progress:Proposed fix
- { file: 'rebase-apply', type: 'rebase' as const }, + // rebase-apply/rebasing is written only by `git rebase --apply`, not by a bare `git am` + { file: 'rebase-apply/rebasing', type: 'rebase' as const },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/git-utils/src/status.ts` around lines 199 - 202, The code currently treats presence of the 'rebase-apply' directory as mergeOperationType: 'rebase', which false-positively flags interrupted git am sessions; update the detection so that instead of checking for the directory 'rebase-apply' you check for the sentinel file 'rebase-apply/rebasing' (i.e. only consider it a rebase if that file exists). Modify the status detection logic that uses the entries { file: 'rebase-apply', type: 'rebase' } (and any code that sets mergeOperationType) to test for 'rebase-apply/rebasing' before returning type 'rebase', leaving other sentinels like 'MERGE_HEAD', 'rebase-merge', and 'CHERRY_PICK_HEAD' unchanged.
199-202:⚠️ Potential issue | 🟡 Minor
rebase-applyis created by bothgit amandgit rebase --apply— potential false-positive rebase detection.
.git/rebase-applyis the state directory for the apply backend, created by bothgit am(direct mailbox application) andgit rebase --apply(rebase using apply backend internally). Any ongoinggit amsession will causedetectMergeStateto returnmergeOperationType: 'rebase', which is incorrect.To differentiate, check for the presence of
.git/rebase-apply/rebasingbefore reporting the type as'rebase'. This marker file is created only duringgit rebase --apply, not duringgit am:🛡️ Proposed fix — distinguish `git am` from `git rebase --apply`
- { file: 'rebase-apply', type: 'rebase' as const }, + { file: 'rebase-apply/rebasing', type: 'rebase' as const }, + { file: 'rebase-apply', type: 'am' as const },Or, if adding
'am'as a merge operation type is not desired, check only for therebasingmarker:- { file: 'rebase-apply', type: 'rebase' as const }, + { file: 'rebase-apply/rebasing', type: 'rebase' as const },Note: The
rebasingmarker file is an internal implementation detail and may vary across Git versions; test with the minimum supported Git version in your environment.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/git-utils/src/status.ts` around lines 199 - 202, detectMergeState currently treats presence of the 'rebase-apply' file as mergeOperationType: 'rebase', which yields false positives for git am; update the logic that handles the 'rebase-apply' entry to additionally check for the presence of the 'rebase-apply/rebasing' marker file before returning 'rebase' (if the marker is missing, treat it as not a rebase or handle as 'am' per your domain rules). Locate the check around the 'rebase-apply' array entry in libs/git-utils/src/status.ts and modify the branch that sets mergeOperationType (and any function that consumes it) to perform a file-existence check for the 'rebasing' marker first. Ensure mergeOperationType remains unchanged for git am scenarios when the marker is absent.apps/ui/src/store/app-store.ts (1)
2460-2482:⚠️ Potential issue | 🟠 MajorSync defaultFeatureModel when you mutate it.
When
defaultThinkingLevelis compatible, the code mutatesdefaultFeatureModel.thinkingLevelin local state, but the server sync only sendsdefaultThinkingLevel. After reload, the server can overwrite the local feature model thinking level, causing drift. Consider persisting both when you update the feature model.🛠️ Suggested fix
- if (availableLevels.includes(level)) { - set({ - defaultThinkingLevel: level, - defaultFeatureModel: { ...currentModel, thinkingLevel: level }, - }); - } else { - set({ defaultThinkingLevel: level }); - } + const supportsLevel = availableLevels.includes(level); + const nextFeatureModel = supportsLevel + ? { ...currentModel, thinkingLevel: level } + : currentModel; + set({ + defaultThinkingLevel: level, + ...(supportsLevel ? { defaultFeatureModel: nextFeatureModel } : {}), + }); @@ - await httpApi.settings.updateGlobal({ defaultThinkingLevel: level }); + await httpApi.settings.updateGlobal( + supportsLevel + ? { defaultThinkingLevel: level, defaultFeatureModel: nextFeatureModel } + : { defaultThinkingLevel: level } + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/store/app-store.ts` around lines 2460 - 2482, The handler setDefaultThinkingLevel currently updates defaultFeatureModel.thinkingLevel locally but only syncs defaultThinkingLevel to the server; change it so when availableLevels.includes(level) you also send the updated defaultFeatureModel (or at minimum its thinkingLevel) in the HTTP sync: call getHttpApiClient().settings.updateGlobal with both defaultThinkingLevel and the updated defaultFeatureModel (reflecting the new thinkingLevel) so server state won’t overwrite the local model after reload; keep the existing try/catch and logger.error for failures.
🧹 Nitpick comments (12)
apps/ui/tests/projects/overview-dashboard.spec.ts (1)
76-107: Good addition of try/catch aroundroute.fetch()to handle post-teardown calls.The guard on line 105 prevents flaky test failures from routes invoked after the browser context closes. One minor observation:
The
catchblock silently swallows all errors, including unexpected ones during the test's active lifecycle (e.g., malformed JSON on line 78). Consider logging or re-throwing when the page is still alive to avoid hiding real failures — though in practice, such failures would likely surface as test assertion timeouts anyway.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/tests/projects/overview-dashboard.spec.ts` around lines 76 - 107, The catch currently swallows all errors from the route.fetch()/route.fulfill() block which can hide real test failures; change the catch to capture the error (e.g., catch (err)) and only swallow it when the page/context is closed, otherwise log or re-throw the error so real failures surface; locate the try/catch around route.fetch() / route.fulfill() in the test and use the route.request().frame().page().isClosed() (or equivalent context/frame closed check) to decide whether to swallow or re-throw/log the caught error.libs/prompts/src/enhancement.ts (1)
131-132:ADDITIVE_MODESis duplicated — export it from the shared package instead.
apps/ui/.../enhancement-constants.tsindependently maintains the identical list['technical', 'acceptance', 'ux-reviewer']. Because this constant is not exported from@automaker/prompts, the UI can't import the single source of truth and must keep its own copy in sync. Adding a new additive mode requires updating two separate files with no compiler-enforced link between them.♻️ Proposed fix: export from the prompts library
/** Modes that append additional content rather than rewriting the description */ -const ADDITIVE_MODES: EnhancementMode[] = ['technical', 'acceptance', 'ux-reviewer']; +export const ADDITIVE_MODES: EnhancementMode[] = ['technical', 'acceptance', 'ux-reviewer'];Then in
apps/ui/.../enhancement-constants.ts:-export const ADDITIVE_MODES: EnhancementMode[] = ['technical', 'acceptance', 'ux-reviewer']; +export { ADDITIVE_MODES } from '@automaker/prompts';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 `@libs/prompts/src/enhancement.ts` around lines 131 - 132, The ADDITIVE_MODES constant (type EnhancementMode) is duplicated; export ADDITIVE_MODES from the prompts package and remove the local copy in the UI so both consumers share a single source of truth. Modify libs/prompts/src/enhancement.ts to export the existing ADDITIVE_MODES (keeping the current value ['technical','acceptance','ux-reviewer']), and update the UI’s enhancement-constants.ts to import { ADDITIVE_MODES } from `@automaker/prompts` instead of defining its own array; ensure the exported symbol name and type (EnhancementMode) match the consumers.apps/ui/src/components/views/board-view/shared/enhancement/enhancement-constants.ts (1)
23-30:REWRITE_MODES/ADDITIVE_MODESare mutable and lack compile-time exhaustiveness.Two related concerns:
Mutability: Both exported arrays are
EnhancementMode[], so any caller canpush/splicethem and silently corruptisAdditiveModefor all subsequent calls in the same module lifetime. They should bereadonly.Exhaustiveness gap:
ENHANCEMENT_MODE_LABELS(typedRecord<EnhancementMode, string>) will cause a compile error if a newEnhancementModevalue is added to@automaker/typeswithout being listed—butREWRITE_MODESandADDITIVE_MODEScarry no such guarantee. A new mode would silently fall throughisAdditiveModeasfalse(treated as rewrite) and disappear from the dropdown entirely.Replacing the two arrays with a
satisfies-annotated category map gives bothreadonly-ness and exhaustive compile-time checking:♻️ Proposed fix
-/** Modes that rewrite/replace the entire description */ -export const REWRITE_MODES: EnhancementMode[] = ['improve', 'simplify']; - -/** Modes that append additional content below the original description */ -export const ADDITIVE_MODES: EnhancementMode[] = ['technical', 'acceptance', 'ux-reviewer']; - -/** Check if a mode appends content rather than replacing */ -export function isAdditiveMode(mode: EnhancementMode): boolean { - return ADDITIVE_MODES.includes(mode); -} +/** Per-mode category: 'rewrite' replaces description; 'additive' appends below it. */ +const MODE_CATEGORIES = { + improve: 'rewrite', + simplify: 'rewrite', + technical: 'additive', + acceptance: 'additive', + 'ux-reviewer': 'additive', +} satisfies Record<EnhancementMode, 'rewrite' | 'additive'>; + +/** Modes that rewrite/replace the entire description */ +export const REWRITE_MODES = (Object.keys(MODE_CATEGORIES) as EnhancementMode[]).filter( + (m) => MODE_CATEGORIES[m] === 'rewrite', +); + +/** Modes that append additional content below the original description */ +export const ADDITIVE_MODES = (Object.keys(MODE_CATEGORIES) as EnhancementMode[]).filter( + (m) => MODE_CATEGORIES[m] === 'additive', +); + +/** Check if a mode appends content rather than replacing */ +export function isAdditiveMode(mode: EnhancementMode): boolean { + return MODE_CATEGORIES[mode] === 'additive'; +}🤖 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/shared/enhancement/enhancement-constants.ts` around lines 23 - 30, REWRITE_MODES and ADDITIVE_MODES are mutable and not exhaustively checked against the EnhancementMode union; replace them with a single readonly category map (keyed by EnhancementMode) using the TypeScript "satisfies" operator to enforce exhaustiveness (e.g., a Record<EnhancementMode, 'additive'|'rewrite'>) so the compiler errors if a new EnhancementMode is added, make the map readonly (as const) to prevent runtime mutation, and update isAdditiveMode to look up the mode in that map (referencing REWRITE_MODES, ADDITIVE_MODES, isAdditiveMode, ENHANCEMENT_MODE_LABELS, and EnhancementMode to locate and replace the arrays).apps/server/src/lib/settings-helpers.ts (1)
37-40:DEFAULT_MAX_TURNSnow equalsMAX_ALLOWED_TURNS(both 10000).Since the default and the ceiling are identical, users can only lower the configured value — there's no headroom above the default. This is a deliberate "don't limit by default" stance, but it's worth noting:
- Users who don't configure
defaultMaxTurnswill get 10,000 turns, which could be expensive on high-cost models.- The clamp on line 152 (
Math.max(1, Math.min(MAX_ALLOWED_TURNS, ...))) effectively makes the range [1, 10000] with 10000 as the default.If the intent is to allow power users to go even higher in the future, consider keeping
MAX_ALLOWED_TURNSaboveDEFAULT_MAX_TURNS.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/lib/settings-helpers.ts` around lines 37 - 40, DEFAULT_MAX_TURNS equals MAX_ALLOWED_TURNS (both 10000) so the default sits at the ceiling and users cannot increase turns above the default; update the constants to restore headroom by either raising MAX_ALLOWED_TURNS above DEFAULT_MAX_TURNS (e.g., set MAX_ALLOWED_TURNS to a larger value) or lowering DEFAULT_MAX_TURNS below MAX_ALLOWED_TURNS, and adjust the clamp usage (Math.max(1, Math.min(MAX_ALLOWED_TURNS, ...))) comment to reflect the intended behavior; change the values where DEFAULT_MAX_TURNS and MAX_ALLOWED_TURNS are defined and add a short clarifying comment near the clamp to document the allowed range.apps/ui/src/components/views/file-editor-view/components/editor-tabs.tsx (2)
111-183: Scroll chevrons are always visible, even when all tabs fit without scrolling.The left/right chevron buttons are always rendered and clickable regardless of whether the tab list actually overflows. Consider hiding them (or lowering their opacity) when there's no overflow to avoid a confusing affordance on small tab sets.
This is a minor UX polish item that can be addressed later.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/views/file-editor-view/components/editor-tabs.tsx` around lines 111 - 183, Tabs chevrons are always shown; detect overflow using the scroll container (scrollRef) and hide or dim the left/right buttons when no overflow or when already scrolled to the ends. Add state (e.g., canScrollLeft, canScrollRight or isOverflowing) computed from scrollRef.current.scrollWidth vs clientWidth and updated on mount/resize/scroll (use ResizeObserver or window resize + scroll listener) and in the existing scrollBy logic, then conditionally render or apply reduced-opacity/disabled styles to the ChevronLeft/ChevronRight buttons (the onClick handlers that call scrollBy) so they are not clickable/visible when no scrolling is possible.
144-150: Fragiletext-→bg-class substitution for the dot indicator.
fileColor.replace('text-', 'bg-')works for the current set of returns fromgetFileColor, but if a future variant liketext-muted-foreground/50were added, the replacement would break. Consider a small mapping or a parallelgetFileDotColorfunction that returnsbg-*classes directly.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/views/file-editor-view/components/editor-tabs.tsx` around lines 144 - 150, The dot color substitution using fileColor.replace('text-', 'bg-') is fragile; replace it by deriving the dot class from a dedicated mapping or helper (e.g., add a getFileDotColor(file) or a COLORS_DOT_MAP) and use that in editor-tabs.tsx instead of string replace where tab.isDirty is checked (the JSX that currently uses fileColor.replace). Ensure the helper returns explicit 'bg-*' classes for each file color variant and include a safe fallback class so future `text-*` variants like `text-muted-foreground/50` won’t break.apps/ui/src/lib/diff-utils.ts (1)
192-201: Context lines without a leading space are silently included as-is.When a line inside a hunk doesn't start with
+,-,\, or a space, theelsebranch (line 196) treats it as a context line. This is a reasonable fallback for well-formed diffs but could silently produce incorrect output for corrupted or non-standard diff formats. A minor robustness concern — not blocking.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/lib/diff-utils.ts` around lines 192 - 201, The else branch currently treats any non '+', '-' line as a context line and pushes it into oldLines/newLines, which can silently accept corrupted hunks; update the branch that examines line (the code using line.startsWith and pushing into oldLines/newLines) to explicitly check for a leading space or a backslash escape (e.g., line.startsWith(' ') or line.startsWith('\\')) and only treat those as context lines, and for any other leading character log a warning (e.g., console.warn or project logger) including the offending line content and skip adding it (or optionally throw an error if you prefer fail-fast), so unexpected diff formats are surfaced rather than silently accepted.apps/server/src/services/agent-executor.ts (1)
41-41: ImportDEFAULT_MAX_TURNSfromsettings-helpers.tsto avoid duplication.
DEFAULT_MAX_TURNSis defined locally here (10000) and also exported fromapps/server/src/lib/settings-helpers.ts(10000). Sinceagent-executor.tsalready imports from that module, consolidate the constant by importing it instead of maintaining a duplicate.♻️ Proposed fix
-const DEFAULT_MAX_TURNS = 10000; +import { DEFAULT_MAX_TURNS } from '../lib/settings-helpers.js';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/agent-executor.ts` at line 41, Replace the locally declared DEFAULT_MAX_TURNS in agent-executor.ts with the exported constant from settings-helpers.ts: remove the line declaring const DEFAULT_MAX_TURNS = 10000 and add DEFAULT_MAX_TURNS to the existing import list from apps/server/src/lib/settings-helpers.ts so the file uses the shared DEFAULT_MAX_TURNS symbol instead of a duplicate local value.apps/ui/src/hooks/use-settings-sync.ts (1)
788-788: Consider centralizing the max-turns fallback.
10000is now a cross-cutting default; using a shared constant would reduce drift risk over time.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/hooks/use-settings-sync.ts` at line 788, Replace the magic literal 10000 used as the fallback for max turns with a centralized exported constant (e.g., DEFAULT_MAX_TURNS) and import that constant into use-settings-sync; update the line using serverSettings.defaultMaxTurns ?? 10000 to use serverSettings.defaultMaxTurns ?? DEFAULT_MAX_TURNS, and add the new constant to a shared constants module (export it from a common settings/constants file) so all code paths reference the single source of truth.apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx (1)
249-263: Consider deduplicating thinking-level selection logic.The same
getThinkingLevelsForModel+ fallback logic is repeated on open and reset; a small helper would reduce drift risk.♻️ Example helper
+const resolveThinkingLevel = (modelId: string, defaultLevel: ThinkingLevel) => { + const available = getThinkingLevelsForModel(modelId); + return available.includes(defaultLevel) ? defaultLevel : available[0]; +};Then use
resolveThinkingLevel(...)in both the open effect andresetForm.Also applies to: 418-430
🤖 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/add-feature-dialog.tsx` around lines 249 - 263, Extract the repeated logic that picks a thinking level into a helper like resolveThinkingLevel(modelId: string, defaultThinkingLevel: ThinkingLevel) that calls getThinkingLevelsForModel(modelId) and returns defaultThinkingLevel if included or the first available level otherwise; then replace the duplicated blocks in the effect that sets modelEntry (using effectiveDefaultFeatureModel, setModelEntry) and in resetForm with calls to resolveThinkingLevel(effectiveModelId, defaultThinkingLevel) and assign the returned value to thinkingLevel when building the new model entry.apps/ui/src/hooks/use-project-settings-loader.ts (1)
107-123: Clamp worktree display values before storing.If project settings ever contain out-of-range values (legacy/corrupt sync), negative counts or thresholds can break the panel layout. Consider clamping to a sane non‑negative range (and UI max if applicable) before writing into the store.
✅ Possible hardening
+ const clampNonNegative = (value: number) => Math.max(0, value); + if (settings.pinnedWorktreesCount !== undefined) { - setPinnedWorktreesCount(projectPath, settings.pinnedWorktreesCount); + setPinnedWorktreesCount(projectPath, clampNonNegative(settings.pinnedWorktreesCount)); } if (settings.worktreeDropdownThreshold !== undefined) { - setWorktreeDropdownThreshold(projectPath, settings.worktreeDropdownThreshold); + setWorktreeDropdownThreshold( + projectPath, + clampNonNegative(settings.worktreeDropdownThreshold) + ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/hooks/use-project-settings-loader.ts` around lines 107 - 123, Clamp numeric worktree settings before storing: when applying settings in use-project-settings-loader, validate settings.pinnedWorktreesCount and settings.worktreeDropdownThreshold by coercing them to safe numeric ranges (e.g., ensure >= 0 and cap to UI max). Modify the branches that call setPinnedWorktreesCount(projectPath, settings.pinnedWorktreesCount) and setWorktreeDropdownThreshold(projectPath, settings.worktreeDropdownThreshold) to compute sanitized values (use Number(...) or parseInt + isFinite checks, then Math.max(0, value) and Math.min(value, MAX_*) with defined constants like MAX_PINNED_WORKTREES / MAX_DROPDOWN_THRESHOLD) and pass those sanitized values; leave boolean fields (useWorktrees, alwaysUseWorktreeDropdown) unchanged.libs/types/src/settings.ts (1)
333-363: Avoid string matching for adaptive-capable models.
isAdaptiveThinkingModelstill relies on substring checks. Consider routing this through model definitions (capabilities flags) and querying by model ID to keep capability detection consistent across providers and model updates. Based on learnings: When modeling AI capabilities, add per-model flags to model definitions (e.g., supportsThinking: boolean) and check capabilities by model ID rather than assuming all models from a provider share the same features. This should be applied to TypeScript files under libs/types (not just a single file) to ensure consistent handling across models.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/types/src/settings.ts` around lines 333 - 363, Replace the brittle substring check in isAdaptiveThinkingModel with a capability lookup against the canonical model definitions registry: add/ensure a per-model flag (e.g., supportsThinking: boolean) on your model definition type in libs/types, and change isAdaptiveThinkingModel(model: string) to query the registry by model ID to return that flag; then update getThinkingLevelsForModel and getDefaultThinkingLevel to call the revised isAdaptiveThinkingModel (or read the supportsThinking flag directly) so capability detection is consistent across providers and model updates (refer to the functions isAdaptiveThinkingModel, getThinkingLevelsForModel, getDefaultThinkingLevel and the model definition/type in libs/types).
🤖 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/event-hook-service.ts`:
- Around line 269-304: handleFeatureCompletedEvent may trigger duplicate hooks
because it doesn't mark the feature handled; call this.markFeatureHandled with
the payload's projectPath and featureId before executing hooks. Specifically, in
the private method handleFeatureCompletedEvent (which builds HookContext from
FeatureCompletedPayload), invoke this.markFeatureHandled(payload.projectPath,
payload.featureId) after you assemble the context (and after any featureName
resolution) but before calling this.executeHooksForTrigger(trigger, context, {
passes }); this mirrors the existing usage in the auto_mode_feature_complete /
auto_mode_error handlers and prevents duplicate execution by
feature_status_changed.
In
`@apps/ui/src/components/views/board-view/worktree-panel/components/worktree-dropdown.tsx`:
- Line 254: The computed label displayBranch currently falls back to
`+${worktrees.length} more`, which shows `+0 more` when worktrees is empty;
update the fallback logic in the displayBranch assignment (referencing
selectedWorktree, displayBranch, and worktrees) to check if worktrees.length > 0
and use `+N more` only then, otherwise return a clearer label such as 'No
worktrees' or an empty string.
In `@apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx`:
- Around line 142-227: The effect that initializes pinned branches only seeds
when storedBranches is empty, so increasing pinnedWorktreesCount later doesn't
persist new slots; update the useEffect (the effect that reads
useAppStore.getState().pinnedWorktreeBranchesByProject and calls
setPinnedWorktreeBranches) to handle storedBranches.length < otherSlotCount by
computing defaultBranches for the missing slots (using otherWts.slice(...) →
map(w => w.branch)) and concatenating them onto the existing storedBranches
(clamped to otherWts length) before calling
setPinnedWorktreeBranches(projectPath, updatedBranches), ensuring you never
reference beyond available otherWts and preserving existing assignments.
In `@apps/ui/src/components/views/file-editor-view/components/code-editor.tsx`:
- Around line 191-199: The parsed hunk new-line number (currentNewLine) can be 0
and later is used as a key in deletedGroups/addedLines and passed to doc.line(),
which throws if n < 1; clamp/validate positions wherever you set or consume
currentNewLine (the match handling that assigns currentNewLine) and before calls
to doc.line() (the usages around addedLines handling and the flush/lookup that
call doc.line at ~lines 295 and 319): replace direct uses of the parsed value
with a validated position like const pos = Math.max(1, Math.min(parsedOrCurrent,
doc.lines)) and use pos for object keys and doc.line(pos), or add an early guard
that skips/adjusts operations when parsed/new positions are < 1 or > doc.lines
to avoid RangeError.
In `@apps/ui/src/components/views/file-editor-view/file-editor-view.tsx`:
- Around line 463-488: loadFileDiff currently sets setActiveFileDiff from an
async result without ensuring the response matches the currently active
file/tab, which can cause stale diffs after fast tab switches; fix by
introducing a local request id or capture the requested filePath (and
effectivePath) before the await, then verify that the stored/current active
file/tab (or the same filePath + effectivePath) still matches the request
id/filePath before calling setActiveFileDiff (apply the same pattern to the
other async diff loaders referenced: the blocks around lines 500-505, 527-532,
and 1075-1087); use existing symbols like loadFileDiff, setActiveFileDiff,
effectivePath and getElectronAPI().git.getFileDiff to locate and update the
code.
In
`@apps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsx`:
- Around line 70-75: The component uses a local fallback of 0 for
pinnedWorktreesCount while the store getter defaults to 1, causing UI/store
drift; update the fallback to match the store's default (use 1) or consume the
store's getter directly so both slider and state use the same single source of
truth; specifically adjust the useAppStore selector that reads
pinnedWorktreesCountByProject[project.path] (and any slider fallback logic) to
default to 1 and ensure setPinnedWorktreesCount interactions remain consistent.
In `@apps/ui/src/lib/codemirror-languages.ts`:
- Around line 31-35: The filename extraction in getLanguageExtension only splits
on '/' which fails on Windows paths; update the logic that computes name to
split on both '/' and '\' (e.g., use filePath.split(/[\\/]/).pop() or similar)
so names like Dockerfile or .env are detected on Windows as well, then keep the
rest of the ext calculation (dotIndex/lastIndexOf) unchanged; ensure you
reference the getLanguageExtension function and the variable name when making
the change.
In `@apps/ui/src/store/app-store.ts`:
- Around line 2662-2720: The getters getPinnedWorktreesCount and
getAlwaysUseWorktreeDropdown return defaults (1 and false) that conflict with
the documented ProjectSettings defaults (0 and true); update
getPinnedWorktreesCount to default to 0 and getAlwaysUseWorktreeDropdown to
default to true so the UI uses the same defaults as ProjectSettings, ensuring
the change is made where these functions reference pinnedWorktreesCountByProject
and alwaysUseWorktreeDropdownByProject respectively.
---
Outside diff comments:
In `@apps/server/src/services/worktree-service.ts`:
- Around line 137-162: Split the try/catch so fs.stat(sourcePath) is awaited in
its own try block and only ENOENT from that call emits the
'worktree:copy-files:skipped' event (using normalized) while
returning/continuing; perform fs.mkdir/fs.cp/fs.copyFile in a separate try/catch
so ENOENT or other errors from mkdir/cp/copyFile are treated as real failures
(push into failures[] and eventually raise CopyFilesError). Locate the logic
around fs.stat, fs.mkdir, fs.cp/fs.copyFile,
emitter.emit('worktree:copy-files:skipped', ...) and ensure only the
stat-related ENOENT is mapped to "File not found", other errors fall through to
the failures handling that constructs/throws CopyFilesError for
sourcePath/destPath.
In `@apps/server/tests/unit/lib/enhancement-prompts.test.ts`:
- Around line 194-200: The test iterating ENHANCEMENT_MODES using
buildUserPrompt lacks assertions that verify the additive-mode instruction and
examples strings; update the test in enhancement-prompts.test.ts to, for the
additive-related modes (e.g., 'technical', 'acceptance', 'ux-reviewer'), assert
that the generated prompt includes the exact additive instruction phrase and the
additive examples intro phrase (the strings that begin with "Generate ONLY the
additional details section..." and "Here are examples of the additional details
section...") in addition to the existing contains and length checks so
regressions in those specific strings are caught.
In `@apps/ui/src/store/app-store.ts`:
- Around line 2460-2482: The handler setDefaultThinkingLevel currently updates
defaultFeatureModel.thinkingLevel locally but only syncs defaultThinkingLevel to
the server; change it so when availableLevels.includes(level) you also send the
updated defaultFeatureModel (or at minimum its thinkingLevel) in the HTTP sync:
call getHttpApiClient().settings.updateGlobal with both defaultThinkingLevel and
the updated defaultFeatureModel (reflecting the new thinkingLevel) so server
state won’t overwrite the local model after reload; keep the existing try/catch
and logger.error for failures.
In `@libs/git-utils/src/status.ts`:
- Around line 199-202: The code currently treats presence of the 'rebase-apply'
directory as mergeOperationType: 'rebase', which false-positively flags
interrupted git am sessions; update the detection so that instead of checking
for the directory 'rebase-apply' you check for the sentinel file
'rebase-apply/rebasing' (i.e. only consider it a rebase if that file exists).
Modify the status detection logic that uses the entries { file: 'rebase-apply',
type: 'rebase' } (and any code that sets mergeOperationType) to test for
'rebase-apply/rebasing' before returning type 'rebase', leaving other sentinels
like 'MERGE_HEAD', 'rebase-merge', and 'CHERRY_PICK_HEAD' unchanged.
- Around line 199-202: detectMergeState currently treats presence of the
'rebase-apply' file as mergeOperationType: 'rebase', which yields false
positives for git am; update the logic that handles the 'rebase-apply' entry to
additionally check for the presence of the 'rebase-apply/rebasing' marker file
before returning 'rebase' (if the marker is missing, treat it as not a rebase or
handle as 'am' per your domain rules). Locate the check around the
'rebase-apply' array entry in libs/git-utils/src/status.ts and modify the branch
that sets mergeOperationType (and any function that consumes it) to perform a
file-existence check for the 'rebasing' marker first. Ensure mergeOperationType
remains unchanged for git am scenarios when the marker is absent.
In `@libs/prompts/tests/enhancement.test.ts`:
- Around line 334-348: The test's hard-coded modes array in enhancement.test.ts
is missing 'ux-reviewer', so update the all-modes test to cover that mode:
either add 'ux-reviewer' to the modes array or replace the manual array with
getAvailableEnhancementModes() and iterate over its results, calling
buildUserPrompt(mode, testText) and asserting as before; ensure the test
references buildUserPrompt and uses the same expectations so ux-reviewer prompt
generation is validated.
---
Nitpick comments:
In `@apps/server/src/lib/settings-helpers.ts`:
- Around line 37-40: DEFAULT_MAX_TURNS equals MAX_ALLOWED_TURNS (both 10000) so
the default sits at the ceiling and users cannot increase turns above the
default; update the constants to restore headroom by either raising
MAX_ALLOWED_TURNS above DEFAULT_MAX_TURNS (e.g., set MAX_ALLOWED_TURNS to a
larger value) or lowering DEFAULT_MAX_TURNS below MAX_ALLOWED_TURNS, and adjust
the clamp usage (Math.max(1, Math.min(MAX_ALLOWED_TURNS, ...))) comment to
reflect the intended behavior; change the values where DEFAULT_MAX_TURNS and
MAX_ALLOWED_TURNS are defined and add a short clarifying comment near the clamp
to document the allowed range.
In `@apps/server/src/services/agent-executor.ts`:
- Line 41: Replace the locally declared DEFAULT_MAX_TURNS in agent-executor.ts
with the exported constant from settings-helpers.ts: remove the line declaring
const DEFAULT_MAX_TURNS = 10000 and add DEFAULT_MAX_TURNS to the existing import
list from apps/server/src/lib/settings-helpers.ts so the file uses the shared
DEFAULT_MAX_TURNS symbol instead of a duplicate local value.
In `@apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx`:
- Around line 249-263: Extract the repeated logic that picks a thinking level
into a helper like resolveThinkingLevel(modelId: string, defaultThinkingLevel:
ThinkingLevel) that calls getThinkingLevelsForModel(modelId) and returns
defaultThinkingLevel if included or the first available level otherwise; then
replace the duplicated blocks in the effect that sets modelEntry (using
effectiveDefaultFeatureModel, setModelEntry) and in resetForm with calls to
resolveThinkingLevel(effectiveModelId, defaultThinkingLevel) and assign the
returned value to thinkingLevel when building the new model entry.
In
`@apps/ui/src/components/views/board-view/shared/enhancement/enhancement-constants.ts`:
- Around line 23-30: REWRITE_MODES and ADDITIVE_MODES are mutable and not
exhaustively checked against the EnhancementMode union; replace them with a
single readonly category map (keyed by EnhancementMode) using the TypeScript
"satisfies" operator to enforce exhaustiveness (e.g., a Record<EnhancementMode,
'additive'|'rewrite'>) so the compiler errors if a new EnhancementMode is added,
make the map readonly (as const) to prevent runtime mutation, and update
isAdditiveMode to look up the mode in that map (referencing REWRITE_MODES,
ADDITIVE_MODES, isAdditiveMode, ENHANCEMENT_MODE_LABELS, and EnhancementMode to
locate and replace the arrays).
In `@apps/ui/src/components/views/file-editor-view/components/editor-tabs.tsx`:
- Around line 111-183: Tabs chevrons are always shown; detect overflow using the
scroll container (scrollRef) and hide or dim the left/right buttons when no
overflow or when already scrolled to the ends. Add state (e.g., canScrollLeft,
canScrollRight or isOverflowing) computed from scrollRef.current.scrollWidth vs
clientWidth and updated on mount/resize/scroll (use ResizeObserver or window
resize + scroll listener) and in the existing scrollBy logic, then conditionally
render or apply reduced-opacity/disabled styles to the ChevronLeft/ChevronRight
buttons (the onClick handlers that call scrollBy) so they are not
clickable/visible when no scrolling is possible.
- Around line 144-150: The dot color substitution using
fileColor.replace('text-', 'bg-') is fragile; replace it by deriving the dot
class from a dedicated mapping or helper (e.g., add a getFileDotColor(file) or a
COLORS_DOT_MAP) and use that in editor-tabs.tsx instead of string replace where
tab.isDirty is checked (the JSX that currently uses fileColor.replace). Ensure
the helper returns explicit 'bg-*' classes for each file color variant and
include a safe fallback class so future `text-*` variants like
`text-muted-foreground/50` won’t break.
In `@apps/ui/src/hooks/use-project-settings-loader.ts`:
- Around line 107-123: Clamp numeric worktree settings before storing: when
applying settings in use-project-settings-loader, validate
settings.pinnedWorktreesCount and settings.worktreeDropdownThreshold by coercing
them to safe numeric ranges (e.g., ensure >= 0 and cap to UI max). Modify the
branches that call setPinnedWorktreesCount(projectPath,
settings.pinnedWorktreesCount) and setWorktreeDropdownThreshold(projectPath,
settings.worktreeDropdownThreshold) to compute sanitized values (use Number(...)
or parseInt + isFinite checks, then Math.max(0, value) and Math.min(value,
MAX_*) with defined constants like MAX_PINNED_WORKTREES /
MAX_DROPDOWN_THRESHOLD) and pass those sanitized values; leave boolean fields
(useWorktrees, alwaysUseWorktreeDropdown) unchanged.
In `@apps/ui/src/hooks/use-settings-sync.ts`:
- Line 788: Replace the magic literal 10000 used as the fallback for max turns
with a centralized exported constant (e.g., DEFAULT_MAX_TURNS) and import that
constant into use-settings-sync; update the line using
serverSettings.defaultMaxTurns ?? 10000 to use serverSettings.defaultMaxTurns ??
DEFAULT_MAX_TURNS, and add the new constant to a shared constants module (export
it from a common settings/constants file) so all code paths reference the single
source of truth.
In `@apps/ui/src/lib/diff-utils.ts`:
- Around line 192-201: The else branch currently treats any non '+', '-' line as
a context line and pushes it into oldLines/newLines, which can silently accept
corrupted hunks; update the branch that examines line (the code using
line.startsWith and pushing into oldLines/newLines) to explicitly check for a
leading space or a backslash escape (e.g., line.startsWith(' ') or
line.startsWith('\\')) and only treat those as context lines, and for any other
leading character log a warning (e.g., console.warn or project logger) including
the offending line content and skip adding it (or optionally throw an error if
you prefer fail-fast), so unexpected diff formats are surfaced rather than
silently accepted.
In `@apps/ui/tests/projects/overview-dashboard.spec.ts`:
- Around line 76-107: The catch currently swallows all errors from the
route.fetch()/route.fulfill() block which can hide real test failures; change
the catch to capture the error (e.g., catch (err)) and only swallow it when the
page/context is closed, otherwise log or re-throw the error so real failures
surface; locate the try/catch around route.fetch() / route.fulfill() in the test
and use the route.request().frame().page().isClosed() (or equivalent
context/frame closed check) to decide whether to swallow or re-throw/log the
caught error.
In `@libs/prompts/src/enhancement.ts`:
- Around line 131-132: The ADDITIVE_MODES constant (type EnhancementMode) is
duplicated; export ADDITIVE_MODES from the prompts package and remove the local
copy in the UI so both consumers share a single source of truth. Modify
libs/prompts/src/enhancement.ts to export the existing ADDITIVE_MODES (keeping
the current value ['technical','acceptance','ux-reviewer']), and update the UI’s
enhancement-constants.ts to import { ADDITIVE_MODES } from `@automaker/prompts`
instead of defining its own array; ensure the exported symbol name and type
(EnhancementMode) match the consumers.
In `@libs/types/src/settings.ts`:
- Around line 333-363: Replace the brittle substring check in
isAdaptiveThinkingModel with a capability lookup against the canonical model
definitions registry: add/ensure a per-model flag (e.g., supportsThinking:
boolean) on your model definition type in libs/types, and change
isAdaptiveThinkingModel(model: string) to query the registry by model ID to
return that flag; then update getThinkingLevelsForModel and
getDefaultThinkingLevel to call the revised isAdaptiveThinkingModel (or read the
supportsThinking flag directly) so capability detection is consistent across
providers and model updates (refer to the functions isAdaptiveThinkingModel,
getThinkingLevelsForModel, getDefaultThinkingLevel and the model definition/type
in libs/types).
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (56)
apps/server/package.jsonapps/server/src/lib/sdk-options.tsapps/server/src/lib/settings-helpers.tsapps/server/src/providers/codex-provider.tsapps/server/src/routes/features/index.tsapps/server/src/routes/features/routes/update.tsapps/server/src/services/agent-executor.tsapps/server/src/services/agent-service.tsapps/server/src/services/auto-mode/facade.tsapps/server/src/services/event-hook-service.tsapps/server/src/services/execution-service.tsapps/server/src/services/pipeline-orchestrator.tsapps/server/src/services/worktree-service.tsapps/server/tests/unit/lib/enhancement-prompts.test.tsapps/server/tests/unit/services/event-hook-service.test.tsapps/server/tests/unit/services/settings-service.test.tsapps/ui/nginx.confapps/ui/package.jsonapps/ui/playwright.config.tsapps/ui/src/components/session-manager.tsxapps/ui/src/components/ui/codemirror-diff-view.tsxapps/ui/src/components/ui/git-diff-panel.tsxapps/ui/src/components/views/board-view.tsxapps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsxapps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsxapps/ui/src/components/views/board-view/shared/enhancement/enhance-with-ai.tsxapps/ui/src/components/views/board-view/shared/enhancement/enhancement-constants.tsapps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsxapps/ui/src/components/views/board-view/worktree-panel/components/worktree-dropdown.tsxapps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsxapps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsxapps/ui/src/components/views/file-editor-view/components/code-editor.tsxapps/ui/src/components/views/file-editor-view/components/editor-tabs.tsxapps/ui/src/components/views/file-editor-view/components/file-tree.tsxapps/ui/src/components/views/file-editor-view/file-editor-view.tsxapps/ui/src/components/views/file-editor-view/use-file-editor-store.tsapps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsxapps/ui/src/hooks/use-project-settings-loader.tsapps/ui/src/hooks/use-settings-migration.tsapps/ui/src/hooks/use-settings-sync.tsapps/ui/src/lib/codemirror-languages.tsapps/ui/src/lib/diff-utils.tsapps/ui/src/lib/http-api-client.tsapps/ui/src/store/app-store.tsapps/ui/src/store/types/state-types.tsapps/ui/tests/projects/overview-dashboard.spec.tsapps/ui/tests/settings/settings-startup-sync-race.spec.tsapps/ui/tests/utils/navigation/views.tslibs/git-utils/src/status.tslibs/prompts/src/enhancement-modes/acceptance.tslibs/prompts/src/enhancement-modes/technical.tslibs/prompts/src/enhancement-modes/ux-reviewer.tslibs/prompts/src/enhancement.tslibs/prompts/tests/enhancement.test.tslibs/types/src/settings.tspackage.json
| /** | ||
| * Handle feature:completed events and trigger matching hooks | ||
| */ | ||
| private async handleFeatureCompletedEvent(payload: FeatureCompletedPayload): Promise<void> { | ||
| if (!payload.featureId || !payload.projectPath) return; | ||
|
|
||
| const passes = payload.passes ?? true; | ||
| const trigger: EventHookTrigger = passes ? 'feature_success' : 'feature_error'; | ||
|
|
||
| // Load feature name if we have featureId but no featureName | ||
| let featureName: string | undefined = undefined; | ||
| if (payload.projectPath && this.featureLoader) { | ||
| try { | ||
| const feature = await this.featureLoader.get(payload.projectPath, payload.featureId); | ||
| if (feature?.title) { | ||
| featureName = feature.title; | ||
| } | ||
| } catch (error) { | ||
| logger.warn(`Failed to load feature ${payload.featureId} for event hook:`, error); | ||
| } | ||
| } | ||
|
|
||
| const isErrorTrigger = trigger === 'feature_error'; | ||
| const context: HookContext = { | ||
| featureId: payload.featureId, | ||
| featureName: featureName || payload.featureName, | ||
| projectPath: payload.projectPath, | ||
| projectName: this.extractProjectName(payload.projectPath), | ||
| error: isErrorTrigger ? payload.message : undefined, | ||
| errorType: undefined, | ||
| timestamp: new Date().toISOString(), | ||
| eventType: trigger, | ||
| }; | ||
|
|
||
| await this.executeHooksForTrigger(trigger, context, { passes }); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find where feature:completed, auto_mode_feature_complete, and feature_status_changed are emitted
rg -n "feature:completed|auto_mode_feature_complete|feature_status_changed" -g '*.ts' -B 2 -A 2Repository: AutoMaker-Org/automaker
Length of output: 38926
🏁 Script executed:
# Check if executionMode exists in FeatureCompletedPayload and related types
rg -n "FeatureCompletedPayload|executionMode" -g '*.ts' -B 3 -A 3Repository: AutoMaker-Org/automaker
Length of output: 18218
🏁 Script executed:
# Check if markFeatureHandled method exists in EventHookService
rg -n "markFeatureHandled|class EventHookService" -g '*.ts' -B 2 -A 10Repository: AutoMaker-Org/automaker
Length of output: 4493
Call markFeatureHandled() in handleFeatureCompletedEvent to prevent duplicate hook execution.
When feature:completed is emitted for manual completions, the same status update also triggers feature_status_changed from the feature-state-manager. Since handleFeatureCompletedEvent doesn't mark the feature as handled, handleFeatureStatusChanged will also fire hooks for the same feature, causing duplicate execution. Add markFeatureHandled() to prevent this, similar to how it's already used in the auto_mode_feature_complete and auto_mode_error handlers.
🔧 Suggested fix
private async handleFeatureCompletedEvent(payload: FeatureCompletedPayload): Promise<void> {
if (!payload.featureId || !payload.projectPath) return;
+ // Mark as handled to prevent duplicate firing if feature_status_changed also fires
+ this.markFeatureHandled(payload.featureId);
+
const passes = payload.passes ?? true;
const trigger: EventHookTrigger = passes ? 'feature_success' : 'feature_error';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** | |
| * Handle feature:completed events and trigger matching hooks | |
| */ | |
| private async handleFeatureCompletedEvent(payload: FeatureCompletedPayload): Promise<void> { | |
| if (!payload.featureId || !payload.projectPath) return; | |
| const passes = payload.passes ?? true; | |
| const trigger: EventHookTrigger = passes ? 'feature_success' : 'feature_error'; | |
| // Load feature name if we have featureId but no featureName | |
| let featureName: string | undefined = undefined; | |
| if (payload.projectPath && this.featureLoader) { | |
| try { | |
| const feature = await this.featureLoader.get(payload.projectPath, payload.featureId); | |
| if (feature?.title) { | |
| featureName = feature.title; | |
| } | |
| } catch (error) { | |
| logger.warn(`Failed to load feature ${payload.featureId} for event hook:`, error); | |
| } | |
| } | |
| const isErrorTrigger = trigger === 'feature_error'; | |
| const context: HookContext = { | |
| featureId: payload.featureId, | |
| featureName: featureName || payload.featureName, | |
| projectPath: payload.projectPath, | |
| projectName: this.extractProjectName(payload.projectPath), | |
| error: isErrorTrigger ? payload.message : undefined, | |
| errorType: undefined, | |
| timestamp: new Date().toISOString(), | |
| eventType: trigger, | |
| }; | |
| await this.executeHooksForTrigger(trigger, context, { passes }); | |
| } | |
| /** | |
| * Handle feature:completed events and trigger matching hooks | |
| */ | |
| private async handleFeatureCompletedEvent(payload: FeatureCompletedPayload): Promise<void> { | |
| if (!payload.featureId || !payload.projectPath) return; | |
| // Mark as handled to prevent duplicate firing if feature_status_changed also fires | |
| this.markFeatureHandled(payload.featureId); | |
| const passes = payload.passes ?? true; | |
| const trigger: EventHookTrigger = passes ? 'feature_success' : 'feature_error'; | |
| // Load feature name if we have featureId but no featureName | |
| let featureName: string | undefined = undefined; | |
| if (payload.projectPath && this.featureLoader) { | |
| try { | |
| const feature = await this.featureLoader.get(payload.projectPath, payload.featureId); | |
| if (feature?.title) { | |
| featureName = feature.title; | |
| } | |
| } catch (error) { | |
| logger.warn(`Failed to load feature ${payload.featureId} for event hook:`, error); | |
| } | |
| } | |
| const isErrorTrigger = trigger === 'feature_error'; | |
| const context: HookContext = { | |
| featureId: payload.featureId, | |
| featureName: featureName || payload.featureName, | |
| projectPath: payload.projectPath, | |
| projectName: this.extractProjectName(payload.projectPath), | |
| error: isErrorTrigger ? payload.message : undefined, | |
| errorType: undefined, | |
| timestamp: new Date().toISOString(), | |
| eventType: trigger, | |
| }; | |
| await this.executeHooksForTrigger(trigger, context, { passes }); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/server/src/services/event-hook-service.ts` around lines 269 - 304,
handleFeatureCompletedEvent may trigger duplicate hooks because it doesn't mark
the feature handled; call this.markFeatureHandled with the payload's projectPath
and featureId before executing hooks. Specifically, in the private method
handleFeatureCompletedEvent (which builds HookContext from
FeatureCompletedPayload), invoke this.markFeatureHandled(payload.projectPath,
payload.featureId) after you assemble the context (and after any featureName
resolution) but before calling this.executeHooksForTrigger(trigger, context, {
passes }); this mirrors the existing usage in the auto_mode_feature_complete /
auto_mode_error handlers and prevents duplicate execution by
feature_status_changed.
| // ─── Load File Diff for Inline Display ─────────────────────────────────── | ||
| const loadFileDiff = useCallback( | ||
| async (filePath: string) => { | ||
| if (!effectivePath) return; | ||
| const { setActiveFileDiff } = useFileEditorStore.getState(); | ||
| try { | ||
| const api = getElectronAPI(); | ||
| if (!api.git?.getFileDiff) return; | ||
|
|
||
| // Get relative path | ||
| const relativePath = filePath.startsWith(effectivePath) | ||
| ? filePath.substring(effectivePath.length + 1) | ||
| : filePath; | ||
|
|
||
| const result = await api.git.getFileDiff(effectivePath, relativePath); | ||
| if (result.success && result.diff) { | ||
| setActiveFileDiff(result.diff); | ||
| } else { | ||
| setActiveFileDiff(null); | ||
| } | ||
| } catch { | ||
| setActiveFileDiff(null); | ||
| } | ||
| }, | ||
| [effectivePath] | ||
| ); |
There was a problem hiding this comment.
Guard against stale inline diffs on fast tab switches.
loadFileDiff sets activeFileDiff asynchronously without verifying the active tab; if a request resolves after a tab change, the previous file’s diff can render on the new tab. Consider a request id or filePath check before setting the diff.
Also applies to: 500-505, 527-532, 1075-1087
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/ui/src/components/views/file-editor-view/file-editor-view.tsx` around
lines 463 - 488, loadFileDiff currently sets setActiveFileDiff from an async
result without ensuring the response matches the currently active file/tab,
which can cause stale diffs after fast tab switches; fix by introducing a local
request id or capture the requested filePath (and effectivePath) before the
await, then verify that the stored/current active file/tab (or the same filePath
+ effectivePath) still matches the request id/filePath before calling
setActiveFileDiff (apply the same pattern to the other async diff loaders
referenced: the blocks around lines 500-505, 527-532, and 1075-1087); use
existing symbols like loadFileDiff, setActiveFileDiff, effectivePath and
getElectronAPI().git.getFileDiff to locate and update the code.
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (2)
apps/server/src/services/event-hook-service.ts (1)
269-307:markFeatureHandled()placement is correct;executionModeonFeatureCompletedPayloadis unused.The
markFeatureHandled(payload.featureId)call at line 276 is placed before the firstawait, ensuring the deduplication guard is set atomically in the Node.js event loop before any concurrentfeature_status_changedhandler can run. This correctly addresses deduplication. ✅The
executionModefield declared onFeatureCompletedPayload(line 112) is never read insidehandleFeatureCompletedEvent— the handler fires for allfeature:completedevents regardless. If this field is truly unused, consider removing it to avoid misleading future readers:♻️ Remove unused field from FeatureCompletedPayload
interface FeatureCompletedPayload { featureId: string; featureName?: string; projectPath: string; passes?: boolean; message?: string; - executionMode?: 'auto' | 'manual'; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/event-hook-service.ts` around lines 269 - 307, The FeatureCompletedPayload type's executionMode field is never used by handleFeatureCompletedEvent (the handler ignores executionMode), so remove the unused executionMode property from the FeatureCompletedPayload definition to avoid confusion; update any related type/interface declaration named FeatureCompletedPayload and any tests or callers that set executionMode to stop setting it, and run type checks to ensure no remaining references to executionMode exist.apps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsx (1)
558-576:⚠️ Potential issue | 🟡 MinorMissing rollback on server persist failure.
onValueChangeupdates the store optimistically, butonValueCommit's error path only shows a toast — the store retains the uncommitted value while the server keeps the old one. Every other setting action in this file (handleAddCopyFile,handleRemoveCopyFile,handleFileSelectorSelect) correctly rolls back on failure.🛡️ Proposed fix
onValueCommit={async (value) => { const newValue = value[0] ?? pinnedWorktreesCount; + const prevValue = pinnedWorktreesCount; // Persist to server try { const httpClient = getHttpApiClient(); await httpClient.settings.updateProject(project.path, { pinnedWorktreesCount: newValue, }); } catch (error) { + // Rollback optimistic update on failure + setPinnedWorktreesCount(project.path, prevValue); console.error('Failed to persist pinnedWorktreesCount:', error); toast.error('Failed to save pinned worktrees setting'); } }}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsx` around lines 558 - 576, The optimistic update in onValueChange for pinnedWorktreesCount needs a rollback on server persist failure: capture the previous value (e.g., const prev = pinnedWorktreesCount) before calling setPinnedWorktreesCount in onValueChange or at the start of onValueCommit, and in the catch block of onValueCommit (where getHttpApiClient is used and toast.error is called) call setPinnedWorktreesCount(project.path, prev) to restore the prior value; ensure you reference the same project.path and pinnedWorktreesCount variables so behavior matches other handlers like handleAddCopyFile/handleRemoveCopyFile.
🧹 Nitpick comments (8)
libs/prompts/tests/enhancement.test.ts (3)
353-396: Missing positive test case forisValidEnhancementMode('ux-reviewer').All four pre-existing modes each have a dedicated
it("should return true for '...'")block, but'ux-reviewer'does not. The only indirect coverage is the integration test at line 426, which is conditional ongetAvailableEnhancementModes()already returning the mode.♻️ Suggested addition
it("should return true for 'acceptance'", () => { expect(isValidEnhancementMode('acceptance')).toBe(true); }); + + it("should return true for 'ux-reviewer'", () => { + expect(isValidEnhancementMode('ux-reviewer')).toBe(true); + });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/prompts/tests/enhancement.test.ts` around lines 353 - 396, Test suite lacks a direct positive unit test asserting isValidEnhancementMode('ux-reviewer') returns true; add a new it block in the describe('isValidEnhancementMode') group that calls isValidEnhancementMode('ux-reviewer') and expects true so the mode is explicitly covered (similar to the existing cases for 'improve', 'technical', 'simplify', 'acceptance'); locate the test file libs/prompts/tests/enhancement.test.ts and add the new assertion next to the other per-mode it(...) tests.
2-17: MissingUX_REVIEWER_SYSTEM_PROMPT/UX_REVIEWER_EXAMPLESimports and constant-level tests.By analogy with every other mode,
enhancement.tsshould exportUX_REVIEWER_SYSTEM_PROMPTandUX_REVIEWER_EXAMPLES. Neither is imported here, so theSystem Prompt ConstantsandExamples Constantsdescribe blocks have zero direct coverage for the new mode's exported values.♻️ Suggested additions
import { ... ACCEPTANCE_EXAMPLES, + UX_REVIEWER_SYSTEM_PROMPT, + UX_REVIEWER_EXAMPLES, } from '../src/enhancement.js';Then add corresponding
itblocks inside the existingSystem Prompt ConstantsandExamples Constantsdescribe blocks (mirroring theACCEPTANCE_SYSTEM_PROMPT/ACCEPTANCE_EXAMPLEStests).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/prompts/tests/enhancement.test.ts` around lines 2 - 17, Import the new UX_REVIEWER_SYSTEM_PROMPT and UX_REVIEWER_EXAMPLES symbols into the enhancement.test.ts imports and add constant-level tests mirroring the ACCEPTANCE cases: add an it block in the "System Prompt Constants" describe that asserts UX_REVIEWER_SYSTEM_PROMPT equals the exported value for the UX reviewer mode, and add an it block in the "Examples Constants" describe that asserts UX_REVIEWER_EXAMPLES equals the exported examples; reference the exported symbols UX_REVIEWER_SYSTEM_PROMPT and UX_REVIEWER_EXAMPLES and follow the same assertion pattern used for ACCEPTANCE_SYSTEM_PROMPT / ACCEPTANCE_EXAMPLES so the new mode has direct coverage.
113-175:getEnhancementPrompt,getSystemPrompt, andgetExamplesunit tests don't cover'ux-reviewer'.Each block has an explicit
itfor every previously existing mode; consistency and isolation suggest adding equivalent tests for the new mode in all three sections (similar to the'acceptance'cases already there).Also applies to: 177-223
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/prompts/tests/enhancement.test.ts` around lines 113 - 175, Add unit tests covering the new 'ux-reviewer' mode in all three test suites (the getEnhancementPrompt, getSystemPrompt, and getExamples blocks) mirroring the existing 'acceptance' tests: assert that getEnhancementPrompt('ux-reviewer') returns systemPrompt === UX_REVIEWER_SYSTEM_PROMPT and a description containing the expected UX-related keyword(s), add case-insensitivity checks (e.g., 'UX-REVIEWER' and mixed-case) to ensure getSystemPrompt/getEnhancementPrompt handle casing, and add a corresponding test in the getExamples suite to verify getExamples('ux-reviewer') returns the expected UX_REVIEWER_EXAMPLES (or the correct example structure); follow the same structure and assertions used for 'acceptance' to keep tests consistent and isolated.apps/ui/src/components/views/board-view.tsx (1)
72-74: Prefer the dialogs barrel export for intra-app imports.Keep board-view dialog imports consistent by pulling
ChangePRNumberDialogfrom the dialogs index instead of a direct path.♻️ Suggested change
-import { ChangePRNumberDialog } from './board-view/dialogs/change-pr-number-dialog'; +import { ChangePRNumberDialog } from './board-view/dialogs';Based on learnings: In the apps/ui codebase, 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.tsx` around lines 72 - 74, Replace the direct-path import of ChangePRNumberDialog with the dialogs barrel export to match the other dialog imports and intra-app convention: update the import statement that references ChangePRNumberDialog so it imports from the dialogs index/barrel (the same module that would export CreatePRDialog and CreateBranchDialog) instead of the nested file path; ensure the symbol ChangePRNumberDialog is referenced unchanged in board-view.tsx so existing usages remain valid.apps/ui/src/components/views/file-editor-view/components/file-tree.tsx (1)
125-154: Optional: pre-compute folder rollups once to avoid O(N×M) per-node scans
computeFolderGitRollupiterates the entiregitStatusMapfor every directoryTreeNode. WithuseMemothis only re-runs when the map reference changes, so the cost is acceptable for typical codebases. In large monorepos (many folders × many modified files), a single pass that builds aMap<folderPath, Rollup>once (e.g., in the store or a top-leveluseMemoinFileTree) and passes it down would reduce the work to O(M) total rather than O(N×M).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/views/file-editor-view/components/file-tree.tsx` around lines 125 - 154, computeFolderGitRollup currently scans gitStatusMap for each TreeNode causing O(N×M) work; instead precompute a single Map<folderPath, Rollup> in a higher scope (e.g., in FileTree using useMemo or in the store) by making one pass over gitStatusMap and enhancedGitStatusMap to aggregate count, dominantStatus (use STATUS_PRIORITY), totalAdded and totalRemoved, then pass that precomputed map down to TreeNode and replace calls to computeFolderGitRollup with lookups into the map; ensure the aggregation logic mirrors computeFolderGitRollup (prefix matching of folderPath, STATUS_PRIORITY tie-breaking, summing EnhancedGitFileStatus.linesAdded/linesRemoved).apps/ui/src/store/app-store.ts (1)
2680-2701: Guard against emptynewBranchto avoid inadvertent slot shuffling.When
newBranch === ''(clearing a slot),current.indexOf('')matches the first pre-filled empty slot (not necessarilyslotIndex). The swap logic then moves the current slot's branch to that unrelated empty slot before writing''intoslotIndex, silently reordering branch assignments.🛡️ Proposed fix
swapPinnedWorktreeBranch: (projectPath, slotIndex, newBranch) => set((state) => { + if (!newBranch) { + // Clearing a slot: just blank it out, no swap needed + const src = state.pinnedWorktreeBranchesByProject[projectPath] ?? []; + if (slotIndex >= src.length) return state; + const updated = [...src]; + updated[slotIndex] = ''; + return { + pinnedWorktreeBranchesByProject: { + ...state.pinnedWorktreeBranchesByProject, + [projectPath]: updated, + }, + }; + } const src = state.pinnedWorktreeBranchesByProject[projectPath] ?? [];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/store/app-store.ts` around lines 2680 - 2701, The swapPinnedWorktreeBranch logic can inadvertently swap when newBranch is an empty string; update swapPinnedWorktreeBranch to skip the "existingIndex" swap when newBranch is empty: only run the existingIndex check and swap if newBranch !== '' (and existingIndex !== slotIndex). Keep the rest of the pre-fill and assignment behavior (current[slotIndex] = newBranch) so clearing a slot simply writes '' without moving another slot's branch; reference swapPinnedWorktreeBranch, current, existingIndex, and slotIndex when making this change.apps/ui/src/components/views/file-editor-view/components/code-editor.tsx (2)
258-261: Addinstanceofguard toeq()to match the base-class contract.The base class signature is
eq(widget: WidgetType): boolean. TypeScript method bivariance lets the narrowed parameter compile, but at runtime any call passing a differentWidgetTypesubclass would accessother.linesasundefinedand throwTypeErroron.length. While CodeMirror typically compares same-type widgets, a defensive type guard is the standard practice forWidgetTypeoverrides.🛡️ Proposed fix
eq(other: DeletedLinesWidget) { + if (!(other instanceof DeletedLinesWidget)) return false; return ( this.lines.length === other.lines.length && this.lines.every((l, i) => l === other.lines[i]) ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/views/file-editor-view/components/code-editor.tsx` around lines 258 - 261, The eq method in DeletedLinesWidget should first check that the incoming widget is an instance of DeletedLinesWidget before accessing other.lines; update DeletedLinesWidget.eq(widget) to return false if !(widget instanceof DeletedLinesWidget) and only then compare lengths and contents (this.lines and widget.lines) to satisfy the base-class contract and avoid runtime TypeError.
222-227: Dead guard:!line.startsWith('\\')is unreachable in this branch.The
\ No newline at end of filemessage starts with a backslash (\), so it never matchesline.startsWith(' ')orline === ''. The outerelse ifcondition is never entered for such lines, making the inner guard permanentlytrueand thus dead code. Those lines silently fall through all conditions (correct behavior), but the comment and guard are misleading.🧹 Suggested cleanup
- } else if (line.startsWith(' ') || line === '') { + } else if (line.startsWith(' ') || line === '') { flushDeletions(); - if (!line.startsWith('\\')) { - // Skip "\ No newline at end of file" - currentNewLine++; - } + currentNewLine++; + } else { + // Ignore "\ No newline at end of file" and any other unrecognized prefixes }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/views/file-editor-view/components/code-editor.tsx` around lines 222 - 227, The inner guard and comment inside the else-if branch are dead: when line.startsWith(' ') || line === '' is true, line cannot start with '\' so the check !line.startsWith('\\') and its comment should be removed; instead in the branch for those lines (around the code using line, flushDeletions, and currentNewLine in code-editor.tsx) simply call flushDeletions(); increment currentNewLine; and remove the misleading comment so behavior and intent are clear (leave handling of "\ No newline at end of file" to its existing branch).
🤖 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/index.ts`:
- Around line 100-104: The new POST route registers
validatePathParams('worktreePath') but the handler also expects projectPath and
there is no worktree/git guard; update the middleware chain on the route that
calls createUpdatePRNumberHandler() to also validate projectPath and to run the
existing worktree guard middleware (e.g., add validatePathParams('projectPath')
and the project’s worktree guard such as worktreeGuard() or
ensureWorktreeExists() before createUpdatePRNumberHandler()) so both path params
are validated and the request is rejected if the target worktree is not
present/valid.
In `@apps/server/src/routes/worktree/routes/update-pr-number.ts`:
- Around line 17-157: Extract the Git/PR-fetch/update logic out of
createUpdatePRNumberHandler into a new WorktreeService method (e.g.,
WorktreeService.updatePRNumber(worktreePath, projectPath?, prNumber)) that
encapsulates execAsync calls, isGhCliAvailable(), remote parsing, gh pr view
handling, validatePRState, and updateWorktreePRInfo; have the route handler call
this service method and return its result. After calling updateWorktreePRInfo in
the service, emit an event using createEventEmitter() (e.g.,
emitter.emit('worktree:pr-updated', { projectPath: effectiveProjectPath, branch:
branchName, prInfo })) so the frontend can react. Ensure the service returns the
branch and prInfo (and ghCliUnavailable flag) so the route handler can respond,
and retain the same error handling and fallback behavior currently in
createUpdatePRNumberHandler.
In `@apps/server/src/services/event-hook-service.ts`:
- Around line 205-207: The guard "if (payload.executionMode !== 'auto') return;"
is dropping UI-initiated feature completions because the UI payloads lack
executionMode; add executionMode?: 'auto' | 'manual' to the
auto_mode_feature_complete type definition (auto_mode_feature_complete in the
Electron types) and update the four emitters in apps/ui/src/lib/electron.ts that
emit auto_mode_feature_complete so each payload includes executionMode: 'auto'
(or adjust routing if UI completions should use a different event), thereby
ensuring payload.executionMode checks in EventHookService
(payload.executionMode) no longer silently return for UI events.
In `@apps/ui/src/components/views/board-view/dialogs/change-pr-number-dialog.tsx`:
- Around line 63-67: The validation in ChangePrNumberDialog uses
parseInt(prNumberInput.trim(), 10) which silently truncates values like "1.5";
update the validation in the handler that reads prNumberInput to first trim and
verify the string is a positive whole number (e.g. use a regex /^\d+$/ or
Number+Number.isInteger checks) before parsing, then convert to a Number and
ensure > 0; if the input fails this stricter integer check call setError('Please
enter a valid positive PR number') and return, otherwise proceed with the parsed
integer (refer to variables/functions prNumberInput, setError in
change-pr-number-dialog.tsx).
In `@apps/ui/src/components/views/file-editor-view/components/file-tree.tsx`:
- Around line 129-153: Count currently includes all git statuses and
STATUS_PRIORITY lacks '!' so dominantStatus can remain null while count > 0,
causing an invisible dot and a misleading "Modified files" label; fix by (1)
updating STATUS_PRIORITY to include an entry for '!' (ignored) with a low
priority (so it no longer equals the sentinel dominantPriority), or
alternatively ensure after the loop that if count > 0 and dominantStatus is null
you assign a safe fallback status (e.g., 'M' or '!' depending on desired visual)
before returning; and (2) change the UI label strings that read "Modified files"
to a more accurate "Changed files" or "Files with changes" wherever the
count/dominantStatus result is used (references: gitStatusMap,
enhancedGitStatusMap, STATUS_PRIORITY, dominantStatus, dominantPriority, and the
components that render the "Modified files"/"Changed (N)" label).
In `@apps/ui/src/store/app-store.ts`:
- Around line 2461-2463: The typeof guard is unnecessary because
PhaseModelEntry.model (accessed via currentModel which comes from
get().defaultFeatureModel) is always a ModelId (a string union); remove the
redundant typeof check and set modelId directly from currentModel.model, then
pass that modelId into getThinkingLevelsForModel (update the code around
currentModel, modelId, and getThinkingLevelsForModel accordingly).
---
Duplicate comments:
In `@apps/server/src/services/event-hook-service.ts`:
- Around line 269-307: The FeatureCompletedPayload type's executionMode field is
never used by handleFeatureCompletedEvent (the handler ignores executionMode),
so remove the unused executionMode property from the FeatureCompletedPayload
definition to avoid confusion; update any related type/interface declaration
named FeatureCompletedPayload and any tests or callers that set executionMode to
stop setting it, and run type checks to ensure no remaining references to
executionMode exist.
In
`@apps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsx`:
- Around line 558-576: The optimistic update in onValueChange for
pinnedWorktreesCount needs a rollback on server persist failure: capture the
previous value (e.g., const prev = pinnedWorktreesCount) before calling
setPinnedWorktreesCount in onValueChange or at the start of onValueCommit, and
in the catch block of onValueCommit (where getHttpApiClient is used and
toast.error is called) call setPinnedWorktreesCount(project.path, prev) to
restore the prior value; ensure you reference the same project.path and
pinnedWorktreesCount variables so behavior matches other handlers like
handleAddCopyFile/handleRemoveCopyFile.
---
Nitpick comments:
In `@apps/ui/src/components/views/board-view.tsx`:
- Around line 72-74: Replace the direct-path import of ChangePRNumberDialog with
the dialogs barrel export to match the other dialog imports and intra-app
convention: update the import statement that references ChangePRNumberDialog so
it imports from the dialogs index/barrel (the same module that would export
CreatePRDialog and CreateBranchDialog) instead of the nested file path; ensure
the symbol ChangePRNumberDialog is referenced unchanged in board-view.tsx so
existing usages remain valid.
In `@apps/ui/src/components/views/file-editor-view/components/code-editor.tsx`:
- Around line 258-261: The eq method in DeletedLinesWidget should first check
that the incoming widget is an instance of DeletedLinesWidget before accessing
other.lines; update DeletedLinesWidget.eq(widget) to return false if !(widget
instanceof DeletedLinesWidget) and only then compare lengths and contents
(this.lines and widget.lines) to satisfy the base-class contract and avoid
runtime TypeError.
- Around line 222-227: The inner guard and comment inside the else-if branch are
dead: when line.startsWith(' ') || line === '' is true, line cannot start with
'\' so the check !line.startsWith('\\') and its comment should be removed;
instead in the branch for those lines (around the code using line,
flushDeletions, and currentNewLine in code-editor.tsx) simply call
flushDeletions(); increment currentNewLine; and remove the misleading comment so
behavior and intent are clear (leave handling of "\ No newline at end of file"
to its existing branch).
In `@apps/ui/src/components/views/file-editor-view/components/file-tree.tsx`:
- Around line 125-154: computeFolderGitRollup currently scans gitStatusMap for
each TreeNode causing O(N×M) work; instead precompute a single Map<folderPath,
Rollup> in a higher scope (e.g., in FileTree using useMemo or in the store) by
making one pass over gitStatusMap and enhancedGitStatusMap to aggregate count,
dominantStatus (use STATUS_PRIORITY), totalAdded and totalRemoved, then pass
that precomputed map down to TreeNode and replace calls to
computeFolderGitRollup with lookups into the map; ensure the aggregation logic
mirrors computeFolderGitRollup (prefix matching of folderPath, STATUS_PRIORITY
tie-breaking, summing EnhancedGitFileStatus.linesAdded/linesRemoved).
In `@apps/ui/src/store/app-store.ts`:
- Around line 2680-2701: The swapPinnedWorktreeBranch logic can inadvertently
swap when newBranch is an empty string; update swapPinnedWorktreeBranch to skip
the "existingIndex" swap when newBranch is empty: only run the existingIndex
check and swap if newBranch !== '' (and existingIndex !== slotIndex). Keep the
rest of the pre-fill and assignment behavior (current[slotIndex] = newBranch) so
clearing a slot simply writes '' without moving another slot's branch; reference
swapPinnedWorktreeBranch, current, existingIndex, and slotIndex when making this
change.
In `@libs/prompts/tests/enhancement.test.ts`:
- Around line 353-396: Test suite lacks a direct positive unit test asserting
isValidEnhancementMode('ux-reviewer') returns true; add a new it block in the
describe('isValidEnhancementMode') group that calls
isValidEnhancementMode('ux-reviewer') and expects true so the mode is explicitly
covered (similar to the existing cases for 'improve', 'technical', 'simplify',
'acceptance'); locate the test file libs/prompts/tests/enhancement.test.ts and
add the new assertion next to the other per-mode it(...) tests.
- Around line 2-17: Import the new UX_REVIEWER_SYSTEM_PROMPT and
UX_REVIEWER_EXAMPLES symbols into the enhancement.test.ts imports and add
constant-level tests mirroring the ACCEPTANCE cases: add an it block in the
"System Prompt Constants" describe that asserts UX_REVIEWER_SYSTEM_PROMPT equals
the exported value for the UX reviewer mode, and add an it block in the
"Examples Constants" describe that asserts UX_REVIEWER_EXAMPLES equals the
exported examples; reference the exported symbols UX_REVIEWER_SYSTEM_PROMPT and
UX_REVIEWER_EXAMPLES and follow the same assertion pattern used for
ACCEPTANCE_SYSTEM_PROMPT / ACCEPTANCE_EXAMPLES so the new mode has direct
coverage.
- Around line 113-175: Add unit tests covering the new 'ux-reviewer' mode in all
three test suites (the getEnhancementPrompt, getSystemPrompt, and getExamples
blocks) mirroring the existing 'acceptance' tests: assert that
getEnhancementPrompt('ux-reviewer') returns systemPrompt ===
UX_REVIEWER_SYSTEM_PROMPT and a description containing the expected UX-related
keyword(s), add case-insensitivity checks (e.g., 'UX-REVIEWER' and mixed-case)
to ensure getSystemPrompt/getEnhancementPrompt handle casing, and add a
corresponding test in the getExamples suite to verify getExamples('ux-reviewer')
returns the expected UX_REVIEWER_EXAMPLES (or the correct example structure);
follow the same structure and assertions used for 'acceptance' to keep tests
consistent and isolated.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (20)
apps/server/src/routes/worktree/index.tsapps/server/src/routes/worktree/routes/update-pr-number.tsapps/server/src/services/event-hook-service.tsapps/ui/src/components/views/board-view.tsxapps/ui/src/components/views/board-view/dialogs/change-pr-number-dialog.tsxapps/ui/src/components/views/board-view/dialogs/index.tsapps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsxapps/ui/src/components/views/board-view/worktree-panel/components/worktree-dropdown.tsxapps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsxapps/ui/src/components/views/board-view/worktree-panel/types.tsapps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsxapps/ui/src/components/views/file-editor-view/components/code-editor.tsxapps/ui/src/components/views/file-editor-view/components/file-tree.tsxapps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsxapps/ui/src/lib/codemirror-languages.tsapps/ui/src/lib/electron.tsapps/ui/src/lib/http-api-client.tsapps/ui/src/store/app-store.tsapps/ui/src/types/electron.d.tslibs/prompts/tests/enhancement.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/ui/src/lib/http-api-client.ts
- apps/ui/src/lib/codemirror-languages.ts
| router.post( | ||
| '/update-pr-number', | ||
| validatePathParams('worktreePath'), | ||
| createUpdatePRNumberHandler() | ||
| ); |
There was a problem hiding this comment.
Add projectPath validation + worktree guard on the new route.
projectPath is accepted by the handler but isn’t validated here, and there’s no worktree/git guard. This can lead to unsafe paths or 500s when worktreePath isn’t a valid worktree.
✅ Suggested middleware update
router.post(
'/update-pr-number',
- validatePathParams('worktreePath'),
- createUpdatePRNumberHandler()
+ validatePathParams('worktreePath', 'projectPath?'),
+ requireValidWorktree,
+ createUpdatePRNumberHandler()
);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/server/src/routes/worktree/index.ts` around lines 100 - 104, The new
POST route registers validatePathParams('worktreePath') but the handler also
expects projectPath and there is no worktree/git guard; update the middleware
chain on the route that calls createUpdatePRNumberHandler() to also validate
projectPath and to run the existing worktree guard middleware (e.g., add
validatePathParams('projectPath') and the project’s worktree guard such as
worktreeGuard() or ensureWorktreeExists() before createUpdatePRNumberHandler())
so both path params are validated and the request is rejected if the target
worktree is not present/valid.
| export function createUpdatePRNumberHandler() { | ||
| return async (req: Request, res: Response): Promise<void> => { | ||
| try { | ||
| const { worktreePath, projectPath, prNumber } = req.body as { | ||
| worktreePath: string; | ||
| projectPath?: string; | ||
| prNumber: number; | ||
| }; | ||
|
|
||
| if (!worktreePath) { | ||
| res.status(400).json({ success: false, error: 'worktreePath required' }); | ||
| return; | ||
| } | ||
|
|
||
| if ( | ||
| !prNumber || | ||
| typeof prNumber !== 'number' || | ||
| prNumber <= 0 || | ||
| !Number.isInteger(prNumber) | ||
| ) { | ||
| res.status(400).json({ success: false, error: 'prNumber must be a positive integer' }); | ||
| return; | ||
| } | ||
|
|
||
| const effectiveProjectPath = projectPath || worktreePath; | ||
|
|
||
| // Get current branch name | ||
| const { stdout: branchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', { | ||
| cwd: worktreePath, | ||
| env: execEnv, | ||
| }); | ||
| const branchName = branchOutput.trim(); | ||
|
|
||
| if (!branchName || branchName === 'HEAD') { | ||
| res.status(400).json({ | ||
| success: false, | ||
| error: 'Cannot update PR number in detached HEAD state', | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| // Try to fetch PR info from GitHub for the given PR number | ||
| const ghCliAvailable = await isGhCliAvailable(); | ||
|
|
||
| if (ghCliAvailable) { | ||
| try { | ||
| // Detect repository for gh CLI | ||
| let repoFlag = ''; | ||
| try { | ||
| const { stdout: remotes } = await execAsync('git remote -v', { | ||
| cwd: worktreePath, | ||
| env: execEnv, | ||
| }); | ||
| const lines = remotes.split(/\r?\n/); | ||
| let upstreamRepo: string | null = null; | ||
| let originOwner: string | null = null; | ||
| let originRepo: string | null = null; | ||
|
|
||
| for (const line of lines) { | ||
| const match = | ||
| line.match(/^(\w+)\s+.*[:/]([^/]+)\/([^/\s]+?)(?:\.git)?\s+\(fetch\)/) || | ||
| line.match(/^(\w+)\s+git@[^:]+:([^/]+)\/([^\s]+?)(?:\.git)?\s+\(fetch\)/) || | ||
| line.match(/^(\w+)\s+https?:\/\/[^/]+\/([^/]+)\/([^\s]+?)(?:\.git)?\s+\(fetch\)/); | ||
|
|
||
| if (match) { | ||
| const [, remoteName, owner, repo] = match; | ||
| if (remoteName === 'upstream') { | ||
| upstreamRepo = `${owner}/${repo}`; | ||
| } else if (remoteName === 'origin') { | ||
| originOwner = owner; | ||
| originRepo = repo; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const targetRepo = | ||
| upstreamRepo || (originOwner && originRepo ? `${originOwner}/${originRepo}` : null); | ||
| if (targetRepo) { | ||
| repoFlag = ` --repo "${targetRepo}"`; | ||
| } | ||
| } catch { | ||
| // Ignore remote parsing errors | ||
| } | ||
|
|
||
| // Fetch PR info from GitHub using the PR number | ||
| const viewCmd = `gh pr view ${prNumber}${repoFlag} --json number,title,url,state,createdAt`; | ||
| const { stdout: prOutput } = await execAsync(viewCmd, { | ||
| cwd: worktreePath, | ||
| env: execEnv, | ||
| }); | ||
|
|
||
| const prData = JSON.parse(prOutput); | ||
|
|
||
| const prInfo = { | ||
| number: prData.number, | ||
| url: prData.url, | ||
| title: prData.title, | ||
| state: validatePRState(prData.state), | ||
| createdAt: prData.createdAt || new Date().toISOString(), | ||
| }; | ||
|
|
||
| await updateWorktreePRInfo(effectiveProjectPath, branchName, prInfo); | ||
|
|
||
| logger.info(`Updated PR tracking to #${prNumber} for branch ${branchName}`); | ||
|
|
||
| res.json({ | ||
| success: true, | ||
| result: { | ||
| branch: branchName, | ||
| prInfo, | ||
| }, | ||
| }); | ||
| return; | ||
| } catch (error) { | ||
| logger.warn(`Failed to fetch PR #${prNumber} from GitHub:`, error); | ||
| // Fall through to simple update below | ||
| } | ||
| } | ||
|
|
||
| // Fallback: update with just the number, preserving existing PR info structure | ||
| // or creating minimal info if no GitHub data available | ||
| const prInfo = { | ||
| number: prNumber, | ||
| url: `https://github.com/pulls/${prNumber}`, | ||
| title: `PR #${prNumber}`, | ||
| state: validatePRState('OPEN'), | ||
| createdAt: new Date().toISOString(), | ||
| }; | ||
|
|
||
| await updateWorktreePRInfo(effectiveProjectPath, branchName, prInfo); | ||
|
|
||
| logger.info(`Updated PR tracking to #${prNumber} for branch ${branchName} (no GitHub data)`); | ||
|
|
||
| res.json({ | ||
| success: true, | ||
| result: { | ||
| branch: branchName, | ||
| prInfo, | ||
| ghCliUnavailable: !ghCliAvailable, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Move update logic into a service and emit an event.
This route handler performs substantial git/metadata work directly and doesn’t emit any event to the frontend. Please extract the logic into a service (e.g., WorktreeService) and emit a worktree PR-update event via createEventEmitter() so the UI can react.
As per coding guidelines: 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; 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/update-pr-number.ts` around lines 17 -
157, Extract the Git/PR-fetch/update logic out of createUpdatePRNumberHandler
into a new WorktreeService method (e.g.,
WorktreeService.updatePRNumber(worktreePath, projectPath?, prNumber)) that
encapsulates execAsync calls, isGhCliAvailable(), remote parsing, gh pr view
handling, validatePRState, and updateWorktreePRInfo; have the route handler call
this service method and return its result. After calling updateWorktreePRInfo in
the service, emit an event using createEventEmitter() (e.g.,
emitter.emit('worktree:pr-updated', { projectPath: effectiveProjectPath, branch:
branchName, prInfo })) so the frontend can react. Ensure the service returns the
branch and prInfo (and ghCliUnavailable flag) so the route handler can respond,
and retain the same error handling and fallback behavior currently in
createUpdatePRNumberHandler.
| // Only map explicit auto-mode completion events. | ||
| // Manual feature completions are emitted as feature:completed. | ||
| if (payload.executionMode !== 'auto') return; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find all emitters of auto_mode_feature_complete and inspect their payloads
rg "auto_mode_feature_complete" --type ts -B 3 -A 10Repository: AutoMaker-Org/automaker
Length of output: 50379
🏁 Script executed:
#!/bin/bash
# Search for emitAutoModeEvent calls with auto_mode_feature_complete in server code
rg "emitAutoModeEvent" apps/server/src --type ts -B 2 -A 5 | grep -A 5 "auto_mode_feature_complete"Repository: AutoMaker-Org/automaker
Length of output: 5374
Fix the guard at lines 205-207: it will silently drop all UI-initiated feature completions.
The executionMode field is not present in the Electron UI type definition (apps/ui/src/types/electron.d.ts) or in any of the four auto_mode_feature_complete emitters in apps/ui/src/lib/electron.ts. When the guard evaluates payload.executionMode !== 'auto' on these payloads, undefined !== 'auto' is true, causing an immediate silent return that skips all configured hooks.
Meanwhile, all server-side emitters (pipeline-orchestrator.ts, execution-service.ts, auto-mode/facade.ts) have been updated to include executionMode: 'auto', but the UI—a primary source of feature completions—was not. This creates an asymmetric regression: server-side auto-mode features will trigger hooks; UI-initiated completions will not.
Required fix:
- Add
executionMode?: 'auto' | 'manual'to theauto_mode_feature_completetype definition inapps/ui/src/types/electron.d.ts - Update all four emitters in
apps/ui/src/lib/electron.tsto includeexecutionMode: 'auto'in their payloads
Alternatively, if UI-initiated completions should not route through auto_mode_feature_complete, clarify the routing logic and update the Electron type definition accordingly.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/server/src/services/event-hook-service.ts` around lines 205 - 207, The
guard "if (payload.executionMode !== 'auto') return;" is dropping UI-initiated
feature completions because the UI payloads lack executionMode; add
executionMode?: 'auto' | 'manual' to the auto_mode_feature_complete type
definition (auto_mode_feature_complete in the Electron types) and update the
four emitters in apps/ui/src/lib/electron.ts that emit
auto_mode_feature_complete so each payload includes executionMode: 'auto' (or
adjust routing if UI completions should use a different event), thereby ensuring
payload.executionMode checks in EventHookService (payload.executionMode) no
longer silently return for UI events.
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/ui/src/components/views/board-view/hooks/use-board-actions.ts (1)
500-553:⚠️ Potential issue | 🟡 MinorAdd
currentProjecttohandleDeleteFeaturedependencies to avoid stale cleanup.The callback uses
currentProject.idat lines 513 and 518 to identify and clean worktree state, butcurrentProjectis missing from the dependency array at line 552. This creates a stale closure: if the user switches projects, the callback will still reference the oldcurrentProject.id, causing incorrect worktree cleanup.🛠️ Proposed fix
- [features, runningAutoTasks, autoMode, removeFeature, persistFeatureDelete] + [features, runningAutoTasks, autoMode, removeFeature, persistFeatureDelete, currentProject]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/views/board-view/hooks/use-board-actions.ts` around lines 500 - 553, handleDeleteFeature's useCallback closes over currentProject but currentProject is not listed in the dependency array, which can cause stale cleanup when switching projects; update the dependency array for the handleDeleteFeature callback to include currentProject (so the callback is re-created when currentProject changes) and ensure any referenced symbols (currentProject.id, removeRunningTask usage on store.autoModeByWorktree) are still valid within the new closure.
♻️ Duplicate comments (4)
apps/server/src/routes/worktree/index.ts (1)
100-105:⚠️ Potential issue | 🟠 MajorAdd a worktree guard to
/update-pr-number.This endpoint operates on
worktreePathbut does not enforcerequireValidWorktree, which risks 500s or updating non-worktree paths. Align it with other worktree-specific routes.✅ Suggested fix
router.post( '/update-pr-number', validatePathParams('worktreePath', 'projectPath?'), + requireValidWorktree, requireGitRepoOnly, createUpdatePRNumberHandler() );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/routes/worktree/index.ts` around lines 100 - 105, The POST /update-pr-number route currently validates path params and enforces requireGitRepoOnly but misses the worktree-specific guard; add the requireValidWorktree middleware into the middleware chain for this route (i.e., between requireGitRepoOnly and createUpdatePRNumberHandler) so the route that uses worktreePath runs only for valid worktrees, matching the pattern used by other worktree routes.apps/ui/src/components/views/file-editor-view/components/file-tree.tsx (2)
443-447:⚠️ Potential issue | 🟡 MinorInconsistent label: "modified" (tooltip) vs. "changed" (badge title).
Line 444 says
"modified file(s)"but line 526 says"changed file(s)". The count includes all git statuses (untracked, deleted, renamed, etc.), so "changed" is more accurate. Align both to use "changed".Proposed fix
- tooltip += ` (${folderRollup.count} modified file${folderRollup.count !== 1 ? 's' : ''})`; + tooltip += ` (${folderRollup.count} changed file${folderRollup.count !== 1 ? 's' : ''})`;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/views/file-editor-view/components/file-tree.tsx` around lines 443 - 447, The tooltip string in file-tree.tsx uses "modified file(s)" while the badge title uses "changed file(s)"; update the tooltip construction (where node.isDirectory and folderRollup are used, specifically the tooltip variable built around folderRollup.count) to use the word "changed" instead of "modified" so both labels read "changed file(s)" and remain pluralized the same way as the badge title.
530-542:⚠️ Potential issue | 🟡 MinorMissing
bg-gray-600for'!'(ignored) status in the folder dot.The file-level dot (line 591) includes
'bg-gray-600': gitStatus === '!', but the folder dot omits it. If a folder contains only ignored files,dominantStatuswill be'!'and the dot renders with no background color — invisible.Proposed fix
<span className={cn('w-1.5 h-1.5 rounded-full shrink-0', { 'bg-yellow-500': folderRollup.dominantStatus === 'M', 'bg-green-500': folderRollup.dominantStatus === 'A' || folderRollup.dominantStatus === 'S', 'bg-red-500': folderRollup.dominantStatus === 'D', 'bg-gray-400': folderRollup.dominantStatus === '?', + 'bg-gray-600': folderRollup.dominantStatus === '!', 'bg-purple-500': folderRollup.dominantStatus === 'R', 'bg-cyan-500': folderRollup.dominantStatus === 'C', 'bg-orange-500': folderRollup.dominantStatus === 'U', })}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/views/file-editor-view/components/file-tree.tsx` around lines 530 - 542, The folder-status dot rendering omits styling for the ignored status ('!') so folders with only ignored files show no background; in the span that builds className via cn (the one referencing folderRollup.dominantStatus), add the mapping 'bg-gray-600': folderRollup.dominantStatus === '!' so the ignored folder dot matches the file-level dot (which uses gitStatus === '!') and remains visible; keep the existing title that uses getGitStatusLabel(folderRollup.dominantStatus).apps/ui/src/components/views/board-view/hooks/use-board-actions.ts (1)
1189-1202: Covered by the helper extraction suggestion above.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/views/board-view/hooks/use-board-actions.ts` around lines 1189 - 1202, This block duplicates logic that removes running tasks from autoModeByWorktree for features; refactor to reuse the existing helper (or extract one) instead of repeating code: identify the duplicated logic in use-board-actions.ts that iterates useAppStore.getState().autoModeByWorktree and calls store.removeRunningTask for each runningVerified feature, and replace it with a call to the shared helper (or create a new function named e.g. removeRunningTasksForFeatures(currentProject, runningVerified)) and invoke that helper here; reference useAppStore, autoModeByWorktree, runningVerified, and removeRunningTask (or the existing handleForceStopFeature helper) when locating and replacing the code.
🧹 Nitpick comments (11)
libs/prompts/tests/enhancement.test.ts (3)
372-390:ux-revieweradditive phrasing not explicitly asserted inbuildUserPrompttestsThe "all modes" loop only verifies output existence and that
testTextis present. Sinceux-revieweris an additive mode, a dedicated test analogous to thetechnicalblock at lines 279–286 would confirm it emits'Here are examples of the additional details section'rather than'Here are some examples of how to enhance task descriptions:'.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/prompts/tests/enhancement.test.ts` around lines 372 - 390, The current test loop for buildUserPrompt only checks general output; add a focused test for the 'ux-reviewer' mode that calls buildUserPrompt('ux-reviewer', testText) and asserts the result contains testText and the specific additive phrasing 'Here are examples of the additional details section' (mirroring the pattern used in the existing technical test block that looks for its mode-specific phrasing); reference buildUserPrompt and the 'ux-reviewer' mode to locate where to add this assertion.
1-19: Missing test coverage forisAdditiveMode,ADDITIVE_MODES, andREWRITE_MODESThese three exports are the backbone of the issue
#803fix —buildUserPromptbranches onADDITIVE_MODES.includes(mode)— but they are neither imported nor exercised in this suite. Consider adding adescribe('Additive vs Rewrite modes')block covering:
ADDITIVE_MODEScontains exactly['technical', 'acceptance', 'ux-reviewer'](and not'improve'or'simplify').REWRITE_MODEScontains exactly['improve', 'simplify'].isAdditiveMode(mode)returnstruefor each additive mode andfalsefor each rewrite mode.This would provide a safety net preventing future mode reclassification regressions.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/prompts/tests/enhancement.test.ts` around lines 1 - 19, Add tests that import and assert the new mode constants and predicate: verify ADDITIVE_MODES strictly equals ['technical','acceptance','ux-reviewer'] and REWRITE_MODES strictly equals ['improve','simplify'], and assert isAdditiveMode(mode) returns true for each entry in ADDITIVE_MODES and false for each entry in REWRITE_MODES; also add a quick assertion that buildUserPrompt branches on ADDITIVE_MODES by calling buildUserPrompt with one additive mode and one rewrite mode and asserting the different expected behavior (to guard the branching change).
172-178:getEnhancementPrompt('ux-reviewer')test omitsdescriptionassertionAll other mode tests (lines 135–170) validate the
descriptionfield with atoContaincheck (e.g.,'vague','implementation','verbose','acceptance'). Theux-reviewertest only asserts onsystemPrompt.♻️ Proposed fix
it("should return prompt config for 'ux-reviewer' mode", () => { const result = getEnhancementPrompt('ux-reviewer'); expect(result).toHaveProperty('systemPrompt'); expect(result).toHaveProperty('description'); expect(result.systemPrompt).toBe(UX_REVIEWER_SYSTEM_PROMPT); + expect(result.description).toContain('user experience'); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/prompts/tests/enhancement.test.ts` around lines 172 - 178, Add the missing assertion for the description field in the ux-reviewer unit test: after calling getEnhancementPrompt('ux-reviewer') (in libs/prompts/tests/enhancement.test.ts) add an expectation that result.description contains a UX-specific keyword (e.g., use toContain('ux') or toContain('user experience') so it matches how other mode tests validate description), while keeping the existing check for UX_REVIEWER_SYSTEM_PROMPT.apps/ui/src/types/electron.d.ts (1)
54-58:tool_resultevent carries no result content —ToolUseonly exposes invocation fields.The new
tool_resultvariant reusesToolUse(name+input), making it structurally identical totool_use. There is no field to surface the actual tool output. In Anthropic's streaming protocol a tool result carries a distinct result/content payload. If any consumer ever needs to display or process the result value, the type will need to be extended.♻️ Suggested extension if result content is needed
export interface ToolUse { name: string; input: unknown; } + +export interface ToolResult { + name: string; + input: unknown; + output?: unknown; // result content returned by the tool +} export type StreamEvent = ... | { type: 'tool_result'; sessionId: string; - tool: ToolUse; + tool: ToolResult; }🤖 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 54 - 58, The 'tool_result' event variant currently reuses the ToolUse shape (which only has invocation fields like name and input) and thus lacks any field for the actual tool output; update the type definition for the 'tool_result' union variant to include a result/content payload (e.g., add a 'result' or 'output' field with an appropriate type such as string | Record<string, unknown> | a dedicated ToolResult interface) so consumers can access/display the tool output; specifically modify the union member currently declared with type: 'tool_result' and sessionId/tool: ToolUse to instead include the new result property or change tool to a ToolUseWithResult type to preserve backwards-compatible names while surfacing the tool output.apps/ui/src/hooks/use-electron-agent.ts (1)
311-315:onToolUserepurposed for tool-result notification via a string-suffix protocol.
onToolUseis called fortool_resultevents with the tool's input (not result output) as the second argument, distinguished from a true tool invocation only by the" (done)"suffix on the name. Consumers need to string-match the tool name to differentiate invocation from completion, which is fragile.Consider adding a dedicated optional callback to
UseElectronAgentOptions:♻️ Suggested refactor
interface UseElectronAgentOptions { sessionId: string; workingDirectory?: string; model?: string; thinkingLevel?: string; onToolUse?: (toolName: string, toolInput: unknown) => void; + onToolResult?: (toolName: string, toolInput: unknown) => void; }- case 'tool_result': - // Tool completed - surface as progress update - logger.info('Tool result:', event.tool.name); - onToolUse?.(`${event.tool.name} (done)`, event.tool.input); - break; + case 'tool_result': + logger.info('Tool result:', event.tool.name); + onToolResult?.(event.tool.name, event.tool.input); + break;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/hooks/use-electron-agent.ts` around lines 311 - 315, The handler for the 'tool_result' event misuses onToolUse by signaling completion via a " (done)" name suffix and passing event.tool.input instead of the tool output; add an optional onToolResult callback to UseElectronAgentOptions and invoke it from the 'tool_result' case (in the same switch that currently logs 'Tool result:'), passing the actual result/output and any metadata, and leave onToolUse solely for real invocations; update the type/interface UseElectronAgentOptions, any call sites that construct the options (to optionally provide onToolResult), and replace the current onToolUse call in the 'tool_result' branch with a call to onToolResult while keeping the log line.apps/server/tests/unit/services/agent-service.test.ts (1)
191-255: Good coverage of the happy path; consider adding a test for the'unknown'name fallback.The new test correctly verifies that
tool_use_id-to-name resolution works end-to-end. The one gap is thetoolName || 'unknown'branch inagent-service.ts— when atool_resultarrives with atool_use_idthat was never registered (e.g., thetool_useblock was missing or had notool_use_id). Adding a second case strengthens the coverage:🧪 Suggested additional test
it('should emit tool_result with name "unknown" when tool_use_id is not in the map', async () => { const mockProvider = { getName: () => 'gemini', executeQuery: async function* () { // Emit tool_result directly without a preceding tool_use yield { type: 'assistant', message: { role: 'assistant', content: [ { type: 'tool_result', tool_use_id: 'unregistered-id', content: 'Some result', }, ], }, }; yield { type: 'result', subtype: 'success' }; }, }; vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any); vi.mocked(promptBuilder.buildPromptWithImages).mockResolvedValue({ content: 'Hello', hasImages: false, }); await service.sendMessage({ sessionId: 'session-1', message: 'Hello' }); expect(mockEvents.emit).toHaveBeenCalledWith( 'agent:stream', expect.objectContaining({ type: 'tool_result', tool: expect.objectContaining({ name: 'unknown' }), }) ); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/tests/unit/services/agent-service.test.ts` around lines 191 - 255, Add a second unit test that exercises the fallback branch where a tool_result arrives with an unregistered tool_use_id so the code path using toolName || 'unknown' in agent-service.ts is covered: create a mock provider for ProviderFactory.getProviderForModel whose executeQuery async generator yields an assistant message containing a tool_result with tool_use_id 'unregistered-id' (no preceding tool_use), then a result success; mock promptBuilder.buildPromptWithImages as in the existing test; call service.sendMessage({ sessionId: 'session-1', message: 'Hello' }) and assert mockEvents.emit was called with an 'agent:stream' payload containing type 'tool_result' and tool.name === 'unknown'. Ensure the test references the same setup helpers and uses the same expect.objectContaining structure as the existing test.apps/server/src/services/agent-service.ts (1)
640-653:block.contentmay be an array — consider normalizing to string before emitting.Per the Anthropic SDK,
tool_resultcontent is typed asstring | Array<TextBlock | ImageBlock>. If a provider emits array-valued content,block.content || ''passes the array through as-is, while downstream consumers (UI, tests) expect astringfor thecontentfield.♻️ Optional: normalize content to string
- content: block.content || '', + content: Array.isArray(block.content) + ? block.content.map((b: any) => (b.type === 'text' ? b.text : '')).join('') + : block.content || '',🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/services/agent-service.ts` around lines 640 - 653, Normalize block.content to a string before emitting the 'tool_result' event: in the tool_result branch inside agent-service where you read block.content (and use toolUseId/toolNamesById and call this.emitAgentEvent), detect if block.content is a string and use it directly; if Array.isArray(block.content) map the elements to extract text fields (e.g., TextBlock.text or TextBlock.content) and convert non-text/Image blocks (or ImageBlocks) to a sensible string (e.g., image URL or JSON.stringify) then join parts into a single string; otherwise fallback to JSON.stringify(block.content) so the emitted payload always has content as a string.apps/ui/src/components/views/board-view/hooks/use-board-actions.ts (1)
1018-1072: Extract cross-worktree cleanup into a helper to avoid drift.The same “remove running task from all worktrees” block now appears in
handleDeleteFeature,handleForceStopFeature, andhandleArchiveAllVerified. A small helper will reduce copy/paste divergence.♻️ Suggested refactor
+const removeRunningTaskFromAllWorktrees = (projectId: string, taskId: string) => { + const store = useAppStore.getState(); + const prefix = `${projectId}::`; + for (const [key, worktreeState] of Object.entries(store.autoModeByWorktree)) { + if (key.startsWith(prefix) && worktreeState.runningTasks?.includes(taskId)) { + const branchPart = key.slice(prefix.length); + const branch = branchPart === '__main__' ? null : branchPart; + store.removeRunningTask(projectId, branch, taskId); + } + } +}; ... - if (currentProject) { - const store = useAppStore.getState(); - const prefix = `${currentProject.id}::`; - for (const [key, worktreeState] of Object.entries(store.autoModeByWorktree)) { - if (key.startsWith(prefix) && worktreeState.runningTasks?.includes(feature.id)) { - const branchPart = key.slice(prefix.length); - const branch = branchPart === '__main__' ? null : branchPart; - store.removeRunningTask(currentProject.id, branch, feature.id); - } - } - } + if (currentProject) { + removeRunningTaskFromAllWorktrees(currentProject.id, feature.id); + }Apply the same helper in
handleDeleteFeatureandhandleArchiveAllVerified.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/views/board-view/hooks/use-board-actions.ts` around lines 1018 - 1072, Extract the repeated "remove running task from ALL worktrees" logic into a single helper (e.g. removeRunningTaskFromAllWorktrees(projectId: string, featureId: string)) that calls useAppStore.getState(), reads store.autoModeByWorktree, computes the prefix `${projectId}::`, iterates entries, maps '__main__' to null for the branch, and calls store.removeRunningTask(projectId, branch, featureId) for matching keys; then replace the duplicated blocks in handleForceStopFeature, handleDeleteFeature, and handleArchiveAllVerified with calls to this helper (pass currentProject.id and feature.id or appropriate featureId) to avoid copy/paste drift.apps/ui/src/components/views/file-editor-view/components/code-editor.tsx (3)
618-675:diffContentinextensionsuseMemo deps forces a full extension set rebuild on every diff change.When
diffContentchanges, the entire extension array (language extension, keybinding, update listener, theme) is rebuilt and reconfigured. While@uiw/react-codemirrorpreserves undo history via its internal compartment, the unnecessary recreation of stable extensions is wasteful and recreates a newStateField.define()type each time.The idiomatic CodeMirror approach is to wrap hot-swappable extensions in a
Compartmentand dispatchcompartment.reconfigure(newExtension)on changes, leaving the rest of the configuration untouched.♻️ Suggested refactor: isolate diff extensions in a Compartment
Hoist a stable
Compartmentoutside the component (module-level or viauseRef):+import { Compartment } from '@codemirror/state'; + +// Module-level compartment for diff decorations +const diffCompartment = new Compartment();In the component, remove
diffContentfrom theextensionsuseMemo deps and instead use a separateuseEffect:const extensions = useMemo(() => { // ... (remove diffContent-related logic) exts.push(diffCompartment.of([])); // placeholder initially return exts; - }, [filePath, wordWrap, tabSize, editorTheme, diffContent]); + }, [filePath, wordWrap, tabSize, editorTheme]); + // Hot-swap only the diff decorations when diffContent changes + useEffect(() => { + const view = editorRef.current?.view; + if (!view) return; + view.dispatch({ + effects: diffCompartment.reconfigure(createDiffDecorations(diffContent)), + }); + }, [diffContent]);Note: If
diffCompartmentis module-level, ensure it's per-instance (viauseRef) if multipleCodeEditorinstances can coexist.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/views/file-editor-view/components/code-editor.tsx` around lines 618 - 675, The useMemo for extensions rebuilds the whole extension array whenever diffContent changes; instead create a Compartment (e.g., diffCompartment via useRef or module-level per-instance) and remove diffContent from the dependencies of the extensions useMemo so stable extensions (syntaxHighlighting, editorTheme, search, EditorView.updateListener, keymap, lineWrapping, editorAttributes, langExt) are not recreated; then in a useEffect watching diffContent, call diffCompartment.current.reconfigure(createDiffDecorations(diffContent) ?? []) to update only the diff decorations (refer to createDiffDecorations, extensions useMemo, diffContent, and diffCompartment).
238-253: Hardcoded OKLCH colors inDeletedLinesWidget.toDOM()bypass the CSS variable theme system.All other editor styling uses
var(--chart-*)/var(--foreground)/ etc. The inlineoklch(...)values here (andaddedLineDecorationat line 280) won't adapt if the host app applies a custom palette via CSS variables. If the design system exposes semantic tokens for destructive/success colours, prefer those — e.g.,var(--destructive)/var(--success)or equivalent design tokens.♻️ Suggested refactor: use CSS variables
- container.style.cssText = - 'background-color: oklch(0.55 0.22 25 / 0.1); border-left: 3px solid oklch(0.55 0.22 25 / 0.5);'; + container.style.cssText = + 'background-color: color-mix(in oklab, var(--destructive) 10%, transparent); border-left: 3px solid color-mix(in oklab, var(--destructive) 50%, transparent);'; for (const line of this.lines) { const lineEl = document.createElement('div'); - lineEl.style.cssText = - 'text-decoration: line-through; color: oklch(0.55 0.22 25 / 0.8); background-color: oklch(0.55 0.22 25 / 0.15); padding: 0 0.5rem; padding-left: calc(0.5rem - 3px); white-space: pre; font-family: inherit;'; + lineEl.style.cssText = + 'text-decoration: line-through; color: color-mix(in oklab, var(--destructive) 80%, transparent); background-color: color-mix(in oklab, var(--destructive) 15%, transparent); padding: 0 0.5rem; padding-left: calc(0.5rem - 3px); white-space: pre; font-family: inherit;';const addedLineDecoration = Decoration.line({ class: 'cm-diff-added-line', - attributes: { style: 'background-color: oklch(0.65 0.2 145 / 0.15);' }, + attributes: { style: 'background-color: color-mix(in oklab, var(--success, oklch(0.65 0.2 145)) 15%, transparent);' }, });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/views/file-editor-view/components/code-editor.tsx` around lines 238 - 253, DeletedLinesWidget.toDOM currently hardcodes oklch(...) colors (and similarly addedLineDecoration near addedLineDecoration) which bypass the app's CSS variable theme; update the widget to use semantic CSS variables instead by replacing inline oklch(...) values with var(...) tokens (e.g., var(--destructive), var(--destructive-foreground), var(--background-destructive) or the project's equivalent) and/or move visual rules into a CSS class (cm-diff-deleted-widget and child line elements) so theming is applied via CSS; ensure you update both the container.style.cssText and each lineEl.style.cssText usage in DeletedLinesWidget.toDOM and the addedLineDecoration styling to reference the semantic variables or class names rather than hardcoded colors.
283-301:EditorView.decorations.of(fn)is called on every view update — consider aStateFieldfor consistency.The official CodeMirror 6 docs state that decoration functions provided via
EditorView.decorations.of(fn)are called after the new viewport is computed on every view update, and must not introduce block widgets or replacing decorations that cover line breaks. This means the closure iteratesaddedLineson every keystroke, cursor move, and scroll — whereas the deleted-lines side correctly uses aStateFieldthat only recomputes on document changes.For large diffs the per-update iteration is wasteful. Aligning the added-line decorations with the same
StateFieldpattern used for the deleted-widget block (lines 344–357) would restrict recomputation to document changes only.♻️ Suggested refactor: StateField for added-line decorations
- // Line decorations for added lines - if (addedLines.size > 0) { - extensions.push( - EditorView.decorations.of((view) => { - const builder = new RangeSetBuilder<Decoration>(); - const doc = view.state.doc; - - for (const lineNum of addedLines) { - if (lineNum >= 1 && lineNum <= doc.lines) { - const linePos = doc.line(lineNum).from; - builder.add(linePos, linePos, addedLineDecoration); - } - } - - return builder.finish(); - }) - ); - } + // Line decorations for added lines via StateField (only recomputes on docChanged) + if (addedLines.size > 0) { + const buildAddedDecorations = (doc: { lines: number; line(n: number): { from: number } }) => { + const builder = new RangeSetBuilder<Decoration>(); + for (const lineNum of addedLines) { + if (lineNum >= 1 && lineNum <= doc.lines) { + builder.add(doc.line(lineNum).from, doc.line(lineNum).from, addedLineDecoration); + } + } + return builder.finish(); + }; + extensions.push( + StateField.define({ + create(state) { return buildAddedDecorations(state.doc); }, + update(decorations, tr) { + return tr.docChanged ? decorations.map(tr.changes) : decorations; + }, + provide: (f) => EditorView.decorations.from(f), + }) + ); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/views/file-editor-view/components/code-editor.tsx` around lines 283 - 301, The current added-line decorations are recomputed on every view update because they use EditorView.decorations.of(fn) and iterate addedLines in the closure; replace this with a StateField that computes and stores a DecorationSet from addedLines and the document (similar to the existing StateField used for deleted-line widgets) so decorations only recompute on document/addedLines changes. Implement a StateField (e.g. addedLineDecorationField) whose create/update builds a RangeSetBuilder using addedLines and addedLineDecoration against view.state.doc, export that field and then push EditorView.decorations.of((state) => state.field(addedLineDecorationField)) into extensions instead of the current inline function.
🤖 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/gemini-provider.ts`:
- Around line 632-663: The ensureGeminiIgnore function currently does an
fs.access() followed by fs.writeFile(), which can race and overwrite a
user-created .geminiignore; replace the access-then-write flow by attempting
atomic creation with fs.writeFile(ignorePath, content, { encoding: 'utf-8',
flag: 'wx' }) and handle the EEXIST error (treat as non-fatal/preserve existing
file) while logging other write errors as before, removing the separate
fs.access() branch.
In `@apps/ui/src/components/views/board-view/dialogs/change-pr-number-dialog.tsx`:
- Around line 152-165: Change the native numeric input to a text input so
invalid keystrokes aren't coerced to "": in the Input element used in
change-pr-number-dialog (the JSX with id="pr-number" that currently has
type="number", min="1", placeholder and onChange updating prNumberInput via
setPrNumberInput and clearing error with setError), set type="text" and add
inputMode="numeric" (and remove the min attribute and any spinner-related props)
so mobile shows a numeric keyboard but the component retains full control over
the value string; keep the existing onChange handler
(setPrNumberInput(e.target.value); setError(null)) and validation that relies on
prNumberInput.trim().
In
`@apps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsx`:
- Around line 552-579: The rollback uses the wrong value because prev is read
from pinnedWorktreesCount after the optimistic update; change the flow so you
capture the current stored value before mutating it (e.g., read and store the
current pinnedWorktreesCount into a local variable like prevCount), then call
setPinnedWorktreesCount(project.path, newValue) in onValueChange, and in
onValueCommit use that previously captured prevCount for rollback on error;
update handlers referenced (Slider onValueChange/onValueCommit,
setPinnedWorktreesCount, pinnedWorktreesCount) so the pre-change value is
preserved and used for the rollback and toast/error handling.
In `@libs/platform/src/subprocess.ts`:
- Around line 170-207: The current stdout 'data' handler uses chunk.toString()
which can split multibyte UTF‑8 sequences across buffers; create a StringDecoder
from 'string_decoder' (e.g., const decoder = new StringDecoder('utf8')) outside
the childProcess.stdout.on('data') handler, replace lineBuffer +=
chunk.toString() with lineBuffer += decoder.write(chunk), and on process
end/close call decoder.end() to flush any remaining bytes; keep the existing
lineBuffer, eventQueue, JSON.parse logic and the notifyConsumer wake-up but
switch decoding to decoder.write/decoder.end to avoid UTF‑8 boundary corruption
when parsing JSONL.
---
Outside diff comments:
In `@apps/ui/src/components/views/board-view/hooks/use-board-actions.ts`:
- Around line 500-553: handleDeleteFeature's useCallback closes over
currentProject but currentProject is not listed in the dependency array, which
can cause stale cleanup when switching projects; update the dependency array for
the handleDeleteFeature callback to include currentProject (so the callback is
re-created when currentProject changes) and ensure any referenced symbols
(currentProject.id, removeRunningTask usage on store.autoModeByWorktree) are
still valid within the new closure.
---
Duplicate comments:
In `@apps/server/src/routes/worktree/index.ts`:
- Around line 100-105: The POST /update-pr-number route currently validates path
params and enforces requireGitRepoOnly but misses the worktree-specific guard;
add the requireValidWorktree middleware into the middleware chain for this route
(i.e., between requireGitRepoOnly and createUpdatePRNumberHandler) so the route
that uses worktreePath runs only for valid worktrees, matching the pattern used
by other worktree routes.
In `@apps/ui/src/components/views/board-view/hooks/use-board-actions.ts`:
- Around line 1189-1202: This block duplicates logic that removes running tasks
from autoModeByWorktree for features; refactor to reuse the existing helper (or
extract one) instead of repeating code: identify the duplicated logic in
use-board-actions.ts that iterates useAppStore.getState().autoModeByWorktree and
calls store.removeRunningTask for each runningVerified feature, and replace it
with a call to the shared helper (or create a new function named e.g.
removeRunningTasksForFeatures(currentProject, runningVerified)) and invoke that
helper here; reference useAppStore, autoModeByWorktree, runningVerified, and
removeRunningTask (or the existing handleForceStopFeature helper) when locating
and replacing the code.
In `@apps/ui/src/components/views/file-editor-view/components/file-tree.tsx`:
- Around line 443-447: The tooltip string in file-tree.tsx uses "modified
file(s)" while the badge title uses "changed file(s)"; update the tooltip
construction (where node.isDirectory and folderRollup are used, specifically the
tooltip variable built around folderRollup.count) to use the word "changed"
instead of "modified" so both labels read "changed file(s)" and remain
pluralized the same way as the badge title.
- Around line 530-542: The folder-status dot rendering omits styling for the
ignored status ('!') so folders with only ignored files show no background; in
the span that builds className via cn (the one referencing
folderRollup.dominantStatus), add the mapping 'bg-gray-600':
folderRollup.dominantStatus === '!' so the ignored folder dot matches the
file-level dot (which uses gitStatus === '!') and remains visible; keep the
existing title that uses getGitStatusLabel(folderRollup.dominantStatus).
---
Nitpick comments:
In `@apps/server/src/services/agent-service.ts`:
- Around line 640-653: Normalize block.content to a string before emitting the
'tool_result' event: in the tool_result branch inside agent-service where you
read block.content (and use toolUseId/toolNamesById and call
this.emitAgentEvent), detect if block.content is a string and use it directly;
if Array.isArray(block.content) map the elements to extract text fields (e.g.,
TextBlock.text or TextBlock.content) and convert non-text/Image blocks (or
ImageBlocks) to a sensible string (e.g., image URL or JSON.stringify) then join
parts into a single string; otherwise fallback to JSON.stringify(block.content)
so the emitted payload always has content as a string.
In `@apps/server/tests/unit/services/agent-service.test.ts`:
- Around line 191-255: Add a second unit test that exercises the fallback branch
where a tool_result arrives with an unregistered tool_use_id so the code path
using toolName || 'unknown' in agent-service.ts is covered: create a mock
provider for ProviderFactory.getProviderForModel whose executeQuery async
generator yields an assistant message containing a tool_result with tool_use_id
'unregistered-id' (no preceding tool_use), then a result success; mock
promptBuilder.buildPromptWithImages as in the existing test; call
service.sendMessage({ sessionId: 'session-1', message: 'Hello' }) and assert
mockEvents.emit was called with an 'agent:stream' payload containing type
'tool_result' and tool.name === 'unknown'. Ensure the test references the same
setup helpers and uses the same expect.objectContaining structure as the
existing test.
In `@apps/ui/src/components/views/board-view/hooks/use-board-actions.ts`:
- Around line 1018-1072: Extract the repeated "remove running task from ALL
worktrees" logic into a single helper (e.g.
removeRunningTaskFromAllWorktrees(projectId: string, featureId: string)) that
calls useAppStore.getState(), reads store.autoModeByWorktree, computes the
prefix `${projectId}::`, iterates entries, maps '__main__' to null for the
branch, and calls store.removeRunningTask(projectId, branch, featureId) for
matching keys; then replace the duplicated blocks in handleForceStopFeature,
handleDeleteFeature, and handleArchiveAllVerified with calls to this helper
(pass currentProject.id and feature.id or appropriate featureId) to avoid
copy/paste drift.
In `@apps/ui/src/components/views/file-editor-view/components/code-editor.tsx`:
- Around line 618-675: The useMemo for extensions rebuilds the whole extension
array whenever diffContent changes; instead create a Compartment (e.g.,
diffCompartment via useRef or module-level per-instance) and remove diffContent
from the dependencies of the extensions useMemo so stable extensions
(syntaxHighlighting, editorTheme, search, EditorView.updateListener, keymap,
lineWrapping, editorAttributes, langExt) are not recreated; then in a useEffect
watching diffContent, call
diffCompartment.current.reconfigure(createDiffDecorations(diffContent) ?? []) to
update only the diff decorations (refer to createDiffDecorations, extensions
useMemo, diffContent, and diffCompartment).
- Around line 238-253: DeletedLinesWidget.toDOM currently hardcodes oklch(...)
colors (and similarly addedLineDecoration near addedLineDecoration) which bypass
the app's CSS variable theme; update the widget to use semantic CSS variables
instead by replacing inline oklch(...) values with var(...) tokens (e.g.,
var(--destructive), var(--destructive-foreground), var(--background-destructive)
or the project's equivalent) and/or move visual rules into a CSS class
(cm-diff-deleted-widget and child line elements) so theming is applied via CSS;
ensure you update both the container.style.cssText and each lineEl.style.cssText
usage in DeletedLinesWidget.toDOM and the addedLineDecoration styling to
reference the semantic variables or class names rather than hardcoded colors.
- Around line 283-301: The current added-line decorations are recomputed on
every view update because they use EditorView.decorations.of(fn) and iterate
addedLines in the closure; replace this with a StateField that computes and
stores a DecorationSet from addedLines and the document (similar to the existing
StateField used for deleted-line widgets) so decorations only recompute on
document/addedLines changes. Implement a StateField (e.g.
addedLineDecorationField) whose create/update builds a RangeSetBuilder using
addedLines and addedLineDecoration against view.state.doc, export that field and
then push EditorView.decorations.of((state) =>
state.field(addedLineDecorationField)) into extensions instead of the current
inline function.
In `@apps/ui/src/hooks/use-electron-agent.ts`:
- Around line 311-315: The handler for the 'tool_result' event misuses onToolUse
by signaling completion via a " (done)" name suffix and passing event.tool.input
instead of the tool output; add an optional onToolResult callback to
UseElectronAgentOptions and invoke it from the 'tool_result' case (in the same
switch that currently logs 'Tool result:'), passing the actual result/output and
any metadata, and leave onToolUse solely for real invocations; update the
type/interface UseElectronAgentOptions, any call sites that construct the
options (to optionally provide onToolResult), and replace the current onToolUse
call in the 'tool_result' branch with a call to onToolResult while keeping the
log line.
In `@apps/ui/src/types/electron.d.ts`:
- Around line 54-58: The 'tool_result' event variant currently reuses the
ToolUse shape (which only has invocation fields like name and input) and thus
lacks any field for the actual tool output; update the type definition for the
'tool_result' union variant to include a result/content payload (e.g., add a
'result' or 'output' field with an appropriate type such as string |
Record<string, unknown> | a dedicated ToolResult interface) so consumers can
access/display the tool output; specifically modify the union member currently
declared with type: 'tool_result' and sessionId/tool: ToolUse to instead include
the new result property or change tool to a ToolUseWithResult type to preserve
backwards-compatible names while surfacing the tool output.
In `@libs/prompts/tests/enhancement.test.ts`:
- Around line 372-390: The current test loop for buildUserPrompt only checks
general output; add a focused test for the 'ux-reviewer' mode that calls
buildUserPrompt('ux-reviewer', testText) and asserts the result contains
testText and the specific additive phrasing 'Here are examples of the additional
details section' (mirroring the pattern used in the existing technical test
block that looks for its mode-specific phrasing); reference buildUserPrompt and
the 'ux-reviewer' mode to locate where to add this assertion.
- Around line 1-19: Add tests that import and assert the new mode constants and
predicate: verify ADDITIVE_MODES strictly equals
['technical','acceptance','ux-reviewer'] and REWRITE_MODES strictly equals
['improve','simplify'], and assert isAdditiveMode(mode) returns true for each
entry in ADDITIVE_MODES and false for each entry in REWRITE_MODES; also add a
quick assertion that buildUserPrompt branches on ADDITIVE_MODES by calling
buildUserPrompt with one additive mode and one rewrite mode and asserting the
different expected behavior (to guard the branching change).
- Around line 172-178: Add the missing assertion for the description field in
the ux-reviewer unit test: after calling getEnhancementPrompt('ux-reviewer') (in
libs/prompts/tests/enhancement.test.ts) add an expectation that
result.description contains a UX-specific keyword (e.g., use toContain('ux') or
toContain('user experience') so it matches how other mode tests validate
description), while keeping the existing check for UX_REVIEWER_SYSTEM_PROMPT.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (19)
apps/server/src/providers/gemini-provider.tsapps/server/src/routes/worktree/index.tsapps/server/src/services/agent-service.tsapps/server/src/services/execution-service.tsapps/server/tests/unit/providers/gemini-provider.test.tsapps/server/tests/unit/services/agent-service.test.tsapps/server/tests/unit/services/execution-service.test.tsapps/ui/src/components/views/board-view.tsxapps/ui/src/components/views/board-view/dialogs/change-pr-number-dialog.tsxapps/ui/src/components/views/board-view/hooks/use-board-actions.tsapps/ui/src/components/views/file-editor-view/components/code-editor.tsxapps/ui/src/components/views/file-editor-view/components/file-tree.tsxapps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsxapps/ui/src/hooks/use-auto-mode.tsapps/ui/src/hooks/use-electron-agent.tsapps/ui/src/store/app-store.tsapps/ui/src/types/electron.d.tslibs/platform/src/subprocess.tslibs/prompts/tests/enhancement.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/ui/src/components/views/board-view.tsx
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
apps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsx (1)
561-586:⚠️ Potential issue | 🟡 Minor
sliderPrevRefstill gets overwritten with intermediate values during a drag.
sliderPrevRef.currentis unconditionally reassigned on everyonValueChangecall. Because Radix UI firesonValueChangeon each mouse-move (separate React events, not a single batched transaction), the component re-renders between calls andpinnedWorktreesCountadvances to the intermediate store value. The secondonValueChangetherefore captures that intermediate value asprevCountand overwrites the ref — so byonValueCommit,sliderPrevRef.currentholds the value from just before the last step, not the original pre-drag position.Additionally, the ref is never reset to
nullafter a commit (success or failure), leaving stale data across drag sessions.🛠️ Proposed fix — capture-once + reset after commit
onValueChange={(value) => { - // Capture previous value before mutation for potential rollback - const prevCount = pinnedWorktreesCount; - // Update local state immediately for visual feedback + // Capture the pre-drag value only on the first onValueChange of each drag + if (sliderPrevRef.current === null) { + sliderPrevRef.current = pinnedWorktreesCount; + } const newValue = value[0] ?? pinnedWorktreesCount; setPinnedWorktreesCount(project.path, newValue); - // Store prev for onValueCommit rollback - sliderPrevRef.current = prevCount; }} onValueCommit={async (value) => { const newValue = value[0] ?? pinnedWorktreesCount; const prev = sliderPrevRef.current ?? pinnedWorktreesCount; + sliderPrevRef.current = null; // Reset for the next drag session try { const httpClient = getHttpApiClient(); await httpClient.settings.updateProject(project.path, { pinnedWorktreesCount: newValue, }); } catch (error) { console.error('Failed to persist pinnedWorktreesCount:', error); toast.error('Failed to save pinned worktrees setting'); setPinnedWorktreesCount(project.path, prev); } }}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsx` around lines 561 - 586, The ref sliderPrevRef is being overwritten on every onValueChange causing it to hold intermediate values; change onValueChange so it only captures the starting value once (i.e., set sliderPrevRef.current = pinnedWorktreesCount only if sliderPrevRef.current == null) and continue to optimistically call setPinnedWorktreesCount for visual feedback; in onValueCommit read prev = sliderPrevRef.current ?? pinnedWorktreesCount, persist via getHttpApiClient().settings.updateProject, and then always reset sliderPrevRef.current = null after the commit completes (both in the try success path and in the catch rollback path that calls setPinnedWorktreesCount(project.path, prev)) so the ref doesn't leak stale state across drag sessions.
🧹 Nitpick comments (6)
libs/prompts/tests/enhancement.test.ts (1)
51-55: UX_REVIEWER_SYSTEM_PROMPT test has fewer content assertions than sibling tests.Every other system prompt test asserts two keywords (e.g.,
'vague, unclear'+'actionable'for IMPROVE,'verbose'+'concise'for SIMPLIFY). This test only checks for'User Experience'. Adding a second assertion would keep the tests consistent and catch regressions in prompt wording more effectively.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/prompts/tests/enhancement.test.ts` around lines 51 - 55, The UX_REVIEWER_SYSTEM_PROMPT test only asserts presence of "User Experience" but other prompt tests check two keywords; update the test in enhancement.test.ts to assert a second distinctive keyword from the UX_REVIEWER_SYSTEM_PROMPT string (e.g., assert it also contains "usability" or "accessibility") so the test mirrors sibling tests' dual-keyword checks; locate the test referencing UX_REVIEWER_SYSTEM_PROMPT and add one more expect(...).toContain(...) assertion for that second keyword.apps/ui/src/components/views/file-editor-view/components/file-tree.tsx (1)
112-155: Consider precomputing folder rollups to avoid O(folders × files) scans.
computeFolderGitRollupiterates the fullgitStatusMapfor every directory node; large trees could get expensive. A single prepass that builds a folderPath → rollup map (then passed intoTreeNode) would keep this linear.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/views/file-editor-view/components/file-tree.tsx` around lines 112 - 155, The current computeFolderGitRollup function causes O(folders × files) work by scanning gitStatusMap for each folder; replace this by adding a single prepass that iterates gitStatusMap once and builds a Map<string, {count, dominantStatus, dominantPriority, totalAdded, totalRemoved}> keyed by every ancestor folder path (compute ancestors by walking up filePath prefixes), updating counts, totals from enhancedGitStatusMap, and choosing dominantStatus using STATUS_PRIORITY exactly as computeFolderGitRollup does; then change TreeNode (or wherever computeFolderGitRollup is called) to accept and use this precomputed folderRollup map instead of calling computeFolderGitRollup per node.apps/ui/src/components/views/board-view/hooks/use-board-actions.ts (3)
1036-1050: Good optimistic cache update with clear rationale.The comment explaining the 30s poll-cycle race condition is helpful. The
undefinedguard onoldFeaturesis correct.Note: this update runs unconditionally, even when
targetStatus === feature.status(Line 1052 guards onlymoveFeature/persistFeatureUpdate). It's harmless (idempotent map), but if you want to be precise you could wrap lines 1040–1050 inside the sameif (targetStatus !== feature.status)guard.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/views/board-view/hooks/use-board-actions.ts` around lines 1036 - 1050, The optimistic cache update runs even when the feature's status is unchanged; wrap the queryClient.setQueryData call (the block that maps oldFeatures using queryKeys.features.all and feature.id) in the same conditional used elsewhere so it only runs when targetStatus !== feature.status (and still only when currentProject is truthy), i.e., guard the setQueryData block with if (targetStatus !== feature.status) to avoid redundant/idiomatic updates.
1-1:@ts-nocheckdisables all type safety for a 1,363-line file with significant business logic.This suppresses type errors across the entire file, including the new
removeRunningTaskFromAllWorktreesfunction, the optimisticqueryClient.setQueryDatacallback, and all the dependency array contents. Any type regressions introduced here (or already present) will be invisible. Consider removing it and fixing the underlying type issues, or at minimum scoping suppressions to specific lines with@ts-expect-error.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/views/board-view/hooks/use-board-actions.ts` at line 1, Remove the blanket `@ts-nocheck` at the top of the file and restore type safety: annotate key functions and variables (e.g., give explicit types to removeRunningTaskFromAllWorktrees, the optimistic updater passed to queryClient.setQueryData, and the hook return type), tighten types for the worktree/task models (use interfaces or Partial<T> for partial updates), and explicitly type dependency arrays for hooks (use React dependencies with correct types). If there are a few stubborn errors you cannot fix immediately, replace those specific lines with targeted suppressions using // `@ts-expect-error` and a brief comment rather than disabling checks for the whole file; prioritize adding proper typings to functions mentioned above and to the queryClient instance so the optimistic update callback type-checks.
558-562: Consider adding a React Query cache update for consistency withhandleForceStopFeature.
handleForceStopFeatureoptimistically updates the React Query cache (line 1041:queryClient.setQueryData(queryKeys.features.all(...))) to avoid race conditions with the 30-second poll cycle.handleDeleteFeaturerelies solely on Zustand'sremoveFeatureto hide the card. Since the board sources features from the React Query cache viauseFeatures, the deleted feature could briefly reappear after a background refetch races with thepersistFeatureDeletecall.Adding a
queryClient.setQueryDatacall to filter out the deleted feature would keep both deletion and stop-feature paths consistent and prevent this potential race condition.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ui/src/components/views/board-view/hooks/use-board-actions.ts` around lines 558 - 562, The deletion currently only calls removeFeature and persistFeatureDelete in handleDeleteFeature, which can race with background refetches; mirror handleForceStopFeature by calling queryClient.setQueryData(queryKeys.features.all(currentProjectId, ...)) to immediately filter out the deleted featureId from the cached features used by useFeatures, placing this optimistic cache update right after removeFeature (and before await persistFeatureDelete) so the React Query cache and Zustand stay consistent and the deleted card cannot reappear during the poll cycle.apps/ui/src/components/views/board-view/dialogs/change-pr-number-dialog.tsx (1)
18-29: Consider renaming the localWorktreeInfoto avoid shadowing the exportedWorktreeInfofromelectron.d.ts.The exported
WorktreeInfoinelectron.d.tsuses{ worktreePath, branchName, head?, baseBranch? }, while this local interface uses{ path, branch, isMain, pr? }— a completely different shape. TypeScript scoping handles this correctly, but having two identically named interfaces with different fields in the same app is a source of confusion, especially when both are in scope.Consider naming this local type
WorktreeListItemorWorktreeDetailto match whatlistAll()returns.♻️ Suggested rename
-interface WorktreeInfo { +interface WorktreeListItem { path: string; branch: string; isMain: boolean; pr?: { number: number; url: string; title: string; state: string; createdAt: string; }; } interface ChangePRNumberDialogProps { open: boolean; onOpenChange: (open: boolean) => void; - worktree: WorktreeInfo | null; + worktree: WorktreeListItem | null; projectPath: string | null; onChanged: () => void; }🤖 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/change-pr-number-dialog.tsx` around lines 18 - 29, The local interface WorktreeInfo conflicts in name with the exported WorktreeInfo from electron.d.ts and should be renamed to avoid confusion; rename the local interface (and all its usages) to a clearer name such as WorktreeListItem or WorktreeDetail (e.g., change interface WorktreeInfo { ... } → interface WorktreeListItem { ... }) and update any references in this file (including where listAll() results are typed or consumed and any props/state expecting WorktreeInfo) to use the new name so the code no longer shadows the exported type.
🤖 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/components/views/project-settings-view/worktree-preferences-section.tsx`:
- Around line 542-556: The Label-to-Slider association isn't accessible because
the Slider's root is a non-labelable span; update the Slider with an explicit
aria-label (e.g., aria-label="Pinned worktree tabs") or an aria-labelledby that
references the Label text so screen readers and keyboard users can
identify/focus the control; modify the Slider component instance with
aria-label/aria-labelledby near id="pinned-worktrees-count" (and keep the
existing Label) so the Slider component and its thumb are properly announced and
operable.
In `@libs/prompts/tests/enhancement.test.ts`:
- Around line 342-347: The test currently calls buildUserPrompt('ux-reviewer',
testText, true) which enables examples but is placed inside the "without
examples" describe block; move this it(...) block out of the "without examples"
describe and into the "with examples (default)" or "all modes" describe so its
semantics match the includeExamples=true argument, ensuring the test remains
adjacent to other example-enabled cases for buildUserPrompt.
---
Duplicate comments:
In
`@apps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsx`:
- Around line 561-586: The ref sliderPrevRef is being overwritten on every
onValueChange causing it to hold intermediate values; change onValueChange so it
only captures the starting value once (i.e., set sliderPrevRef.current =
pinnedWorktreesCount only if sliderPrevRef.current == null) and continue to
optimistically call setPinnedWorktreesCount for visual feedback; in
onValueCommit read prev = sliderPrevRef.current ?? pinnedWorktreesCount, persist
via getHttpApiClient().settings.updateProject, and then always reset
sliderPrevRef.current = null after the commit completes (both in the try success
path and in the catch rollback path that calls
setPinnedWorktreesCount(project.path, prev)) so the ref doesn't leak stale state
across drag sessions.
---
Nitpick comments:
In `@apps/ui/src/components/views/board-view/dialogs/change-pr-number-dialog.tsx`:
- Around line 18-29: The local interface WorktreeInfo conflicts in name with the
exported WorktreeInfo from electron.d.ts and should be renamed to avoid
confusion; rename the local interface (and all its usages) to a clearer name
such as WorktreeListItem or WorktreeDetail (e.g., change interface WorktreeInfo
{ ... } → interface WorktreeListItem { ... }) and update any references in this
file (including where listAll() results are typed or consumed and any
props/state expecting WorktreeInfo) to use the new name so the code no longer
shadows the exported type.
In `@apps/ui/src/components/views/board-view/hooks/use-board-actions.ts`:
- Around line 1036-1050: The optimistic cache update runs even when the
feature's status is unchanged; wrap the queryClient.setQueryData call (the block
that maps oldFeatures using queryKeys.features.all and feature.id) in the same
conditional used elsewhere so it only runs when targetStatus !== feature.status
(and still only when currentProject is truthy), i.e., guard the setQueryData
block with if (targetStatus !== feature.status) to avoid redundant/idiomatic
updates.
- Line 1: Remove the blanket `@ts-nocheck` at the top of the file and restore type
safety: annotate key functions and variables (e.g., give explicit types to
removeRunningTaskFromAllWorktrees, the optimistic updater passed to
queryClient.setQueryData, and the hook return type), tighten types for the
worktree/task models (use interfaces or Partial<T> for partial updates), and
explicitly type dependency arrays for hooks (use React dependencies with correct
types). If there are a few stubborn errors you cannot fix immediately, replace
those specific lines with targeted suppressions using // `@ts-expect-error` and a
brief comment rather than disabling checks for the whole file; prioritize adding
proper typings to functions mentioned above and to the queryClient instance so
the optimistic update callback type-checks.
- Around line 558-562: The deletion currently only calls removeFeature and
persistFeatureDelete in handleDeleteFeature, which can race with background
refetches; mirror handleForceStopFeature by calling
queryClient.setQueryData(queryKeys.features.all(currentProjectId, ...)) to
immediately filter out the deleted featureId from the cached features used by
useFeatures, placing this optimistic cache update right after removeFeature (and
before await persistFeatureDelete) so the React Query cache and Zustand stay
consistent and the deleted card cannot reappear during the poll cycle.
In `@apps/ui/src/components/views/file-editor-view/components/file-tree.tsx`:
- Around line 112-155: The current computeFolderGitRollup function causes
O(folders × files) work by scanning gitStatusMap for each folder; replace this
by adding a single prepass that iterates gitStatusMap once and builds a
Map<string, {count, dominantStatus, dominantPriority, totalAdded, totalRemoved}>
keyed by every ancestor folder path (compute ancestors by walking up filePath
prefixes), updating counts, totals from enhancedGitStatusMap, and choosing
dominantStatus using STATUS_PRIORITY exactly as computeFolderGitRollup does;
then change TreeNode (or wherever computeFolderGitRollup is called) to accept
and use this precomputed folderRollup map instead of calling
computeFolderGitRollup per node.
In `@libs/prompts/tests/enhancement.test.ts`:
- Around line 51-55: The UX_REVIEWER_SYSTEM_PROMPT test only asserts presence of
"User Experience" but other prompt tests check two keywords; update the test in
enhancement.test.ts to assert a second distinctive keyword from the
UX_REVIEWER_SYSTEM_PROMPT string (e.g., assert it also contains "usability" or
"accessibility") so the test mirrors sibling tests' dual-keyword checks; locate
the test referencing UX_REVIEWER_SYSTEM_PROMPT and add one more
expect(...).toContain(...) assertion for that second keyword.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
apps/server/src/providers/gemini-provider.tsapps/server/src/routes/worktree/index.tsapps/server/src/services/agent-service.tsapps/server/tests/unit/services/agent-service.test.tsapps/ui/src/components/views/board-view/dialogs/change-pr-number-dialog.tsxapps/ui/src/components/views/board-view/hooks/use-board-actions.tsapps/ui/src/components/views/file-editor-view/components/file-tree.tsxapps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsxapps/ui/src/hooks/use-electron-agent.tsapps/ui/src/types/electron.d.tslibs/platform/src/subprocess.tslibs/prompts/tests/enhancement.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- libs/platform/src/subprocess.ts
- apps/ui/src/hooks/use-electron-agent.ts
- apps/server/tests/unit/services/agent-service.test.ts
- apps/server/src/routes/worktree/index.ts
| <Label | ||
| htmlFor="pinned-worktrees-count" | ||
| className="text-foreground cursor-pointer font-medium" | ||
| > | ||
| Pinned Worktree Tabs | ||
| </Label> | ||
| <span className="text-sm font-medium text-foreground tabular-nums"> | ||
| {pinnedWorktreesCount} | ||
| </span> | ||
| </div> | ||
| <p className="text-xs text-muted-foreground/80 leading-relaxed"> | ||
| Number of worktree tabs to pin (excluding the main worktree, which is always shown). | ||
| </p> | ||
| <Slider | ||
| id="pinned-worktrees-count" |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
shadcn-ui Slider component id prop implementation documentation
💡 Result:
In shadcn/ui, the Slider component’s props are typed as:
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
and are forwarded onto SliderPrimitive.Root via {...props}—so id isn’t specially implemented; it’s simply passed through to the underlying Radix Slider root DOM element. (github.com)
Docs / examples:
- shadcn/ui’s “Controlled” example shows using
id="slider-demo-temperature"on<Slider />(paired with<Label htmlFor="...">). (ui.shadcn.com) - Radix Slider docs show labeling via
aria-labelonSlider.Thumb(common for accessibility). (radix-ui.com)
Citations:
- 1: Using Slider as FormField shadcn-ui/ui#783
- 2: https://ui.shadcn.com/docs/components/base/slider?utm_source=openai
- 3: https://www.radix-ui.com/primitives/docs/components/slider
Consider using aria-label on the Slider for better accessibility.
While the id="pinned-worktrees-count" on the Slider component (which applies to the root <span> element) can technically pair with the Label's htmlFor, this doesn't create a standard label–input association since <span> is not a labelable element. Clicking the Label won't focus the slider thumb. shadcn-ui's own examples use this pattern, but for more robust accessibility, use aria-label on the Slider component directly or wrap it appropriately for screen reader clarity.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsx`
around lines 542 - 556, The Label-to-Slider association isn't accessible because
the Slider's root is a non-labelable span; update the Slider with an explicit
aria-label (e.g., aria-label="Pinned worktree tabs") or an aria-labelledby that
references the Label text so screen readers and keyboard users can
identify/focus the control; modify the Slider component instance with
aria-label/aria-labelledby near id="pinned-worktrees-count" (and keep the
existing Label) so the Slider component and its thumb are properly announced and
operable.
| it('should use additive phrasing for ux-reviewer mode', () => { | ||
| const result = buildUserPrompt('ux-reviewer', testText, true); | ||
|
|
||
| expect(result).toContain(testText); | ||
| expect(result).toContain('Here are examples of the additional details section'); | ||
| }); |
There was a problem hiding this comment.
Test is inside the 'without examples' describe block but passes includeExamples = true.
buildUserPrompt('ux-reviewer', testText, true) explicitly enables examples, yet this test lives inside the describe('without examples', ...) block (line 320). This is misleading and could confuse future contributors.
Move this test to the describe('with examples (default)', ...) block (starting at line 269) or to the describe('all modes', ...) block, where it fits semantically.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@libs/prompts/tests/enhancement.test.ts` around lines 342 - 347, The test
currently calls buildUserPrompt('ux-reviewer', testText, true) which enables
examples but is placed inside the "without examples" describe block; move this
it(...) block out of the "without examples" describe and into the "with examples
(default)" or "all modes" describe so its semantics match the
includeExamples=true argument, ensuring the test remains adjacent to other
example-enabled cases for buildUserPrompt.
Summary
This PR introduces per-project worktree view customization, allowing users to control how worktrees are displayed in the board view panel. It also ships several quality-of-life improvements across the file editor, diff viewer, and AI enhancement workflow.
Worktree Panel — Pinned Tabs & Swap
WorktreeDropdownwith an updated trigger label (+N more) and ahighlightTriggerprop to distinguish primary vs. secondary dropdowns.pinnedWorktreesCountis persisted to project settings via the server API.Project Settings — Display Settings Section
WorktreePreferencesSectionwith a labelled slider (0–25) to configure the number of pinned worktree tabs.onValueCommit) and persisted to the server.libs/types—ProjectSettingsadditionspinnedWorktreesCountandworktreeDropdownThresholdtoProjectSettings.getDefaultThinkingLevel()now returns'adaptive'for Opus/adaptive-thinking models instead of hardcoded'none'.File Editor Improvements
showInlineDiff/activeFileDiffstate; the editor can now render inline diff decorations via CodeMirrorStateFieldandDecoration.CodeEditorHandlegains agetSelection()method returning{ text, fromLine, toLine }for use by downstream actions (e.g. "Add Feature from selection").AddFeatureDialogintegration allows creating a feature card directly from a code selection in the file editor.getLanguageExtension()moved to a sharedcodemirror-languages.tsmodule to keepcode-editor.tsxfocused.FolderKanbanicon).Diff Viewer — CodeMirror-based Diff
parseDiff()andsplitDiffByFile()extracted fromgit-diff-panel.tsxinto a reusablediff-utils.tsmodule (exportingParsedFileDifftype).CodeMirrorDiffViewcomponent replaces the hand-rolled HTML diff renderer for richer syntax-highlighted diffs.git-diff-panel.tsxsignificantly slimmed down (~250 lines removed).AI Enhancement — Additive vs. Rewrite Modes
improve,simplify): replaces the description.technical,acceptance,ux-reviewer): appends AI-generated content below the original description.isAdditiveMode()helper andREWRITE_MODES/ADDITIVE_MODESarrays exported fromenhancement-constants.ts.Server — Event Hook & Feature Completion
EventHookServicenow handlesfeature:completedevents (emitted for manual feature runs) and maps them tofeature_success/feature_errorhooks.auto_mode_feature_completeevents are now guarded to only fire hooks whenexecutionMode === 'auto', preventing double-firing.AutoModeEventPayloadand a newFeatureCompletedPayloadinterface captureexecutionModecontext.event-hook-service.test.tsupdated to cover the new event handling paths.Other
sdk-options.ts: minor additions.codex-provider.ts: minor additions.features/update.tsroute: minor adjustments.defaultThinkingLevelinitial state changed from'none'to'adaptive'.Summary by CodeRabbit
New Features
Improvements