Skip to content

perf: reduce UI lag via render optimization, event consolidation, and debug cleanup#440

Merged
pedramamini merged 10 commits intomainfrom
0.15.0-RC-polish-round-two
Feb 24, 2026
Merged

perf: reduce UI lag via render optimization, event consolidation, and debug cleanup#440
pedramamini merged 10 commits intomainfrom
0.15.0-RC-polish-round-two

Conversation

@pedramamini
Copy link
Collaborator

@pedramamini pedramamini commented Feb 22, 2026

Summary

  • Reduce render cascades via conditional modal mounting, context splitting, and memoization (AppModals, MainPanel, GitStatusContext, settingsStore)
  • Consolidate 13 activity event listeners into a shared passive bus (activityBus.ts), reducing per-keystroke overhead from 13 handlers to 1
  • Remove streaming debug logs from useSessionDebounce (7 console.log calls firing at high frequency during AI streaming)
  • Fix index-based React keys in 6 components where items can be removed/filtered (staged images, diff tabs, log entries, quit modal agents)

Test plan

  • Verify UI responsiveness during AI streaming (no debug log overhead)
  • Verify staged image removal works correctly in InputArea, GroupChatInput, WizardInputPanel
  • Verify git diff viewer tab switching works with file-path keys
  • Verify modal open/close transitions remain smooth
  • npx vitest run — existing tests pass (3 pre-existing failures from missing generated/prompts module, unrelated)

Summary by CodeRabbit

  • Performance
    • Reduced re-renders with memoization and lazy evaluation; improved tab stability for smoother UI updates.
  • Bug Fixes
    • Fixed unstable removal and key handling for staged images, logs, and lists to prevent UI glitches.
  • Accessibility
    • Expanded colorblind-safe palettes and centralized extension color handling for clearer, consistent file-type badges.
  • Tests
    • Hardened and expanded tests for responsive rendering, auto-scroll, and activity-listener behavior.
  • Chores / Refactor
    • Consolidated activity tracking, removed debug logging, and memo-wrapped modal components to avoid unnecessary rendering.

…splitting, and memoization

- Conditionally mount NewInstanceModal, EditAgentModal, PromptComposerModal,
  ExecutionQueueBrowser (eliminates ~115+ wasted renders when closed)
- Wrap all AppModals sub-groups + parent with React.memo
- Memoize gitInfo derivation in MainPanel with primitive deps
- Remove unused isLoading from GitFileStatusContext (no consumer uses it)
- Move inline sessions.find() to useMemo in AppModals
- Update test mocks to match new GitFileStatusContextValue interface
Three hooks (useActivityTracker, useHandsOnTimeTracker, useGitStatusPolling)
each registered their own keydown/mousedown/wheel/touchstart listeners,
totaling 13+ global event listeners all doing the same thing. Consolidated
into a single shared activity bus with 5 passive listeners.

Also fixes showSessionJumpNumbers re-registering 3 listeners on every
modifier key press/release (deps array included state → now uses ref),
and converts canSummarizeActiveTab from eager IIFE to lazy getter.
Remove 7 console.log calls from useSessionDebounce that fired at high
frequency during AI streaming. Replace index-based keys with stable
identifiers in 6 components where items can be removed or filtered
(staged images, diff tabs, log entries, quit modal agents).
@coderabbitai
Copy link

coderabbitai bot commented Feb 22, 2026

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a shared activity bus; memoizes and conditionalizes modal renderers; centralizes extension color logic; stabilizes React keys and removes index-based removals; drops git-status isLoading; removes debug logs; tweaks TerminalOutput auto-scroll; and updates tests for responsive/dual-render behavior.

Changes

Cohort / File(s) Summary
Activity Bus (new)
src/renderer/utils/activityBus.ts
New subscribeToActivity(callback): () => void API that lazily attaches passive global listeners (keydown, mousedown, wheel, touchstart, click) and detaches when last subscriber unsubscribes.
Activity Bus Integrations
src/renderer/hooks/session/useActivityTracker.ts, src/renderer/hooks/session/useHandsOnTimeTracker.ts, src/renderer/hooks/git/useGitStatusPolling.ts
Replace direct addEventListener/removeEventListener usage with subscribeToActivity subscriptions and rely on bus-provided cleanup.
Keyboard listener refactor
src/renderer/hooks/keyboard/useMainKeyboardHandler.ts
Use a ref (showSessionJumpNumbersRef) to read latest state inside a stable keydown handler; effect deps emptied to avoid re-registering listener.
Modal components refactor
src/renderer/components/AppModals.tsx
Wrap top-level modal components with memo, compute sourceSession via useMemo, and conditionally render several modals only when open flags are true.
Extension color utility & palettes
src/renderer/utils/extensionColors.ts, src/renderer/constants/colorblindPalettes.ts
Add centralized getExtensionColor with hex->rgba helpers and mapping; expand colorblind palette with new categories and export getExtensionColor.
Tab components updated to use utility
src/renderer/components/TabBar.tsx, src/renderer/components/TabSwitcherModal.tsx
Replace in-file extension color logic with imported getExtensionColor usage.
React key stability & removal semantics
src/renderer/components/GitDiffViewer.tsx, src/renderer/components/GroupChatInput.tsx, src/renderer/components/InlineWizard/WizardInputPanel.tsx, src/renderer/components/InputArea.tsx, src/renderer/components/LogViewer.tsx, src/renderer/components/QuitConfirmModal.tsx
Use stable keys (file paths, image URLs, composite log keys, ${name}-${index}) and change staged-image removal to remove by value rather than index.
MainPanel memoization
src/renderer/components/MainPanel.tsx
Memoize gitInfo with useMemo and derive fileCount separately to avoid unnecessary recomputation.
TerminalOutput auto-scroll update
src/renderer/components/TerminalOutput.tsx
Add isAtBottomRef for MutationObserver closures and extend AI-mode auto-scroll gating to use isAtBottomRef.
Git status context & tests
src/renderer/contexts/GitStatusContext.tsx, src/__tests__/renderer/components/GitStatusWidget.test.tsx, src/__tests__/renderer/components/MainPanel.test.tsx, src/__tests__/renderer/components/SessionList.test.tsx
Remove isLoading from GitFileStatusContextValue and provider; update mocks and tests to omit isLoading and use getAllByText/regex selectors to handle compact/full dual-render variants.
Debug log removal
src/renderer/hooks/batch/useSessionDebounce.ts
Removed console.log/debug statements from debounce hook.
Tests: timing & coverage
src/__tests__/renderer/components/InlineWizard/WizardConversationView.test.tsx, src/__tests__/renderer/components/TerminalOutput.test.tsx, src/__tests__/renderer/utils/extensionColors.test.ts
Flush RAF in wizard scroll test; add/duplicate TerminalOutput auto-scroll suites; add comprehensive extensionColors tests covering themes and colorblind paths.
Minor & misc
src/renderer/App.tsx, src/renderer/components/GitDiffViewer.tsx
Change canSummarizeActiveTab to a lazy getter; stabilize some tab keys using file path fallbacks.

Sequence Diagram(s)

sequenceDiagram
    participant Hook as Activity Hook
    participant Bus as Activity Bus
    participant DOM as Global Listeners
    participant CB as Subscriber Callback

    Note over Hook,Bus: Subscription flow
    Hook->>Bus: subscribeToActivity(callback)
    activate Bus
    Bus->>DOM: attach passive listeners (keydown/mousedown/wheel/touchstart/click)
    Bus->>CB: register callback
    deactivate Bus

    Note over DOM,Bus: On user activity
    DOM->>Bus: handleActivity(event)
    Bus->>CB: dispatch(event) to registered callbacks
    CB->>Hook: onActivity invoked

    Note over Hook,Bus: Unsubscribe flow
    Hook->>Bus: unsubscribe()
    activate Bus
    Bus->>CB: deregister callback
    Bus->>DOM: if no subscribers -> detach listeners
    deactivate Bus
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'perf: reduce UI lag via render optimization, event consolidation, and debug cleanup' accurately and specifically summarizes the main changes: performance improvements through render optimization, event listener consolidation, and debug log removal.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 0.15.0-RC-polish-round-two

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

@greptile-apps
Copy link

greptile-apps bot commented Feb 22, 2026

Greptile Summary

This PR implements comprehensive performance optimizations focused on reducing UI lag during AI streaming and high-frequency user interactions.

Major changes:

  • Activity Bus Consolidation - Created shared activityBus.ts that consolidates 13+ individual event listeners from useActivityTracker, useHandsOnTimeTracker, and useGitStatusPolling into a single set of passive listeners. This reduces per-keystroke overhead from 13 handlers to 1.
  • Render Cascade Prevention - Added React.memo to 7 modal component groups in AppModals.tsx and implemented conditional mounting for 4 heavy modals (NewInstanceModal, EditAgentModal, PromptComposerModal, ExecutionQueueBrowser), preventing unnecessary renders when modals are closed.
  • Context Optimization - Removed unused isLoading field from GitFileStatusContextValue in GitStatusContext.tsx, reducing re-renders across all consumers of this context.
  • Memoization Improvements - Memoized gitInfo object in MainPanel.tsx, converted canSummarizeActiveTab from eager evaluation to lazy getter in App.tsx, and added sourceSession memoization in AppModals.tsx.
  • Debug Log Cleanup - Removed 7 console.log statements from useSessionDebounce.ts that were firing at high frequency during AI streaming.
  • React Key Fixes - Fixed index-based keys in 6 components (InputArea, GroupChatInput, WizardInputPanel, GitDiffViewer, LogViewer, QuitConfirmModal) to use stable identifiers (file paths, image URLs, composite keys), preventing incorrect reconciliation when items are removed or filtered.
  • Event Listener Optimization - Fixed useMainKeyboardHandler to use ref pattern for reading state, preventing listener re-registration on every state change.

All test files have been updated to reflect the new patterns. The changes align with performance best practices from CLAUDE-PERFORMANCE.md and maintain the existing API contracts.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • All changes are focused performance optimizations with no breaking API changes. The activity bus consolidation is well-architected with proper cleanup, passive listeners are correctly implemented, React key fixes prevent reconciliation bugs, and all tests have been updated to match the new behavior. The changes follow established patterns from the codebase's performance guide.
  • No files require special attention

Important Files Changed

Filename Overview
src/renderer/utils/activityBus.ts New shared event bus consolidates 13+ activity listeners into single passive set
src/renderer/hooks/session/useActivityTracker.ts Migrated from individual listeners to shared activity bus, reducing overhead
src/renderer/hooks/batch/useSessionDebounce.ts Removed 7 debug console.log statements that fired during AI streaming
src/renderer/components/AppModals.tsx Added React.memo to 7 modal groups and conditional mounting to 4 heavy modals
src/renderer/components/MainPanel.tsx Memoized gitInfo object to prevent unnecessary re-renders
src/renderer/contexts/GitStatusContext.tsx Removed unused isLoading from GitFileStatusContextValue to reduce re-renders
src/renderer/components/InputArea.tsx Fixed React key from index to image path for staged images
src/renderer/hooks/keyboard/useMainKeyboardHandler.ts Fixed keyboard handler to use ref pattern, preventing listener re-registration

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    subgraph Before["Before: 13+ Event Listeners"]
        UAT[useActivityTracker]
        UHOT[useHandsOnTimeTracker]
        UGSP[useGitStatusPolling]
        
        UAT -->|keydown| W1[Window Event]
        UAT -->|mousedown| W2[Window Event]
        UAT -->|wheel| W3[Window Event]
        UAT -->|touchstart| W4[Window Event]
        
        UHOT -->|keydown| W5[Window Event]
        UHOT -->|mousedown| W6[Window Event]
        UHOT -->|wheel| W7[Window Event]
        UHOT -->|touchstart| W8[Window Event]
        UHOT -->|click| W9[Window Event]
        
        UGSP -->|keydown| W10[Window Event]
        UGSP -->|mousedown| W11[Window Event]
        UGSP -->|wheel| W12[Window Event]
        UGSP -->|touchstart| W13[Window Event]
    end
    
    subgraph After["After: Shared Activity Bus"]
        AB[activityBus.ts]
        
        AB -->|passive: true| E1[keydown]
        AB -->|passive: true| E2[mousedown]
        AB -->|passive: true| E3[wheel]
        AB -->|passive: true| E4[touchstart]
        AB -->|passive: true| E5[click]
        
        H1[useActivityTracker] -->|subscribe| AB
        H2[useHandsOnTimeTracker] -->|subscribe| AB
        H3[useGitStatusPolling] -->|subscribe| AB
        
        AB -->|notify| H1
        AB -->|notify| H2
        AB -->|notify| H3
    end
    
    style Before fill:#fee,stroke:#f66
    style After fill:#efe,stroke:#6f6
    style AB fill:#ffd,stroke:#cc6
Loading

Last reviewed commit: 0e76d33

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@src/renderer/components/QuitConfirmModal.tsx`:
- Around line 147-149: The key for the mapped elements in displayNames.map
currently uses only the agent name (key={name}), which can produce duplicate
keys if names repeat; change the key to a composite that guarantees uniqueness
(e.g., include the map index or a unique id) so React can track items reliably —
update the mapping in displayNames.map and the span key to something like a
fallback composite (e.g., `${name}-${index}`) or use the agent's unique id if
available.

Update test queries to handle compact + full responsive views that
both render in DOM (hidden via CSS). Use getAllByText for ambiguous
text matches in GitStatusWidget and MainPanel. Flush rAF callback
in WizardConversationView to reset programmatic scroll guard before
simulating user scroll.
…ottom

Previously, auto-scrolling to show new content required the autoScrollAiMode
preference to be enabled. Now, if the user is already at the bottom of the
chat stream, new content (including their own sent messages) will
automatically scroll into view regardless of the preference setting.
Consolidate duplicate getExtensionColor from TabBar and TabSwitcherModal
into shared utility. Add 6 missing categories (images, Java, C/C++, Ruby,
SQL, PDF/Office). Default fallback now derives from theme accent instead
of invisible border/dim colors.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
src/renderer/components/TabSwitcherModal.tsx (1)

109-116: ⚠️ Potential issue | 🟡 Minor

Remove orphaned JSDoc comment left behind from the deleted inline function.

Lines 109–113 were the docblock for the in-file getExtensionColor that has been removed and replaced with an import. The block now floats immediately before the ContextGauge JSDoc (lines 114–116), resulting in two back-to-back JSDoc blocks with no declaration between them.

🧹 Proposed fix
-/**
- * Get color for file extension badge.
- * Returns a muted color based on file type for visual differentiation.
- * When colorBlindMode is enabled, uses Wong's colorblind-safe palette.
- * (Synchronized with TabBar.tsx for consistency)
- */
 /**
  * Circular progress gauge component
  */
 function ContextGauge({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/TabSwitcherModal.tsx` around lines 109 - 116, Remove
the orphaned JSDoc block that described getExtensionColor (the docblock now
sitting immediately before the ContextGauge JSDoc) so there aren't two
back-to-back JSDoc comments with no declaration; locate the dangling comment
that references getExtensionColor and delete it now that getExtensionColor is
imported, leaving only the ContextGauge JSDoc above its declaration.
🧹 Nitpick comments (1)
src/renderer/constants/colorblindPalettes.ts (1)

176-205: Heads-up: five new categories reuse existing colorblind-palette colors.

The new assignments share exact rgba values with existing categories:

New category Shared color with
image rust (both #EE3377 Magenta)
java html (both #CC3311 Vermillion)
ruby config (both #EE7733 Orange)
data styles (both #AA4499 Purple)
document go (both #44AA99 Blue-Green)

With only 8–10 distinct hues in Wong's palette and 14 categories, perfect uniqueness is impossible, so this is an inherent trade-off. Worth documenting in the JSDoc that these pairings are intentional budget allocations rather than oversights, so future contributors don't try to "fix" them by adding visually similar non-Wong colors.

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

In `@src/renderer/constants/colorblindPalettes.ts` around lines 176 - 205, The new
category color assignments (image, java, ruby, data, document) intentionally
reuse existing palette hues (matching rust, html, config, styles, go) due to the
limited 8–10 distinct Wong hues; add a JSDoc block at the top of the
colorblindPalettes export (in src/renderer/constants/colorblindPalettes.ts)
noting that these pairings are deliberate "budget allocations" rather than
oversights, list the specific reused mappings (image→rust, java→html,
ruby→config, data→styles, document→go), and explain that perfect uniqueness is
impossible with Wong's palette so future contributors should not change colors
to non-Wong variants.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/renderer/utils/extensionColors.ts`:
- Around line 171-183: When colorBlindMode is true, the code should not fall
through to the EXTENSION_MAP lookup if getColorBlindExtensionColor(extension,
isLightTheme) returns null; change the logic in the block using
colorBlindMode/getColorBlindExtensionColor so that if cbColors is falsy you
immediately return the accent-based default (the same fallback used when no
EXTENSION_MAP entry exists) instead of continuing to the EXTENSION_MAP lookup,
referencing getColorBlindExtensionColor, EXTENSION_MAP and EXTENSION_COLORS to
locate where to return the accent default; alternatively, if you prefer not to
change control flow, update the misleading comment to state that a missing
color-blind palette will fall back to the EXTENSION_MAP lookup.

---

Outside diff comments:
In `@src/renderer/components/TabSwitcherModal.tsx`:
- Around line 109-116: Remove the orphaned JSDoc block that described
getExtensionColor (the docblock now sitting immediately before the ContextGauge
JSDoc) so there aren't two back-to-back JSDoc comments with no declaration;
locate the dangling comment that references getExtensionColor and delete it now
that getExtensionColor is imported, leaving only the ContextGauge JSDoc above
its declaration.

---

Nitpick comments:
In `@src/renderer/constants/colorblindPalettes.ts`:
- Around line 176-205: The new category color assignments (image, java, ruby,
data, document) intentionally reuse existing palette hues (matching rust, html,
config, styles, go) due to the limited 8–10 distinct Wong hues; add a JSDoc
block at the top of the colorblindPalettes export (in
src/renderer/constants/colorblindPalettes.ts) noting that these pairings are
deliberate "budget allocations" rather than oversights, list the specific reused
mappings (image→rust, java→html, ruby→config, data→styles, document→go), and
explain that perfect uniqueness is impossible with Wong's palette so future
contributors should not change colors to non-Wong variants.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 69a3381 and d1f6154.

📒 Files selected for processing (7)
  • src/__tests__/renderer/components/TerminalOutput.test.tsx
  • src/__tests__/renderer/utils/extensionColors.test.ts
  • src/renderer/components/TabBar.tsx
  • src/renderer/components/TabSwitcherModal.tsx
  • src/renderer/components/TerminalOutput.tsx
  • src/renderer/constants/colorblindPalettes.ts
  • src/renderer/utils/extensionColors.ts

Resolved 10 merge conflicts across 4 files:
- GitStatusWidget.test.tsx: kept dual-render getAllByText assertions
- MainPanel.test.tsx: kept regex-based context widget queries
- WizardInputPanel.tsx: kept accessibility button wrapper from react-doctor
- InputArea.tsx: kept accessibility button wrapper from react-doctor
- QuitConfirmModal: use composite key `${name}-${index}` to prevent
  duplicate React keys when agents share display names
- extensionColors: early-return accent fallback in colorblind mode so
  we never serve non-colorblind-safe colors from EXTENSION_MAP
Tests were asserting theme.colors.border for unknown extension badges,
but the extensionColors utility now derives from theme accent to ensure
all pills are visible. Updated assertions to match.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
src/__tests__/renderer/components/MainPanel.test.tsx (1)

2390-2418: ⚠️ Potential issue | 🟡 Minor

Test documents a console.error swallow that violates the Sentry reporting guideline.

The test asserts console.error is called when clipboard.writeText rejects, which means the underlying MainPanel component silently catches the clipboard error and logs it. Per coding guidelines, unexpected/unhandled errors must not be absorbed by try/catch→console.error; they should be reported via captureException from src/utils/sentry.

The fix belongs in MainPanel's clipboard handler, and this test should then be updated to assert captureException was called (or let the error propagate if it is truly unrecoverable). As per coding guidelines: "Do NOT silently swallow exceptions with try-catch-console.error blocks. Use Sentry utilities (captureException, captureMessage) from 'src/utils/sentry' for explicit error reporting with context."

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

In `@src/__tests__/renderer/components/MainPanel.test.tsx` around lines 2390 -
2418, The clipboard error is currently swallowed with console.error inside
MainPanel's clipboard handler; update the handler (e.g., the
copy/onCopy/handleCopy method in the MainPanel component) to call
captureException from 'src/utils/sentry' (import it at the top) instead of
console.error (or rethrow if unrecoverable), passing the caught error and
relevant context; then update the test to mock and assert that captureException
was called (or adjust the test to expect the error to propagate) instead of
asserting console.error.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/renderer/utils/extensionColors.ts`:
- Around line 163-206: getExtensionColor currently lowercases the extension only
in the non-colorblind path, so in colorBlindMode calls to
getColorBlindExtensionColor(extension, ...) can miss matches; change the
function to normalize the incoming extension to a lowercase form (e.g., compute
const ext = extension.toLowerCase() at the top) and pass that ext into
getColorBlindExtensionColor and use ext for the EXTENSION_MAP lookup so both
color-blind and regular paths use the same normalized key.

---

Outside diff comments:
In `@src/__tests__/renderer/components/MainPanel.test.tsx`:
- Around line 2390-2418: The clipboard error is currently swallowed with
console.error inside MainPanel's clipboard handler; update the handler (e.g.,
the copy/onCopy/handleCopy method in the MainPanel component) to call
captureException from 'src/utils/sentry' (import it at the top) instead of
console.error (or rethrow if unrecoverable), passing the caught error and
relevant context; then update the test to mock and assert that captureException
was called (or adjust the test to expect the error to propagate) instead of
asserting console.error.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d1f6154 and 16e5924.

📒 Files selected for processing (12)
  • src/__tests__/renderer/components/GitStatusWidget.test.tsx
  • src/__tests__/renderer/components/InlineWizard/WizardConversationView.test.tsx
  • src/__tests__/renderer/components/MainPanel.test.tsx
  • src/renderer/App.tsx
  • src/renderer/components/InlineWizard/WizardInputPanel.tsx
  • src/renderer/components/InputArea.tsx
  • src/renderer/components/MainPanel.tsx
  • src/renderer/components/QuitConfirmModal.tsx
  • src/renderer/components/TabSwitcherModal.tsx
  • src/renderer/components/TerminalOutput.tsx
  • src/renderer/hooks/session/useHandsOnTimeTracker.ts
  • src/renderer/utils/extensionColors.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/renderer/components/InlineWizard/WizardInputPanel.tsx
  • src/renderer/components/TerminalOutput.tsx
  • src/tests/renderer/components/GitStatusWidget.test.tsx
  • src/renderer/App.tsx
  • src/tests/renderer/components/InlineWizard/WizardConversationView.test.tsx
  • src/renderer/components/QuitConfirmModal.tsx
  • src/renderer/components/TabSwitcherModal.tsx
  • src/renderer/components/InputArea.tsx

Comment on lines +163 to +206
export function getExtensionColor(
extension: string,
theme: Theme,
colorBlindMode?: boolean
): { bg: string; text: string } {
const isLightTheme = theme.mode === 'light';

// Colorblind-safe path — never fall through to non-colorblind-safe colors
if (colorBlindMode) {
const cbColors = getColorBlindExtensionColor(extension, isLightTheme);
if (cbColors) return cbColors;
// Unknown extension in colorblind mode: skip the regular palette and
// jump straight to the theme accent so we never serve unsafe colors.
const accentRgb = hexToRgb(theme.colors.accent);
if (accentRgb) {
return isLightTheme
? { bg: `rgba(${accentRgb.r}, ${accentRgb.g}, ${accentRgb.b}, 0.15)`, text: `rgba(${accentRgb.r}, ${accentRgb.g}, ${accentRgb.b}, 0.9)` }
: { bg: `rgba(${accentRgb.r}, ${accentRgb.g}, ${accentRgb.b}, 0.3)`, text: `rgba(${accentRgb.r}, ${accentRgb.g}, ${accentRgb.b}, 0.9)` };
}
return isLightTheme
? { bg: 'rgba(107, 114, 128, 0.15)', text: 'rgba(75, 85, 99, 0.9)' }
: { bg: 'rgba(156, 163, 175, 0.3)', text: 'rgba(209, 213, 219, 0.9)' };
}

// Look up extension in the map
const ext = extension.toLowerCase();
const category = EXTENSION_MAP[ext];
if (category) {
const colors = EXTENSION_COLORS[category];
return isLightTheme ? colors.light : colors.dark;
}

// Default: derive from theme accent so every pill is visible
const rgb = hexToRgb(theme.colors.accent);
if (rgb) {
return isLightTheme
? { bg: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.15)`, text: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.9)` }
: { bg: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.3)`, text: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.9)` };
}

// Ultimate fallback (non-hex accent like rgb/hsl) — still visible
return isLightTheme
? { bg: 'rgba(107, 114, 128, 0.15)', text: 'rgba(75, 85, 99, 0.9)' }
: { bg: 'rgba(156, 163, 175, 0.3)', text: 'rgba(209, 213, 219, 0.9)' };
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Normalize extension case for color-blind path too.
Right now, only the non-colorblind path lowercases the extension. If the caller passes .TS/.JPG, getColorBlindExtensionColor may miss and fall back to the accent, losing consistent per-type coloring.

🛠️ Proposed fix
 export function getExtensionColor(
 	extension: string,
 	theme: Theme,
 	colorBlindMode?: boolean
 ): { bg: string; text: string } {
 	const isLightTheme = theme.mode === 'light';
+	const ext = extension.toLowerCase();

 	// Colorblind-safe path — never fall through to non-colorblind-safe colors
 	if (colorBlindMode) {
-		const cbColors = getColorBlindExtensionColor(extension, isLightTheme);
+		const cbColors = getColorBlindExtensionColor(ext, isLightTheme);
 		if (cbColors) return cbColors;
 		// Unknown extension in colorblind mode: skip the regular palette and
 		// jump straight to the theme accent so we never serve unsafe colors.
 		const accentRgb = hexToRgb(theme.colors.accent);
@@
 	// Look up extension in the map
-	const ext = extension.toLowerCase();
 	const category = EXTENSION_MAP[ext];
📝 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
export function getExtensionColor(
extension: string,
theme: Theme,
colorBlindMode?: boolean
): { bg: string; text: string } {
const isLightTheme = theme.mode === 'light';
// Colorblind-safe path — never fall through to non-colorblind-safe colors
if (colorBlindMode) {
const cbColors = getColorBlindExtensionColor(extension, isLightTheme);
if (cbColors) return cbColors;
// Unknown extension in colorblind mode: skip the regular palette and
// jump straight to the theme accent so we never serve unsafe colors.
const accentRgb = hexToRgb(theme.colors.accent);
if (accentRgb) {
return isLightTheme
? { bg: `rgba(${accentRgb.r}, ${accentRgb.g}, ${accentRgb.b}, 0.15)`, text: `rgba(${accentRgb.r}, ${accentRgb.g}, ${accentRgb.b}, 0.9)` }
: { bg: `rgba(${accentRgb.r}, ${accentRgb.g}, ${accentRgb.b}, 0.3)`, text: `rgba(${accentRgb.r}, ${accentRgb.g}, ${accentRgb.b}, 0.9)` };
}
return isLightTheme
? { bg: 'rgba(107, 114, 128, 0.15)', text: 'rgba(75, 85, 99, 0.9)' }
: { bg: 'rgba(156, 163, 175, 0.3)', text: 'rgba(209, 213, 219, 0.9)' };
}
// Look up extension in the map
const ext = extension.toLowerCase();
const category = EXTENSION_MAP[ext];
if (category) {
const colors = EXTENSION_COLORS[category];
return isLightTheme ? colors.light : colors.dark;
}
// Default: derive from theme accent so every pill is visible
const rgb = hexToRgb(theme.colors.accent);
if (rgb) {
return isLightTheme
? { bg: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.15)`, text: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.9)` }
: { bg: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.3)`, text: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.9)` };
}
// Ultimate fallback (non-hex accent like rgb/hsl) — still visible
return isLightTheme
? { bg: 'rgba(107, 114, 128, 0.15)', text: 'rgba(75, 85, 99, 0.9)' }
: { bg: 'rgba(156, 163, 175, 0.3)', text: 'rgba(209, 213, 219, 0.9)' };
export function getExtensionColor(
extension: string,
theme: Theme,
colorBlindMode?: boolean
): { bg: string; text: string } {
const isLightTheme = theme.mode === 'light';
const ext = extension.toLowerCase();
// Colorblind-safe path — never fall through to non-colorblind-safe colors
if (colorBlindMode) {
const cbColors = getColorBlindExtensionColor(ext, isLightTheme);
if (cbColors) return cbColors;
// Unknown extension in colorblind mode: skip the regular palette and
// jump straight to the theme accent so we never serve unsafe colors.
const accentRgb = hexToRgb(theme.colors.accent);
if (accentRgb) {
return isLightTheme
? { bg: `rgba(${accentRgb.r}, ${accentRgb.g}, ${accentRgb.b}, 0.15)`, text: `rgba(${accentRgb.r}, ${accentRgb.g}, ${accentRgb.b}, 0.9)` }
: { bg: `rgba(${accentRgb.r}, ${accentRgb.g}, ${accentRgb.b}, 0.3)`, text: `rgba(${accentRgb.r}, ${accentRgb.g}, ${accentRgb.b}, 0.9)` };
}
return isLightTheme
? { bg: 'rgba(107, 114, 128, 0.15)', text: 'rgba(75, 85, 99, 0.9)' }
: { bg: 'rgba(156, 163, 175, 0.3)', text: 'rgba(209, 213, 219, 0.9)' };
}
// Look up extension in the map
const category = EXTENSION_MAP[ext];
if (category) {
const colors = EXTENSION_COLORS[category];
return isLightTheme ? colors.light : colors.dark;
}
// Default: derive from theme accent so every pill is visible
const rgb = hexToRgb(theme.colors.accent);
if (rgb) {
return isLightTheme
? { bg: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.15)`, text: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.9)` }
: { bg: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.3)`, text: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.9)` };
}
// Ultimate fallback (non-hex accent like rgb/hsl) — still visible
return isLightTheme
? { bg: 'rgba(107, 114, 128, 0.15)', text: 'rgba(75, 85, 99, 0.9)' }
: { bg: 'rgba(156, 163, 175, 0.3)', text: 'rgba(209, 213, 219, 0.9)' };
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/utils/extensionColors.ts` around lines 163 - 206,
getExtensionColor currently lowercases the extension only in the non-colorblind
path, so in colorBlindMode calls to getColorBlindExtensionColor(extension, ...)
can miss matches; change the function to normalize the incoming extension to a
lowercase form (e.g., compute const ext = extension.toLowerCase() at the top)
and pass that ext into getColorBlindExtensionColor and use ext for the
EXTENSION_MAP lookup so both color-blind and regular paths use the same
normalized key.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/__tests__/renderer/components/TabBar.test.tsx (1)

4816-4817: LGTM — verify hardcoded color stays in sync with darkTheme.colors.accent

rgba(189, 147, 249, 0.3) correctly matches darkTheme.colors.accent = '#bd93f9' at 0.3 opacity. The value is hardcoded, so if getExtensionColor's opacity factor or the Dracula accent ever changes, this test silently diverges. Deriving it inline (e.g. parsing the hex and embedding the channel values from darkTheme.colors.accent) would make the coupling explicit.

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

In `@src/__tests__/renderer/components/TabBar.test.tsx` around lines 4816 - 4817,
The test hardcodes the RGBA value for unknown extensions; update the assertion
to derive the expected color from darkTheme.colors.accent so it stays in sync
with getExtensionColor's opacity; in TabBar.test.tsx import or reference
darkTheme.colors.accent and compute the expected string by converting the hex
(darkTheme.colors.accent) to an rgba with alpha 0.3 (you can add a small helper
hexToRgba used only in the test), then replace the hardcoded 'rgba(189, 147,
249, 0.3)' in the expect(badge).toHaveStyle(...) assertion with the computed
expected value so badge validation uses the derived color.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/__tests__/renderer/components/TabBar.test.tsx`:
- Around line 5271-5272: The colorblind-mode unknown-extension fallback uses
opacity 0.3 instead of the 0.35 used by all other colorblind dark-mode badges;
update the fallback in getExtensionColor (the unknown-extension branch in
src/renderer/utils/extensionColors.ts) to use 0.35 for dark colorblind mode, and
adjust the test expectation in TabBar.test.tsx (the assertion that checks badge
backgroundColor) to expect 'rgba(189, 147, 249, 0.35)'.

---

Nitpick comments:
In `@src/__tests__/renderer/components/TabBar.test.tsx`:
- Around line 4816-4817: The test hardcodes the RGBA value for unknown
extensions; update the assertion to derive the expected color from
darkTheme.colors.accent so it stays in sync with getExtensionColor's opacity; in
TabBar.test.tsx import or reference darkTheme.colors.accent and compute the
expected string by converting the hex (darkTheme.colors.accent) to an rgba with
alpha 0.3 (you can add a small helper hexToRgba used only in the test), then
replace the hardcoded 'rgba(189, 147, 249, 0.3)' in the
expect(badge).toHaveStyle(...) assertion with the computed expected value so
badge validation uses the derived color.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 16e5924 and d02545a.

📒 Files selected for processing (1)
  • src/__tests__/renderer/components/TabBar.test.tsx

Comment on lines +5271 to +5272
// Colorblind mode also uses accent-derived fallback for unknown extensions
expect(badge).toHaveStyle({ backgroundColor: 'rgba(189, 147, 249, 0.3)' });
Copy link

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

🏁 Script executed:

#!/bin/bash
# First, find files related to extension colors
fd -t f -e ts -e tsx -e js -e jsx | xargs rg -l "getExtensionColor" | head -20

Repository: RunMaestro/Maestro

Length of output: 277


🏁 Script executed:

#!/bin/bash
# Search for the getExtensionColor function definition
rg -n "getExtensionColor\s*=|function getExtensionColor" --type ts --type tsx -B2 -A15 | head -100

Repository: RunMaestro/Maestro

Length of output: 89


🏁 Script executed:

#!/bin/bash
# Look for opacity/colorblind related constants and logic
rg -n "0\.35|0\.3\b" --type ts --type tsx -B3 -A3 | grep -E "(colorblind|opacity|0\.3|0\.35)" | head -50

Repository: RunMaestro/Maestro

Length of output: 89


🏁 Script executed:

#!/bin/bash
# Check the test file at the specific lines to understand context
sed -n '5260,5280p' src/__tests__/renderer/components/TabBar.test.tsx

Repository: RunMaestro/Maestro

Length of output: 700


🏁 Script executed:

#!/bin/bash
# Read the extensionColors implementation
cat -n src/renderer/utils/extensionColors.ts

Repository: RunMaestro/Maestro

Length of output: 9570


🏁 Script executed:

#!/bin/bash
# Read colorblind palettes constants
cat -n src/renderer/constants/colorblindPalettes.ts

Repository: RunMaestro/Maestro

Length of output: 12087


🏁 Script executed:

#!/bin/bash
# Check the test file for extensionColors to see expected values
cat -n src/__tests__/renderer/utils/extensionColors.test.ts

Repository: RunMaestro/Maestro

Length of output: 5954


Colorblind-mode unknown-extension fallback uses 0.3 opacity — inconsistent with every other colorblind dark-mode badge (0.35)

Every colorblind-mode known-extension badge in dark mode uses 0.35 opacity (TypeScript, Markdown, JSON, CSS, Python, Rust, Go, Shell, Image, Java, C++, Ruby, SQL, and others all use 0.35). The unknown-extension fallback in getExtensionColor (line 180 of src/renderer/utils/extensionColors.ts), however, hardcodes 0.3. This fallback branch should apply the same 0.35 opacity multiplier used by the colorblind-safe palette for all known extensions.

Update the fallback to use 0.35 instead of 0.3 for dark mode in colorblind mode, and update the test expectation accordingly.

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

In `@src/__tests__/renderer/components/TabBar.test.tsx` around lines 5271 - 5272,
The colorblind-mode unknown-extension fallback uses opacity 0.3 instead of the
0.35 used by all other colorblind dark-mode badges; update the fallback in
getExtensionColor (the unknown-extension branch in
src/renderer/utils/extensionColors.ts) to use 0.35 for dark colorblind mode, and
adjust the test expectation in TabBar.test.tsx (the assertion that checks badge
backgroundColor) to expect 'rgba(189, 147, 249, 0.35)'.

…ertions)

Adopted origin/main's more precise within()-scoped test assertions
for GitStatusWidget, targeting .header-git-status-full and
.header-git-status-compact CSS classes instead of broad getAllByText.
@pedramamini pedramamini merged commit 68bd744 into main Feb 24, 2026
reachraza added a commit that referenced this pull request Feb 25, 2026
…se 3 patterns

- Resolved 3 conflicts (App.tsx, SessionList.tsx, useSessionListProps.ts):
  keep Phase 3 direct store subscriptions; add only handleArchiveGroupChat handler
- groupChatStates, allGroupChatParticipantStates, contextWarning thresholds already
  read from Zustand stores inside SessionList — not re-added as props
- Fixed unused React import in SshRemoteIgnoreSection.tsx (introduced by main)
- Updated groupChat handler registration test for new groupChat:archive IPC channel

New features from main:
- feat: archive state for group chat (#377)
- feat: worktree from Auto Run (WorktreeRunSection in BatchRunnerModal)
- perf: render optimization, event consolidation, debug cleanup (#440)
- fix: hide file path on scroll in file preview header (#448)
- fix: unstable useSessionStore filter selector, worktree detection, PR creation
- docs: Encore Features section, performance profiling guide
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.

2 participants