Skip to content

Feature: worktree view customization and stability fixes#805

Merged
gsxdsm merged 12 commits intoAutoMaker-Org:v0.15.0rcfrom
gsxdsm:feature/worktree-view-customization
Feb 24, 2026
Merged

Feature: worktree view customization and stability fixes#805
gsxdsm merged 12 commits intoAutoMaker-Org:v0.15.0rcfrom
gsxdsm:feature/worktree-view-customization

Conversation

@gsxdsm
Copy link
Copy Markdown
Collaborator

@gsxdsm gsxdsm commented Feb 23, 2026

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

  • Pinned worktree tabs: Users can now pin a configurable number of non-main worktrees as persistent tabs in the panel. The main worktree is always shown; additional slots are user-controlled.
  • Slot assignment: Pinned slots remember specific branch assignments. When a branch no longer exists, the slot falls back gracefully to the next available worktree.
  • Swap Worktree: A new "Swap Worktree" submenu in the worktree actions dropdown lets users swap any pinned slot to a different branch without leaving the view.
  • Overflow dropdown: Worktrees that don't fit into pinned slots appear in a combined overflow WorktreeDropdown with an updated trigger label (+N more) and a highlightTrigger prop to distinguish primary vs. secondary dropdowns.
  • Store state: Four new per-project store slices track pinned counts, branch assignments, dropdown threshold, and always-use-dropdown preference.
  • Settings persistence: pinnedWorktreesCount is persisted to project settings via the server API.

Project Settings — Display Settings Section

  • New Display Settings section in WorktreePreferencesSection with a labelled slider (0–25) to configure the number of pinned worktree tabs.
  • Setting is saved optimistically on commit (onValueCommit) and persisted to the server.

libs/typesProjectSettings additions

  • Added pinnedWorktreesCount and worktreeDropdownThreshold to ProjectSettings.
  • getDefaultThinkingLevel() now returns 'adaptive' for Opus/adaptive-thinking models instead of hardcoded 'none'.

File Editor Improvements

  • Inline diff view: New showInlineDiff / activeFileDiff state; the editor can now render inline diff decorations via CodeMirror StateField and Decoration.
  • Selection API: CodeEditorHandle gains a getSelection() method returning { text, fromLine, toLine } for use by downstream actions (e.g. "Add Feature from selection").
  • Add Feature from editor: New AddFeatureDialog integration allows creating a feature card directly from a code selection in the file editor.
  • Language extension extracted: getLanguageExtension() moved to a shared codemirror-languages.ts module to keep code-editor.tsx focused.
  • Board navigation: File editor now exposes a button to navigate back to the board view (FolderKanban icon).

Diff Viewer — CodeMirror-based Diff

  • parseDiff() and splitDiffByFile() extracted from git-diff-panel.tsx into a reusable diff-utils.ts module (exporting ParsedFileDiff type).
  • New CodeMirrorDiffView component replaces the hand-rolled HTML diff renderer for richer syntax-highlighted diffs.
  • git-diff-panel.tsx significantly slimmed down (~250 lines removed).

AI Enhancement — Additive vs. Rewrite Modes

  • Fixes [Bug]: "Add" enhancement modes replace description instead of adding to it #803
  • Enhancement modes are now split into two categories:
    • Rewrite (improve, simplify): replaces the description.
    • Append Details (technical, acceptance, ux-reviewer): appends AI-generated content below the original description.
  • The dropdown renders these as two labelled sections with a separator.
  • isAdditiveMode() helper and REWRITE_MODES / ADDITIVE_MODES arrays exported from enhancement-constants.ts.

Server — Event Hook & Feature Completion

  • EventHookService now handles feature:completed events (emitted for manual feature runs) and maps them to feature_success / feature_error hooks.
  • Auto-mode auto_mode_feature_complete events are now guarded to only fire hooks when executionMode === 'auto', preventing double-firing.
  • AutoModeEventPayload and a new FeatureCompletedPayload interface capture executionMode context.
  • Unit tests in event-hook-service.test.ts updated to cover the new event handling paths.

Other

  • sdk-options.ts: minor additions.
  • codex-provider.ts: minor additions.
  • features/update.ts route: minor adjustments.
  • Default defaultThinkingLevel initial state changed from 'none' to 'adaptive'.

Summary by CodeRabbit

  • New Features

    • Pinned worktree tabs with branch swapping and a Change PR Number dialog (UI + backend)
    • CodeMirror unified diff viewer, inline diffs, and "Create Feature from Selection"
    • New tools available in tool presets: MultiEdit, LS, Task, Skill, TodoWrite
  • Improvements

    • Default agent max turns increased to 10,000; adaptive thinking enabled by default for capable models
    • Folder-level git status rollups; session-selection now clears when no sessions remain
    • Longer API proxy read timeout for UI requests (reduced buffering)

gsxdsm and others added 9 commits February 23, 2026 02:32
* 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>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 23, 2026

📝 Walkthrough

Walkthrough

Bumps 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

Cohort / File(s) Summary
Version & deps
package.json, apps/server/package.json, apps/ui/package.json
Repo and app versions bumped to 0.15.0; UI adds @codemirror/merge.
Server SDK / Tools / Defaults
apps/server/src/lib/sdk-options.ts, apps/server/src/providers/codex-provider.ts, apps/server/src/lib/settings-helpers.ts, apps/server/src/services/agent-executor.ts, apps/server/src/services/agent-service.ts
Added tools (MultiEdit, LS, Task, Skill, TodoWrite) to presets/allowed lists; increased DEFAULT_MAX_TURNS / MAX_ALLOWED_TURNS to 10000; agent stream now maps tool_use_id → tool name and emits tool_result with resolved name.
Feature update & events
apps/server/src/routes/features/..., apps/server/src/routes/features/routes/update.ts, apps/server/src/services/event-hook-service.ts
createUpdateHandler now accepts optional EventEmitter; update handler may emit feature:completed; EventHookService handles feature-completed payloads and de-duplicates with status_changed.
Auto-mode / execution events
apps/server/src/services/auto-mode/facade.ts, apps/server/src/services/execution-service.ts, apps/server/src/services/pipeline-orchestrator.ts
Added executionMode: 'auto' to auto-mode event payloads; stopFeature updates status early (non-blocking) before abort.
Worktree PR update flow
apps/server/src/routes/worktree/index.ts, apps/server/src/routes/worktree/routes/update-pr-number.ts, apps/ui/src/lib/electron.ts, apps/ui/src/types/electron.d.ts
New POST /api/worktree/update-pr-number handler (queries gh when available) and Electron API mock/typing updatePRNumber; server updates worktree metadata with PR info.
UI: CodeMirror diff & utils
apps/ui/src/components/ui/codemirror-diff-view.tsx, apps/ui/src/lib/diff-utils.ts, apps/ui/src/lib/codemirror-languages.ts
New CodeMirror unified diff viewer component; helpers to split/reconstruct multi-file diffs; centralized language-to-extension mapping.
UI: File editor & inline diff
apps/ui/src/components/views/file-editor-view/..., .../code-editor.tsx, .../editor-tabs.tsx, .../file-tree.tsx, .../use-file-editor-store.ts
Inline diff parsing/decorations, DeletedLinesWidget, getSelection API, diffContent prop, scrollable tabs, folder-level git rollups, store fields setShowInlineDiff/setActiveFileDiff and max-open-tab eviction.
Enhancement modes & prompts
libs/prompts/src/enhancement.ts, libs/prompts/src/enhancement-modes/*, libs/prompts/tests/*, apps/server/tests/unit/lib/enhancement-prompts.test.ts
Introduces additive enhancement modes (technical, acceptance, ux-reviewer) that append specific sections instead of rewriting; prompts, examples and tests updated accordingly.
Thinking-level & settings sync
libs/types/src/settings.ts, apps/ui/src/hooks/use-settings-migration.ts, apps/ui/src/hooks/use-settings-sync.ts, apps/server/src/lib/settings-helpers.ts
Defaults now prefer thinkingLevel: 'adaptive' for adaptive-capable models; DEFAULT/allowed max turns bumped to 10000; settings sync/migration updated.
Store: per-project worktree display
apps/ui/src/store/app-store.ts, apps/ui/src/store/types/state-types.ts, apps/ui/src/hooks/use-project-settings-loader.ts
Added per-project display settings and actions: pinnedWorktreesCount, pinnedWorktreeBranches, worktreeDropdownThreshold, alwaysUseWorktreeDropdown with getters/setters and swap action.
Worktree panel & actions UI
apps/ui/src/components/views/board-view/worktree-panel/...
Pinned-slot layout, swap-worktree UI (availableWorktreesForSwap, onSwapWorktree, slotIndex), highlightTrigger, onChangePRNumber wiring, per-slot remotes and state plumbing.
Change PR Number dialog & wiring
apps/ui/src/components/views/board-view/dialogs/change-pr-number-dialog.tsx, .../dialogs/index.ts, apps/ui/src/components/views/board-view.tsx
New ChangePRNumberDialog component, exported and integrated into board/worktree UI; invokes Electron API to update PR number and triggers onChanged.
HTTP client & settings shape
apps/ui/src/lib/http-api-client.ts
HttpApiClient.added updatePRNumber; Settings.getProject now includes pinnedWorktreesCount, worktreeDropdownThreshold, alwaysUseWorktreeDropdown.
Nginx & tests config
apps/ui/nginx.conf, apps/ui/playwright.config.ts
Disabled proxy buffering and extended proxy_read_timeout for /api; Playwright configured to block service workers.
Tests & test harness updates
apps/ui/tests/*, apps/server/tests/unit/*, libs/prompts/tests/*
New/updated unit and e2e tests for prompts, event-hook behavior, PR update flow, agent tool_result, execution stop behavior, and overview test refactor.
Misc UI fixes & enhancement UI
apps/ui/src/components/session-manager.tsx, apps/ui/src/components/views/board-view/shared/enhancement/*
Session selection cleared when no active sessions; enhancement menu split into Rewrite vs Append and additive modes prepend original text when appending.
Git & subprocess changes
apps/server/src/services/worktree-service.ts, libs/git-utils/src/status.ts, libs/platform/src/subprocess.ts, apps/server/src/providers/gemini-provider.ts
Switched to execGitCommand; rebase detection checks .git/rebase-merge/rebase-apply; subprocess JSONL handling now uses manual buffering and escalation; Gemini provider sends prompt via stdin, ensures .geminiignore, and uses SubprocessOptions.
Agent streaming & UI hook
apps/server/src/services/agent-service.ts, apps/ui/src/hooks/use-electron-agent.ts, apps/server/tests/unit/services/agent-service.test.ts
Tool use → tool result mapping added; server emits tool_result events with tool name and input; UI hook adds onToolResult option and handles tool_result stream events.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

Enhancement, scope: ui

Poem

🐇
I hop through diffs and settings with delight,
Tools learn new names and thinking gains height.
PR numbers change, pins find their place,
Diffs sparkle in mirrors, inline in their space.
A little rabbit cheers — code carrots for the night!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Feature: worktree view customization and stability fixes' clearly summarizes the main changes, which include worktree panel customization features and broader stability improvements.
Linked Issues check ✅ Passed The PR fully addresses issue #803 by implementing additive vs. rewrite mode distinction, preserving original descriptions in additive modes, providing isAdditiveMode() helper, exporting REWRITE_MODES/ADDITIVE_MODES, and updating UI to show two labelled sections.
Out of Scope Changes check ✅ Passed All changes align with PR objectives: worktree customization (pinned tabs, swap feature, dropdown settings), file editor improvements (inline diff, feature-from-selection), diff viewer refactoring, AI enhancement fixes, server event handling, and supporting infrastructure changes.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

❤️ Share

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

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

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

This pull request 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

  • Worktree View Customization: Introduced per-project worktree view customization, allowing users to pin a configurable number of non-main worktrees as persistent tabs. Pinned slots remember branch assignments and offer a 'Swap Worktree' submenu. An overflow dropdown now displays remaining worktrees with an updated trigger label.
  • File Editor Enhancements: The file editor now supports inline diff decorations via CodeMirror, a new getSelection() method for programmatic text selection, and direct integration to create feature cards from code selections. Language extension logic was also extracted to a shared module.
  • CodeMirror-based Diff Viewer: Replaced the previous HTML-based diff renderer with a new CodeMirrorDiffView component, providing richer syntax-highlighted diffs and significantly reducing code in git-diff-panel.tsx.
  • AI Enhancement Workflow Improvements: AI enhancement modes are now categorized into 'Rewrite' (replaces description) and 'Append Details' (adds content below original description), with the UI reflecting these distinctions.
  • Server-side Event Handling & Agent Configuration: The EventHookService now handles feature:completed events for manual feature runs and guards auto-mode completion events to prevent double-firing. Default maximum agent turns (DEFAULT_MAX_TURNS) has been increased from 1000 to 10000, and defaultThinkingLevel for Opus models now defaults to 'adaptive'.
  • Git Tooling Expansion: Added new tools like MultiEdit, LS, Task, Skill, and TodoWrite to the SDK options and Codex provider, enhancing agent capabilities.
  • UI Responsiveness and Stability: Implemented scrollable editor tabs, folder-level Git status rollups in the file tree, and adjusted Nginx proxy settings for improved stability and handling of long-running connections.

🧠 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
  • apps/server/package.json
    • Updated application version to 0.15.0.
  • apps/server/src/lib/sdk-options.ts
    • Added new tools ('MultiEdit', 'LS', 'Task', 'Skill') to the TOOL_PRESETS for both default and chat modes.
  • apps/server/src/lib/settings-helpers.ts
    • Increased DEFAULT_MAX_TURNS and MAX_ALLOWED_TURNS from 1000/2000 to 10000.
  • apps/server/src/providers/codex-provider.ts
    • Added new tools ('MultiEdit', 'LS', 'TodoWrite', 'Task', 'Skill') to DEFAULT_ALLOWED_TOOLS.
  • apps/server/src/routes/features/index.ts
    • Modified the /update route to pass the events emitter to the createUpdateHandler.
  • apps/server/src/routes/features/routes/update.ts
    • Imported EventEmitter and modified createUpdateHandler to accept it.
    • Added logic to emit a feature:completed event when a feature's status transitions to 'verified' or 'completed'.
  • apps/server/src/services/agent-executor.ts
    • Increased DEFAULT_MAX_TURNS from 1000 to 10000.
  • apps/server/src/services/agent-service.ts
    • Expanded baseTools to include 'MultiEdit', 'LS', and 'TodoWrite'.
  • apps/server/src/services/auto-mode/facade.ts
    • Added executionMode: 'auto' to auto_mode_feature_complete event payloads.
  • apps/server/src/services/event-hook-service.ts
    • Added executionMode property to AutoModeEventPayload and defined FeatureCompletedPayload interface.
    • Implemented handleFeatureCompletedEvent to process feature:completed events.
    • Added a guard to auto_mode_feature_complete events to only process those with executionMode: 'auto'.
  • apps/server/src/services/execution-service.ts
    • Added executionMode: 'auto' to auto_mode_feature_complete event payloads.
  • apps/server/src/services/pipeline-orchestrator.ts
    • Added executionMode: 'auto' to auto_mode_feature_complete event payloads in various pipeline completion scenarios.
  • apps/server/src/services/worktree-service.ts
    • Replaced direct child_process.execFile calls with execGitCommand from @automaker/git-utils.
  • apps/server/tests/unit/lib/enhancement-prompts.test.ts
    • Updated expected prompt text for enhancement modes.
  • apps/server/tests/unit/services/event-hook-service.test.ts
    • Added executionMode: 'auto' to simulated auto_mode_feature_complete events in tests.
    • Added a test case to ensure auto_mode_feature_complete events without explicit auto execution mode are ignored.
    • Added a test case for mapping feature:completed events to feature_success.
  • apps/server/tests/unit/services/settings-service.test.ts
    • Updated test expectations for specGenerationModel to include thinkingLevel: 'adaptive'.
  • apps/ui/nginx.conf
    • Added proxy_buffering off; and proxy_read_timeout 3600s; to the /api location block.
  • apps/ui/package.json
    • Updated application version to 0.15.0.
    • Added @codemirror/merge dependency.
  • apps/ui/playwright.config.ts
    • Added serviceWorkers: 'block' to Playwright project configuration.
  • apps/ui/src/components/session-manager.tsx
    • Modified onSelectSession to set the selected session to null if no active sessions are found.
  • apps/ui/src/components/ui/codemirror-diff-view.tsx
    • Added a new component CodeMirrorDiffView for displaying unified diffs with syntax highlighting using @codemirror/merge.
  • apps/ui/src/components/ui/git-diff-panel.tsx
    • Integrated CodeMirrorDiffView to render file diffs, replacing the custom HTML diff rendering logic.
    • Refactored diff parsing by extracting parseDiff and splitDiffByFile to lib/diff-utils.ts.
  • apps/ui/src/components/views/board-view.tsx
    • Added reasoningEffort to feature creation data.
    • Changed branchName to undefined instead of an empty string when addFeatureUseSelectedWorktreeBranch is false.
  • apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx
    • Applied defaultThinkingLevel from project settings to the model entry when initializing or resetting the dialog.
    • Imported getThinkingLevelsForModel to determine available thinking levels.
  • apps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsx
    • Imported resolveModelString and used it for the prDescriptionModelOverride.effectiveModel.
    • Increased the maximum height of the CreatePRDialog content.
  • apps/ui/src/components/views/board-view/shared/enhancement/enhance-with-ai.tsx
    • Categorized enhancement modes into 'Rewrite' and 'Append Details' sections in the dropdown menu.
    • Adjusted the output for additive modes to prepend the original description to the AI-generated content.
  • apps/ui/src/components/views/board-view/shared/enhancement/enhancement-constants.ts
    • Defined REWRITE_MODES and ADDITIVE_MODES arrays.
    • Added isAdditiveMode helper function.
  • apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx
    • Added a 'Swap Worktree' submenu to allow users to change the branch assigned to a pinned worktree slot.
  • apps/ui/src/components/views/board-view/worktree-panel/components/worktree-dropdown.tsx
    • Updated the trigger label to display +N more for overflow worktrees.
    • Added highlightTrigger prop to control button styling based on selection status.
  • apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx
    • Passed availableWorktreesForSwap, slotIndex, onSwapWorktree, and pinnedBranches props to WorktreeActionsDropdown.
  • apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx
    • Implemented logic for pinned worktrees, including resolving pinned worktrees from branch assignments and handling worktree swaps.
    • Introduced availableWorktreesForSwap and handleSwapWorktreeSlot.
    • Refactored worktree rendering to support individual pinned tabs and a combined overflow dropdown.
  • apps/ui/src/components/views/file-editor-view/components/code-editor.tsx
    • Moved language-specific imports and getLanguageExtension to lib/codemirror-languages.ts.
    • Added getSelection() method to CodeEditorHandle to retrieve selected text and line range.
    • Implemented inline diff decorations (createDiffDecorations) to highlight added/deleted lines based on provided diff content.
  • apps/ui/src/components/views/file-editor-view/components/editor-tabs.tsx
    • Implemented horizontal scrolling for editor tabs with left/right navigation buttons.
    • Ensured the active tab scrolls into view automatically.
  • apps/ui/src/components/views/file-editor-view/components/file-tree.tsx
    • Added computeFolderGitRollup to aggregate git status for folders.
    • Displayed folder-level git status rollups with counts and dominant status indicators in the file tree.
  • apps/ui/src/components/views/file-editor-view/file-editor-view.tsx
    • Integrated a toggle button for showing/hiding inline diffs in the editor.
    • Added a 'Create Feature from Selection' button that populates the AddFeatureDialog with selected code.
    • Refactored state updates to use useFileEditorStore.getState() for better stability.
  • apps/ui/src/components/views/file-editor-view/use-file-editor-store.ts
    • Added showInlineDiff and activeFileDiff to the store for managing inline diff display.
    • Implemented a maximum of 25 open tabs, evicting the oldest non-dirty tab when the limit is reached.
  • apps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsx
    • Added a new 'Display Settings' section with a slider to configure the number of 'Pinned Worktree Tabs'.
  • apps/ui/src/hooks/use-project-settings-loader.ts
    • Added logic to load and apply new worktree display settings (pinnedWorktreesCount, worktreeDropdownThreshold, alwaysUseWorktreeDropdown) from project settings.
  • apps/ui/src/hooks/use-settings-migration.ts
    • Updated the default defaultFeatureModel to include thinkingLevel: 'adaptive'.
    • Changed the default defaultThinkingLevel to 'adaptive'.
    • Sanitized currentWorktreeByProject to only retain entries where worktree.path is null during hydration.
  • apps/ui/src/hooks/use-settings-sync.ts
    • Included defaultThinkingLevel and defaultReasoningEffort in the settings fields to sync.
    • Updated default defaultFeatureModel to include thinkingLevel: 'adaptive' and defaultMaxTurns to 10000.
  • apps/ui/src/lib/codemirror-languages.ts
    • Created a new utility file to centralize language detection logic for CodeMirror, extracted from code-editor.tsx.
  • apps/ui/src/lib/diff-utils.ts
    • Added reconstructFilesFromDiff to extract old and new file content from a unified diff.
    • Added splitDiffByFile to break down a combined diff string into individual file diffs.
  • apps/ui/src/lib/http-api-client.ts
    • Added pinnedWorktreesCount, worktreeDropdownThreshold, and alwaysUseWorktreeDropdown to the ProjectSettings update payload.
  • apps/ui/src/store/app-store.ts
    • Updated default defaultThinkingLevel to 'adaptive' and defaultMaxTurns to 10000.
    • Added new state properties (pinnedWorktreesCountByProject, pinnedWorktreeBranchesByProject, worktreeDropdownThresholdByProject, alwaysUseWorktreeDropdownByProject) and corresponding actions for worktree display settings.
    • Modified setDefaultThinkingLevel to also update defaultFeatureModel's thinking level if compatible.
  • apps/ui/src/store/types/state-types.ts
    • Added new interfaces for worktree display settings (pinnedWorktreesCountByProject, pinnedWorktreeBranchesByProject, worktreeDropdownThresholdByProject, alwaysUseWorktreeDropdownByProject) to AppState and AppActions.
  • apps/ui/tests/projects/overview-dashboard.spec.ts
    • Refactored overview API mocking to use a mutable overviewMock for easier test setup.
    • Updated locator for 'Dashboard' link.
  • apps/ui/tests/settings/settings-startup-sync-race.spec.ts
    • Added [data-testid="overview-view"] to the locator for waiting on main views.
  • apps/ui/tests/utils/navigation/views.ts
    • Added [data-testid="overview-view"] to the locator for waiting on main views.
  • libs/git-utils/src/status.ts
    • Updated detectMergeState to check for .git/rebase-merge and .git/rebase-apply in addition to REBASE_HEAD.
  • libs/prompts/src/enhancement-modes/acceptance.ts
    • Modified system prompt to instruct the model to generate only the acceptance criteria section for additive mode.
    • Updated few-shot examples to reflect the additive output format.
  • libs/prompts/src/enhancement-modes/technical.ts
    • Modified system prompt to instruct the model to generate only the technical details section for additive mode.
    • Updated few-shot examples to reflect the additive output format.
  • libs/prompts/src/enhancement-modes/ux-reviewer.ts
    • Modified system prompt to instruct the model to generate only the UX considerations section for additive mode.
    • Updated few-shot examples to reflect the additive output format.
  • libs/prompts/src/enhancement.ts
    • Defined ADDITIVE_MODES and adjusted buildUserPrompt to generate specific instructions and examples for additive modes.
  • libs/prompts/tests/enhancement.test.ts
    • Updated test expectations for prompt content in additive enhancement modes.
  • libs/types/src/settings.ts
    • Modified getDefaultThinkingLevel to return 'adaptive' for models supporting adaptive thinking (e.g., Opus).
    • Increased the defaultMaxTurns range to 1-10000.
    • Added new ProjectSettings properties: pinnedWorktreesCount, worktreeDropdownThreshold, and alwaysUseWorktreeDropdown for worktree display customization.
    • Updated DEFAULT_PHASE_MODELS and DEFAULT_GLOBAL_SETTINGS to set thinkingLevel: 'adaptive' for specGenerationModel and defaultFeatureModel.
  • package-lock.json
    • Updated automaker and workspace package versions to 0.15.0.
    • Added @codemirror/merge entry.
  • package.json
    • Updated automaker package version to 0.15.0.
Using Gemini Code Assist

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

Invoking Gemini

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

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

Customization

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

Limitations & Feedback

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

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

Footnotes

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

Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

Code Review

This pull request 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.

Comment on lines +113 to +121
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
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

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.

Suggested change
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})`}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

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.

Suggested change
title={`${getGitStatusLabel(folderRollup.dominantStatus ?? undefined)} (${folderRollup.count})`}
title="${folderRollup.dominantStatus ? getGitStatusLabel(folderRollup.dominantStatus) : 'Changed'} (${folderRollup.count})"

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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-reviewer is missing from the all modes test array.

The typed modes array 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 over getAvailableEnhancementModes() (which does include ux-reviewer), the dedicated all modes block 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 | 🟡 Minor

Missing assertion coverage for additive-mode instruction strings.

The should work with all enhancement modes test iterates over all five modes but only verifies that the prompt contains testText and 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...') for technical, acceptance, or ux-reviewer modes 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 | 🟡 Minor

ENOENT from mkdir/cp/copyFile is misclassified as "File not found".

The single catch block covers stat, mkdir, and copyFile/cp. If mkdir or cp throws ENOENT (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 reaches failures[] and CopyFilesError is not thrown.

Separating the stat call 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-apply directory is shared between git rebase --apply and git am — causes false-positive rebase detection.

Both git am and git rebase --apply use the .git/rebase-apply/ directory. When git am is interrupted, the directory exists but only contains an applying sentinel; git rebase --apply writes a rebasing sentinel instead. The current code will incorrectly report mergeOperationType: 'rebase' for a stuck git am session.

Check for rebase-apply/rebasing instead to distinguish an actual rebase from a bare git am in 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-apply is created by both git am and git rebase --apply — potential false-positive rebase detection.

.git/rebase-apply is the state directory for the apply backend, created by both git am (direct mailbox application) and git rebase --apply (rebase using apply backend internally). Any ongoing git am session will cause detectMergeState to return mergeOperationType: 'rebase', which is incorrect.

To differentiate, check for the presence of .git/rebase-apply/rebasing before reporting the type as 'rebase'. This marker file is created only during git rebase --apply, not during git 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 the rebasing marker:

-      { file: 'rebase-apply', type: 'rebase' as const },
+      { file: 'rebase-apply/rebasing', type: 'rebase' as const },

Note: The rebasing marker 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 | 🟠 Major

Sync defaultFeatureModel when you mutate it.

When defaultThinkingLevel is compatible, the code mutates defaultFeatureModel.thinkingLevel in local state, but the server sync only sends defaultThinkingLevel. 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 around route.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 catch block 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_MODES is duplicated — export it from the shared package instead.

apps/ui/.../enhancement-constants.ts independently 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_MODES are mutable and lack compile-time exhaustiveness.

Two related concerns:

  1. Mutability: Both exported arrays are EnhancementMode[], so any caller can push/splice them and silently corrupt isAdditiveMode for all subsequent calls in the same module lifetime. They should be readonly.

  2. Exhaustiveness gap: ENHANCEMENT_MODE_LABELS (typed Record<EnhancementMode, string>) will cause a compile error if a new EnhancementMode value is added to @automaker/types without being listed—but REWRITE_MODES and ADDITIVE_MODES carry no such guarantee. A new mode would silently fall through isAdditiveMode as false (treated as rewrite) and disappear from the dropdown entirely.

Replacing the two arrays with a satisfies-annotated category map gives both readonly-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_TURNS now equals MAX_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 defaultMaxTurns will 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_TURNS above DEFAULT_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: Fragile text-bg- class substitution for the dot indicator.

fileColor.replace('text-', 'bg-') works for the current set of returns from getFileColor, but if a future variant like text-muted-foreground/50 were added, the replacement would break. Consider a small mapping or a parallel getFileDotColor function that returns bg-* 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, the else branch (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: Import DEFAULT_MAX_TURNS from settings-helpers.ts to avoid duplication.

DEFAULT_MAX_TURNS is defined locally here (10000) and also exported from apps/server/src/lib/settings-helpers.ts (10000). Since agent-executor.ts already 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.

10000 is 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 and resetForm.

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.

isAdaptiveThinkingModel still 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

📥 Commits

Reviewing files that changed from the base of the PR and between e7504b2 and 611706d.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (56)
  • apps/server/package.json
  • apps/server/src/lib/sdk-options.ts
  • apps/server/src/lib/settings-helpers.ts
  • apps/server/src/providers/codex-provider.ts
  • apps/server/src/routes/features/index.ts
  • apps/server/src/routes/features/routes/update.ts
  • apps/server/src/services/agent-executor.ts
  • apps/server/src/services/agent-service.ts
  • apps/server/src/services/auto-mode/facade.ts
  • apps/server/src/services/event-hook-service.ts
  • apps/server/src/services/execution-service.ts
  • apps/server/src/services/pipeline-orchestrator.ts
  • apps/server/src/services/worktree-service.ts
  • apps/server/tests/unit/lib/enhancement-prompts.test.ts
  • apps/server/tests/unit/services/event-hook-service.test.ts
  • apps/server/tests/unit/services/settings-service.test.ts
  • apps/ui/nginx.conf
  • apps/ui/package.json
  • apps/ui/playwright.config.ts
  • apps/ui/src/components/session-manager.tsx
  • apps/ui/src/components/ui/codemirror-diff-view.tsx
  • apps/ui/src/components/ui/git-diff-panel.tsx
  • apps/ui/src/components/views/board-view.tsx
  • apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx
  • apps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsx
  • apps/ui/src/components/views/board-view/shared/enhancement/enhance-with-ai.tsx
  • apps/ui/src/components/views/board-view/shared/enhancement/enhancement-constants.ts
  • apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx
  • apps/ui/src/components/views/board-view/worktree-panel/components/worktree-dropdown.tsx
  • apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx
  • apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx
  • apps/ui/src/components/views/file-editor-view/components/code-editor.tsx
  • apps/ui/src/components/views/file-editor-view/components/editor-tabs.tsx
  • apps/ui/src/components/views/file-editor-view/components/file-tree.tsx
  • apps/ui/src/components/views/file-editor-view/file-editor-view.tsx
  • apps/ui/src/components/views/file-editor-view/use-file-editor-store.ts
  • apps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsx
  • apps/ui/src/hooks/use-project-settings-loader.ts
  • apps/ui/src/hooks/use-settings-migration.ts
  • apps/ui/src/hooks/use-settings-sync.ts
  • apps/ui/src/lib/codemirror-languages.ts
  • apps/ui/src/lib/diff-utils.ts
  • apps/ui/src/lib/http-api-client.ts
  • apps/ui/src/store/app-store.ts
  • apps/ui/src/store/types/state-types.ts
  • apps/ui/tests/projects/overview-dashboard.spec.ts
  • apps/ui/tests/settings/settings-startup-sync-race.spec.ts
  • apps/ui/tests/utils/navigation/views.ts
  • libs/git-utils/src/status.ts
  • libs/prompts/src/enhancement-modes/acceptance.ts
  • libs/prompts/src/enhancement-modes/technical.ts
  • libs/prompts/src/enhancement-modes/ux-reviewer.ts
  • libs/prompts/src/enhancement.ts
  • libs/prompts/tests/enhancement.test.ts
  • libs/types/src/settings.ts
  • package.json

Comment on lines +269 to +304
/**
* 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 });
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 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 2

Repository: 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 3

Repository: 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 10

Repository: 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.

Suggested change
/**
* 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.

Comment on lines +463 to +488
// ─── 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]
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Comment thread apps/ui/src/lib/codemirror-languages.ts
Comment thread apps/ui/src/store/app-store.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

♻️ Duplicate comments (2)
apps/server/src/services/event-hook-service.ts (1)

269-307: markFeatureHandled() placement is correct; executionMode on FeatureCompletedPayload is unused.

The markFeatureHandled(payload.featureId) call at line 276 is placed before the first await, ensuring the deduplication guard is set atomically in the Node.js event loop before any concurrent feature_status_changed handler can run. This correctly addresses deduplication. ✅

The executionMode field declared on FeatureCompletedPayload (line 112) is never read inside handleFeatureCompletedEvent — the handler fires for all feature:completed events 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 | 🟡 Minor

Missing rollback on server persist failure.

onValueChange updates the store optimistically, but onValueCommit'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 for isValidEnhancementMode('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 on getAvailableEnhancementModes() 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: Missing UX_REVIEWER_SYSTEM_PROMPT / UX_REVIEWER_EXAMPLES imports and constant-level tests.

By analogy with every other mode, enhancement.ts should export UX_REVIEWER_SYSTEM_PROMPT and UX_REVIEWER_EXAMPLES. Neither is imported here, so the System Prompt Constants and Examples Constants describe 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 it blocks inside the existing System Prompt Constants and Examples Constants describe blocks (mirroring the ACCEPTANCE_SYSTEM_PROMPT / ACCEPTANCE_EXAMPLES tests).

🤖 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, and getExamples unit tests don't cover 'ux-reviewer'.

Each block has an explicit it for 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 ChangePRNumberDialog from 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

computeFolderGitRollup iterates the entire gitStatusMap for every directory TreeNode. With useMemo this 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 a Map<folderPath, Rollup> once (e.g., in the store or a top-level useMemo in FileTree) 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 empty newBranch to avoid inadvertent slot shuffling.

When newBranch === '' (clearing a slot), current.indexOf('') matches the first pre-filled empty slot (not necessarily slotIndex). The swap logic then moves the current slot's branch to that unrelated empty slot before writing '' into slotIndex, 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: Add instanceof guard to eq() 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 different WidgetType subclass would access other.lines as undefined and throw TypeError on .length. While CodeMirror typically compares same-type widgets, a defensive type guard is the standard practice for WidgetType overrides.

🛡️ 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 file message starts with a backslash (\), so it never matches line.startsWith(' ') or line === ''. The outer else if condition is never entered for such lines, making the inner guard permanently true and 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

📥 Commits

Reviewing files that changed from the base of the PR and between 611706d and 1df3202.

📒 Files selected for processing (20)
  • apps/server/src/routes/worktree/index.ts
  • apps/server/src/routes/worktree/routes/update-pr-number.ts
  • apps/server/src/services/event-hook-service.ts
  • apps/ui/src/components/views/board-view.tsx
  • apps/ui/src/components/views/board-view/dialogs/change-pr-number-dialog.tsx
  • apps/ui/src/components/views/board-view/dialogs/index.ts
  • apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx
  • apps/ui/src/components/views/board-view/worktree-panel/components/worktree-dropdown.tsx
  • apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx
  • apps/ui/src/components/views/board-view/worktree-panel/types.ts
  • apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx
  • apps/ui/src/components/views/file-editor-view/components/code-editor.tsx
  • apps/ui/src/components/views/file-editor-view/components/file-tree.tsx
  • apps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsx
  • apps/ui/src/lib/codemirror-languages.ts
  • apps/ui/src/lib/electron.ts
  • apps/ui/src/lib/http-api-client.ts
  • apps/ui/src/store/app-store.ts
  • apps/ui/src/types/electron.d.ts
  • libs/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

Comment on lines +100 to +104
router.post(
'/update-pr-number',
validatePathParams('worktreePath'),
createUpdatePRNumberHandler()
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +17 to +157
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,
},
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +205 to +207
// Only map explicit auto-mode completion events.
// Manual feature completions are emitted as feature:completed.
if (payload.executionMode !== 'auto') return;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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 10

Repository: 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:

  1. Add executionMode?: 'auto' | 'manual' to the auto_mode_feature_complete type definition in apps/ui/src/types/electron.d.ts
  2. Update all four emitters in apps/ui/src/lib/electron.ts to include executionMode: '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.

Comment thread apps/ui/src/components/views/board-view/dialogs/change-pr-number-dialog.tsx Outdated
Comment thread apps/ui/src/store/app-store.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 | 🟡 Minor

Add currentProject to handleDeleteFeature dependencies to avoid stale cleanup.

The callback uses currentProject.id at lines 513 and 518 to identify and clean worktree state, but currentProject is missing from the dependency array at line 552. This creates a stale closure: if the user switches projects, the callback will still reference the old currentProject.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 | 🟠 Major

Add a worktree guard to /update-pr-number.

This endpoint operates on worktreePath but does not enforce requireValidWorktree, 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 | 🟡 Minor

Inconsistent 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 | 🟡 Minor

Missing bg-gray-600 for '!' (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, dominantStatus will 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-reviewer additive phrasing not explicitly asserted in buildUserPrompt tests

The "all modes" loop only verifies output existence and that testText is present. Since ux-reviewer is an additive mode, a dedicated test analogous to the technical block 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 for isAdditiveMode, ADDITIVE_MODES, and REWRITE_MODES

These three exports are the backbone of the issue #803 fix — buildUserPrompt branches on ADDITIVE_MODES.includes(mode) — but they are neither imported nor exercised in this suite. Consider adding a describe('Additive vs Rewrite modes') block covering:

  • ADDITIVE_MODES contains exactly ['technical', 'acceptance', 'ux-reviewer'] (and not 'improve' or 'simplify').
  • REWRITE_MODES contains exactly ['improve', 'simplify'].
  • isAdditiveMode(mode) returns true for each additive mode and false for 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 omits description assertion

All other mode tests (lines 135–170) validate the description field with a toContain check (e.g., 'vague', 'implementation', 'verbose', 'acceptance'). The ux-reviewer test only asserts on systemPrompt.

♻️ 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_result event carries no result content — ToolUse only exposes invocation fields.

The new tool_result variant reuses ToolUse (name + input), making it structurally identical to tool_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: onToolUse repurposed for tool-result notification via a string-suffix protocol.

onToolUse is called for tool_result events 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 the toolName || 'unknown' branch in agent-service.ts — when a tool_result arrives with a tool_use_id that was never registered (e.g., the tool_use block was missing or had no tool_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.content may be an array — consider normalizing to string before emitting.

Per the Anthropic SDK, tool_result content is typed as string | Array<TextBlock | ImageBlock>. If a provider emits array-valued content, block.content || '' passes the array through as-is, while downstream consumers (UI, tests) expect a string for the content field.

♻️ 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, and handleArchiveAllVerified. 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 handleDeleteFeature and handleArchiveAllVerified.

🤖 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: diffContent in extensions useMemo deps forces a full extension set rebuild on every diff change.

When diffContent changes, the entire extension array (language extension, keybinding, update listener, theme) is rebuilt and reconfigured. While @uiw/react-codemirror preserves undo history via its internal compartment, the unnecessary recreation of stable extensions is wasteful and recreates a new StateField.define() type each time.

The idiomatic CodeMirror approach is to wrap hot-swappable extensions in a Compartment and dispatch compartment.reconfigure(newExtension) on changes, leaving the rest of the configuration untouched.

♻️ Suggested refactor: isolate diff extensions in a Compartment

Hoist a stable Compartment outside the component (module-level or via useRef):

+import { Compartment } from '@codemirror/state';
+
+// Module-level compartment for diff decorations
+const diffCompartment = new Compartment();

In the component, remove diffContent from the extensions useMemo deps and instead use a separate useEffect:

   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 diffCompartment is module-level, ensure it's per-instance (via useRef) if multiple CodeEditor instances 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 in DeletedLinesWidget.toDOM() bypass the CSS variable theme system.

All other editor styling uses var(--chart-*) / var(--foreground) / etc. The inline oklch(...) values here (and addedLineDecoration at 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 a StateField for 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 iterates addedLines on every keystroke, cursor move, and scroll — whereas the deleted-lines side correctly uses a StateField that only recomputes on document changes.

For large diffs the per-update iteration is wasteful. Aligning the added-line decorations with the same StateField pattern 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

📥 Commits

Reviewing files that changed from the base of the PR and between 1df3202 and e38b864.

📒 Files selected for processing (19)
  • apps/server/src/providers/gemini-provider.ts
  • apps/server/src/routes/worktree/index.ts
  • apps/server/src/services/agent-service.ts
  • apps/server/src/services/execution-service.ts
  • apps/server/tests/unit/providers/gemini-provider.test.ts
  • apps/server/tests/unit/services/agent-service.test.ts
  • apps/server/tests/unit/services/execution-service.test.ts
  • apps/ui/src/components/views/board-view.tsx
  • apps/ui/src/components/views/board-view/dialogs/change-pr-number-dialog.tsx
  • apps/ui/src/components/views/board-view/hooks/use-board-actions.ts
  • apps/ui/src/components/views/file-editor-view/components/code-editor.tsx
  • apps/ui/src/components/views/file-editor-view/components/file-tree.tsx
  • apps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsx
  • apps/ui/src/hooks/use-auto-mode.ts
  • apps/ui/src/hooks/use-electron-agent.ts
  • apps/ui/src/store/app-store.ts
  • apps/ui/src/types/electron.d.ts
  • libs/platform/src/subprocess.ts
  • libs/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

Comment thread apps/server/src/providers/gemini-provider.ts
Comment thread libs/platform/src/subprocess.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
apps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsx (1)

561-586: ⚠️ Potential issue | 🟡 Minor

sliderPrevRef still gets overwritten with intermediate values during a drag.

sliderPrevRef.current is unconditionally reassigned on every onValueChange call. Because Radix UI fires onValueChange on each mouse-move (separate React events, not a single batched transaction), the component re-renders between calls and pinnedWorktreesCount advances to the intermediate store value. The second onValueChange therefore captures that intermediate value as prevCount and overwrites the ref — so by onValueCommit, sliderPrevRef.current holds the value from just before the last step, not the original pre-drag position.

Additionally, the ref is never reset to null after 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.
computeFolderGitRollup iterates the full gitStatusMap for every directory node; large trees could get expensive. A single prepass that builds a folderPath → rollup map (then passed into TreeNode) 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 undefined guard on oldFeatures is correct.

Note: this update runs unconditionally, even when targetStatus === feature.status (Line 1052 guards only moveFeature/persistFeatureUpdate). It's harmless (idempotent map), but if you want to be precise you could wrap lines 1040–1050 inside the same if (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-nocheck disables all type safety for a 1,363-line file with significant business logic.

This suppresses type errors across the entire file, including the new removeRunningTaskFromAllWorktrees function, the optimistic queryClient.setQueryData callback, 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 with handleForceStopFeature.

handleForceStopFeature optimistically updates the React Query cache (line 1041: queryClient.setQueryData(queryKeys.features.all(...))) to avoid race conditions with the 30-second poll cycle. handleDeleteFeature relies solely on Zustand's removeFeature to hide the card. Since the board sources features from the React Query cache via useFeatures, the deleted feature could briefly reappear after a background refetch races with the persistFeatureDelete call.

Adding a queryClient.setQueryData call 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 local WorktreeInfo to avoid shadowing the exported WorktreeInfo from electron.d.ts.

The exported WorktreeInfo in electron.d.ts uses { 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 WorktreeListItem or WorktreeDetail to match what listAll() 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

📥 Commits

Reviewing files that changed from the base of the PR and between e38b864 and 359f882.

📒 Files selected for processing (12)
  • apps/server/src/providers/gemini-provider.ts
  • apps/server/src/routes/worktree/index.ts
  • apps/server/src/services/agent-service.ts
  • apps/server/tests/unit/services/agent-service.test.ts
  • apps/ui/src/components/views/board-view/dialogs/change-pr-number-dialog.tsx
  • apps/ui/src/components/views/board-view/hooks/use-board-actions.ts
  • apps/ui/src/components/views/file-editor-view/components/file-tree.tsx
  • apps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsx
  • apps/ui/src/hooks/use-electron-agent.ts
  • apps/ui/src/types/electron.d.ts
  • libs/platform/src/subprocess.ts
  • libs/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

Comment on lines +542 to +556
<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"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 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-label on Slider.Thumb (common for accessibility). (radix-ui.com)

Citations:


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.

Comment on lines +342 to 347
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');
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

@gsxdsm gsxdsm merged commit 0330c70 into AutoMaker-Org:v0.15.0rc Feb 24, 2026
7 checks passed
@gsxdsm gsxdsm deleted the feature/worktree-view-customization branch February 24, 2026 04:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant