Skip to content

chore: React Doctor phase-1 cleanup (alt text + duplicate prop)#427

Merged
reachraza merged 5 commits intoRunMaestro:mainfrom
jeffscottward:chore/react-doctor-cleanup-phase-1
Feb 23, 2026
Merged

chore: React Doctor phase-1 cleanup (alt text + duplicate prop)#427
reachraza merged 5 commits intoRunMaestro:mainfrom
jeffscottward:chore/react-doctor-cleanup-phase-1

Conversation

@jeffscottward
Copy link
Contributor

@jeffscottward jeffscottward commented Feb 19, 2026

Summary

  • fix React Doctor Phase 1 errors related to missing image alt attributes in renderer UI surfaces
  • remove a duplicate allSessions prop in SendToAgentModal test fixture
  • keep scope intentionally narrow as an isolated first pass

Why this scope

React Doctor reported a large backlog (22 errors, 807 warnings). This PR targets high-confidence, low-risk fixes only.

Validation

  • npm run test -- src/__tests__/renderer/components/SendToAgentModal.test.tsx
  • npx eslint src/renderer/components/TerminalOutput.tsx src/renderer/components/InlineWizard/WizardInputPanel.tsx src/renderer/components/LightboxModal.tsx src/renderer/components/InputArea.tsx src/renderer/components/PromptComposerModal.tsx
  • npx react-doctor . --yes --offline --no-ami (branch diff scan)

Tracking

Summary by CodeRabbit

  • Improvements

    • Expanded accessibility and keyboard support: alt text, ARIA labels/roles, focus-visible styling, Enter/Space activation for thumbnails/links, and explicit overlay close controls.
    • Replaced non-semantic containers with button semantics for consistent keyboard/focus interactions across lists, modals, and action items.
    • Terminal/UI tweaks: region labeling, image lightbox activation, and refined auto-scroll control behavior.
  • Refactor

    • Centralized handlers for search/view-mode/navigation and consolidated state flows for predictable selection behavior.
  • Tests

    • Multiple tests updated to match UI changes and more robustly locate elements (including a narrowed session test).

@coderabbitai
Copy link

coderabbitai bot commented Feb 19, 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

Wide UI and accessibility refactor across renderer and mobile: images receive alt text and keyboard semantics, many interactive divs converted to buttons, search/view-mode updates centralized into handlers, modal overlays use explicit invisible close buttons, scroll/state logic adjusted, and one test now uses busySessions.

Changes

Cohort / File(s) Summary
Image accessibility & lightbox
src/renderer/components/InlineWizard/WizardInputPanel.tsx, src/renderer/components/PromptComposerModal.tsx, src/renderer/components/LightboxModal.tsx, src/renderer/components/TerminalOutput.tsx
Added alt attributes, switched thumbnail keys to URLs, wrapped images in keyboard-focusable/clickable wrappers (buttons or role/button), added onKeyDown handlers and minor event propagation control.
Interactive elements → buttons
src/renderer/components/InputArea.tsx, src/web/mobile/CommandHistoryDrawer.tsx, src/web/mobile/MobileHistoryPanel.tsx, src/renderer/components/InlineWizard/...
Converted many interactive divs/anchors to button elements (including backdrops and list items), added focus-visible styles, centralized close handlers (e.g., closeDrawer), and adjusted keyboard/focus behavior.
Centralized search & view-mode handlers
src/renderer/components/FileSearchModal.tsx, src/renderer/components/MergeSessionModal.tsx, src/renderer/components/SendToAgentModal.tsx, src/renderer/components/TabSwitcherModal.tsx, src/renderer/components/AgentSessionsBrowser.tsx, src/renderer/components/UsageDashboard/UsageDashboardModal.tsx, src/renderer/components/MarketplaceModal.tsx, src/renderer/components/SymphonyModal.tsx
Introduced handleSearchChange / handleViewModeChange (and variants) to centralize state updates and reset selection indices; removed or replaced some effect-driven resets and wired keyboard navigation into these handlers.
Modal overlay & propagation changes
src/renderer/components/PromptComposerModal.tsx, src/renderer/components/DocumentsPanel.tsx, src/renderer/components/UsageDashboard/UsageDashboardModal.tsx, src/renderer/components/SymphonyModal.tsx, src/renderer/components/DocumentSelectorModal
Replaced overlay click-to-close with full-coverage invisible close buttons (with aria-label), adjusted z-index/layout (relative z-10), and removed some stopPropagation usages inside modals.
Scroll/refactor & streaming preview
src/renderer/components/InlineWizard/StreamingDocumentPreview.tsx, src/renderer/components/InlineWizard/WizardConversationView.test.tsx
Switched user-scroll tracking from state to refs, added filename-aware reset logic and render-tick sync; tests advance timers to allow scroll guard reset before simulating scroll.
TerminalOutput controls & accessibility
src/renderer/components/TerminalOutput.tsx, src/renderer/components/GitStatusWidget.tsx, src/renderer/components/MainPanel.tsx
Adjusted highlight keys to matchStart, wrapped terminal images in buttons with alt, added role="region"/aria-label, refined AI auto-scroll toggle UI and compact view aria-hidden tweaks.
Per-tab dismissal & UI state simplification
src/renderer/components/ContextWarningSash.tsx, src/renderer/components/DocumentsPanel.tsx
Replaced single dismissal state with per-tab dismissedByTab map; simplified loop/max toggle logic and modal overlay propagation handling in DocumentsPanel.
Agent/new instance refactors
src/renderer/components/NewInstanceModal.tsx, src/renderer/components/SendToAgentModal.tsx
Centralized handleWorkingDirChange, added handleAgentHeaderActivate for agent expansion/SSH-config transfer and model loading; SendToAgentModal now uses a search handler to reset selection index instead of an effect.
Settings & SSH modal changes
src/renderer/components/SettingsModal.tsx, src/renderer/components/Settings/SshRemoteModal.tsx
Centralized wakatime/ssh filter handlers, removed effects that reset derived validation state, added ARIA attributes and some structural label→div changes.
Marketplace & Symphony UI wiring
src/renderer/components/MarketplaceModal.tsx, src/renderer/components/SymphonyModal.tsx
Added handler functions for category/search, replaced anchor links with buttons that call external helpers, stabilized map keys, and introduced loading skeleton ID constants.
Tests & small formatting tweaks
src/__tests__/renderer/components/SendToAgentModal.test.tsx, other tests, various main/ipc handlers
Test: changed allSessions to busySessions in busy-agent path; multiple tests updated to target buttons or robust selectors. Several unrelated formatting/refactor-only edits across handlers and hooks.

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 68.75% 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 accurately describes the main objectives: fixing React Doctor phase-1 errors related to missing alt attributes and removing duplicate props, which aligns with the substantial changes across multiple components for accessibility improvements.

✏️ 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

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

@greptile-apps
Copy link

greptile-apps bot commented Feb 19, 2026

Greptile Summary

Fixed React Doctor phase-1 accessibility and duplicate prop errors across 6 files. Changes include adding descriptive alt text to all <img> elements in renderer UI components and removing a duplicate allSessions prop in test fixture.

  • Added alt text to staged image previews in InputArea, WizardInputPanel, and PromptComposerModal (format: "Staged [context] image {n}")
  • Added alt text to lightbox expanded preview and terminal output images
  • Removed duplicate allSessions={mockSessions} prop in SendToAgentModal.test.tsx (kept allSessions={busySessions})

All changes are low-risk, focused fixes that improve accessibility without affecting functionality.

Confidence Score: 5/5

  • This PR is safe to merge with no risk
  • All changes are additive accessibility improvements (alt text) and a test fixture cleanup (duplicate prop removal). No logic changes, no runtime behavior changes, and scope is intentionally narrow as stated.
  • No files require special attention

Important Files Changed

Filename Overview
src/tests/renderer/components/SendToAgentModal.test.tsx Removed duplicate allSessions prop in test fixture (line 808 kept, line 808 duplicate removed)
src/renderer/components/InlineWizard/WizardInputPanel.tsx Added descriptive alt text "Staged wizard image {idx + 1}" to image previews for accessibility
src/renderer/components/InputArea.tsx Added descriptive alt text "Staged image {idx + 1}" to staged image previews for accessibility
src/renderer/components/LightboxModal.tsx Added descriptive alt text "Expanded image preview" to lightbox image for accessibility
src/renderer/components/PromptComposerModal.tsx Added descriptive alt text "Prompt composer staged image {idx + 1}" to staged images for accessibility
src/renderer/components/TerminalOutput.tsx Added descriptive alt text "Terminal output image {imgIdx + 1}" to terminal images for accessibility

Last reviewed commit: a625816

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

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/InlineWizard/StreamingDocumentPreview.tsx (1)

349-367: ⚠️ Potential issue | 🟡 Minor

Add relative positioning to parent container.

The scroll indicator uses absolute bottom-4 positioning, but the parent container at line 239 lacks position: relative. This causes the button to position relative to the viewport instead of the preview container, potentially placing it outside the intended area. Add the relative class to establish a proper positioning context.

🛠️ Fix
-		<div className="flex flex-col h-full streaming-preview">
+		<div className="relative flex flex-col h-full streaming-preview">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/InlineWizard/StreamingDocumentPreview.tsx` around
lines 349 - 367, The scroll-resume button is absolutely positioned but its
parent container lacks a positioning context, so add the CSS positioning context
by adding the "relative" class to the parent element that wraps the preview and
contains userScrolledRef/containerRef in the StreamingDocumentPreview component;
this ensures the absolute-styled button (which references userScrolledRef,
containerRef, and calls updateUserScrolled) is positioned relative to the
preview container rather than the viewport.
🧹 Nitpick comments (6)
src/renderer/components/Settings/SshRemoteModal.tsx (1)

570-580: Add tabIndex to the listbox container for proper focus handling.

The dropdown container has role="listbox" and onKeyDown but lacks tabIndex. Per coding guidelines, interactive elements need tabIndex={0} or tabIndex={-1} with outline-none to ensure focus works correctly. While the filter input inside receives focus, the listbox itself should be focusable for full accessibility compliance.

♻️ Proposed fix
 <div
 	className="absolute top-full left-0 right-0 mt-1 rounded border shadow-lg z-10"
 	style={{
 		backgroundColor: theme.colors.bgMain,
 		borderColor: theme.colors.border,
 	}}
 	onKeyDown={handleDropdownKeyDown}
 	role="listbox"
 	aria-label="SSH config hosts"
+	tabIndex={-1}
 >

As per coding guidelines: "Add tabIndex={0} or tabIndex={-1} and outline-none class to ensure focus works correctly."

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

In `@src/renderer/components/Settings/SshRemoteModal.tsx` around lines 570 - 580,
The listbox container rendered when showSshConfigDropdown is true is missing a
tabIndex which prevents it from being focusable for keyboard handling; update
the dropdown div (the element with role="listbox" and
onKeyDown={handleDropdownKeyDown}) to include tabIndex={0} (or tabIndex={-1}
plus an "outline-none" class per guidelines) so it can receive focus and
keyboard events properly while preserving the existing onKeyDown handler and
ARIA attributes.
src/renderer/components/InputArea.tsx (1)

464-499: Good accessibility improvements for staged images.

Alt text, role="button", tabIndex={0}, and keyboard activation are properly implemented.

Same as TerminalOutput images: consider adding explicit focus styles for keyboard navigation visibility.

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

In `@src/renderer/components/InputArea.tsx` around lines 464 - 499, Add explicit
visible focus styles for the staged image elements to improve keyboard
navigation visibility: update the image element rendered from stagedImages (the
<img> with onClick/onKeyDown that calls setLightboxImage) to include a clear
focus indicator (e.g., outline or ring) via a focus-visible or focus CSS class
and ensure the remove <button> (the one that calls setStagedImages) also has
matching focus styles; prefer using existing theme colors (theme.colors.border)
or a dedicated utility class so keyboard users can see which staged image or
delete button is focused.
src/renderer/components/TerminalOutput.tsx (1)

427-451: Good accessibility improvements for terminal images.

The additions of alt text, role="button", tabIndex={0}, and keyboard activation (Enter/Space) are well-implemented and align with the PR's accessibility goals.

Consider adding an explicit focus style (e.g., focus:ring-2 focus:ring-accent) for better visual feedback when users navigate via keyboard, rather than relying on browser defaults. Based on learnings: "Add tabIndex={0} or tabIndex={-1} and outline-none class to ensure focus works correctly."

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

In `@src/renderer/components/TerminalOutput.tsx` around lines 427 - 451, The
terminal image elements lack an explicit visible focus style; update the <img>
rendered inside the log.images.map in TerminalOutput (the element that calls
setLightboxImage) to include a focus utility class such as "focus:ring-2
focus:ring-accent" (and optionally "outline-none" if you prefer custom ring-only
focus) so keyboard users get clear visual feedback when tabbing to the image;
ensure tabIndex={0} remains and that the onKeyDown handler still triggers
setLightboxImage so keyboard activation and the new focus style work together.
src/renderer/components/InlineWizard/StreamingDocumentPreview.tsx (2)

139-186: Inconsistent indentation in template string.

Lines 140 and 182-186 have mixed tabs/spaces. As per coding guidelines, use tabs for indentation consistently.

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

In `@src/renderer/components/InlineWizard/StreamingDocumentPreview.tsx` around
lines 139 - 186, The template string assigned to the proseStyles constant in
StreamingDocumentPreview.tsx contains mixed spaces and tabs (notably around the
beginning of the string and the checkbox CSS block), so replace the leading
spaces on those lines with tabs to match project style; open the proseStyles
template string, convert lines with mixed indentation (including the h1 start
line and the checkbox/li:has(...) block) to use tabs only, and run the repo's
formatter/linter to ensure consistency.

98-112: Render-phase ref mutation works but has subtle risks.

Mutating refs during render (lines 102-105) can cause issues with React's Concurrent Mode and breaks the expectation that render is pure. While this "derived state in render" pattern avoids an extra effect, consider wrapping this in useMemo or a custom hook to make the intent clearer and safer:

useMemo(() => {
  if (lastFilenameRef.current !== (filename ?? '')) {
    lastFilenameRef.current = filename ?? '';
    userScrolledRef.current = false;
  }
}, [filename]);

This preserves the synchronous reset behavior while being explicit about dependencies.

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

In `@src/renderer/components/InlineWizard/StreamingDocumentPreview.tsx` around
lines 98 - 112, The render mutates refs directly (lastFilenameRef and
userScrolledRef) which breaks render purity; move the filename-change reset
logic out of the top-level render and into a memoized callback such as
useMemo(() => { ... }, [filename]) so the check and updates to
lastFilenameRef.current and userScrolledRef.current run synchronously but only
when filename changes; keep updateUserScrolled and setScrollRenderTick as-is so
state updates still trigger a re-render when userScrolledRef changes.
src/renderer/components/MarketplaceModal.tsx (1)

1009-1019: Stale dependency: setSelectedCategory is no longer used in the effect.

The effect body now uses handleCategoryChange instead of calling setSelectedCategory directly, but setSelectedCategory remains in the dependency array at line 1013.

Proposed fix
 }, [
 	isOpen,
 	categories,
 	selectedCategory,
-	setSelectedCategory,
 	showDetailView,
 	selectedPlaybook,
 	selectedDocFilename,
 	handleSelectDocument,
 	handleCategoryChange,
 ]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/MarketplaceModal.tsx` around lines 1009 - 1019, The
effect in MarketplaceModal has a stale dependency: remove setSelectedCategory
from the dependency array (it’s no longer referenced in the effect body) and
keep the real dependencies (isOpen, categories, selectedCategory,
showDetailView, selectedPlaybook, selectedDocFilename, handleSelectDocument,
handleCategoryChange); if handleCategoryChange is not stable, wrap it in
useCallback so the effect doesn’t re-run unnecessarily. Ensure you update the
dependency array where it’s declared to match the actual variables used and
verify no direct calls to setSelectedCategory remain inside the effect.
🤖 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/DocumentsPanel.tsx`:
- Around line 414-423: The overlay full-screen close button in DocumentsPanel
(the button with onClose and tabIndex={-1}) is missing the required focus
styling class; update that button element inside DocumentsPanel to include the
"outline-none" class (keeping tabIndex={-1} is fine per guidelines) and, if the
component should auto-focus, add a ref={(el) => el?.focus()} to the same button
so focus behavior is consistent across renderer components.

In `@src/renderer/components/MarketplaceModal.tsx`:
- Around line 417-427: Replace the native anchor used for author links in
MarketplaceModal (the block referencing playbook.authorLink, playbook.author and
ExternalLink) with the same external-open pattern used elsewhere: render a
button that calls window.maestro.shell.openExternal(playbook.authorLink) in its
onClick, preserve the inline styling (style={{ color: theme.colors.accent }})
and the ExternalLink icon, and add tabIndex={0} and the "outline-none" class to
the button to satisfy renderer accessibility/focus guidelines; remove
target/_blank and rel attributes since openExternal handles navigation.

In `@src/renderer/components/MergeSessionModal.tsx`:
- Around line 643-649: The tab buttons rendered in MergeSessionModal (the
element with props key={mode} role="tab" aria-selected={viewMode === mode}
aria-controls={`merge-tabpanel-${mode}`} onClick={() =>
handleViewModeChange(mode)`) are missing the id that tabpanels reference; add an
id attribute that matches the tabpanel's aria-labelledby (for example
id={`merge-tab-${mode}`}) so each button provides the label for its
corresponding panel, ensure the id is unique per mode and preserved when
rendering.

In `@src/renderer/components/UsageDashboard/UsageDashboardModal.tsx`:
- Line 148: The timeRange state is initialized to a hardcoded 'week' causing an
extra fetch when defaultTimeRange differs; change the initialization of
timeRange to use the incoming prop (defaultTimeRange) instead of the literal
'week' so the first render/fetch uses the correct range (update the useState
call that sets timeRange and keep setTimeRange unchanged); also ensure any
open/modal effect that resets timeRange uses defaultTimeRange when opening
(reference: timeRange, setTimeRange, defaultTimeRange in
UsageDashboardModal.tsx).

In `@src/web/mobile/CommandHistoryDrawer.tsx`:
- Around line 212-215: In CommandHistoryDrawer, the inline style ordering sets
borderBottom then border: 'none', which clears the bottom border; fix by placing
the border: 'none' shorthand before borderBottom (or remove the shorthand
entirely) in the style object used for the list/item (the style block containing
borderBottom, touchAction, border, width) so borderBottom: `1px solid
${colors.border}` takes effect.

In `@src/web/mobile/MobileHistoryPanel.tsx`:
- Around line 878-901: The fetchHistory catch block must report errors to Sentry
and only swallow known/recoverable fetch failures: import captureException and
captureMessage from 'src/utils/sentry' and replace the current catch body in
fetchHistory so it first calls captureException(err, { extra: { projectPath,
sessionId } }) (and optionally captureMessage for specific status text), then if
the error is a known/recoverable fetch error (e.g., err.message includes 'Failed
to fetch history', err.name === 'AbortError', or err instanceof TypeError for
network failures) call webLogger.error(...) and setError(err.message || 'Failed
to load history') and do not rethrow; otherwise rethrow the error so it bubbles
to Sentry/outer handlers; keep the existing finally with setIsLoading(false).
Reference symbols: fetchHistory, webLogger, setError, setIsLoading, buildApiUrl,
and use captureException/captureMessage from 'src/utils/sentry'.

---

Outside diff comments:
In `@src/renderer/components/InlineWizard/StreamingDocumentPreview.tsx`:
- Around line 349-367: The scroll-resume button is absolutely positioned but its
parent container lacks a positioning context, so add the CSS positioning context
by adding the "relative" class to the parent element that wraps the preview and
contains userScrolledRef/containerRef in the StreamingDocumentPreview component;
this ensures the absolute-styled button (which references userScrolledRef,
containerRef, and calls updateUserScrolled) is positioned relative to the
preview container rather than the viewport.

---

Nitpick comments:
In `@src/renderer/components/InlineWizard/StreamingDocumentPreview.tsx`:
- Around line 139-186: The template string assigned to the proseStyles constant
in StreamingDocumentPreview.tsx contains mixed spaces and tabs (notably around
the beginning of the string and the checkbox CSS block), so replace the leading
spaces on those lines with tabs to match project style; open the proseStyles
template string, convert lines with mixed indentation (including the h1 start
line and the checkbox/li:has(...) block) to use tabs only, and run the repo's
formatter/linter to ensure consistency.
- Around line 98-112: The render mutates refs directly (lastFilenameRef and
userScrolledRef) which breaks render purity; move the filename-change reset
logic out of the top-level render and into a memoized callback such as
useMemo(() => { ... }, [filename]) so the check and updates to
lastFilenameRef.current and userScrolledRef.current run synchronously but only
when filename changes; keep updateUserScrolled and setScrollRenderTick as-is so
state updates still trigger a re-render when userScrolledRef changes.

In `@src/renderer/components/InputArea.tsx`:
- Around line 464-499: Add explicit visible focus styles for the staged image
elements to improve keyboard navigation visibility: update the image element
rendered from stagedImages (the <img> with onClick/onKeyDown that calls
setLightboxImage) to include a clear focus indicator (e.g., outline or ring) via
a focus-visible or focus CSS class and ensure the remove <button> (the one that
calls setStagedImages) also has matching focus styles; prefer using existing
theme colors (theme.colors.border) or a dedicated utility class so keyboard
users can see which staged image or delete button is focused.

In `@src/renderer/components/MarketplaceModal.tsx`:
- Around line 1009-1019: The effect in MarketplaceModal has a stale dependency:
remove setSelectedCategory from the dependency array (it’s no longer referenced
in the effect body) and keep the real dependencies (isOpen, categories,
selectedCategory, showDetailView, selectedPlaybook, selectedDocFilename,
handleSelectDocument, handleCategoryChange); if handleCategoryChange is not
stable, wrap it in useCallback so the effect doesn’t re-run unnecessarily.
Ensure you update the dependency array where it’s declared to match the actual
variables used and verify no direct calls to setSelectedCategory remain inside
the effect.

In `@src/renderer/components/Settings/SshRemoteModal.tsx`:
- Around line 570-580: The listbox container rendered when showSshConfigDropdown
is true is missing a tabIndex which prevents it from being focusable for
keyboard handling; update the dropdown div (the element with role="listbox" and
onKeyDown={handleDropdownKeyDown}) to include tabIndex={0} (or tabIndex={-1}
plus an "outline-none" class per guidelines) so it can receive focus and
keyboard events properly while preserving the existing onKeyDown handler and
ARIA attributes.

In `@src/renderer/components/TerminalOutput.tsx`:
- Around line 427-451: The terminal image elements lack an explicit visible
focus style; update the <img> rendered inside the log.images.map in
TerminalOutput (the element that calls setLightboxImage) to include a focus
utility class such as "focus:ring-2 focus:ring-accent" (and optionally
"outline-none" if you prefer custom ring-only focus) so keyboard users get clear
visual feedback when tabbing to the image; ensure tabIndex={0} remains and that
the onKeyDown handler still triggers setLightboxImage so keyboard activation and
the new focus style work together.

Comment on lines +878 to +901
const fetchHistory = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const params = new URLSearchParams();
if (projectPath) params.set('projectPath', projectPath);
if (sessionId) params.set('sessionId', sessionId);

fetchHistory();
const queryString = params.toString();
const apiUrl = buildApiUrl(`/history${queryString ? `?${queryString}` : ''}`);

const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error(`Failed to fetch history: ${response.statusText}`);
}
const data = await response.json();
setEntries(data.entries || []);
webLogger.debug(`Fetched ${data.entries?.length || 0} history entries`, 'MobileHistory');
} catch (err: any) {
webLogger.error('Failed to fetch history', 'MobileHistory', err);
setError(err.message || 'Failed to load history');
} finally {
setIsLoading(false);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Route unexpected fetch errors to Sentry and avoid swallowing them.

The catch block logs to webLogger and converts all failures into UI error state. This violates the guideline to report via Sentry and let unexpected errors bubble. Please capture exceptions and only handle expected/recoverable failures explicitly (e.g., known HTTP fetch failures), rethrowing everything else.

🔧 Suggested update (capture + selective handling)
-import { webLogger } from '../utils/logger';
+import { webLogger } from '../utils/logger';
+import { captureException } from '../utils/sentry';

 	} catch (err: any) {
-		webLogger.error('Failed to fetch history', 'MobileHistory', err);
-		setError(err.message || 'Failed to load history');
+		captureException(err, { tags: { surface: 'MobileHistoryPanel' }, extra: { projectPath, sessionId } });
+		webLogger.error('Failed to fetch history', 'MobileHistory', err);
+		if (err instanceof Error && err.message.startsWith('Failed to fetch history')) {
+			setError(err.message);
+		} else {
+			setError('Failed to load history');
+			throw err;
+		}
 	} finally {

As per coding guidelines: “Do NOT silently swallow exceptions… Let unexpected errors bubble up to Sentry… Use Sentry utilities (captureException, captureMessage) from 'src/utils/sentry' for explicit error reporting with context.”

📝 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
const fetchHistory = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const params = new URLSearchParams();
if (projectPath) params.set('projectPath', projectPath);
if (sessionId) params.set('sessionId', sessionId);
fetchHistory();
const queryString = params.toString();
const apiUrl = buildApiUrl(`/history${queryString ? `?${queryString}` : ''}`);
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error(`Failed to fetch history: ${response.statusText}`);
}
const data = await response.json();
setEntries(data.entries || []);
webLogger.debug(`Fetched ${data.entries?.length || 0} history entries`, 'MobileHistory');
} catch (err: any) {
webLogger.error('Failed to fetch history', 'MobileHistory', err);
setError(err.message || 'Failed to load history');
} finally {
setIsLoading(false);
}
import { webLogger } from '../utils/logger';
import { captureException } from '../utils/sentry';
const fetchHistory = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const params = new URLSearchParams();
if (projectPath) params.set('projectPath', projectPath);
if (sessionId) params.set('sessionId', sessionId);
const queryString = params.toString();
const apiUrl = buildApiUrl(`/history${queryString ? `?${queryString}` : ''}`);
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error(`Failed to fetch history: ${response.statusText}`);
}
const data = await response.json();
setEntries(data.entries || []);
webLogger.debug(`Fetched ${data.entries?.length || 0} history entries`, 'MobileHistory');
} catch (err: any) {
captureException(err, { tags: { surface: 'MobileHistoryPanel' }, extra: { projectPath, sessionId } });
webLogger.error('Failed to fetch history', 'MobileHistory', err);
if (err instanceof Error && err.message.startsWith('Failed to fetch history')) {
setError(err.message);
} else {
setError('Failed to load history');
throw err;
}
} finally {
setIsLoading(false);
}
}, [projectPath, sessionId]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/mobile/MobileHistoryPanel.tsx` around lines 878 - 901, The
fetchHistory catch block must report errors to Sentry and only swallow
known/recoverable fetch failures: import captureException and captureMessage
from 'src/utils/sentry' and replace the current catch body in fetchHistory so it
first calls captureException(err, { extra: { projectPath, sessionId } }) (and
optionally captureMessage for specific status text), then if the error is a
known/recoverable fetch error (e.g., err.message includes 'Failed to fetch
history', err.name === 'AbortError', or err instanceof TypeError for network
failures) call webLogger.error(...) and setError(err.message || 'Failed to load
history') and do not rethrow; otherwise rethrow the error so it bubbles to
Sentry/outer handlers; keep the existing finally with setIsLoading(false).
Reference symbols: fetchHistory, webLogger, setError, setIsLoading, buildApiUrl,
and use captureException/captureMessage from 'src/utils/sentry'.

- DocumentsPanel: add outline-none to backdrop close button
- MarketplaceModal: replace <a href> with button+openExternal for author links; remove stale setSelectedCategory from effect dep array
- MergeSessionModal: add id={merge-tab-${mode}} to tab buttons for aria-labelledby
- UsageDashboardModal: initialize timeRange from defaultTimeRange prop instead of hardcoded 'week'
- CommandHistoryDrawer: move border:'none' before borderBottom so shorthand no longer overrides it
- StreamingDocumentPreview: add relative to outer container for absolute scroll-resume button; normalize proseStyles indentation to tabs; move render-time ref mutations into useMemo
- InputArea: add focus-visible ring styles to staged image and remove button
- SshRemoteModal: add tabIndex={0} and outline-none to listbox div
- TerminalOutput: add outline-none focus:ring-2 focus:ring-accent to terminal images
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: 6

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/InputArea.tsx (1)

252-364: ⚠️ Potential issue | 🟡 Minor

Use a ref callback for command-history autofocus and add tabIndex to the filter input.

The setTimeout-based focus diverges from the repo's autofocus guidance. Replace the useEffect with a ref callback and add tabIndex={0} to the input element for proper focus accessibility.

Proposed change
-	const commandHistoryFilterRef = React.useRef<HTMLInputElement>(null);
+	const commandHistoryFilterRef = React.useRef<HTMLInputElement>(null);
+	const setCommandHistoryFilterRef = React.useCallback((el: HTMLInputElement | null) => {
+		commandHistoryFilterRef.current = el;
+		if (el) {
+			el.focus();
+		}
+	}, []);
@@
-	useEffect(() => {
-		if (commandHistoryOpen) {
-			setTimeout(() => commandHistoryFilterRef.current?.focus(), 0);
-		}
-	}, [commandHistoryOpen]);
@@
-							ref={commandHistoryFilterRef}
+							ref={setCommandHistoryFilterRef}
+							tabIndex={0}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/InputArea.tsx` around lines 252 - 364, Replace the
setTimeout-based autofocus useEffect that focuses commandHistoryFilterRef when
commandHistoryOpen is true with a ref callback on the filter input that
immediately focuses when the element mounts (use the existing
commandHistoryFilterRef ref logic but convert to a callback ref), and remove the
useEffect and setTimeout; also ensure the filter input element has tabIndex={0}
for keyboard accessibility so it can receive programmatic focus (refer to
commandHistoryFilterRef, commandHistoryOpen, the old useEffect/setTimeout, and
the filter input element).
♻️ Duplicate comments (1)
src/renderer/components/MarketplaceModal.tsx (1)

418-426: Author link → openExternal button: correctly applied.

tabIndex={0}, outline-none, and window.maestro.shell.openExternal match the previously reviewed fix. The non-null assertion on playbook.authorLink! is safe within the {playbook.authorLink ? …} guard.

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

In `@src/renderer/components/MarketplaceModal.tsx` around lines 418 - 426, The
Author link button in MarketplaceModal is correct—ensure the button in the
MarketplaceModal component continues to use tabIndex={0}, className including
"outline-none", and the onClick handler window.maestro.shell.openExternal with
the non-null asserted playbook.authorLink! (this is safe because the button is
rendered inside a guard that checks playbook.authorLink); no code changes
required, just keep these symbols (MarketplaceModal, the button element, and
window.maestro.shell.openExternal) as-is.
🧹 Nitpick comments (3)
src/renderer/components/UsageDashboard/UsageDashboardModal.tsx (1)

278-303: Optional: use a viewModeRef to avoid listener re-registration on every tab switch.

viewMode in the dep array re-registers the global keydown listener on each tab change. The file already applies this pattern for onClose (lines 169–170); the same approach works here.

♻️ Proposed refactor
+	const viewModeRef = useRef(viewMode);
+	viewModeRef.current = viewMode;
+
 	useEffect(() => {
 		if (!isOpen) return;

 		const handleKeyDown = (e: KeyboardEvent) => {
 			if (e.metaKey && e.shiftKey && (e.key === '[' || e.key === ']')) {
 				e.preventDefault();
 				e.stopPropagation();

-				const currentIndex = VIEW_MODE_TABS.findIndex((tab) => tab.value === viewMode);
+				const currentIndex = VIEW_MODE_TABS.findIndex((tab) => tab.value === viewModeRef.current);
 				...
 			}
 		};

 		window.addEventListener('keydown', handleKeyDown, true);
 		return () => window.removeEventListener('keydown', handleKeyDown, true);
-	}, [isOpen, switchViewMode, viewMode]);
+	}, [isOpen, switchViewMode]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/UsageDashboard/UsageDashboardModal.tsx` around lines
278 - 303, The keydown listener currently re-registers whenever viewMode
changes; create a ref to hold the latest viewMode (e.g., viewModeRef =
useRef(viewMode)), update viewModeRef.current in a small useEffect that runs on
viewMode changes, and modify the useEffect that registers handleKeyDown to read
viewMode from viewModeRef.current (instead of the viewMode state) so the
listener is only added/removed when isOpen (and stable switchViewMode) change;
keep references to VIEW_MODE_TABS, handleKeyDown, and switchViewMode intact but
remove viewMode from the dependency array of the registration effect.
src/renderer/components/TerminalOutput.tsx (2)

1834-1834: Add outline-none to the auto-scroll button per renderer component guidelines.

The button's className string does not include outline-none, leaving the default browser focus ring visible inconsistently with the rest of the UI.

♻️ Proposed fix
- className={`absolute bottom-4 ${userMessageAlignment === 'right' ? 'left-6' : 'right-6'} flex items-center gap-2 px-3 py-2 rounded-full shadow-lg transition-all hover:scale-105 z-20`}
+ className={`absolute bottom-4 ${userMessageAlignment === 'right' ? 'left-6' : 'right-6'} flex items-center gap-2 px-3 py-2 rounded-full shadow-lg transition-all hover:scale-105 z-20 outline-none`}

Based on learnings: "Add tabIndex={0} or tabIndex={-1} and outline-none class to ensure focus works correctly" for src/renderer/**/*.{tsx,jsx}.

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

In `@src/renderer/components/TerminalOutput.tsx` at line 1834, The auto-scroll
button in TerminalOutput (the element with the long className containing
"absolute bottom-4 ... flex items-center ...") is missing the outline-none
utility; update that element's className to include "outline-none" (and if not
already present, add tabIndex={0} or tabIndex={-1} as per renderer guidelines)
so the browser focus ring is suppressed and keyboard focus behavior remains
correct; look for the element in TerminalOutput.tsx where the className equals
`absolute bottom-4 ${userMessageAlignment === 'right' ? 'left-6' : 'right-6'}
flex items-center gap-2 px-3 py-2 rounded-full shadow-lg transition-all
hover:scale-105 z-20` and add the changes there.

432-449: role="button" on <img> — prefer a <button> wrapper for cleaner semantics.

All required contracts (focus, keyboard activation, tabIndex) are correctly applied, so this is functional. But wrapping in a <button> avoids the semantic mismatch and removes the need for role="button" + manual key handler.

♻️ Proposed `` wrapper approach
 {log.images.map((img, imgIdx) => (
-  <img
-    key={img}
-    src={img}
-    alt={`Terminal output image ${imgIdx + 1}`}
-    className="h-20 rounded border cursor-zoom-in shrink-0 outline-none focus:ring-2 focus:ring-accent"
-    style={{ objectFit: 'contain', maxWidth: '200px' }}
-    role="button"
-    tabIndex={0}
-    onClick={() => setLightboxImage(img, log.images, 'history')}
-    onKeyDown={(e) => {
-      if (e.key === 'Enter' || e.key === ' ') {
-        e.preventDefault();
-        setLightboxImage(img, log.images, 'history');
-      }
-    }}
-  />
+  <button
+    key={`${img}-${imgIdx}`}
+    type="button"
+    className="shrink-0 outline-none focus:ring-2 focus:ring-accent rounded border cursor-zoom-in p-0"
+    onClick={() => setLightboxImage(img, log.images, 'history')}
+  >
+    <img
+      src={img}
+      alt={`Terminal output image ${imgIdx + 1}`}
+      className="h-20 rounded"
+      style={{ objectFit: 'contain', maxWidth: '200px' }}
+    />
+  </button>
 ))}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/TerminalOutput.tsx` around lines 432 - 449, The <img>
elements inside the log.images map are using role="button", tabIndex and a
manual onKeyDown handler; replace this with a semantic <button> wrapper (e.g.,
wrap the <img> in a <button type="button">) and move the onClick handler to the
button so keyboard activation is handled natively; remove role="button",
tabIndex and the onKeyDown from the <img>, keep the alt text on the <img>,
preserve styling (transfer interactive classes to the button or adjust CSS) and
still call setLightboxImage(img, log.images, 'history') from the button's
onClick; update code in TerminalOutput.tsx where log.images.map and
setLightboxImage are used.
🤖 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/InlineWizard/StreamingDocumentPreview.tsx`:
- Around line 219-235: In StreamingDocumentPreview's custom renderer for links
(the a: ({ href, children }: any) => ... function) add explicit keyboard-focus
affordances: set tabIndex={0} on the <a> element, add the "outline-none"
className (or style outline: 'none') so the element meets the renderer focus
guideline, and—if the link should be auto-focused when mounted—add ref={(el) =>
el?.focus()}. Also mirror tabIndex and outline-none on the fallback <span> used
when href is falsy so non-link items are keyboard-focusable if intended.
- Around line 102-113: The current useMemo block mutates refs
(lastFilenameRef.current and userScrolledRef.current) which is a side effect;
replace that useMemo with a useEffect (or useLayoutEffect if you need to run
before paint) that depends on filename, move the logic that compares filename
and sets lastFilenameRef.current and userScrolledRef.current into that effect,
and keep updateUserScrolled and setScrollRenderTick unchanged; ensure the effect
uses filename and updates the refs exactly as the original code did.

In `@src/renderer/components/Settings/SshRemoteModal.tsx`:
- Around line 572-580: The container in SshRemoteModal currently uses
role="listbox" but the host entries are plain <button>s without role="option" or
aria-selected; update the host-rendering code (where host buttons are created
inside SshRemoteModal — the map/render function for hosts, and any helper like
handleDropdownKeyDown) to add role="option" and set
aria-selected={selectedHostId === host.id} (or equivalent boolean), and ensure
your keyboard handlers (handleDropdownKeyDown / selection state) update
selectedHostId so aria-selected reflects the keyboard focus/selection and the
visual highlight matches; alternatively, if you prefer the menu pattern, change
the container to role="menu" and each host button to role="menuitem" instead
(keep keyboard behavior consistent with the chosen pattern).

In `@src/renderer/components/TerminalOutput.tsx`:
- Around line 1619-1620: The aria-label "Terminal output" on the TerminalOutput
component is ambiguous when multiple panes exist; update the region label to
include a session-specific identifier (for example use the session.name or
session.id or the tab title) so each instance is unique: locate the
TerminalOutput JSX where role="region" and aria-label="Terminal output" are set
and change aria-label to a template that falls back gracefully (e.g., `Terminal
output — ${session?.name ?? 'session ' + session?.id ?? index}`) to ensure
screen readers can distinguish concurrent terminals.
- Around line 434-437: Replace the unstable key and the non-existent Tailwind
ring color: change the list key from key={img} to key={`${img}-${imgIdx}`}
(references: img, imgIdx, log.images) to avoid duplicate-key warnings; replace
the static class focus:ring-accent with the generic focus ring classes (e.g.,
keep focus:ring and focus:ring-offset-0 if present) and add an inline style that
sets the CSS variable --tw-ring-color to theme.colors.accent (style={{
['--tw-ring-color' as any]: theme.colors.accent }}) so the focus ring uses the
dynamic accent color (references: className, theme.colors.accent, TerminalOutput
component).

In `@src/renderer/components/UsageDashboard/UsageDashboardModal.tsx`:
- Around line 493-500: The backdrop button in UsageDashboardModal is missing the
required focus style suppression; update the invisible backdrop <button> (the
element using tabIndex={-1} and onClick={onClose} inside the UsageDashboardModal
component) to include the "outline-none" class so it never shows a focus ring
when clicked, and optionally add aria-hidden="true" to prevent it being exposed
to assistive tech (keep tabIndex and onClick as-is). Ensure the change targets
the same button element that sits as the absolute inset-0 overlay.

---

Outside diff comments:
In `@src/renderer/components/InputArea.tsx`:
- Around line 252-364: Replace the setTimeout-based autofocus useEffect that
focuses commandHistoryFilterRef when commandHistoryOpen is true with a ref
callback on the filter input that immediately focuses when the element mounts
(use the existing commandHistoryFilterRef ref logic but convert to a callback
ref), and remove the useEffect and setTimeout; also ensure the filter input
element has tabIndex={0} for keyboard accessibility so it can receive
programmatic focus (refer to commandHistoryFilterRef, commandHistoryOpen, the
old useEffect/setTimeout, and the filter input element).

---

Duplicate comments:
In `@src/renderer/components/MarketplaceModal.tsx`:
- Around line 418-426: The Author link button in MarketplaceModal is
correct—ensure the button in the MarketplaceModal component continues to use
tabIndex={0}, className including "outline-none", and the onClick handler
window.maestro.shell.openExternal with the non-null asserted
playbook.authorLink! (this is safe because the button is rendered inside a guard
that checks playbook.authorLink); no code changes required, just keep these
symbols (MarketplaceModal, the button element, and
window.maestro.shell.openExternal) as-is.

---

Nitpick comments:
In `@src/renderer/components/TerminalOutput.tsx`:
- Line 1834: The auto-scroll button in TerminalOutput (the element with the long
className containing "absolute bottom-4 ... flex items-center ...") is missing
the outline-none utility; update that element's className to include
"outline-none" (and if not already present, add tabIndex={0} or tabIndex={-1} as
per renderer guidelines) so the browser focus ring is suppressed and keyboard
focus behavior remains correct; look for the element in TerminalOutput.tsx where
the className equals `absolute bottom-4 ${userMessageAlignment === 'right' ?
'left-6' : 'right-6'} flex items-center gap-2 px-3 py-2 rounded-full shadow-lg
transition-all hover:scale-105 z-20` and add the changes there.
- Around line 432-449: The <img> elements inside the log.images map are using
role="button", tabIndex and a manual onKeyDown handler; replace this with a
semantic <button> wrapper (e.g., wrap the <img> in a <button type="button">) and
move the onClick handler to the button so keyboard activation is handled
natively; remove role="button", tabIndex and the onKeyDown from the <img>, keep
the alt text on the <img>, preserve styling (transfer interactive classes to the
button or adjust CSS) and still call setLightboxImage(img, log.images,
'history') from the button's onClick; update code in TerminalOutput.tsx where
log.images.map and setLightboxImage are used.

In `@src/renderer/components/UsageDashboard/UsageDashboardModal.tsx`:
- Around line 278-303: The keydown listener currently re-registers whenever
viewMode changes; create a ref to hold the latest viewMode (e.g., viewModeRef =
useRef(viewMode)), update viewModeRef.current in a small useEffect that runs on
viewMode changes, and modify the useEffect that registers handleKeyDown to read
viewMode from viewModeRef.current (instead of the viewMode state) so the
listener is only added/removed when isOpen (and stable switchViewMode) change;
keep references to VIEW_MODE_TABS, handleKeyDown, and switchViewMode intact but
remove viewMode from the dependency array of the registration effect.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 12a3431 and a8b3a4f.

📒 Files selected for processing (9)
  • src/renderer/components/DocumentsPanel.tsx
  • src/renderer/components/InlineWizard/StreamingDocumentPreview.tsx
  • src/renderer/components/InputArea.tsx
  • src/renderer/components/MarketplaceModal.tsx
  • src/renderer/components/MergeSessionModal.tsx
  • src/renderer/components/Settings/SshRemoteModal.tsx
  • src/renderer/components/TerminalOutput.tsx
  • src/renderer/components/UsageDashboard/UsageDashboardModal.tsx
  • src/web/mobile/CommandHistoryDrawer.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/renderer/components/MergeSessionModal.tsx
  • src/web/mobile/CommandHistoryDrawer.tsx

Comment on lines +102 to +113
useMemo(() => {
if (lastFilenameRef.current !== (filename ?? '')) {
lastFilenameRef.current = filename ?? '';
userScrolledRef.current = false;
}
}, [filename]);

const updateUserScrolled = (next: boolean) => {
if (userScrolledRef.current !== next) {
userScrolledRef.current = next;
setScrollRenderTick((tick) => tick + 1);
}
Copy link

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:

cat -n src/renderer/components/InlineWizard/StreamingDocumentPreview.tsx | sed -n '95,125p'

Repository: RunMaestro/Maestro

Length of output: 1307


🌐 Web query:

React useMemo side effects mutating refs should use useEffect instead concurrent mode

💡 Result:

useMemo runs during render, so its callback must be pure (no observable side effects). React may render more than once (or start rendering and abandon it) to support concurrency, so side effects in render can run extra times and/or at the wrong moment. React’s docs explicitly say side effects must run outside of render. [1]

That’s why mutating ref.current inside useMemo is a side effect and is not “concurrent-safe” in general. In Strict Mode (development), React may intentionally double-invoke functions that should be pure—including the functions you pass to useMemo—to surface these bugs. [2]

What to do instead:

  • If you’re doing this just to “compute once” but also mutate something: don’t—useMemo is for computing/caching a value, not effects. [3]
  • If you need to perform something when inputs change (including writing to refs, subscribing, logging, calling imperative APIs): move it to useEffect (or useLayoutEffect if it must happen before paint). This keeps render pure. [1][4]

Rule of thumb: useMemo returns a value; useEffect performs work. If your useMemo callback “does” something (mutates refs, touches globals/DOM, triggers I/O), it belongs in an effect. [1][3]


Replace side-effectful useMemo with useEffect.

useMemo callbacks must be pure and free of side effects, but this code mutates refs (lastFilenameRef.current, userScrolledRef.current). React's documentation explicitly requires side effects to run in useEffect or useLayoutEffect, not during render. In Strict Mode, React intentionally double-invokes the callback to detect impurity, which would cause the refs to be reset twice. Move this logic to useEffect:

Proposed fix
-useMemo(() => {
-	if (lastFilenameRef.current !== (filename ?? '')) {
-		lastFilenameRef.current = filename ?? '';
-		userScrolledRef.current = false;
-	}
-}, [filename]);
+useEffect(() => {
+	if (lastFilenameRef.current !== (filename ?? '')) {
+		lastFilenameRef.current = filename ?? '';
+		userScrolledRef.current = false;
+	}
+}, [filename]);
📝 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
useMemo(() => {
if (lastFilenameRef.current !== (filename ?? '')) {
lastFilenameRef.current = filename ?? '';
userScrolledRef.current = false;
}
}, [filename]);
const updateUserScrolled = (next: boolean) => {
if (userScrolledRef.current !== next) {
userScrolledRef.current = next;
setScrollRenderTick((tick) => tick + 1);
}
useEffect(() => {
if (lastFilenameRef.current !== (filename ?? '')) {
lastFilenameRef.current = filename ?? '';
userScrolledRef.current = false;
}
}, [filename]);
const updateUserScrolled = (next: boolean) => {
if (userScrolledRef.current !== next) {
userScrolledRef.current = next;
setScrollRenderTick((tick) => tick + 1);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/InlineWizard/StreamingDocumentPreview.tsx` around
lines 102 - 113, The current useMemo block mutates refs (lastFilenameRef.current
and userScrolledRef.current) which is a side effect; replace that useMemo with a
useEffect (or useLayoutEffect if you need to run before paint) that depends on
filename, move the logic that compares filename and sets lastFilenameRef.current
and userScrolledRef.current into that effect, and keep updateUserScrolled and
setScrollRenderTick unchanged; ensure the effect uses filename and updates the
refs exactly as the original code did.

Comment on lines +219 to +235
a: ({ href, children }: any) =>
href ? (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
style={{
color: theme.colors.accent,
textDecoration: 'underline',
cursor: 'pointer',
}}
>
{children}
</a>
) : (
<span style={{ color: theme.colors.accent, textDecoration: 'underline' }}>
{children}
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

Add focus affordances to markdown links.

The new anchor is focusable but missing tabIndex and outline-none, which are required by the renderer focus guideline. Consider adding them to the <a> element.

Proposed fix
<a
	href={href}
	target="_blank"
	rel="noopener noreferrer"
+	tabIndex={0}
+	className="outline-none"
	style={{
		color: theme.colors.accent,
		textDecoration: 'underline',
		cursor: 'pointer',
	}}
>

As per coding guidelines, "Add tabIndex={0} or tabIndex={-1} and outline-none class to ensure focus works correctly. Use ref={(el) => el?.focus()} for auto-focus in React components."

📝 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
a: ({ href, children }: any) =>
href ? (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
style={{
color: theme.colors.accent,
textDecoration: 'underline',
cursor: 'pointer',
}}
>
{children}
</a>
) : (
<span style={{ color: theme.colors.accent, textDecoration: 'underline' }}>
{children}
a: ({ href, children }: any) =>
href ? (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
tabIndex={0}
className="outline-none"
style={{
color: theme.colors.accent,
textDecoration: 'underline',
cursor: 'pointer',
}}
>
{children}
</a>
) : (
<span style={{ color: theme.colors.accent, textDecoration: 'underline' }}>
{children}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/InlineWizard/StreamingDocumentPreview.tsx` around
lines 219 - 235, In StreamingDocumentPreview's custom renderer for links (the a:
({ href, children }: any) => ... function) add explicit keyboard-focus
affordances: set tabIndex={0} on the <a> element, add the "outline-none"
className (or style outline: 'none') so the element meets the renderer focus
guideline, and—if the link should be auto-focused when mounted—add ref={(el) =>
el?.focus()}. Also mirror tabIndex and outline-none on the fallback <span> used
when href is falsy so non-link items are keyboard-focusable if intended.

Comment on lines +572 to +580
className="absolute top-full left-0 right-0 mt-1 rounded border shadow-lg z-10 outline-none"
style={{
backgroundColor: theme.colors.bgMain,
borderColor: theme.colors.border,
}}
onKeyDown={handleDropdownKeyDown}
role="listbox"
aria-label="SSH config hosts"
tabIndex={0}
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

role="listbox" requires role="option" children — ARIA semantic mismatch.

The WAI-ARIA listbox pattern requires each child interactive element to carry role="option" (and aria-selected). The host items are plain <button> elements with neither attribute, so screen readers navigating the "listbox" will find no options and behave unpredictably.

Two conformant paths:

Option A — fix to the listbox pattern (keeps keyboard highlight semantics):

♿ Proposed fix – add role="option" + aria-selected to each host button
-								role="listbox"
-								aria-label="SSH config hosts"
+								role="listbox"
+								aria-label="SSH config hosts"
+								aria-activedescendant={
+									filteredSshConfigHosts[sshConfigHighlightIndex]
+										? `ssh-host-${filteredSshConfigHosts[sshConfigHighlightIndex].host}`
+										: undefined
+								}

And on each host <button>:

 								<button
 									key={configHost.host}
 									type="button"
+									id={`ssh-host-${configHost.host}`}
+									role="option"
+									aria-selected={index === sshConfigHighlightIndex}
 									onClick={() => handleSelectSshConfigHost(configHost)}

Option B — switch to a menu pattern (simpler, buttons stay buttons):

♿ Proposed fix – use role="menu" / role="menuitem"
-								role="listbox"
-								aria-label="SSH config hosts"
+								role="menu"
+								aria-label="SSH config hosts"
 								<button
 									key={configHost.host}
 									type="button"
+									role="menuitem"
 									onClick={() => handleSelectSshConfigHost(configHost)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/Settings/SshRemoteModal.tsx` around lines 572 - 580,
The container in SshRemoteModal currently uses role="listbox" but the host
entries are plain <button>s without role="option" or aria-selected; update the
host-rendering code (where host buttons are created inside SshRemoteModal — the
map/render function for hosts, and any helper like handleDropdownKeyDown) to add
role="option" and set aria-selected={selectedHostId === host.id} (or equivalent
boolean), and ensure your keyboard handlers (handleDropdownKeyDown / selection
state) update selectedHostId so aria-selected reflects the keyboard
focus/selection and the visual highlight matches; alternatively, if you prefer
the menu pattern, change the container to role="menu" and each host button to
role="menuitem" instead (keep keyboard behavior consistent with the chosen
pattern).

Comment on lines 434 to 437
key={img}
src={img}
className="h-20 rounded border cursor-zoom-in shrink-0"
alt={`Terminal output image ${imgIdx + 1}`}
className="h-20 rounded border cursor-zoom-in shrink-0 outline-none focus:ring-2 focus:ring-accent"
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:

# Check the actual code in TerminalOutput.tsx around the mentioned lines
cat -n src/renderer/components/TerminalOutput.tsx | sed -n '430,440p'

Repository: RunMaestro/Maestro

Length of output: 556


🏁 Script executed:

# Check if tailwind.config.js or tailwind.config.ts exists and examine accent color configuration
find . -maxdepth 2 -name "tailwind.config.*" -type f | head -5

Repository: RunMaestro/Maestro

Length of output: 83


🏁 Script executed:

# Search for how accent colors are used elsewhere in the renderer components
rg -t tsx "theme\.colors\.accent|focus:ring-accent" src/renderer --max-count 20

Repository: RunMaestro/Maestro

Length of output: 89


🏁 Script executed:

cat -n tailwind.config.mjs

Repository: RunMaestro/Maestro

Length of output: 490


🏁 Script executed:

# Search for how accent is used in TerminalOutput or similar components
rg "theme\.colors\.accent|accent" src/renderer/components/TerminalOutput.tsx -A 2 -B 2

Repository: RunMaestro/Maestro

Length of output: 5010


🏁 Script executed:

# Search more broadly for accent color usage with inline styles
rg "backgroundColor.*accent|color.*accent|theme\.colors" src/renderer/components --max-count 30

Repository: RunMaestro/Maestro

Length of output: 50375


Two concerns: key={img} and focus:ring-accent.

  1. key={img} — using the image URL as the key can produce duplicate-key warnings if log.images contains the same URL more than once. key={${img}-${imgIdx}} is safe either way.

  2. focus:ring-accent — this is a static Tailwind utility that requires accent to be registered as a Tailwind ring color. The tailwind.config.mjs does not extend ring colors, so this will silently do nothing. The component uses dynamic theme.colors.accent via inline style throughout, but the focus ring needs to use the same pattern: inline style={{ ['--tw-ring-color' as any]: theme.colors.accent }} with Tailwind's base focus ring classes instead.

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

In `@src/renderer/components/TerminalOutput.tsx` around lines 434 - 437, Replace
the unstable key and the non-existent Tailwind ring color: change the list key
from key={img} to key={`${img}-${imgIdx}`} (references: img, imgIdx, log.images)
to avoid duplicate-key warnings; replace the static class focus:ring-accent with
the generic focus ring classes (e.g., keep focus:ring and focus:ring-offset-0 if
present) and add an inline style that sets the CSS variable --tw-ring-color to
theme.colors.accent (style={{ ['--tw-ring-color' as any]: theme.colors.accent
}}) so the focus ring uses the dynamic accent color (references: className,
theme.colors.accent, TerminalOutput component).

Comment on lines +1619 to +1620
role="region"
aria-label="Terminal output"
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

aria-label="Terminal output" becomes ambiguous with multiple concurrent instances.

Good landmark addition, but if multiple TerminalOutput panes are visible simultaneously (split-pane), screen readers encounter two identical "Terminal output" regions. Consider incorporating a session-level identifier (e.g., the tab name or session.name) to disambiguate.

💡 Example disambiguation
- aria-label="Terminal output"
+ aria-label={`Terminal output${activeTab?.name ? ` — ${activeTab.name}` : ''}`}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/TerminalOutput.tsx` around lines 1619 - 1620, The
aria-label "Terminal output" on the TerminalOutput component is ambiguous when
multiple panes exist; update the region label to include a session-specific
identifier (for example use the session.name or session.id or the tab title) so
each instance is unique: locate the TerminalOutput JSX where role="region" and
aria-label="Terminal output" are set and change aria-label to a template that
falls back gracefully (e.g., `Terminal output — ${session?.name ?? 'session ' +
session?.id ?? index}`) to ensure screen readers can distinguish concurrent
terminals.

Comment on lines 493 to 500
<div className="fixed inset-0 modal-overlay flex items-center justify-center z-[9999] animate-in fade-in duration-100">
<button
type="button"
className="absolute inset-0"
tabIndex={-1}
onClick={onClose}
aria-label="Close usage dashboard"
/>
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

Missing outline-none on the backdrop button — guideline violation.

Without outline-none, clicking the backdrop can briefly render a focus outline on the invisible button in some browsers/OS themes. The coding guidelines require both tabIndex and outline-none on interactive renderer elements.

🛠️ Proposed fix
 		<button
 			type="button"
-			className="absolute inset-0"
+			className="absolute inset-0 outline-none"
 			tabIndex={-1}
 			onClick={onClose}
 			aria-label="Close usage dashboard"
 		/>

Optionally, since aria-modal="true" on the dialog already restricts screen-reader virtual cursor navigation, adding aria-hidden="true" to this invisible button would prevent it from being surfaced by assistive technologies at all.

📝 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
<div className="fixed inset-0 modal-overlay flex items-center justify-center z-[9999] animate-in fade-in duration-100">
<button
type="button"
className="absolute inset-0"
tabIndex={-1}
onClick={onClose}
aria-label="Close usage dashboard"
/>
<button
type="button"
className="absolute inset-0 outline-none"
tabIndex={-1}
onClick={onClose}
aria-label="Close usage dashboard"
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/UsageDashboard/UsageDashboardModal.tsx` around lines
493 - 500, The backdrop button in UsageDashboardModal is missing the required
focus style suppression; update the invisible backdrop <button> (the element
using tabIndex={-1} and onClick={onClose} inside the UsageDashboardModal
component) to include the "outline-none" class so it never shows a focus ring
when clicked, and optionally add aria-hidden="true" to prevent it being exposed
to assistive tech (keep tabIndex and onClick as-is). Ensure the change targets
the same button element that sits as the absolute inset-0 overlay.

- InputArea: replace useEffect+setTimeout autofocus with ref callback on
  command history filter input; add tabIndex={0} per focus guidelines
- TerminalOutput: wrap log images in <button> for proper ARIA semantics
  (fixes getByRole('img') test queries); add outline-none to auto-scroll
  button per renderer focus guidelines
- UsageDashboardModal: use viewModeRef to avoid re-registering keydown
  listener on every tab switch (matches existing onCloseRef pattern)
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.

♻️ Duplicate comments (3)
src/renderer/components/UsageDashboard/UsageDashboardModal.tsx (1)

495-510: ⚠️ Potential issue | 🟡 Minor

Add outline-none to the backdrop button.

This keeps focus styling consistent with renderer focus guidelines and avoids a visible outline flash on click.

🛠️ Proposed fix
 		<button
 			type="button"
-			className="absolute inset-0"
+			className="absolute inset-0 outline-none"
 			tabIndex={-1}
 			onClick={onClose}
 			aria-label="Close usage dashboard"
 		/>
As per coding guidelines, `src/renderer/**/*.{tsx,jsx}: Add tabIndex={0} or tabIndex={-1} and outline-none class to ensure focus works correctly. Use ref={(el) => el?.focus()} for auto-focus in React components.`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/UsageDashboard/UsageDashboardModal.tsx` around lines
495 - 510, The backdrop button inside UsageDashboardModal (the <button
type="button" className="absolute inset-0" tabIndex={-1} onClick={onClose}
aria-label="Close usage dashboard" />) needs the renderer focus styling applied:
add the outline-none class to its className (and keep tabIndex={-1}) so
clicks/focus don’t show a visible outline; follow the component guideline for
focusable elements in UsageDashboardModal and apply similar outline-none to
other backdrop buttons if present.
src/renderer/components/TerminalOutput.tsx (2)

1616-1617: aria-label="Terminal output" remains ambiguous with multiple concurrent instances.

This was flagged in the prior review. If multiple TerminalOutput panes are visible simultaneously, screen readers encounter identical region labels. Consider incorporating a session/tab identifier.

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

In `@src/renderer/components/TerminalOutput.tsx` around lines 1616 - 1617, The
aria-label "Terminal output" on the TerminalOutput component is ambiguous when
multiple panes exist; update the component to append a unique session/tab
identifier (e.g., a prop like sessionId, tabId or index) to the aria-label (or
switch to aria-labelledby referencing a unique header id) so each role="region"
reads distinctly (update TerminalOutput props and where it is rendered to pass
the identifier and use it in aria-label/aria-labelledby).

433-445: focus:ring-accent still won't resolve — same issue flagged in the previous review.

The duplicate key concern is now resolved with key={${img}-${imgIdx}}. However, focus:ring-accent on line 436 still uses a Tailwind utility that requires accent to be registered as a ring color in your Tailwind config. Use an inline style for --tw-ring-color as suggested in the prior review.

Proposed fix
-								className="shrink-0 p-0 bg-transparent outline-none focus:ring-2 focus:ring-accent rounded"
+								className="shrink-0 p-0 bg-transparent outline-none focus:ring-2 rounded"
+								style={{ ['--tw-ring-color' as any]: theme.colors.accent }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/TerminalOutput.tsx` around lines 433 - 445, The
focus:ring-accent utility is invalid because "accent" isn't registered in
Tailwind; update the button used to open the lightbox (the element that calls
setLightboxImage) to remove the class "focus:ring-accent" and instead set the
ring color via an inline CSS variable for Tailwind rings (e.g. add a style prop
that sets --tw-ring-color to the desired accent color, merging with any existing
style), keeping the rest of the classes (focus:ring-2, rounded, etc.) intact so
the focus ring still appears with the correct color.
🧹 Nitpick comments (1)
src/renderer/components/InputArea.tsx (1)

465-494: Inconsistent image-click pattern: role="button" on <img> vs. <button> wrapper in TerminalOutput.

In TerminalOutput.tsx (lines 433–445), the same type of clickable image is wrapped in a <button> element. Here, the <img> itself carries role="button" + tabIndex={0} + onKeyDown. Using a real <button> wrapper is preferred for native keyboard/activation semantics and is consistent with the approach taken in TerminalOutput.

Additionally, focus-visible:ring-accent (line 469) likely resolves to nothing because accent isn't registered as a Tailwind ring color in the config. The past review on TerminalOutput flagged the same issue. Use an inline style for --tw-ring-color like is done elsewhere.

Proposed fix: wrap img in button, use inline ring color
-						<div key={img} className="relative group shrink-0">
-							<img
-								src={img}
-								alt={`Staged image ${idx + 1}`}
-								className="h-16 rounded border cursor-pointer hover:opacity-80 transition-opacity outline-none focus-visible:ring-2 focus-visible:ring-accent"
-								style={{
-									borderColor: theme.colors.border,
-									objectFit: 'contain',
-									maxWidth: '200px',
-								}}
-								role="button"
-								tabIndex={0}
-								onClick={() => setLightboxImage(img, stagedImages, 'staged')}
-								onKeyDown={(e) => {
-									if (e.key === 'Enter' || e.key === ' ') {
-										e.preventDefault();
-										setLightboxImage(img, stagedImages, 'staged');
-									}
-								}}
-							/>
+						<div key={img} className="relative group shrink-0">
+							<button
+								type="button"
+								className="shrink-0 p-0 bg-transparent outline-none focus-visible:ring-2 rounded"
+								style={{ ['--tw-ring-color' as any]: theme.colors.accent }}
+								onClick={() => setLightboxImage(img, stagedImages, 'staged')}
+							>
+								<img
+									src={img}
+									alt={`Staged image ${idx + 1}`}
+									className="h-16 rounded border cursor-pointer hover:opacity-80 transition-opacity block"
+									style={{
+										borderColor: theme.colors.border,
+										objectFit: 'contain',
+										maxWidth: '200px',
+									}}
+								/>
+							</button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/InputArea.tsx` around lines 465 - 494, Wrap the
staged image in a real <button> (like TerminalOutput) and move keyboard/click
handlers from the <img> to that button: call setLightboxImage on button onClick
and onKeyDown (or rely on button default activation), remove role="button" and
tabIndex from the <img>, and keep the image purely presentational
(objectFit/maxWidth styles remain). Make the focus ring use an inline CSS
variable for --tw-ring-color set to theme.colors.accent (i.e., keep
focus-visible:ring but set --tw-ring-color via the button's style) so Tailwind
picks up the correct color. Ensure the existing remove button still stops
propagation and setStagedImages behavior is unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/renderer/components/TerminalOutput.tsx`:
- Around line 1616-1617: The aria-label "Terminal output" on the TerminalOutput
component is ambiguous when multiple panes exist; update the component to append
a unique session/tab identifier (e.g., a prop like sessionId, tabId or index) to
the aria-label (or switch to aria-labelledby referencing a unique header id) so
each role="region" reads distinctly (update TerminalOutput props and where it is
rendered to pass the identifier and use it in aria-label/aria-labelledby).
- Around line 433-445: The focus:ring-accent utility is invalid because "accent"
isn't registered in Tailwind; update the button used to open the lightbox (the
element that calls setLightboxImage) to remove the class "focus:ring-accent" and
instead set the ring color via an inline CSS variable for Tailwind rings (e.g.
add a style prop that sets --tw-ring-color to the desired accent color, merging
with any existing style), keeping the rest of the classes (focus:ring-2,
rounded, etc.) intact so the focus ring still appears with the correct color.

In `@src/renderer/components/UsageDashboard/UsageDashboardModal.tsx`:
- Around line 495-510: The backdrop button inside UsageDashboardModal (the
<button type="button" className="absolute inset-0" tabIndex={-1}
onClick={onClose} aria-label="Close usage dashboard" />) needs the renderer
focus styling applied: add the outline-none class to its className (and keep
tabIndex={-1}) so clicks/focus don’t show a visible outline; follow the
component guideline for focusable elements in UsageDashboardModal and apply
similar outline-none to other backdrop buttons if present.

---

Nitpick comments:
In `@src/renderer/components/InputArea.tsx`:
- Around line 465-494: Wrap the staged image in a real <button> (like
TerminalOutput) and move keyboard/click handlers from the <img> to that button:
call setLightboxImage on button onClick and onKeyDown (or rely on button default
activation), remove role="button" and tabIndex from the <img>, and keep the
image purely presentational (objectFit/maxWidth styles remain). Make the focus
ring use an inline CSS variable for --tw-ring-color set to theme.colors.accent
(i.e., keep focus-visible:ring but set --tw-ring-color via the button's style)
so Tailwind picks up the correct color. Ensure the existing remove button still
stops propagation and setStagedImages behavior is unchanged.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a8b3a4f and 7fb9e81.

📒 Files selected for processing (3)
  • src/renderer/components/InputArea.tsx
  • src/renderer/components/TerminalOutput.tsx
  • src/renderer/components/UsageDashboard/UsageDashboardModal.tsx

…ixes

Resolved merge conflict in TerminalOutput.tsx (kept outline-none on auto-scroll button).

Fixed 18 pre-existing test failures (9 from branch, 9 from main):
- LightboxModal: add onClick stopPropagation to img
- PromptComposerModal: add onClick to outer div, change onMouseDown to onClick
- UsageDashboardModal: add onClick to outer div, add stopPropagation to dialog
- DocumentsPanel: add onClick to outer div, add stopPropagation to inner modal
- InputArea/WizardInputPanel: replace img[role=button] with button>img wrapper
- GitStatusWidget: aria-hidden on compact span; tests use getAllByText for duplicates
- MainPanel: aria-hidden on compact Context span; tests use getByText('Context Window')
- InputArea.test: fix tab completion to use closest('button') not closest('div')
- WizardConversationView.test: advance fake timers before user scroll dispatch
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: 2

Caution

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

⚠️ Outside diff range comments (4)
src/renderer/components/InputArea.tsx (4)

507-533: ⚠️ Potential issue | 🟡 Minor

Add focus styling + tabIndex for slash command items.

These buttons are now focusable; add explicit tabIndex and outline-none/focus-visible styles to meet renderer focus rules.

♻️ Suggested fix
 						<button
 							type="button"
+							tabIndex={0}
 							key={cmd.command}
 							ref={(el) => (slashCommandItemRefs.current[idx] = el)}
-							className={`w-full px-4 py-3 text-left transition-colors ${
+							className={`w-full px-4 py-3 text-left transition-colors outline-none focus-visible:ring-2 focus-visible:ring-accent ${
 								idx === safeSelectedIndex ? 'font-semibold' : ''
 							}`}
As per coding guidelines: `src/renderer/**/*.{tsx,jsx}`: Add tabIndex={0} or tabIndex={-1} and outline-none class to ensure focus works correctly. Use ref={(el) => el?.focus()} for auto-focus in React components.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/InputArea.tsx` around lines 507 - 533, The slash
command buttons need explicit keyboard focus support: add a tabIndex prop (e.g.,
tabIndex={idx === safeSelectedIndex ? 0 : -1}) and include focus styling classes
like "outline-none focus-visible:ring" (or "focus-visible" plus your theme
class) on the button rendered in the slash command list; also update the ref
callback used in slashCommandItemRefs.current[idx] to call el?.focus() when idx
=== safeSelectedIndex so the active item receives auto-focus—keep the existing
handlers (setSelectedSlashCommandIndex, setInputValue, setSlashCommandOpen,
inputRef) intact.

591-617: ⚠️ Potential issue | 🟡 Minor

Add tabIndex + focus styles for command history items.

These new buttons should include explicit tabIndex and outline-none/focus-visible classes per renderer focus rules.

♻️ Suggested fix
 								<button
 									type="button"
+									tabIndex={0}
 									key={cmd}
-									className={`w-full px-3 py-2 text-left text-sm font-mono ${isSelected ? 'ring-1 ring-inset' : ''} ${isMostRecent ? 'font-semibold' : ''}`}
+									className={`w-full px-3 py-2 text-left text-sm font-mono outline-none focus-visible:ring-2 focus-visible:ring-accent ${isSelected ? 'ring-1 ring-inset' : ''} ${isMostRecent ? 'font-semibold' : ''}`}
As per coding guidelines: `src/renderer/**/*.{tsx,jsx}`: Add tabIndex={0} or tabIndex={-1} and outline-none class to ensure focus works correctly. Use ref={(el) => el?.focus()} for auto-focus in React components.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/InputArea.tsx` around lines 591 - 617, The command
history <button> items lack explicit keyboard focus behavior; update the button
rendered in InputArea.tsx (the element using setInputValue,
setCommandHistoryOpen, setCommandHistoryFilter, inputRef,
setCommandHistorySelectedIndex) to include tabIndex (use tabIndex={0} for
focusable items or tabIndex={-1} for non-focusable ones as appropriate), add
focus styling classes (e.g., outline-none and focus-visible or equivalent) to
the className string, and when an item becomes selected (isSelected) auto-focus
it by adding a ref={(el) => isSelected && el?.focus()} so keyboard users can
tab/arrow into entries properly. Ensure these changes keep the existing
onClick/onMouseEnter handlers and do not alter other logic.

710-747: ⚠️ Potential issue | 🟡 Minor

Add tabIndex + focus styles for tab completion items.

These buttons need explicit tabIndex and outline-none/focus-visible styles to meet the renderer focus guideline.

♻️ Suggested fix
 								<button
 									type="button"
+									tabIndex={0}
 									key={`${suggestion.type}-${suggestion.value}`}
 									ref={(el) => (tabCompletionItemRefs.current[idx] = el)}
-									className={`w-full px-3 py-2 text-left text-sm font-mono flex items-center gap-2 ${isSelected ? 'ring-1 ring-inset' : ''}`}
+									className={`w-full px-3 py-2 text-left text-sm font-mono flex items-center gap-2 outline-none focus-visible:ring-2 focus-visible:ring-accent ${isSelected ? 'ring-1 ring-inset' : ''}`}
As per coding guidelines: `src/renderer/**/*.{tsx,jsx}`: Add tabIndex={0} or tabIndex={-1} and outline-none class to ensure focus works correctly. Use ref={(el) => el?.focus()} for auto-focus in React components.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/InputArea.tsx` around lines 710 - 747, The tab
completion buttons need explicit keyboard focusability and focus styles: add
tabIndex={0} to the button, include outline-none and an appropriate
focus-visible class in the className string (e.g., focus-visible:ring or similar
using theme colors), and update the ref assignment in the existing ref={(el) =>
(tabCompletionItemRefs.current[idx] = el)} to also auto-focus the currently
selected item (e.g., ref={(el) => { tabCompletionItemRefs.current[idx] = el; if
(isSelected) el?.focus(); }}). Change the button rendering inside InputArea.tsx
(the button using tabCompletionItemRefs, setSelectedTabCompletionIndex,
setTabCompletionOpen, inputRef, IconComponent) to apply these changes so
keyboard tabbing and focus styles work correctly.

789-836: ⚠️ Potential issue | 🟡 Minor

Add tabIndex + focus styles for @-mention items.

Same focus guideline applies here; these buttons should include explicit tabIndex and outline-none/focus-visible classes.

♻️ Suggested fix
 								<button
 									type="button"
+									tabIndex={0}
 									key={`${suggestion.type}-${suggestion.value}`}
 									ref={(el) => (atMentionItemRefs.current[idx] = el)}
-									className={`w-full px-3 py-2 text-left text-sm font-mono flex items-center gap-2 ${isSelected ? 'ring-1 ring-inset' : ''}`}
+									className={`w-full px-3 py-2 text-left text-sm font-mono flex items-center gap-2 outline-none focus-visible:ring-2 focus-visible:ring-accent ${isSelected ? 'ring-1 ring-inset' : ''}`}
As per coding guidelines: `src/renderer/**/*.{tsx,jsx}`: Add tabIndex={0} or tabIndex={-1} and outline-none class to ensure focus works correctly. Use ref={(el) => el?.focus()} for auto-focus in React components.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/InputArea.tsx` around lines 789 - 836, The `@-mention`
suggestion buttons need explicit keyboard-focus support: add tabIndex={-1} to
the button, add the classes outline-none and focus-visible:ring (or appropriate
focus-visible styles) to the className, and update the ref callback
(atMentionItemRefs.current[idx]) to also call el?.focus() when isSelected is
true so keyboard navigation visually focuses the item; adjust
setSelectedAtMentionIndex usage as needed to ensure isSelected controls
programmatic focus. Ensure these changes are applied to the button in
InputArea.tsx that references atMentionItemRefs, setSelectedAtMentionIndex,
inputRef and the onClick/onMouseEnter handlers.
♻️ Duplicate comments (1)
src/renderer/components/UsageDashboard/UsageDashboardModal.tsx (1)

499-508: ⚠️ Potential issue | 🟡 Minor

Backdrop button still missing outline-none — guideline violation (unresolved from prior review).

The className on line 501 is "absolute inset-0" with no outline-none, so some browsers/OS themes will briefly render a focus ring on this invisible button when it receives focus after a click.

🛠️ Proposed fix
-			className="absolute inset-0"
+			className="absolute inset-0 outline-none"

Based on learnings: "Add tabIndex={0} or tabIndex={-1} and outline-none class to ensure focus works correctly."

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

In `@src/renderer/components/UsageDashboard/UsageDashboardModal.tsx` around lines
499 - 508, The invisible backdrop button inside the UsageDashboardModal
component currently has className="absolute inset-0" and can show a focus ring
on some browsers; update that button's className to include the Tailwind utility
to suppress focus outlines (e.g., add "outline-none" or "focus:outline-none") so
it becomes "absolute inset-0 outline-none" (or include both "outline-none
focus:outline-none" if desired); this change should be applied to the button
element in UsageDashboardModal where tabIndex={-1} and onClick calls onClose().
🧹 Nitpick comments (4)
src/main/ipc/handlers/filesystem.ts (1)

288-305: Asymmetric binary support: local writes only accept strings while remote writes support binary.

Verification of all call sites confirms callers only pass text (content is string in MainPanel.tsx, UsageDashboardModal.tsx, and SaveMarkdownModal.tsx). However, the local write path forces UTF-8 encoding while the remote writeFileRemote() already supports string | Buffer with proper base64 encoding. To maintain consistency and support future binary write use cases, widen the IPC handler type:

Suggested fix
-		async (_, filePath: string, content: string, sshRemoteId?: string) => {
+		async (_, filePath: string, content: string | Buffer, sshRemoteId?: string) => {
 			// ...
-			await fs.writeFile(filePath, content, 'utf-8');
+			if (typeof content === 'string') {
+				await fs.writeFile(filePath, content, 'utf-8');
+			} else {
+				await fs.writeFile(filePath, content);
+			}
 			return { success: true };
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/filesystem.ts` around lines 288 - 305, The IPC handler
currently types content as string and always calls fs.writeFile with 'utf-8',
causing asymmetry with writeFileRemote which accepts string|Buffer; change the
handler signature to accept content: string | Buffer, update any IPC type defs
accordingly, and in the local branch call fs.writeFile(filePath, content) (no
forced 'utf-8' encoding) so Buffers are written correctly; keep the SSH branch
using writeFileRemote(filePath, content, sshConfig) which already supports
string|Buffer and error handling with getSshRemoteById and writeFileRemote
remains unchanged.
src/renderer/components/UsageDashboard/UsageDashboardModal.tsx (1)

494-498: onClick={onClose} on the outer wrapper div is now dead code.

With the new backdrop <button> using absolute inset-0 + e.stopPropagation() (lines 499-508), and the modal container also calling e.stopPropagation() (line 516), no click event will ever reach this outer div — the button covers the full backdrop area and stops propagation, while the modal container blocks its own area. The outer handler can be removed.

♻️ Proposed cleanup
 		<div
 			className="fixed inset-0 modal-overlay flex items-center justify-center z-[9999] animate-in fade-in duration-100"
-			onClick={onClose}
 		>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/UsageDashboard/UsageDashboardModal.tsx` around lines
494 - 498, The outer wrapper div in UsageDashboardModal has an onClick={onClose}
that is now dead because the full-screen backdrop button (absolute inset-0)
calls e.stopPropagation() and the modal container also stops propagation, so
remove the outer onClick assignment; update the JSX in the UsageDashboardModal
component to delete onClick={onClose} from the top-level wrapper and rely on the
backdrop button's onClick handler (and modal container propagation guards) to
close the modal via the existing onClose function.
src/renderer/components/SettingsModal.tsx (2)

1243-1246: Redundant block class carried over to <div> replacements throughout the file.

Every converted section-header <div> (e.g. Lines 1243, 1279, 1563, 1609, 1688, 1744, 1814, 1956, 2257, 2640, 2721) retains block from the original <label>. <label> is inline by default so block was meaningful there; <div> is already block-level so block is a no-op. When flex is also present (as it is in all of these), Tailwind's CSS ordering means flex wins and block is completely overridden. No functional impact, but it's vestigial noise from the label→div migration.

🔧 Pattern to apply across all affected headings
-<div className="block text-xs font-bold opacity-70 uppercase mb-1 flex items-center gap-2">
+<div className="text-xs font-bold opacity-70 uppercase mb-1 flex items-center gap-2">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/SettingsModal.tsx` around lines 1243 - 1246, Remove
the redundant "block" Tailwind class from the section-header divs in
SettingsModal.tsx — e.g., the div with className "block text-xs font-bold
opacity-70 uppercase mb-1 flex items-center gap-2" that wraps <User /> and the
"About Me" label, and the other converted heading divs that retained "block"
during the label→div migration; simply delete the "block" token from those
className strings so they read "text-xs font-bold opacity-70 uppercase mb-1 flex
items-center gap-2" (and analogous className values) for each affected heading.

1440-1440: Input-specific labels converted to <div> rather than gaining htmlFor/id associations.

Lines 1440 ("Custom Path"), 1470–1472 ("Additional Arguments"), 1571 ("Custom Path" for gh), 2199 ("API Key" for WakaTime), and 2929 ("API Key" for LLM) are labels that sit directly above a specific <input>. The React Doctor concern here is the missing htmlFor — the ideal fix is adding paired htmlFor + id attributes rather than stripping the <label> semantic entirely. Converting to <div> silences the warning but leaves the input without a programmatic label, which tools such as screen readers and automated a11y audits rely on.

Consider addressing these in a Phase 2 pass:

-<div className="block text-xs opacity-60 mb-1">API Key</div>
+<label htmlFor="wakatime-api-key" className="text-xs opacity-60 mb-1 block">API Key</label>-<input type="password" value={wakatimeApiKey} …
+<input id="wakatime-api-key" type="password" value={wakatimeApiKey} …

Also note: Line 2557 (Window Chrome section label) was not converted in this pass and still carries a bare <label> without htmlFor — that one can be swept up at the same time.

Also applies to: 1470-1472, 1571-1571, 2199-2199, 2929-2929

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

In `@src/renderer/components/SettingsModal.tsx` at line 1440, Several inline text
blocks in SettingsModal (e.g., the "Custom Path", "Additional Arguments",
WakaTime "API Key", LLM "API Key", and the "Window Chrome" section label) were
changed from <label> to <div>, losing the htmlFor/id association; revert those
<div className="block ..."> elements back to <label> and add matching htmlFor
attributes, then give the corresponding <input> elements unique id attributes
(e.g., customPathInput,
additionalArgsInput,wakaApiKeyInput,llmApiKeyInput,windowChromeToggleInput) and
ensure each label's htmlFor matches its input id; also update the remaining bare
<label> in the Window Chrome section to reference its input via htmlFor so all
inputs regain proper programmatic labels for accessibility.
🤖 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/main/ipc/handlers/filesystem.ts`:
- Around line 289-307: The catch is replacing the original error (and its
code/errno) with a new Error; update the error handling in this handler so you
either rethrow the original error or wrap it while preserving its properties:
when using getSshRemoteById, writeFileRemote, or fs.writeFile, avoid throwing
new Error(`Failed to write file: ${error}`); instead rethrow the caught error
(or attach its .code/.errno and message to a new error) and only map specific
known/recoverable error codes to friendly messages — leave unexpected errors
untouched so they bubble up to Sentry.

In `@src/renderer/components/InputArea.tsx`:
- Around line 465-488: The staged-image buttons lack explicit tabIndex which
breaks keyboard focus; update the image-opening button (the one calling
setLightboxImage) and the delete button (the one calling setStagedImages) to
include an explicit tabIndex (use tabIndex={0} for focusable or tabIndex={-1} if
intentionally removed), add the outline-none class as required by the renderer
guideline, and if either button should auto-focus when rendered, add a ref prop
like ref={(el) => el?.focus()} to that button; ensure these changes apply to the
buttons wrapping the <img> (setLightboxImage) and the absolute delete button
(setStagedImages) so keyboard navigation and focus-visible styles work
correctly.

---

Outside diff comments:
In `@src/renderer/components/InputArea.tsx`:
- Around line 507-533: The slash command buttons need explicit keyboard focus
support: add a tabIndex prop (e.g., tabIndex={idx === safeSelectedIndex ? 0 :
-1}) and include focus styling classes like "outline-none focus-visible:ring"
(or "focus-visible" plus your theme class) on the button rendered in the slash
command list; also update the ref callback used in
slashCommandItemRefs.current[idx] to call el?.focus() when idx ===
safeSelectedIndex so the active item receives auto-focus—keep the existing
handlers (setSelectedSlashCommandIndex, setInputValue, setSlashCommandOpen,
inputRef) intact.
- Around line 591-617: The command history <button> items lack explicit keyboard
focus behavior; update the button rendered in InputArea.tsx (the element using
setInputValue, setCommandHistoryOpen, setCommandHistoryFilter, inputRef,
setCommandHistorySelectedIndex) to include tabIndex (use tabIndex={0} for
focusable items or tabIndex={-1} for non-focusable ones as appropriate), add
focus styling classes (e.g., outline-none and focus-visible or equivalent) to
the className string, and when an item becomes selected (isSelected) auto-focus
it by adding a ref={(el) => isSelected && el?.focus()} so keyboard users can
tab/arrow into entries properly. Ensure these changes keep the existing
onClick/onMouseEnter handlers and do not alter other logic.
- Around line 710-747: The tab completion buttons need explicit keyboard
focusability and focus styles: add tabIndex={0} to the button, include
outline-none and an appropriate focus-visible class in the className string
(e.g., focus-visible:ring or similar using theme colors), and update the ref
assignment in the existing ref={(el) => (tabCompletionItemRefs.current[idx] =
el)} to also auto-focus the currently selected item (e.g., ref={(el) => {
tabCompletionItemRefs.current[idx] = el; if (isSelected) el?.focus(); }}).
Change the button rendering inside InputArea.tsx (the button using
tabCompletionItemRefs, setSelectedTabCompletionIndex, setTabCompletionOpen,
inputRef, IconComponent) to apply these changes so keyboard tabbing and focus
styles work correctly.
- Around line 789-836: The `@-mention` suggestion buttons need explicit
keyboard-focus support: add tabIndex={-1} to the button, add the classes
outline-none and focus-visible:ring (or appropriate focus-visible styles) to the
className, and update the ref callback (atMentionItemRefs.current[idx]) to also
call el?.focus() when isSelected is true so keyboard navigation visually focuses
the item; adjust setSelectedAtMentionIndex usage as needed to ensure isSelected
controls programmatic focus. Ensure these changes are applied to the button in
InputArea.tsx that references atMentionItemRefs, setSelectedAtMentionIndex,
inputRef and the onClick/onMouseEnter handlers.

---

Duplicate comments:
In `@src/renderer/components/UsageDashboard/UsageDashboardModal.tsx`:
- Around line 499-508: The invisible backdrop button inside the
UsageDashboardModal component currently has className="absolute inset-0" and can
show a focus ring on some browsers; update that button's className to include
the Tailwind utility to suppress focus outlines (e.g., add "outline-none" or
"focus:outline-none") so it becomes "absolute inset-0 outline-none" (or include
both "outline-none focus:outline-none" if desired); this change should be
applied to the button element in UsageDashboardModal where tabIndex={-1} and
onClick calls onClose().

---

Nitpick comments:
In `@src/main/ipc/handlers/filesystem.ts`:
- Around line 288-305: The IPC handler currently types content as string and
always calls fs.writeFile with 'utf-8', causing asymmetry with writeFileRemote
which accepts string|Buffer; change the handler signature to accept content:
string | Buffer, update any IPC type defs accordingly, and in the local branch
call fs.writeFile(filePath, content) (no forced 'utf-8' encoding) so Buffers are
written correctly; keep the SSH branch using writeFileRemote(filePath, content,
sshConfig) which already supports string|Buffer and error handling with
getSshRemoteById and writeFileRemote remains unchanged.

In `@src/renderer/components/SettingsModal.tsx`:
- Around line 1243-1246: Remove the redundant "block" Tailwind class from the
section-header divs in SettingsModal.tsx — e.g., the div with className "block
text-xs font-bold opacity-70 uppercase mb-1 flex items-center gap-2" that wraps
<User /> and the "About Me" label, and the other converted heading divs that
retained "block" during the label→div migration; simply delete the "block" token
from those className strings so they read "text-xs font-bold opacity-70
uppercase mb-1 flex items-center gap-2" (and analogous className values) for
each affected heading.
- Line 1440: Several inline text blocks in SettingsModal (e.g., the "Custom
Path", "Additional Arguments", WakaTime "API Key", LLM "API Key", and the
"Window Chrome" section label) were changed from <label> to <div>, losing the
htmlFor/id association; revert those <div className="block ..."> elements back
to <label> and add matching htmlFor attributes, then give the corresponding
<input> elements unique id attributes (e.g., customPathInput,
additionalArgsInput,wakaApiKeyInput,llmApiKeyInput,windowChromeToggleInput) and
ensure each label's htmlFor matches its input id; also update the remaining bare
<label> in the Window Chrome section to reference its input via htmlFor so all
inputs regain proper programmatic labels for accessibility.

In `@src/renderer/components/UsageDashboard/UsageDashboardModal.tsx`:
- Around line 494-498: The outer wrapper div in UsageDashboardModal has an
onClick={onClose} that is now dead because the full-screen backdrop button
(absolute inset-0) calls e.stopPropagation() and the modal container also stops
propagation, so remove the outer onClick assignment; update the JSX in the
UsageDashboardModal component to delete onClick={onClose} from the top-level
wrapper and rely on the backdrop button's onClick handler (and modal container
propagation guards) to close the modal via the existing onClose function.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7fb9e81 and e51d2cb.

📒 Files selected for processing (23)
  • src/__tests__/main/ipc/handlers/filesystem.test.ts
  • src/__tests__/renderer/components/GitStatusWidget.test.tsx
  • src/__tests__/renderer/components/InlineWizard/WizardConversationView.test.tsx
  • src/__tests__/renderer/components/InputArea.test.tsx
  • src/__tests__/renderer/components/MainPanel.test.tsx
  • src/__tests__/renderer/hooks/useSettings.test.ts
  • src/__tests__/renderer/utils/markdownConfig.test.ts
  • src/main/ipc/handlers/attachments.ts
  • src/main/ipc/handlers/filesystem.ts
  • src/renderer/components/DocumentsPanel.tsx
  • src/renderer/components/GitStatusWidget.tsx
  • src/renderer/components/InlineWizard/WizardInputPanel.tsx
  • src/renderer/components/InlineWizard/WizardMessageBubble.tsx
  • src/renderer/components/InputArea.tsx
  • src/renderer/components/LightboxModal.tsx
  • src/renderer/components/MainPanel.tsx
  • src/renderer/components/PromptComposerModal.tsx
  • src/renderer/components/SettingsModal.tsx
  • src/renderer/components/UsageDashboard/UsageDashboardModal.tsx
  • src/renderer/hooks/batch/useInlineWizard.ts
  • src/renderer/hooks/input/useInputProcessing.ts
  • src/renderer/hooks/session/useHandsOnTimeTracker.ts
  • src/renderer/stores/settingsStore.ts
✅ Files skipped from review due to trivial changes (7)
  • src/renderer/hooks/batch/useInlineWizard.ts
  • src/tests/renderer/hooks/useSettings.test.ts
  • src/renderer/hooks/session/useHandsOnTimeTracker.ts
  • src/renderer/stores/settingsStore.ts
  • src/tests/renderer/utils/markdownConfig.test.ts
  • src/tests/main/ipc/handlers/filesystem.test.ts
  • src/renderer/components/InlineWizard/WizardMessageBubble.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/renderer/components/InlineWizard/WizardInputPanel.tsx
  • src/renderer/components/DocumentsPanel.tsx
  • src/renderer/components/PromptComposerModal.tsx

Comment on lines +289 to +307
try {
// SSH remote: dispatch to remote fs operations
if (sshRemoteId) {
const sshConfig = getSshRemoteById(sshRemoteId);
if (!sshConfig) {
throw new Error(`SSH remote not found: ${sshRemoteId}`);
}
const result = await writeFileRemote(filePath, content, sshConfig);
if (!result.success) {
throw new Error(result.error || 'Failed to write remote file');
}
return { success: true };
}

// Local: use standard fs operations
await fs.writeFile(filePath, content, 'utf-8');
return { success: true };
} catch (error) {
throw new Error(`Failed to write file: ${error}`);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid blanket catch that strips error details.

Catching the whole handler and throwing a new Error drops code/errno (e.g., EACCES, ENOENT) and conflicts with the guideline to let unexpected errors bubble. Prefer rethrowing the original error or only mapping known, recoverable ones.

✅ Suggested fix (preserve original errors)
-		try {
+		// Let unexpected errors bubble up to preserve codes/stack
 			// SSH remote: dispatch to remote fs operations
 			if (sshRemoteId) {
 				const sshConfig = getSshRemoteById(sshRemoteId);
 				if (!sshConfig) {
 					throw new Error(`SSH remote not found: ${sshRemoteId}`);
 				}
 				const result = await writeFileRemote(filePath, content, sshConfig);
 				if (!result.success) {
 					throw new Error(result.error || 'Failed to write remote file');
 				}
 				return { success: true };
 			}

 			// Local: use standard fs operations
 			await fs.writeFile(filePath, content, 'utf-8');
 			return { success: true };
-		} catch (error) {
-			throw new Error(`Failed to write file: ${error}`);
-		}

As per coding guidelines: “Handle only expected/recoverable errors explicitly with specific error codes. Let unexpected errors bubble up to Sentry.”

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

In `@src/main/ipc/handlers/filesystem.ts` around lines 289 - 307, The catch is
replacing the original error (and its code/errno) with a new Error; update the
error handling in this handler so you either rethrow the original error or wrap
it while preserving its properties: when using getSshRemoteById,
writeFileRemote, or fs.writeFile, avoid throwing new Error(`Failed to write
file: ${error}`); instead rethrow the caught error (or attach its .code/.errno
and message to a new error) and only map specific known/recoverable error codes
to friendly messages — leave unexpected errors untouched so they bubble up to
Sentry.

Comment on lines +465 to 488
<div key={img} className="relative group shrink-0">
<button
type="button"
className="p-0 bg-transparent outline-none focus-visible:ring-2 focus-visible:ring-accent rounded"
onClick={() => setLightboxImage(img, stagedImages, 'staged')}
/>
>
<img
src={img}
alt={`Staged image ${idx + 1}`}
className="h-16 rounded border cursor-pointer hover:opacity-80 transition-opacity block"
style={{
borderColor: theme.colors.border,
objectFit: 'contain',
maxWidth: '200px',
}}
/>
</button>
<button
onClick={(e) => {
e.stopPropagation();
setStagedImages((p) => p.filter((_, i) => i !== idx));
}}
className="absolute top-0.5 right-0.5 bg-red-500 text-white rounded-full p-1 shadow-md hover:bg-red-600 transition-colors opacity-90 hover:opacity-100"
className="absolute top-0.5 right-0.5 bg-red-500 text-white rounded-full p-1 shadow-md hover:bg-red-600 transition-colors opacity-90 hover:opacity-100 outline-none focus-visible:ring-2 focus-visible:ring-white"
>
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

Add tabIndex to staged-image buttons.

The new buttons already have focus-visible styles, but the renderer guideline also requires explicit tabIndex.

♻️ Suggested fix
 						<button
 							type="button"
+							tabIndex={0}
 							className="p-0 bg-transparent outline-none focus-visible:ring-2 focus-visible:ring-accent rounded"
 							onClick={() => setLightboxImage(img, stagedImages, 'staged')}
 						>
@@
 							<button
+								tabIndex={0}
 								onClick={(e) => {
 									e.stopPropagation();
 									setStagedImages((p) => p.filter((_, i) => i !== idx));
 								}}
As per coding guidelines: `src/renderer/**/*.{tsx,jsx}`: Add tabIndex={0} or tabIndex={-1} and outline-none class to ensure focus works correctly. Use ref={(el) => el?.focus()} for auto-focus in React components.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/InputArea.tsx` around lines 465 - 488, The
staged-image buttons lack explicit tabIndex which breaks keyboard focus; update
the image-opening button (the one calling setLightboxImage) and the delete
button (the one calling setStagedImages) to include an explicit tabIndex (use
tabIndex={0} for focusable or tabIndex={-1} if intentionally removed), add the
outline-none class as required by the renderer guideline, and if either button
should auto-focus when rendered, add a ref prop like ref={(el) => el?.focus()}
to that button; ensure these changes apply to the buttons wrapping the <img>
(setLightboxImage) and the absolute delete button (setStagedImages) so keyboard
navigation and focus-visible styles work correctly.

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