Skip to content

refactor: decompose SettingsModal and SessionList components#502

Merged
reachrazamair merged 3 commits into
mainfrom
code-refactor
Mar 3, 2026
Merged

refactor: decompose SettingsModal and SessionList components#502
reachrazamair merged 3 commits into
mainfrom
code-refactor

Conversation

@reachrazamair

@reachrazamair reachrazamair commented Mar 3, 2026

Copy link
Copy Markdown
Contributor

Extract 6 components from the monolithic SettingsModal.tsx (3,649 → 601 lines, −84%):

  • EnvVarsEditor: environment variable editing sub-component (219 lines)
  • GeneralTab: shell, input, toggles, WakaTime, sync/stats (1,530 lines)
  • DisplayTab: fonts, terminal width, log buffer, alignment (558 lines)
  • EncoreTab: feature flags, Director's Notes agent config (509 lines)
  • ShortcutsTab: shortcut recording, filtering, escape coordination (215 lines)
  • ThemeTab: theme selection, grouping, keyboard navigation (162 lines)

Each tab self-sources settings via useSettings() and owns its local state. SettingsModal.tsx retains modal chrome, tab bar, layer stack, and tab routing.

308 new tests across 6 test files; 416 total Settings tests passing.

Summary by CodeRabbit

  • New Features

    • Redesigned Settings modal with tabbed sections: General, Display, Encore (Director's Notes), Shortcuts, and Theme.
    • Added Environment Variables editor with per-entry validation, add/remove, unique-name generation and live sync.
    • New UI elements: font/display controls, shortcuts manager, theme picker, hamburger menu, session context menu, and enhanced session tooltips.
  • Tests

    • Extensive unit tests added for Settings tabs, EnvVars editor, and shortcuts; updated modal/shortcut behavior tests.

Extract 6 components from the monolithic SettingsModal.tsx (3,649 → 601 lines, −84%):

- EnvVarsEditor: environment variable editing sub-component (219 lines)
- GeneralTab: shell, input, toggles, WakaTime, sync/stats (1,530 lines)
- DisplayTab: fonts, terminal width, log buffer, alignment (558 lines)
- EncoreTab: feature flags, Director's Notes agent config (509 lines)
- ShortcutsTab: shortcut recording, filtering, escape coordination (215 lines)
- ThemeTab: theme selection, grouping, keyboard navigation (162 lines)

Each tab self-sources settings via useSettings() and owns its local state.
SettingsModal.tsx retains modal chrome, tab bar, layer stack, and tab routing.

308 new tests across 6 test files; 416 total Settings tests passing.
@coderabbitai

coderabbitai Bot commented Mar 3, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a Settings modal refactor: five new tab components, a new EnvVarsEditor (with exports), new SessionList UI components, many test suites, and several non-functional formatting edits across renderer hooks and docs.

Changes

Cohort / File(s) Summary
Settings Tabs & Index
src/renderer/components/Settings/tabs/GeneralTab.tsx, src/renderer/components/Settings/tabs/DisplayTab.tsx, src/renderer/components/Settings/tabs/EncoreTab.tsx, src/renderer/components/Settings/tabs/ShortcutsTab.tsx, src/renderer/components/Settings/tabs/ThemeTab.tsx, src/renderer/components/Settings/tabs/index.ts
Adds five new tab components with exported props types and a tabs index re-exporting them; each implements substantial UI sections and local state/handlers (fonts, windows chrome, director notes agent flows, shortcuts recording, theme selection).
EnvVarsEditor & Tests
src/renderer/components/Settings/EnvVarsEditor.tsx, src/__tests__/renderer/components/Settings/EnvVarsEditor.test.tsx, src/renderer/components/Settings/index.ts
New EnvVarsEditor component (EnvVarEntry/EnvVarsEditorProps exports): per-entry IDs, key/value validation, quoted-value allowance, add/update/remove, parent sync via setEnvVars; comprehensive unit tests and re-exports added.
SessionList UI
src/renderer/components/SessionList/HamburgerMenuContent.tsx, src/renderer/components/SessionList/SessionContextMenu.tsx, src/renderer/components/SessionList/SessionTooltipContent.tsx
New hamburger menu content, contextual session menu with nested "Move to Group" submenu and viewport-aware positioning, and a memoized session tooltip component; themed and wired to many action callbacks.
Tests — Settings Tab Suites & Modal
src/__tests__/renderer/components/Settings/tabs/*, src/__tests__/renderer/components/SettingsModal.test.tsx
Large, comprehensive test suites added for each new tab (UI states, accessibility, keyboard flows) and adjustments to SettingsModal tests (Escape/recording behavior, removed EnvVarsEditor block).
Formatting / Minor Test Edits
src/__tests__/renderer/hooks/useAgentSessionManagement.test.ts, src/renderer/hooks/agent/useAgentSessionManagement.ts, src/renderer/hooks/modal/useModalHandlers.ts, docs/releases.md
Non-functional formatting and small test data tweaks; release notes text normalization.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant SettingsUI
    participant EncoreTab
    participant AgentAPI
    participant SettingsStore

    User->>SettingsUI: Open Settings, select Encore tab
    SettingsUI->>EncoreTab: mount(isOpen=true)
    EncoreTab->>AgentAPI: detectAgents()
    AgentAPI-->>EncoreTab: agent list
    User->>EncoreTab: Click "Customize" for provider X
    EncoreTab->>AgentAPI: getConfig(provider X)
    AgentAPI-->>EncoreTab: provider config
    EncoreTab->>AgentAPI: getModels(provider X) (if supported)
    AgentAPI-->>EncoreTab: model list
    User->>EncoreTab: change customPath/args/envVars
    EncoreTab->>SettingsStore: setDirectorNotesSettings(updated config)
    SettingsStore-->>EncoreTab: persisted
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% 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 PR title 'refactor: decompose SettingsModal and SessionList components' accurately summarizes the main objective: extracting multiple tab components from monolithic SettingsModal and improving SessionList components, reducing SettingsModal by 84%.

✏️ 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
  • Commit unit tests in branch code-refactor

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

❤️ Share

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

@greptile-apps

greptile-apps Bot commented Mar 3, 2026

Copy link
Copy Markdown

Greptile Summary

This PR successfully decomposes a 3,649-line monolithic SettingsModal.tsx into six focused tab components (GeneralTab, DisplayTab, EncoreTab, ShortcutsTab, ThemeTab, EnvVarsEditor), each self-sourcing its own settings via useSettings(). The refactor is architecturally sound and ships with 308 new tests (416 total).

Key findings:

  • TypeScript error in GeneralTab.tsx (lines 349, 411): React.CSSProperties is used as a type cast, but React is never imported as a default export — only named hooks are imported. Every other tab that uses this pattern (ShortcutsTab, ThemeTab) correctly imports React. This will cause a TypeScript compilation failure.
  • Stale closure in EnvVarsEditor.tsx (line 91): The parent-sync useEffect reads entries state inside its body but lists only [envVars] as a dependency. If envVars changes while entries is stale (e.g., rapid external updates), the guard comparison can produce incorrect results, leading to either missed syncs or spurious resets of in-progress edits. This logic was moved verbatim from the original SettingsModal.tsx, so it is not a new regression, but the extraction into a standalone component makes it easier to address.
  • All other extracted tab components (DisplayTab, EncoreTab, ShortcutsTab, ThemeTab) are clean and correctly structured.
  • The EncoreTab intentionally uses an empty-dep useEffect for per-mount state reset, which is correct because the component is conditionally rendered and unmounts on tab navigation.

Confidence Score: 3/5

  • Not safe to merge until the React.CSSProperties TypeScript error in GeneralTab.tsx is resolved; the stale-closure bug in EnvVarsEditor is low-risk but should also be addressed.
  • The architectural refactor is clean and well-tested, but GeneralTab.tsx references React.CSSProperties without importing React, which is a hard TypeScript compilation error that would block the build. The stale-closure in EnvVarsEditor's parent-sync effect is a pre-existing logic bug that is low-severity in practice but is now more visible as a standalone component.
  • src/renderer/components/Settings/tabs/GeneralTab.tsx (TypeScript error at lines 349 & 411) and src/renderer/components/Settings/EnvVarsEditor.tsx (stale closure in useEffect at line 91).

Important Files Changed

Filename Overview
src/renderer/components/Settings/tabs/GeneralTab.tsx Largest extracted tab (1,485 lines); uses React.CSSProperties at lines 349 and 411 without importing React — TypeScript compilation error.
src/renderer/components/Settings/EnvVarsEditor.tsx Newly extracted component; contains a stale-closure bug in the parent-sync useEffectentries is read inside the effect but omitted from the dependency array [envVars].
src/renderer/components/SettingsModal.tsx Successfully slimmed from 3,649 to 601 lines; retains modal chrome, tab bar, layer-stack registration, and LLM test panel. No issues found.
src/renderer/components/Settings/tabs/DisplayTab.tsx Clean extraction of display settings; self-sources via useSettings(), lazy-loads fonts on interaction. No issues found.
src/renderer/components/Settings/tabs/EncoreTab.tsx Clean extraction with correct empty-dep useEffect (intentional per-mount reset since tab is conditionally rendered). No issues found.
src/renderer/components/Settings/tabs/ShortcutsTab.tsx Clean extraction; correctly notifies parent of recording state via onRecordingChange callback for escape-handler coordination. No issues found.
src/renderer/components/Settings/tabs/ThemeTab.tsx Clean extraction; handles Tab/Shift+Tab keyboard navigation through theme list including the custom theme. No issues found.
src/tests/renderer/components/Settings/EnvVarsEditor.test.tsx 20 well-structured tests covering add/remove, validation (name & value), external sync, and unique key generation. No issues found.
src/tests/renderer/components/Settings/tabs/GeneralTab.test.tsx Comprehensive test file for the largest tab; mocks useSettings cleanly with per-test overrides. No issues found.
src/tests/renderer/components/SettingsModal.test.tsx Updated modal-level tests covering tab navigation, layer registration, and keyboard shortcuts. No issues found.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    SM["SettingsModal.tsx\n(modal chrome + tab bar)"]

    SM -->|activeTab = general| GT["GeneralTab\n(shell, input, WakaTime, sync, stats)"]
    SM -->|activeTab = display| DT["DisplayTab\n(fonts, width, buffer, alignment)"]
    SM -->|activeTab = shortcuts| ST["ShortcutsTab\n(recording, filtering, escape coord)"]
    SM -->|activeTab = theme| TT["ThemeTab\n(grouping, keyboard nav)"]
    SM -->|activeTab = encore| ET["EncoreTab\n(feature flags, Director's Notes)"]
    SM -->|activeTab = notifications| NP["NotificationsPanel\n(existing component)"]
    SM -->|activeTab = aicommands| ACP["AICommandsPanel\n(existing component)"]
    SM -->|activeTab = ssh| SSH["SshRemotesSection\n(existing component)"]

    GT --> EVE["EnvVarsEditor\n(env var add/edit/remove)"]

    GT & DT & ST & TT & ET -->|self-sources| US["useSettings()"]
    SM -->|retains for LLM + notifications + ssh| US

    US --> SS["settingsStore"]
Loading

Last reviewed commit: 7baef09

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

16 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

defaultShell === shell.id ? theme.colors.accentDim : theme.colors.bgMain,
'--tw-ring-color': theme.colors.accent,
color: theme.colors.textMain,
} as React.CSSProperties

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React not imported but used as a namespace

React.CSSProperties is referenced here (and again at line 411), but GeneralTab.tsx only imports named exports from 'react' — it never imports React as a default:

import { useState, useEffect, useCallback } from 'react';

Every other tab in this PR that uses the same pattern (ShortcutsTab.tsx, ThemeTab.tsx) imports React explicitly. This will fail TypeScript compilation with an error such as 'React' refers to a UMD global, but the current file is a module.

Suggested change
} as React.CSSProperties
} as import('react').CSSProperties

Or, more consistently with the rest of the codebase, add React to the import at the top of the file:

import React, { useState, useEffect, useCallback } from 'react';

The same fix is needed at line 411.

Comment on lines +88 to +110
useEffect(() => {
const parentEntries = Object.entries(envVars);
// Only reset if the keys/values actually differ
const currentKeys = entries
.filter((e) => e.key.trim())
.map((e) => `${e.key}=${e.value}`)
.sort()
.join(',');
const parentKeys = parentEntries
.map(([k, v]) => `${k}=${v}`)
.sort()
.join(',');
if (currentKeys !== parentKeys) {
setEntries(
parentEntries.map(([key, value], index) => ({
id: index,
key,
value,
}))
);
setNextId(parentEntries.length);
}
}, [envVars]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale closure: entries used inside useEffect but omitted from dependencies

The effect reads entries to build currentKeys, but entries is not listed in the dependency array — only envVars is. When envVars changes (e.g. the parent re-opens the modal), React will re-run this closure with a potentially stale snapshot of entries, making the currentKeys !== parentKeys guard unreliable. This can lead to either:

  • Missed resets: if the stale entries accidentally produces a matching string, the UI won't sync to the new envVars.
  • Spurious resets: if the stale value differs from the already-correct current state, in-progress edits are wiped.

The safest fix is to use the functional form of setEntries so that entries is never captured stale:

useEffect(() => {
  const parentEntries = Object.entries(envVars);
  const parentKeys = parentEntries
    .map(([k, v]) => `${k}=${v}`)
    .sort()
    .join(',');

  setEntries((current) => {
    const currentKeys = current
      .filter((e) => e.key.trim())
      .map((e) => `${e.key}=${e.value}`)
      .sort()
      .join(',');
    if (currentKeys === parentKeys) return current; // no change
    setNextId(parentEntries.length);
    return parentEntries.map(([key, value], index) => ({ id: index, key, value }));
  });
}, [envVars]);

Note: setNextId inside setEntries is not ideal (two separate state updates), but it avoids the stale closure. Alternatively, merging entries and nextId into a single state object would be cleaner.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 14

🧹 Nitpick comments (1)
src/renderer/components/Settings/EnvVarsEditor.tsx (1)

159-164: Remove the no-op conditional class fragment.

This ternary always resolves to an empty string, so it doesn’t affect rendering and adds noise.

🧹 Proposed cleanup
-									className={`flex-1 p-2 rounded border bg-transparent outline-none text-xs font-mono ${
-										entry.key.trim() &&
-										!validateEntry({ id: entry.id, key: entry.key, value: entry.value })
-											? ''
-											: ''
-									}`}
+									className="flex-1 p-2 rounded border bg-transparent outline-none text-xs font-mono"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/Settings/EnvVarsEditor.tsx` around lines 159 - 164,
The className string contains a no-op ternary that always returns an empty
string; remove the conditional fragment and simplify the className for the input
element in EnvVarsEditor so it no longer includes the useless expression
referencing entry.key and validateEntry({ id: entry.id, key: entry.key, value:
entry.value }); update the JSX for that element (the input/textarea rendering
within EnvVarsEditor) to use a single static or meaningful dynamic class list
without the empty ternary.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/__tests__/renderer/components/Settings/EnvVarsEditor.test.tsx`:
- Around line 139-142: The test currently can pass without exercising the
invalid-key assertion because it only checks mockSetEnvVars.mock.calls length
conditionally; change the assertion to require that mockSetEnvVars was called
and then inspect its last call. Specifically, assert that mockSetEnvVars was
called (e.g., toHaveBeenCalled or toHaveBeenCalledTimes) and then read the last
call from mockSetEnvVars.mock.calls[...] to assert that ['MY-VAR'] is undefined;
keep the reference to mockSetEnvVars and the lastCall extraction logic
(mockSetEnvVars.mock.calls and lastCall) but make the call presence check
deterministic instead of conditional so the test fails if setEnvVars is never
invoked.

In `@src/__tests__/renderer/components/Settings/tabs/GeneralTab.test.tsx`:
- Around line 818-830: The test hardcodes the platform-specific label "Ctrl +
Enter" causing brittleness; instead, change the expectation in the GeneralTab
test to use the same formatter or a platform-safe assertion: call or import
formatEnterToSend(false) and assert toggleButton?.textContent equals that value
(or assert it contains "Enter" and either "Ctrl" or "⌘"), referencing
formatEnterToSend, GeneralTab, and the enterToSendAI setting so the test passes
on macOS and other platforms.

In `@src/__tests__/renderer/components/Settings/tabs/ThemeTab.test.tsx`:
- Around line 322-339: The test currently only asserts CustomThemeBuilder is
rendered but doesn't verify the import callbacks are forwarded; update the test
to find the CustomThemeBuilder (screen.getByTestId('custom-theme-builder')) and
call its forwarded props for import error and success to assert
onThemeImportError and onThemeImportSuccess (the vi.fn() mocks) are invoked;
specifically, after rendering ThemeTab, access the component instance or
element, read its props-like handlers (e.g., getByTestId(...).props or use
ReactTestUtils/act to invoke the child callbacks) and add
expect(onError).toHaveBeenCalledWith(...) and
expect(onSuccess).toHaveBeenCalledWith(...) assertions to confirm wiring in
ThemeTab -> CustomThemeBuilder.
- Around line 240-243: The assertion is scoped to the wrong DOM node because
darkSection is set via screen.getByText('dark Mode').closest('div'), which
targets the label wrapper and can miss the theme buttons; update the query so
darkButtons come from the actual theme-group container (e.g., locate the
section/fieldset that wraps the dark-mode themes using a more specific selector
or accessible query such as getByRole/getByTestId/getByLabelText, or use
within() on that container) then recompute darkThemeIds and assert it does not
contain 'custom'; adjust references to darkSection, darkButtons, and
darkThemeIds accordingly.

In `@src/__tests__/renderer/components/SettingsModal.test.tsx`:
- Around line 1671-1675: The test currently calls capturedEscapeHandler?.()
which can mask missing registration; update the test in SettingsModal.test.tsx
to assert capturedEscapeHandler is defined before invoking it (e.g.,
expect(capturedEscapeHandler).toBeDefined() or toBeTruthy()), then call
capturedEscapeHandler(); ensure you reference the capturedEscapeHandler variable
used in the escape simulation and keep the existing comments about ShortcutsTab
handling escape intact.

In `@src/renderer/components/Settings/EnvVarsEditor.tsx`:
- Around line 76-81: The loop that builds newEnvVars from newEntries silently
overwrites earlier values when two entries share the same entry.key; change the
logic in the block that iterates newEntries so it tracks seen keys (e.g., a Set)
and when encountering a duplicate key it does NOT assign into newEnvVars and
instead adds/sets a validation error for that entry (use the existing errors map
keyed by entry.id), leaving the first-occurrence value intact; update the
handling around newEntries/newEnvVars/errors to prevent clobbering and surface
duplicates to the user.

In `@src/renderer/components/Settings/tabs/EncoreTab.tsx`:
- Around line 72-75: Wrap all calls to window.maestro.agents.getConfig and
agents.setConfig in proper async error handling (either use try/catch in an
async function or append .catch) so transient backend failures don't produce
unhandled rejections or leave stale UI; on error log the error (e.g., via
console.error or the existing logger), setDnAgentConfig to a safe fallback ({}),
update dnAgentConfigRef.current to the same fallback, and surface a non-blocking
user-facing error state if available; apply the same pattern around the other
usages of getConfig/setConfig in this file (referencing
window.maestro.agents.getConfig, setDnAgentConfig, dnAgentConfigRef, and
agents.setConfig) so every external call is guarded and state is kept consistent
on failure.
- Around line 60-67: The effect that syncs local DN state (useEffect) currently
has an empty dependency array so it only runs on mount; change its dependency
array to include directorNotesSettings (and any prop indicating the tab is
active, if present) so setDnCustomPath, setDnCustomArgs, setDnCustomEnvVars, and
setDnIsConfigExpanded are updated whenever directorNotesSettings changes (or the
tab is reopened) — locate the useEffect block referencing those setters and
replace [] with [directorNotesSettings] (or [directorNotesSettings, isActiveTab]
if an is-active-tab prop/state exists).

In `@src/renderer/components/Settings/tabs/GeneralTab.tsx`:
- Around line 786-799: The switch buttons with role="switch" are missing
accessible names; update each switch element (e.g., the one toggling
preventSleepEnabled via setPreventSleepEnabled in GeneralTab.tsx) to include an
explicit accessible label using aria-label or aria-labelledby that describes the
control (e.g., "Prevent computer from sleeping") and preserve aria-checked
binding; apply the same fix to the other switch buttons referenced (the switches
around lines 854-867, 902-913, 986-998, 1164-1172, 1199-1209) so every
role="switch" has a meaningful aria-label or aria-labelledby matching the state
it toggles.
- Around line 1344-1368: The click handler currently awaits
window.maestro.sync.selectSyncFolder and window.maestro.sync.setCustomPath
inside a try/finally but has no catch, so promise rejections can become
unhandled and skip setting user-visible error state; wrap the awaits in a
try/catch/finally (or add an inner catch) for both onClick handlers that call
selectSyncFolder and setCustomPath, and in the catch setSyncError with the
caught error message (or a fallback string) and ensure setSyncMigrating(false)
still runs in finally; update references in the handler to the symbols
setSyncMigrating, setSyncError, setSyncMigratedCount, setSyncRestartRequired,
setCustomSyncPath, and setCurrentStoragePath so error cases properly update UI
state.
- Around line 256-274: The detect button becomes inert when detection returns an
empty array because shellsLoaded is set true and handleShellInteraction prevents
further calls; update the logic so an empty detection does not permanently mark
shells as loaded — either (A) in loadShells only call setShellsLoaded(true) when
detected.length > 0 (and still setShells([]) / shellsLoading false on empty), or
(B) change handleShellInteraction to call loadShells when shellsLoaded is true
but shells.length === 0; apply the same fix to the other occurrence that uses
the same pattern (the other handleShellInteraction-like block referenced in the
file).
- Around line 536-545: The focusable help-icon wrapper (the div with className
"group relative flex-shrink-0 mt-0.5 outline-none" that contains <HelpCircle />
and uses tabIndex={0}) is missing explicit keyboard focus handlers; add onFocus
and onBlur handlers to that wrapper (or corresponding focus-visible handlers) to
toggle the same visibility state used by hover (so the tooltip div currently
shown via "group-hover:block group-focus-visible:block" is reliably shown/hidden
for keyboard users), and ensure the handlers update any local state or call the
same CSS-visible class logic so keyboard focus triggers the tooltip just like
mouse hover.

In `@src/renderer/components/Settings/tabs/ShortcutsTab.tsx`:
- Around line 27-31: The current useEffect in ShortcutsTab calls
onRecordingChange when recordingId changes but has no cleanup on unmount; modify
the useEffect (the one referencing useEffect, onRecordingChange, and
recordingId) to return a cleanup function that calls onRecordingChange?.(false)
so the parent is notified if the tab unmounts during a recording, ensuring no
stale "recording" state remains; keep onRecordingChange and recordingId in the
dependency array.

In `@src/renderer/components/Settings/tabs/ThemeTab.tsx`:
- Around line 55-89: The Tab handling in handleThemePickerKeyDown currently
prevents Tab at the root which blocks focus inside nested controls (e.g.,
CustomThemeBuilder); change the logic to only intercept and call
e.preventDefault()/e.stopPropagation() when the Tab originated on a theme item
inside the picker (use themePickerRef.current.contains(e.target as Node) AND
check the target or its closest ancestor has a data-theme-id or matches your
theme button selector), otherwise let the event propagate so nested
inputs/buttons keep normal focus behavior; apply the same targeted-check
approach to the other Tab handler block referenced (lines 93–99) that also
prevents Tab.

---

Nitpick comments:
In `@src/renderer/components/Settings/EnvVarsEditor.tsx`:
- Around line 159-164: The className string contains a no-op ternary that always
returns an empty string; remove the conditional fragment and simplify the
className for the input element in EnvVarsEditor so it no longer includes the
useless expression referencing entry.key and validateEntry({ id: entry.id, key:
entry.key, value: entry.value }); update the JSX for that element (the
input/textarea rendering within EnvVarsEditor) to use a single static or
meaningful dynamic class list without the empty ternary.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 85d0fad and 7baef09.

📒 Files selected for processing (16)
  • src/__tests__/renderer/components/Settings/EnvVarsEditor.test.tsx
  • src/__tests__/renderer/components/Settings/tabs/DisplayTab.test.tsx
  • src/__tests__/renderer/components/Settings/tabs/EncoreTab.test.tsx
  • src/__tests__/renderer/components/Settings/tabs/GeneralTab.test.tsx
  • src/__tests__/renderer/components/Settings/tabs/ShortcutsTab.test.tsx
  • src/__tests__/renderer/components/Settings/tabs/ThemeTab.test.tsx
  • src/__tests__/renderer/components/SettingsModal.test.tsx
  • src/renderer/components/Settings/EnvVarsEditor.tsx
  • src/renderer/components/Settings/index.ts
  • src/renderer/components/Settings/tabs/DisplayTab.tsx
  • src/renderer/components/Settings/tabs/EncoreTab.tsx
  • src/renderer/components/Settings/tabs/GeneralTab.tsx
  • src/renderer/components/Settings/tabs/ShortcutsTab.tsx
  • src/renderer/components/Settings/tabs/ThemeTab.tsx
  • src/renderer/components/Settings/tabs/index.ts
  • src/renderer/components/SettingsModal.tsx

Comment on lines +139 to +142
if (mockSetEnvVars.mock.calls.length > 0) {
const lastCall = mockSetEnvVars.mock.calls[mockSetEnvVars.mock.calls.length - 1][0];
expect(lastCall['MY-VAR']).toBeUndefined();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

This assertion can skip validation entirely.

If setEnvVars is never called, the test still passes without checking the invalid-key invariant.

Suggested deterministic assertion
-// The setEnvVars should NOT have been called with this invalid entry
-if (mockSetEnvVars.mock.calls.length > 0) {
-	const lastCall = mockSetEnvVars.mock.calls[mockSetEnvVars.mock.calls.length - 1][0];
-	expect(lastCall['MY-VAR']).toBeUndefined();
-}
+// Ensure no emitted payload ever contains the invalid key
+expect(
+	mockSetEnvVars.mock.calls.every(([vars]) => vars['MY-VAR'] === undefined)
+).toBe(true);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/renderer/components/Settings/EnvVarsEditor.test.tsx` around
lines 139 - 142, The test currently can pass without exercising the invalid-key
assertion because it only checks mockSetEnvVars.mock.calls length conditionally;
change the assertion to require that mockSetEnvVars was called and then inspect
its last call. Specifically, assert that mockSetEnvVars was called (e.g.,
toHaveBeenCalled or toHaveBeenCalledTimes) and then read the last call from
mockSetEnvVars.mock.calls[...] to assert that ['MY-VAR'] is undefined; keep the
reference to mockSetEnvVars and the lastCall extraction logic
(mockSetEnvVars.mock.calls and lastCall) but make the call presence check
deterministic instead of conditional so the test fails if setEnvVars is never
invoked.

Comment thread src/__tests__/renderer/components/Settings/tabs/GeneralTab.test.tsx
Comment on lines +240 to +243
const darkSection = screen.getByText('dark Mode').closest('div');
const darkButtons = darkSection?.querySelectorAll('button[data-theme-id]') || [];
const darkThemeIds = Array.from(darkButtons).map((b) => b.getAttribute('data-theme-id'));
expect(darkThemeIds).not.toContain('custom');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

exclude custom theme assertion is scoped to the wrong DOM node.

screen.getByText('dark Mode').closest('div') targets the mode label wrapper, so button[data-theme-id] lookup can be empty even when grouping is wrong.

Suggested test hardening
-const darkSection = screen.getByText('dark Mode').closest('div');
-const darkButtons = darkSection?.querySelectorAll('button[data-theme-id]') || [];
-const darkThemeIds = Array.from(darkButtons).map((b) => b.getAttribute('data-theme-id'));
-expect(darkThemeIds).not.toContain('custom');
+const customButtonsInRegularGrids = document.querySelectorAll('.grid [data-theme-id="custom"]');
+expect(customButtonsInRegularGrids).toHaveLength(0);
📝 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 darkSection = screen.getByText('dark Mode').closest('div');
const darkButtons = darkSection?.querySelectorAll('button[data-theme-id]') || [];
const darkThemeIds = Array.from(darkButtons).map((b) => b.getAttribute('data-theme-id'));
expect(darkThemeIds).not.toContain('custom');
const customButtonsInRegularGrids = document.querySelectorAll('.grid [data-theme-id="custom"]');
expect(customButtonsInRegularGrids).toHaveLength(0);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/renderer/components/Settings/tabs/ThemeTab.test.tsx` around
lines 240 - 243, The assertion is scoped to the wrong DOM node because
darkSection is set via screen.getByText('dark Mode').closest('div'), which
targets the label wrapper and can miss the theme buttons; update the query so
darkButtons come from the actual theme-group container (e.g., locate the
section/fieldset that wraps the dark-mode themes using a more specific selector
or accessible query such as getByRole/getByTestId/getByLabelText, or use
within() on that container) then recompute darkThemeIds and assert it does not
contain 'custom'; adjust references to darkSection, darkButtons, and
darkThemeIds accordingly.

Comment on lines +322 to +339
it('should pass import callbacks to CustomThemeBuilder', async () => {
const onError = vi.fn();
const onSuccess = vi.fn();

render(
<ThemeTab
theme={mockTheme}
themes={mockThemes}
onThemeImportError={onError}
onThemeImportSuccess={onSuccess}
/>
);

await act(async () => {
await vi.advanceTimersByTimeAsync(100);
});

expect(screen.getByTestId('custom-theme-builder')).toBeInTheDocument();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

import callbacks test does not verify callback wiring.

The current assertion only checks render presence, so it won’t fail if onThemeImportError/onThemeImportSuccess stop being forwarded.

Suggested assertion upgrade
+const mockCustomThemeBuilderProps = vi.fn();

 vi.mock('../../../../../renderer/components/CustomThemeBuilder', () => ({
-	CustomThemeBuilder: ({ isSelected, onSelect }: { isSelected: boolean; onSelect: () => void }) => (
-		<div data-testid="custom-theme-builder">
-			<button onClick={onSelect} data-theme-id="custom" className={isSelected ? 'ring-2' : ''}>
-				Custom Theme
-			</button>
-		</div>
-	),
+	CustomThemeBuilder: (props: {
+		isSelected: boolean;
+		onSelect: () => void;
+		onImportError?: (message: string) => void;
+		onImportSuccess?: (themeName: string) => void;
+	}) => {
+		mockCustomThemeBuilderProps(props);
+		const { isSelected, onSelect } = props;
+		return (
+			<div data-testid="custom-theme-builder">
+				<button onClick={onSelect} data-theme-id="custom" className={isSelected ? 'ring-2' : ''}>
+					Custom Theme
+				</button>
+			</div>
+		);
+	},
 }));

 // ...
 expect(screen.getByTestId('custom-theme-builder')).toBeInTheDocument();
+expect(mockCustomThemeBuilderProps).toHaveBeenCalledWith(
+	expect.objectContaining({
+		onImportError: onError,
+		onImportSuccess: onSuccess,
+	})
+);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/renderer/components/Settings/tabs/ThemeTab.test.tsx` around
lines 322 - 339, The test currently only asserts CustomThemeBuilder is rendered
but doesn't verify the import callbacks are forwarded; update the test to find
the CustomThemeBuilder (screen.getByTestId('custom-theme-builder')) and call its
forwarded props for import error and success to assert onThemeImportError and
onThemeImportSuccess (the vi.fn() mocks) are invoked; specifically, after
rendering ThemeTab, access the component instance or element, read its
props-like handlers (e.g., getByTestId(...).props or use ReactTestUtils/act to
invoke the child callbacks) and add expect(onError).toHaveBeenCalledWith(...)
and expect(onSuccess).toHaveBeenCalledWith(...) assertions to confirm wiring in
ThemeTab -> CustomThemeBuilder.

Comment on lines 1671 to 1675
// Call the escape handler (simulating layer stack escape)
// ShortcutsTab handles its own escape via onKeyDownCapture, so
// the shell just ignores the escape when recording is active
capturedEscapeHandler?.();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid a false-positive in this Escape regression test.

capturedEscapeHandler?.() on Line 1674 makes the test pass even if no escape handler was registered. Assert the handler exists before invoking it.

🔧 Proposed fix
 			// Call the escape handler (simulating layer stack escape)
 			// ShortcutsTab handles its own escape via onKeyDownCapture, so
 			// the shell just ignores the escape when recording is active
-			capturedEscapeHandler?.();
+			expect(capturedEscapeHandler).toBeTypeOf('function');
+			capturedEscapeHandler!();
📝 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
// Call the escape handler (simulating layer stack escape)
// ShortcutsTab handles its own escape via onKeyDownCapture, so
// the shell just ignores the escape when recording is active
capturedEscapeHandler?.();
// Call the escape handler (simulating layer stack escape)
// ShortcutsTab handles its own escape via onKeyDownCapture, so
// the shell just ignores the escape when recording is active
expect(capturedEscapeHandler).toBeTypeOf('function');
capturedEscapeHandler!();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/renderer/components/SettingsModal.test.tsx` around lines 1671 -
1675, The test currently calls capturedEscapeHandler?.() which can mask missing
registration; update the test in SettingsModal.test.tsx to assert
capturedEscapeHandler is defined before invoking it (e.g.,
expect(capturedEscapeHandler).toBeDefined() or toBeTruthy()), then call
capturedEscapeHandler(); ensure you reference the capturedEscapeHandler variable
used in the escape simulation and keep the existing comments about ShortcutsTab
handling escape intact.

Comment on lines +536 to +545
<div
className="group relative flex-shrink-0 mt-0.5 outline-none"
tabIndex={0}
title="Environment variables configured here are available to all terminal sessions, all AI agent processes (Claude, OpenCode, etc.), and any spawned child processes. Agent-specific settings can override these values."
>
<HelpCircle
className="w-4 h-4 cursor-help"
style={{ color: theme.colors.textDim }}
/>
<div className="absolute hidden group-hover:block group-focus-visible:block bg-black/80 text-white text-xs rounded p-2 z-50 w-60 -right-2 top-5 whitespace-normal">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Focusable help affordance is missing explicit focus handlers.

The help icon wrapper is keyboard-focusable (tabIndex={0}) but has no onFocus/onBlur handling for keyboard interaction state.

As per coding guidelines "Add tabIndex attribute and focus event handlers when implementing components that need keyboard focus".

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

In `@src/renderer/components/Settings/tabs/GeneralTab.tsx` around lines 536 - 545,
The focusable help-icon wrapper (the div with className "group relative
flex-shrink-0 mt-0.5 outline-none" that contains <HelpCircle /> and uses
tabIndex={0}) is missing explicit keyboard focus handlers; add onFocus and
onBlur handlers to that wrapper (or corresponding focus-visible handlers) to
toggle the same visibility state used by hover (so the tooltip div currently
shown via "group-hover:block group-focus-visible:block" is reliably shown/hidden
for keyboard users), and ensure the handlers update any local state or call the
same CSS-visible class logic so keyboard focus triggers the tooltip just like
mouse hover.

Comment thread src/renderer/components/Settings/tabs/GeneralTab.tsx
Comment thread src/renderer/components/Settings/tabs/GeneralTab.tsx
Comment on lines +27 to +31
// Notify parent of recording state changes (for escape handler coordination)
useEffect(() => {
onRecordingChange?.(!!recordingId);
}, [recordingId, onRecordingChange]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reset onRecordingChange on unmount to prevent stale modal state.

The effect reports changes while mounted, but there’s no cleanup on unmount. If this tab unmounts during recording, parent escape coordination can stay stuck in recording mode.

🔧 Proposed fix
 	// Notify parent of recording state changes (for escape handler coordination)
 	useEffect(() => {
 		onRecordingChange?.(!!recordingId);
 	}, [recordingId, onRecordingChange]);
+
+	// Ensure parent state is reset if tab unmounts mid-recording
+	useEffect(() => {
+		return () => {
+			onRecordingChange?.(false);
+		};
+	}, [onRecordingChange]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/Settings/tabs/ShortcutsTab.tsx` around lines 27 - 31,
The current useEffect in ShortcutsTab calls onRecordingChange when recordingId
changes but has no cleanup on unmount; modify the useEffect (the one referencing
useEffect, onRecordingChange, and recordingId) to return a cleanup function that
calls onRecordingChange?.(false) so the parent is notified if the tab unmounts
during a recording, ensuring no stale "recording" state remains; keep
onRecordingChange and recordingId in the dependency array.

Comment on lines +55 to +89
const handleThemePickerKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Tab') {
e.preventDefault();
e.stopPropagation();
// Create ordered array: dark themes first, then light, then vibe, then custom (cycling back to dark)
const allThemes = [
...(groupedThemes['dark'] || []),
...(groupedThemes['light'] || []),
...(groupedThemes['vibe'] || []),
];
// Add 'custom' as the last item in the cycle
const allThemeIds = [...allThemes.map((t) => t.id), 'custom'];
let currentIndex = allThemeIds.findIndex((id: string) => id === activeThemeId);
if (currentIndex === -1) currentIndex = 0;

let newThemeId: string;
if (e.shiftKey) {
// Shift+Tab: go backwards
const prevIndex = currentIndex === 0 ? allThemeIds.length - 1 : currentIndex - 1;
newThemeId = allThemeIds[prevIndex];
} else {
// Tab: go forward
const nextIndex = (currentIndex + 1) % allThemeIds.length;
newThemeId = allThemeIds[nextIndex];
}
setActiveThemeId(newThemeId as ThemeId);

// Scroll the newly selected theme button into view
setTimeout(() => {
const themeButton = themePickerRef.current?.querySelector(
`[data-theme-id="${newThemeId}"]`
);
themeButton?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}, 0);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Scope Tab interception so nested controls keep normal focus behavior.

On Line 55, Tab is prevented at the root container level, so Tab presses originating from descendants (including CustomThemeBuilder inputs/buttons) are also blocked. This can break keyboard navigation inside the custom theme editor.

🔧 Proposed fix
 const handleThemePickerKeyDown = (e: React.KeyboardEvent) => {
-	if (e.key === 'Tab') {
-		e.preventDefault();
-		e.stopPropagation();
+	if (e.key === 'Tab') {
+		// Only hijack Tab when the picker container itself has focus.
+		// Let nested controls (e.g. custom theme inputs) use normal Tab behavior.
+		if (e.target !== themePickerRef.current) return;
+		e.preventDefault();
+		e.stopPropagation();
 		// Create ordered array: dark themes first, then light, then vibe, then custom (cycling back to dark)
 		const allThemes = [
 			...(groupedThemes['dark'] || []),
 			...(groupedThemes['light'] || []),
 			...(groupedThemes['vibe'] || []),
 		];

Also applies to: 93-99

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

In `@src/renderer/components/Settings/tabs/ThemeTab.tsx` around lines 55 - 89, The
Tab handling in handleThemePickerKeyDown currently prevents Tab at the root
which blocks focus inside nested controls (e.g., CustomThemeBuilder); change the
logic to only intercept and call e.preventDefault()/e.stopPropagation() when the
Tab originated on a theme item inside the picker (use
themePickerRef.current.contains(e.target as Node) AND check the target or its
closest ancestor has a data-theme-id or matches your theme button selector),
otherwise let the event propagate so nested inputs/buttons keep normal focus
behavior; apply the same targeted-check approach to the other Tab handler block
referenced (lines 93–99) that also prevents Tab.

Resolves conflict in SettingsModal.tsx: kept decomposed <GeneralTab>
component (from our refactoring) and ported the "Conductor Profile"
heading rename from PR #500 into GeneralTab.tsx.

Auto-merged changes from main (PRs #499, #500, #503):
- Historical agent error modal support (effectiveAgentError)
- OpenCode parser improvements (filePath tool detail)
- TerminalOutput error details callback signature update
- Director's Notes lookback default initialization

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

♻️ Duplicate comments (5)
src/__tests__/renderer/components/Settings/tabs/GeneralTab.test.tsx (1)

818-830: ⚠️ Potential issue | 🟡 Minor

Platform-specific assertion is brittle here.

At Line 829, asserting 'Ctrl + Enter' will fail on macOS where this can be '⌘ + Enter'.

Suggested fix
-expect(toggleButton?.textContent).toBe('Ctrl + Enter');
+expect(toggleButton?.textContent).toMatch(/^(Ctrl \+ Enter|⌘ \+ Enter)$/);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/renderer/components/Settings/tabs/GeneralTab.test.tsx` around
lines 818 - 830, The test "should show Ctrl + Enter label when enterToSendAI is
false" is brittle on macOS because it expects the literal string 'Ctrl + Enter';
update the assertion in the test (GeneralTab.test.tsx, the it block with
mockUseSettingsOverrides = { enterToSendAI: false } that queries aiModeLabel and
toggleButton) to accept both platform variants by either asserting a regex that
matches either 'Ctrl' or the Mac '⌘' (e.g. /(Ctrl|⌘)\s*\+\s*Enter/) or by
splitting the button text and checking it contains 'Enter' and (contains 'Ctrl'
or contains '⌘'), so the test passes on all OSes.
src/renderer/components/Settings/tabs/GeneralTab.tsx (4)

1344-1368: ⚠️ Potential issue | 🟠 Major

Sync migration handlers need explicit rejection handling.

Both async handlers use try/finally without catch; rejected promises from selectSyncFolder/setCustomPath can bubble and skip user-visible error state updates.

Suggested fix
 try {
 	const result = await window.maestro.sync.setCustomPath(folder);
 	// existing success/error handling
+} catch (err) {
+	setSyncError(err instanceof Error ? err.message : 'Failed to change storage location');
 } finally {
 	setSyncMigrating(false);
 }
@@
 try {
 	const result = await window.maestro.sync.setCustomPath(null);
 	// existing success/error handling
+} catch (err) {
+	setSyncError(err instanceof Error ? err.message : 'Failed to reset storage location');
 } finally {
 	setSyncMigrating(false);
 }

Also applies to: 1387-1405

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

In `@src/renderer/components/Settings/tabs/GeneralTab.tsx` around lines 1344 -
1368, The onClick async handler for selecting and setting a custom sync folder
uses try/finally but no catch, so rejections from
window.maestro.sync.selectSyncFolder or window.maestro.sync.setCustomPath can
escape and leave UI state inconsistent; update the handler(s) (the onClick that
calls window.maestro.sync.selectSyncFolder and
window.maestro.sync.setCustomPath, and the similar handler at the other
occurrence) to include a catch block that captures any thrown error, calls
setSyncError with a user-friendly message (e.g., error.message or
String(error)), and ensures setSyncMigrating(false) still runs in finally; keep
existing success/error-result handling inside try and do not remove the finally
cleanup.

536-545: ⚠️ Potential issue | 🟡 Minor

Focusable tooltip trigger is missing explicit focus handlers.

The wrapper is keyboard-focusable (tabIndex={0}) but has no onFocus/onBlur handlers at Line 536-545.

As per coding guidelines "Add tabIndex attribute and focus event handlers when implementing components that need keyboard focus".

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

In `@src/renderer/components/Settings/tabs/GeneralTab.tsx` around lines 536 - 545,
The keyboard-focusable tooltip wrapper div (the element with className "group
relative flex-shrink-0 mt-0.5 outline-none" in GeneralTab.tsx) needs explicit
focus handlers: add onFocus and onBlur that toggle the tooltip visibility the
same way hover does (e.g., set a local piece of state like isTooltipOpen or call
the existing hover logic) so keyboard users see/hide the tooltip when the
wrapper gains/loses focus; ensure the title/help icon (HelpCircle) remains
accessible and that the handlers mirror the group-hover/group-focus-visible
behavior.

786-799: ⚠️ Potential issue | 🟠 Major

Switch controls are missing accessible names.

Each role="switch" button here has aria-checked but no aria-label/aria-labelledby, so screen readers can encounter unnamed controls.

Suggested fix (example pattern)
-<button ... role="switch" aria-checked={preventSleepEnabled}>
+<button ... role="switch" aria-checked={preventSleepEnabled} aria-label="Prevent sleep while working">

-<button ... role="switch" aria-checked={disableGpuAcceleration}>
+<button ... role="switch" aria-checked={disableGpuAcceleration} aria-label="Disable GPU acceleration">

-<button ... role="switch" aria-checked={disableConfetti}>
+<button ... role="switch" aria-checked={disableConfetti} aria-label="Disable confetti animations">

-<button ... role="switch" aria-checked={statsCollectionEnabled}>
+<button ... role="switch" aria-checked={statsCollectionEnabled} aria-label="Enable stats collection">

-<button ... role="switch" aria-checked={wakatimeEnabled}>
+<button ... role="switch" aria-checked={wakatimeEnabled} aria-label="Enable WakaTime tracking">

-<button ... role="switch" aria-checked={wakatimeDetailedTracking}>
+<button ... role="switch" aria-checked={wakatimeDetailedTracking} aria-label="Enable detailed file tracking">

Also applies to: 854-867, 902-913, 986-998, 1164-1172, 1199-1209

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

In `@src/renderer/components/Settings/tabs/GeneralTab.tsx` around lines 786 - 799,
The switch buttons in GeneralTab.tsx (the button using preventSleepEnabled and
setPreventSleepEnabled with role="switch") lack accessible names; update this
button and every other button in GeneralTab.tsx that has role="switch" to
include an aria-label or aria-labelledby that describes the control and its
state (for example include the label text plus on/off state), e.g. add
aria-label derived from the same label shown in the UI or a localized string and
reflect the current state (preventSleepEnabled) so screen readers get a
meaningful name and state.

256-274: ⚠️ Potential issue | 🟠 Major

Shell detection can get stuck after an empty detect result.

At Line 257 and Line 271, a successful detect that returns [] still sets shellsLoaded=true, which makes “Detect other available shells...” non-functional on later clicks.

Suggested fix
-const loadShells = async () => {
-	if (shellsLoaded) return;
+const loadShells = async (force = false) => {
+	if (shellsLoading || (shellsLoaded && !force)) return;
 	setShellsLoading(true);
 	try {
 		const detected = await window.maestro.shells.detect();
 		setShells(detected);
-		setShellsLoaded(true);
+		setShellsLoaded(detected.length > 0);
 	} catch (error) {
 		console.error('Failed to load shells:', error);
 	} finally {
 		setShellsLoading(false);
 	}
 };
@@
-<button
-	onClick={handleShellInteraction}
+<button
+	onClick={() => loadShells(true)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/Settings/tabs/GeneralTab.tsx` around lines 256 - 274,
The bug is that loadShells sets shellsLoaded to true even when detected is an
empty array, preventing subsequent detections; change loadShells so after const
detected = await window.maestro.shells.detect() you set setShells(detected) but
only call setShellsLoaded(true) when detected && detected.length > 0 (leave
shellsLoaded false when no shells found so handleShellInteraction can trigger
detection again); ensure you still setShells([]) on empty results and keep the
existing error/finally behavior.
🧹 Nitpick comments (1)
src/renderer/components/SessionList/SessionTooltipContent.tsx (1)

160-163: Consider renaming “Session Cost” to agent-facing copy for consistency.

This is user-facing UI; “Agent Cost” would align better with the terminology used elsewhere in this PR.

As per coding guidelines: src/**/*.{ts,tsx}: Use the Session interface in code to represent agents (due to historical naming), but use 'agent' in user-facing language and 'session' for provider-level conversation contexts.

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

In `@src/renderer/components/SessionList/SessionTooltipContent.tsx` around lines
160 - 163, The label "Session Cost" in SessionTooltipContent is agent-facing
copy and should be renamed to "Agent Cost" for consistency with the project's
user-facing terminology; update the displayed string in the JSX where
SessionTooltipContent renders the label (the span showing "Session Cost" near
session.usageStats.totalCostUsd) to "Agent Cost" while leaving the underlying
Session interface/variable names (e.g., session) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/releases.md`:
- Line 86: Update the compound adjectives in the release notes by hyphenating
them: replace the phrase "cross context memory" with "cross-context memory" (as
used in the history panel sentence) and replace "built in updater" with
"built-in updater" wherever it appears (e.g., the line referenced as the
built-in updater mention); ensure the hyphenated forms are used consistently in
docs/releases.md.
- Line 147: The document uses both "pre-release" and "prerelease"; pick one
spelling and make it consistent by replacing all occurrences of the other form
in this file (e.g., change every "pre-release" to "prerelease" or vice versa),
including the instances shown in the diff snippet ("Added pre-release channel
support for beta/RC updates 🧪") and the other occurrence ("prerelease"); update
any headings, bullet points, and release notes in docs/releases.md so the chosen
form is used everywhere.
- Line 197: Replace the incorrect trademark spelling "Github" with the official
"GitHub" in the release note text — specifically update the phrase "Github
Worktree support was added" (and any other occurrences of "Github" in this
paragraph) to "GitHub Worktree support was added" so the documentation uses the
correct capital "H" trademark form.
- Line 61: Update the release note line that currently reads "AI responses can
now be saved to markdown on disk 💾" to use the proper noun capitalization:
change "markdown" to "Markdown" so the line reads "AI responses can now be saved
to Markdown on disk 💾".
- Line 20: In the release notes paragraph starting with "🎶 Maestro Symphony -
Contribute to open source with AI assistance!" replace the unhyphenated phrase
"open source projects and features" with the compound-adjective form
"open-source projects and features" (locate the exact occurrence of "open
source" in that sentence and add the hyphen) to correct the compound adjective
hyphenation.

In `@src/renderer/components/SessionList/SessionContextMenu.tsx`:
- Around line 61-111: The submenu hide timeout can persist past unmount and
multiple leave calls; update handleMoveToGroupLeave to clear any existing
timeout before setting a new one (use submenuTimeoutRef.current and
clearTimeout, then set it), and add a cleanup effect that runs on unmount to
clearTimeout(submenuTimeoutRef.current) and set submenuTimeoutRef.current =
null; ensure you also set submenuTimeoutRef.current = null after clearing in
handleMoveToGroupHover/handleMoveToGroupLeave. This touches submenuTimeoutRef,
handleMoveToGroupHover, handleMoveToGroupLeave and the component's effects for
cleanup.
- Around line 176-193: The "Move to Group" submenu is only opened on mouse
hover; update the submenu trigger in SessionContextMenu (the element using
moveToGroupRef and the Move to Group button) to be keyboard-accessible by adding
tabIndex={0}, onFocus and onBlur handlers that call the existing
handleMoveToGroupHover and handleMoveToGroupLeave (or new focus equivalents),
and add an onKeyDown handler that opens the submenu on Enter/Space and closes it
on Escape; ensure the focus/keyboard handlers mirror the hover behavior so
keyboard users can open, navigate, and dismiss the submenu.
- Around line 266-330: The menu currently renders a divider even when no
actionable buttons will appear; update the conditional guards so the divider and
its buttons only render when at least one visible action exists: for the top
section change the condition to require ((hasWorktreeChildren ||
session.isGitRepo) && !session.parentSessionId && ((onQuickCreateWorktree &&
session.worktreeConfig) || onConfigureWorktrees)), and for the worktree-child
section require (session.parentSessionId && session.worktreeBranch &&
(onCreatePR || onDeleteWorktree)); modify the JSX around those blocks
(references: session, hasWorktreeChildren, session.isGitRepo,
session.parentSessionId, session.worktreeConfig, onQuickCreateWorktree,
onConfigureWorktrees, onCreatePR, onDeleteWorktree, worktreeBranch) so the
divider is only shown when a button will follow.

In `@src/renderer/components/SessionList/SessionTooltipContent.tsx`:
- Around line 129-145: Clamp session.contextUsage into the 0–100 range before
using it for the label and progress-bar width: compute a clamped value (e.g.,
via Math.max(0, Math.min(100, session.contextUsage)) or a small clamp helper)
inside SessionTooltipContent and use that clamped variable for the displayed
text and the width string (width: `${clamped}%`), and pass the clamped value to
getContextColor/contextWarning thresholds so the bar renders correctly even if
session.contextUsage is out of range.

---

Duplicate comments:
In `@src/__tests__/renderer/components/Settings/tabs/GeneralTab.test.tsx`:
- Around line 818-830: The test "should show Ctrl + Enter label when
enterToSendAI is false" is brittle on macOS because it expects the literal
string 'Ctrl + Enter'; update the assertion in the test (GeneralTab.test.tsx,
the it block with mockUseSettingsOverrides = { enterToSendAI: false } that
queries aiModeLabel and toggleButton) to accept both platform variants by either
asserting a regex that matches either 'Ctrl' or the Mac '⌘' (e.g.
/(Ctrl|⌘)\s*\+\s*Enter/) or by splitting the button text and checking it
contains 'Enter' and (contains 'Ctrl' or contains '⌘'), so the test passes on
all OSes.

In `@src/renderer/components/Settings/tabs/GeneralTab.tsx`:
- Around line 1344-1368: The onClick async handler for selecting and setting a
custom sync folder uses try/finally but no catch, so rejections from
window.maestro.sync.selectSyncFolder or window.maestro.sync.setCustomPath can
escape and leave UI state inconsistent; update the handler(s) (the onClick that
calls window.maestro.sync.selectSyncFolder and
window.maestro.sync.setCustomPath, and the similar handler at the other
occurrence) to include a catch block that captures any thrown error, calls
setSyncError with a user-friendly message (e.g., error.message or
String(error)), and ensures setSyncMigrating(false) still runs in finally; keep
existing success/error-result handling inside try and do not remove the finally
cleanup.
- Around line 536-545: The keyboard-focusable tooltip wrapper div (the element
with className "group relative flex-shrink-0 mt-0.5 outline-none" in
GeneralTab.tsx) needs explicit focus handlers: add onFocus and onBlur that
toggle the tooltip visibility the same way hover does (e.g., set a local piece
of state like isTooltipOpen or call the existing hover logic) so keyboard users
see/hide the tooltip when the wrapper gains/loses focus; ensure the title/help
icon (HelpCircle) remains accessible and that the handlers mirror the
group-hover/group-focus-visible behavior.
- Around line 786-799: The switch buttons in GeneralTab.tsx (the button using
preventSleepEnabled and setPreventSleepEnabled with role="switch") lack
accessible names; update this button and every other button in GeneralTab.tsx
that has role="switch" to include an aria-label or aria-labelledby that
describes the control and its state (for example include the label text plus
on/off state), e.g. add aria-label derived from the same label shown in the UI
or a localized string and reflect the current state (preventSleepEnabled) so
screen readers get a meaningful name and state.
- Around line 256-274: The bug is that loadShells sets shellsLoaded to true even
when detected is an empty array, preventing subsequent detections; change
loadShells so after const detected = await window.maestro.shells.detect() you
set setShells(detected) but only call setShellsLoaded(true) when detected &&
detected.length > 0 (leave shellsLoaded false when no shells found so
handleShellInteraction can trigger detection again); ensure you still
setShells([]) on empty results and keep the existing error/finally behavior.

---

Nitpick comments:
In `@src/renderer/components/SessionList/SessionTooltipContent.tsx`:
- Around line 160-163: The label "Session Cost" in SessionTooltipContent is
agent-facing copy and should be renamed to "Agent Cost" for consistency with the
project's user-facing terminology; update the displayed string in the JSX where
SessionTooltipContent renders the label (the span showing "Session Cost" near
session.usageStats.totalCostUsd) to "Agent Cost" while leaving the underlying
Session interface/variable names (e.g., session) 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 7baef09 and db1802e.

📒 Files selected for processing (9)
  • docs/releases.md
  • src/__tests__/renderer/components/Settings/tabs/GeneralTab.test.tsx
  • src/__tests__/renderer/hooks/useAgentSessionManagement.test.ts
  • src/renderer/components/SessionList/HamburgerMenuContent.tsx
  • src/renderer/components/SessionList/SessionContextMenu.tsx
  • src/renderer/components/SessionList/SessionTooltipContent.tsx
  • src/renderer/components/Settings/tabs/GeneralTab.tsx
  • src/renderer/hooks/agent/useAgentSessionManagement.ts
  • src/renderer/hooks/modal/useModalHandlers.ts
✅ Files skipped from review due to trivial changes (2)
  • src/renderer/hooks/modal/useModalHandlers.ts
  • src/tests/renderer/hooks/useAgentSessionManagement.test.ts

Comment thread docs/releases.md Outdated
Comment thread docs/releases.md Outdated
Comment thread docs/releases.md Outdated
Comment thread docs/releases.md Outdated
Comment thread docs/releases.md Outdated
Comment on lines +61 to +111
const submenuTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [showMoveSubmenu, setShowMoveSubmenu] = useState(false);
const [submenuPosition, setSubmenuPosition] = useState<{
vertical: 'below' | 'above';
horizontal: 'right' | 'left';
}>({ vertical: 'below', horizontal: 'right' });

const onDismissRef = useRef(onDismiss);
onDismissRef.current = onDismiss;

useClickOutside(menuRef, onDismiss);

useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onDismissRef.current();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, []);

const { left, top, ready } = useContextMenuPosition(menuRef, x, y);

const handleMoveToGroupHover = () => {
if (submenuTimeoutRef.current) {
clearTimeout(submenuTimeoutRef.current);
submenuTimeoutRef.current = null;
}
setShowMoveSubmenu(true);

if (moveToGroupRef.current) {
const rect = moveToGroupRef.current.getBoundingClientRect();
const itemHeight = 28;
const submenuHeight = (groups.length + 1) * itemHeight + 16 + (groups.length > 0 ? 8 : 0);
const submenuWidth = 160;
const spaceBelow = window.innerHeight - rect.top;
const spaceRight = window.innerWidth - rect.right;

const vertical = spaceBelow < submenuHeight && rect.top > submenuHeight ? 'above' : 'below';
const horizontal = spaceRight < submenuWidth && rect.left > submenuWidth ? 'left' : 'right';

setSubmenuPosition({ vertical, horizontal });
}
};

const handleMoveToGroupLeave = () => {
submenuTimeoutRef.current = setTimeout(() => {
setShowMoveSubmenu(false);
}, 300);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Clear pending submenu timeout on unmount.

A pending hide timeout can outlive the menu lifecycle. Add cleanup and clear any existing timeout before creating a new one.

💡 Proposed fix
 	const handleMoveToGroupLeave = () => {
+		if (submenuTimeoutRef.current) {
+			clearTimeout(submenuTimeoutRef.current);
+		}
 		submenuTimeoutRef.current = setTimeout(() => {
 			setShowMoveSubmenu(false);
 		}, 300);
 	};
+
+	useEffect(() => {
+		return () => {
+			if (submenuTimeoutRef.current) {
+				clearTimeout(submenuTimeoutRef.current);
+			}
+		};
+	}, []);
📝 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 submenuTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [showMoveSubmenu, setShowMoveSubmenu] = useState(false);
const [submenuPosition, setSubmenuPosition] = useState<{
vertical: 'below' | 'above';
horizontal: 'right' | 'left';
}>({ vertical: 'below', horizontal: 'right' });
const onDismissRef = useRef(onDismiss);
onDismissRef.current = onDismiss;
useClickOutside(menuRef, onDismiss);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onDismissRef.current();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, []);
const { left, top, ready } = useContextMenuPosition(menuRef, x, y);
const handleMoveToGroupHover = () => {
if (submenuTimeoutRef.current) {
clearTimeout(submenuTimeoutRef.current);
submenuTimeoutRef.current = null;
}
setShowMoveSubmenu(true);
if (moveToGroupRef.current) {
const rect = moveToGroupRef.current.getBoundingClientRect();
const itemHeight = 28;
const submenuHeight = (groups.length + 1) * itemHeight + 16 + (groups.length > 0 ? 8 : 0);
const submenuWidth = 160;
const spaceBelow = window.innerHeight - rect.top;
const spaceRight = window.innerWidth - rect.right;
const vertical = spaceBelow < submenuHeight && rect.top > submenuHeight ? 'above' : 'below';
const horizontal = spaceRight < submenuWidth && rect.left > submenuWidth ? 'left' : 'right';
setSubmenuPosition({ vertical, horizontal });
}
};
const handleMoveToGroupLeave = () => {
submenuTimeoutRef.current = setTimeout(() => {
setShowMoveSubmenu(false);
}, 300);
};
const submenuTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [showMoveSubmenu, setShowMoveSubmenu] = useState(false);
const [submenuPosition, setSubmenuPosition] = useState<{
vertical: 'below' | 'above';
horizontal: 'right' | 'left';
}>({ vertical: 'below', horizontal: 'right' });
const onDismissRef = useRef(onDismiss);
onDismissRef.current = onDismiss;
useClickOutside(menuRef, onDismiss);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onDismissRef.current();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, []);
const { left, top, ready } = useContextMenuPosition(menuRef, x, y);
const handleMoveToGroupHover = () => {
if (submenuTimeoutRef.current) {
clearTimeout(submenuTimeoutRef.current);
submenuTimeoutRef.current = null;
}
setShowMoveSubmenu(true);
if (moveToGroupRef.current) {
const rect = moveToGroupRef.current.getBoundingClientRect();
const itemHeight = 28;
const submenuHeight = (groups.length + 1) * itemHeight + 16 + (groups.length > 0 ? 8 : 0);
const submenuWidth = 160;
const spaceBelow = window.innerHeight - rect.top;
const spaceRight = window.innerWidth - rect.right;
const vertical = spaceBelow < submenuHeight && rect.top > submenuHeight ? 'above' : 'below';
const horizontal = spaceRight < submenuWidth && rect.left > submenuWidth ? 'left' : 'right';
setSubmenuPosition({ vertical, horizontal });
}
};
const handleMoveToGroupLeave = () => {
if (submenuTimeoutRef.current) {
clearTimeout(submenuTimeoutRef.current);
}
submenuTimeoutRef.current = setTimeout(() => {
setShowMoveSubmenu(false);
}, 300);
};
useEffect(() => {
return () => {
if (submenuTimeoutRef.current) {
clearTimeout(submenuTimeoutRef.current);
}
};
}, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/SessionList/SessionContextMenu.tsx` around lines 61 -
111, The submenu hide timeout can persist past unmount and multiple leave calls;
update handleMoveToGroupLeave to clear any existing timeout before setting a new
one (use submenuTimeoutRef.current and clearTimeout, then set it), and add a
cleanup effect that runs on unmount to clearTimeout(submenuTimeoutRef.current)
and set submenuTimeoutRef.current = null; ensure you also set
submenuTimeoutRef.current = null after clearing in
handleMoveToGroupHover/handleMoveToGroupLeave. This touches submenuTimeoutRef,
handleMoveToGroupHover, handleMoveToGroupLeave and the component's effects for
cleanup.

Comment thread src/renderer/components/SessionList/SessionContextMenu.tsx
Comment thread src/renderer/components/SessionList/SessionContextMenu.tsx Outdated
Comment thread src/renderer/components/SessionList/SessionTooltipContent.tsx Outdated
docs/releases.md:
- Hyphenate compound adjectives (cross-context, built-in, open-source)
- Consistent "prerelease" spelling throughout
- Fix "Github" → "GitHub" trademark capitalization
- Capitalize "Markdown" as proper noun

SessionContextMenu.tsx:
- Add unmount cleanup for submenu hide timeout
- Clear existing timeout in leave handler before setting new one
- Add keyboard accessibility (tabIndex, focus/blur, Enter/Space/Escape)
- Guard divider rendering so it only appears when buttons follow

SessionTooltipContent.tsx:
- Clamp contextUsage to 0–100 range for display and progress bar
- Rename "Session Cost" to "Agent Cost" per project terminology

GeneralTab.tsx:
- Add catch block to sync folder handler for error resilience
- Add aria-label to all 6 role="switch" toggle buttons
- Only set shellsLoaded=true when shells are actually detected

GeneralTab.test.tsx:
- Make Ctrl+Enter assertion platform-agnostic via regex

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
src/renderer/components/Settings/tabs/GeneralTab.tsx (2)

538-547: ⚠️ Potential issue | 🟡 Minor

Add explicit focus handlers for the keyboard-focusable help trigger.

This element is focusable (tabIndex={0}) but has no onFocus/onBlur handlers, which is required for keyboard-focus UI affordances here.

As per coding guidelines "Add tabIndex attribute and focus event handlers when implementing components that need keyboard focus".

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

In `@src/renderer/components/Settings/tabs/GeneralTab.tsx` around lines 538 - 547,
The focusable help trigger (the outer div with tabIndex={0} that contains
HelpCircle and the tooltip div using classes "group" and "absolute ...
group-hover:block group-focus-visible:block") lacks onFocus/onBlur handlers
needed for keyboard users; add explicit onFocus and onBlur callbacks (or
controlled boolean state like isHelpFocused) on that div to set/unset a focused
flag, toggle the tooltip's visibility based on that flag (in addition to
existing hover/focus CSS), and add appropriate accessibility attributes such as
aria-describedby linking the HelpCircle to the tooltip and role="tooltip" on the
tooltip div; optionally handle onKeyDown to close the tooltip on Escape within
the same component (GeneralTab / the help-trigger div).

1352-1415: ⚠️ Potential issue | 🟠 Major

Handle rejected sync API calls in both storage-location actions.

selectSyncFolder() (Line 1353) and the reset-path flow (Line 1397-1415) can reject without a catch, causing unhandled promise rejections and skipping setSyncError(...) updates.

Suggested fix
- onClick={async () => {
-   const folder = await window.maestro.sync.selectSyncFolder();
-   if (folder) {
-     setSyncMigrating(true);
-     setSyncError(null);
-     setSyncMigratedCount(null);
-     try {
+ onClick={async () => {
+   try {
+     const folder = await window.maestro.sync.selectSyncFolder();
+     if (!folder) return;
+     setSyncMigrating(true);
+     setSyncError(null);
+     setSyncMigratedCount(null);
+     try {
        const result = await window.maestro.sync.setCustomPath(folder);
        ...
-     } catch (error) {
-       setSyncError(error instanceof Error ? error.message : String(error));
-     } finally {
+     } catch (error) {
+       setSyncError(error instanceof Error ? error.message : 'Failed to change storage location');
+     } finally {
        setSyncMigrating(false);
      }
-   }
+   } catch (error) {
+     setSyncError(error instanceof Error ? error.message : 'Failed to select folder');
+   }
  }}
 try {
   const result = await window.maestro.sync.setCustomPath(null);
   ...
+} catch (error) {
+  setSyncError(error instanceof Error ? error.message : 'Failed to reset storage location');
 } finally {
   setSyncMigrating(false);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/Settings/tabs/GeneralTab.tsx` around lines 1352 -
1415, selectSyncFolder() and the reset-path call to
window.maestro.sync.setCustomPath(null) can reject and currently lack catch
handlers; wrap the initial selectSyncFolder invocation in a try/catch (so any
rejection from selectSyncFolder is caught) and ensure both button handlers wrap
their await window.maestro.sync.setCustomPath(...) calls in try/catch/finally
blocks that setSyncMigrating(false) in finally and call setSyncError(...) in
catch (use error instanceof Error ? error.message : String(error)). Keep
existing success-path updates (setCustomSyncPath, setCurrentStoragePath,
setSyncRestartRequired, setSyncMigratedCount) inside try; also
clear/setSyncMigratedCount and setSyncError(null) as before before the try.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/releases.md`:
- Line 97: Update the sentence in docs/releases.md that currently reads "Launch
from file preview or from the FIle tree panel." to use correct capitalization by
changing "FIle" to "File" so it reads "Launch from file preview or from the File
tree panel."; locate the sentence in the "Document Graphs" section and make the
single-word capitalization correction.
- Line 103: Replace the incorrect release heading text "# Smaller Changes in
014.x" with the consistent format "# Smaller Changes in 0.14.x"; locate the
heading string "Smaller Changes in 014.x" in the document and update it to
"Smaller Changes in 0.14.x" so it matches the other version headings.

In `@src/renderer/components/SessionList/SessionContextMenu.tsx`:
- Around line 149-377: The menu's <button> elements in SessionContextMenu.tsx
(e.g., the buttons invoking onRename/onDismiss, onEdit/onDismiss,
onDuplicate/onDismiss, the Move to Group button and submenu buttons that call
onMoveToGroup, onCreateGroup, onQuickCreateWorktree, onConfigureWorktrees,
onCreatePR, onDeleteWorktree, onDelete, onToggleBookmark, etc.) need explicit
type="button" attributes to avoid accidental form submission when nested in a
form; update every <button> in this component (including those inside the
moveToGroupRef submenu and conditional sections controlled by showMoveSubmenu,
showWorktreeParentSection, and showWorktreeChildSection) to include
type="button".

---

Duplicate comments:
In `@src/renderer/components/Settings/tabs/GeneralTab.tsx`:
- Around line 538-547: The focusable help trigger (the outer div with
tabIndex={0} that contains HelpCircle and the tooltip div using classes "group"
and "absolute ... group-hover:block group-focus-visible:block") lacks
onFocus/onBlur handlers needed for keyboard users; add explicit onFocus and
onBlur callbacks (or controlled boolean state like isHelpFocused) on that div to
set/unset a focused flag, toggle the tooltip's visibility based on that flag (in
addition to existing hover/focus CSS), and add appropriate accessibility
attributes such as aria-describedby linking the HelpCircle to the tooltip and
role="tooltip" on the tooltip div; optionally handle onKeyDown to close the
tooltip on Escape within the same component (GeneralTab / the help-trigger div).
- Around line 1352-1415: selectSyncFolder() and the reset-path call to
window.maestro.sync.setCustomPath(null) can reject and currently lack catch
handlers; wrap the initial selectSyncFolder invocation in a try/catch (so any
rejection from selectSyncFolder is caught) and ensure both button handlers wrap
their await window.maestro.sync.setCustomPath(...) calls in try/catch/finally
blocks that setSyncMigrating(false) in finally and call setSyncError(...) in
catch (use error instanceof Error ? error.message : String(error)). Keep
existing success-path updates (setCustomSyncPath, setCurrentStoragePath,
setSyncRestartRequired, setSyncMigratedCount) inside try; also
clear/setSyncMigratedCount and setSyncError(null) as before before the try.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between db1802e and 76083e2.

📒 Files selected for processing (5)
  • docs/releases.md
  • src/__tests__/renderer/components/Settings/tabs/GeneralTab.test.tsx
  • src/renderer/components/SessionList/SessionContextMenu.tsx
  • src/renderer/components/SessionList/SessionTooltipContent.tsx
  • src/renderer/components/Settings/tabs/GeneralTab.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/tests/renderer/components/Settings/tabs/GeneralTab.test.tsx

Comment thread docs/releases.md

The major contributions to 0.14.x remain:

🗄️ Document Graphs. Launch from file preview or from the FIle tree panel. Explore relationships between Markdown documents that contain links between documents and to URLs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix typo in “FIle tree panel.”

“FIle” should be “File” in this sentence.

📝 Proposed fix
-🗄️ Document Graphs. Launch from file preview or from the FIle tree panel. Explore relationships between Markdown documents that contain links between documents and to URLs.
+🗄️ Document Graphs. Launch from file preview or from the File tree panel. Explore relationships between Markdown documents that contain links between documents and to URLs.
📝 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
🗄️ Document Graphs. Launch from file preview or from the FIle tree panel. Explore relationships between Markdown documents that contain links between documents and to URLs.
🗄️ Document Graphs. Launch from file preview or from the File tree panel. Explore relationships between Markdown documents that contain links between documents and to URLs.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/releases.md` at line 97, Update the sentence in docs/releases.md that
currently reads "Launch from file preview or from the FIle tree panel." to use
correct capitalization by changing "FIle" to "File" so it reads "Launch from
file preview or from the File tree panel."; locate the sentence in the "Document
Graphs" section and make the single-word capitalization correction.

Comment thread docs/releases.md

🧙‍♂️ Added an in-tab wizard for generating Auto Run Playbooks via `/wizard` or a new button in the Auto Run panel.

# Smaller Changes in 014.x

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Correct version heading format for consistency.

The heading says 014.x; this should be 0.14.x to match the rest of the document’s version format.

📝 Proposed fix
-# Smaller Changes in 014.x
+# Smaller Changes in 0.14.x
📝 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
# Smaller Changes in 014.x
# Smaller Changes in 0.14.x
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/releases.md` at line 103, Replace the incorrect release heading text "#
Smaller Changes in 014.x" with the consistent format "# Smaller Changes in
0.14.x"; locate the heading string "Smaller Changes in 014.x" in the document
and update it to "Smaller Changes in 0.14.x" so it matches the other version
headings.

Comment on lines +149 to +377
<button
onClick={() => {
onRename();
onDismiss();
}}
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2"
style={{ color: theme.colors.textMain }}
>
<Edit3 className="w-3.5 h-3.5" />
Rename
</button>

<button
onClick={() => {
onEdit();
onDismiss();
}}
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2"
style={{ color: theme.colors.textMain }}
>
<Settings className="w-3.5 h-3.5" />
Edit Agent...
</button>

<button
onClick={() => {
onDuplicate();
onDismiss();
}}
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2"
style={{ color: theme.colors.textMain }}
>
<Copy className="w-3.5 h-3.5" />
Duplicate...
</button>

{!session.parentSessionId && (
<button
onClick={() => {
onToggleBookmark();
onDismiss();
}}
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2"
style={{ color: theme.colors.textMain }}
>
<Bookmark className="w-3.5 h-3.5" fill={session.bookmarked ? 'currentColor' : 'none'} />
{session.bookmarked ? 'Remove Bookmark' : 'Add Bookmark'}
</button>
)}

{!session.parentSessionId && (
<div
ref={moveToGroupRef}
className="relative"
tabIndex={0}
onMouseEnter={handleMoveToGroupHover}
onMouseLeave={handleMoveToGroupLeave}
onFocus={handleMoveToGroupHover}
onBlur={handleMoveToGroupLeave}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleMoveToGroupHover();
} else if (e.key === 'Escape' && showMoveSubmenu) {
e.stopPropagation();
setShowMoveSubmenu(false);
}
}}
>
<button
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center justify-between"
style={{ color: theme.colors.textMain }}
>
<span className="flex items-center gap-2">
<FolderInput className="w-3.5 h-3.5" />
Move to Group
</span>
<ChevronRight className="w-3 h-3" />
</button>

{showMoveSubmenu && (
<div
className="absolute py-1 rounded-md shadow-xl border"
style={{
backgroundColor: theme.colors.bgSidebar,
borderColor: theme.colors.border,
minWidth: '140px',
...(submenuPosition.vertical === 'above' ? { bottom: 0 } : { top: 0 }),
...(submenuPosition.horizontal === 'left'
? { right: '100%', marginRight: 4 }
: { left: '100%', marginLeft: 4 }),
}}
>
<button
onClick={() => {
onMoveToGroup('');
onDismiss();
}}
className={`w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2 ${!session.groupId ? 'opacity-50' : ''}`}
style={{ color: theme.colors.textMain }}
disabled={!session.groupId}
>
<Folder className="w-3.5 h-3.5" />
Ungrouped
{!session.groupId && <span className="text-[10px] opacity-50">(current)</span>}
</button>

{groups.length > 0 && (
<div className="my-1 border-t" style={{ borderColor: theme.colors.border }} />
)}

{groups.map((group) => (
<button
key={group.id}
onClick={() => {
onMoveToGroup(group.id);
onDismiss();
}}
className={`w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2 ${session.groupId === group.id ? 'opacity-50' : ''}`}
style={{ color: theme.colors.textMain }}
disabled={session.groupId === group.id}
>
<span>{group.emoji}</span>
<span className="truncate">{group.name}</span>
{session.groupId === group.id && (
<span className="text-[10px] opacity-50">(current)</span>
)}
</button>
))}

{onCreateGroup && (
<div className="my-1 border-t" style={{ borderColor: theme.colors.border }} />
)}

{onCreateGroup && (
<button
onClick={() => {
onCreateGroup();
onDismiss();
}}
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2"
style={{ color: theme.colors.accent }}
>
<FolderPlus className="w-3.5 h-3.5" />
Create New Group
</button>
)}
</div>
)}
</div>
)}

{showWorktreeParentSection && (
<>
<div className="my-1 border-t" style={{ borderColor: theme.colors.border }} />
{onQuickCreateWorktree && session.worktreeConfig && (
<button
onClick={() => {
onQuickCreateWorktree();
onDismiss();
}}
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2"
style={{ color: theme.colors.accent }}
>
<GitBranch className="w-3.5 h-3.5" />
Create Worktree
</button>
)}
{onConfigureWorktrees && (
<button
onClick={() => {
onConfigureWorktrees();
onDismiss();
}}
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2"
style={{ color: theme.colors.accent }}
>
<Settings className="w-3.5 h-3.5" />
Configure Worktrees
</button>
)}
</>
)}

{showWorktreeChildSection && (
<>
<div className="my-1 border-t" style={{ borderColor: theme.colors.border }} />
{onCreatePR && (
<button
onClick={() => {
onCreatePR();
onDismiss();
}}
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2"
style={{ color: theme.colors.accent }}
>
<GitPullRequest className="w-3.5 h-3.5" />
Create Pull Request
</button>
)}
{onDeleteWorktree && (
<button
onClick={() => {
onDeleteWorktree();
onDismiss();
}}
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2"
style={{ color: theme.colors.error }}
>
<Trash2 className="w-3.5 h-3.5" />
Remove Worktree
</button>
)}
</>
)}

{!session.parentSessionId && (
<>
<div className="my-1 border-t" style={{ borderColor: theme.colors.border }} />
<button
onClick={() => {
onDelete();
onDismiss();
}}
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2"
style={{ color: theme.colors.error }}
>
<Trash2 className="w-3.5 h-3.5" />
Remove Agent

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -nP '<button(?![^>]*\btype=)' src/renderer/components/SessionList/SessionContextMenu.tsx

Repository: RunMaestro/Maestro

Length of output: 287


🏁 Script executed:

wc -l src/renderer/components/SessionList/SessionContextMenu.tsx

Repository: RunMaestro/Maestro

Length of output: 124


Add explicit type="button" to all menu buttons.

These buttons currently rely on the HTML default type; if this menu is ever nested in a form, actions can unintentionally submit it.

Proposed patch
-			<button
+			<button type="button"
@@
-			<button
+			<button type="button"
@@
-			<button
+			<button type="button"
@@
-				<button
+				<button type="button"
@@
-					<button
+					<button type="button"
@@
-							<button
+							<button type="button"
@@
-								<button
+								<button type="button"
@@
-								<button
+								<button type="button"
@@
-						<button
+						<button type="button"
@@
-						<button
+						<button type="button"
@@
-						<button
+						<button type="button"
@@
-						<button
+						<button type="button"
@@
-					<button
+					<button type="button"
📝 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
<button
onClick={() => {
onRename();
onDismiss();
}}
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2"
style={{ color: theme.colors.textMain }}
>
<Edit3 className="w-3.5 h-3.5" />
Rename
</button>
<button
onClick={() => {
onEdit();
onDismiss();
}}
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2"
style={{ color: theme.colors.textMain }}
>
<Settings className="w-3.5 h-3.5" />
Edit Agent...
</button>
<button
onClick={() => {
onDuplicate();
onDismiss();
}}
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2"
style={{ color: theme.colors.textMain }}
>
<Copy className="w-3.5 h-3.5" />
Duplicate...
</button>
{!session.parentSessionId && (
<button
onClick={() => {
onToggleBookmark();
onDismiss();
}}
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2"
style={{ color: theme.colors.textMain }}
>
<Bookmark className="w-3.5 h-3.5" fill={session.bookmarked ? 'currentColor' : 'none'} />
{session.bookmarked ? 'Remove Bookmark' : 'Add Bookmark'}
</button>
)}
{!session.parentSessionId && (
<div
ref={moveToGroupRef}
className="relative"
tabIndex={0}
onMouseEnter={handleMoveToGroupHover}
onMouseLeave={handleMoveToGroupLeave}
onFocus={handleMoveToGroupHover}
onBlur={handleMoveToGroupLeave}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleMoveToGroupHover();
} else if (e.key === 'Escape' && showMoveSubmenu) {
e.stopPropagation();
setShowMoveSubmenu(false);
}
}}
>
<button
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center justify-between"
style={{ color: theme.colors.textMain }}
>
<span className="flex items-center gap-2">
<FolderInput className="w-3.5 h-3.5" />
Move to Group
</span>
<ChevronRight className="w-3 h-3" />
</button>
{showMoveSubmenu && (
<div
className="absolute py-1 rounded-md shadow-xl border"
style={{
backgroundColor: theme.colors.bgSidebar,
borderColor: theme.colors.border,
minWidth: '140px',
...(submenuPosition.vertical === 'above' ? { bottom: 0 } : { top: 0 }),
...(submenuPosition.horizontal === 'left'
? { right: '100%', marginRight: 4 }
: { left: '100%', marginLeft: 4 }),
}}
>
<button
onClick={() => {
onMoveToGroup('');
onDismiss();
}}
className={`w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2 ${!session.groupId ? 'opacity-50' : ''}`}
style={{ color: theme.colors.textMain }}
disabled={!session.groupId}
>
<Folder className="w-3.5 h-3.5" />
Ungrouped
{!session.groupId && <span className="text-[10px] opacity-50">(current)</span>}
</button>
{groups.length > 0 && (
<div className="my-1 border-t" style={{ borderColor: theme.colors.border }} />
)}
{groups.map((group) => (
<button
key={group.id}
onClick={() => {
onMoveToGroup(group.id);
onDismiss();
}}
className={`w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2 ${session.groupId === group.id ? 'opacity-50' : ''}`}
style={{ color: theme.colors.textMain }}
disabled={session.groupId === group.id}
>
<span>{group.emoji}</span>
<span className="truncate">{group.name}</span>
{session.groupId === group.id && (
<span className="text-[10px] opacity-50">(current)</span>
)}
</button>
))}
{onCreateGroup && (
<div className="my-1 border-t" style={{ borderColor: theme.colors.border }} />
)}
{onCreateGroup && (
<button
onClick={() => {
onCreateGroup();
onDismiss();
}}
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2"
style={{ color: theme.colors.accent }}
>
<FolderPlus className="w-3.5 h-3.5" />
Create New Group
</button>
)}
</div>
)}
</div>
)}
{showWorktreeParentSection && (
<>
<div className="my-1 border-t" style={{ borderColor: theme.colors.border }} />
{onQuickCreateWorktree && session.worktreeConfig && (
<button
onClick={() => {
onQuickCreateWorktree();
onDismiss();
}}
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2"
style={{ color: theme.colors.accent }}
>
<GitBranch className="w-3.5 h-3.5" />
Create Worktree
</button>
)}
{onConfigureWorktrees && (
<button
onClick={() => {
onConfigureWorktrees();
onDismiss();
}}
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2"
style={{ color: theme.colors.accent }}
>
<Settings className="w-3.5 h-3.5" />
Configure Worktrees
</button>
)}
</>
)}
{showWorktreeChildSection && (
<>
<div className="my-1 border-t" style={{ borderColor: theme.colors.border }} />
{onCreatePR && (
<button
onClick={() => {
onCreatePR();
onDismiss();
}}
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2"
style={{ color: theme.colors.accent }}
>
<GitPullRequest className="w-3.5 h-3.5" />
Create Pull Request
</button>
)}
{onDeleteWorktree && (
<button
onClick={() => {
onDeleteWorktree();
onDismiss();
}}
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2"
style={{ color: theme.colors.error }}
>
<Trash2 className="w-3.5 h-3.5" />
Remove Worktree
</button>
)}
</>
)}
{!session.parentSessionId && (
<>
<div className="my-1 border-t" style={{ borderColor: theme.colors.border }} />
<button
onClick={() => {
onDelete();
onDismiss();
}}
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2"
style={{ color: theme.colors.error }}
>
<Trash2 className="w-3.5 h-3.5" />
Remove Agent
<button type="button"
onClick={() => {
onRename();
onDismiss();
}}
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2"
style={{ color: theme.colors.textMain }}
>
<Edit3 className="w-3.5 h-3.5" />
Rename
</button>
<button type="button"
onClick={() => {
onEdit();
onDismiss();
}}
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2"
style={{ color: theme.colors.textMain }}
>
<Settings className="w-3.5 h-3.5" />
Edit Agent...
</button>
<button type="button"
onClick={() => {
onDuplicate();
onDismiss();
}}
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2"
style={{ color: theme.colors.textMain }}
>
<Copy className="w-3.5 h-3.5" />
Duplicate...
</button>
{!session.parentSessionId && (
<button type="button"
onClick={() => {
onToggleBookmark();
onDismiss();
}}
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2"
style={{ color: theme.colors.textMain }}
>
<Bookmark className="w-3.5 h-3.5" fill={session.bookmarked ? 'currentColor' : 'none'} />
{session.bookmarked ? 'Remove Bookmark' : 'Add Bookmark'}
</button>
)}
{!session.parentSessionId && (
<div
ref={moveToGroupRef}
className="relative"
tabIndex={0}
onMouseEnter={handleMoveToGroupHover}
onMouseLeave={handleMoveToGroupLeave}
onFocus={handleMoveToGroupHover}
onBlur={handleMoveToGroupLeave}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleMoveToGroupHover();
} else if (e.key === 'Escape' && showMoveSubmenu) {
e.stopPropagation();
setShowMoveSubmenu(false);
}
}}
>
<button type="button"
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center justify-between"
style={{ color: theme.colors.textMain }}
>
<span className="flex items-center gap-2">
<FolderInput className="w-3.5 h-3.5" />
Move to Group
</span>
<ChevronRight className="w-3 h-3" />
</button>
{showMoveSubmenu && (
<div
className="absolute py-1 rounded-md shadow-xl border"
style={{
backgroundColor: theme.colors.bgSidebar,
borderColor: theme.colors.border,
minWidth: '140px',
...(submenuPosition.vertical === 'above' ? { bottom: 0 } : { top: 0 }),
...(submenuPosition.horizontal === 'left'
? { right: '100%', marginRight: 4 }
: { left: '100%', marginLeft: 4 }),
}}
>
<button type="button"
onClick={() => {
onMoveToGroup('');
onDismiss();
}}
className={`w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2 ${!session.groupId ? 'opacity-50' : ''}`}
style={{ color: theme.colors.textMain }}
disabled={!session.groupId}
>
<Folder className="w-3.5 h-3.5" />
Ungrouped
{!session.groupId && <span className="text-[10px] opacity-50">(current)</span>}
</button>
{groups.length > 0 && (
<div className="my-1 border-t" style={{ borderColor: theme.colors.border }} />
)}
{groups.map((group) => (
<button type="button"
key={group.id}
onClick={() => {
onMoveToGroup(group.id);
onDismiss();
}}
className={`w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2 ${session.groupId === group.id ? 'opacity-50' : ''}`}
style={{ color: theme.colors.textMain }}
disabled={session.groupId === group.id}
>
<span>{group.emoji}</span>
<span className="truncate">{group.name}</span>
{session.groupId === group.id && (
<span className="text-[10px] opacity-50">(current)</span>
)}
</button>
))}
{onCreateGroup && (
<div className="my-1 border-t" style={{ borderColor: theme.colors.border }} />
)}
{onCreateGroup && (
<button type="button"
onClick={() => {
onCreateGroup();
onDismiss();
}}
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2"
style={{ color: theme.colors.accent }}
>
<FolderPlus className="w-3.5 h-3.5" />
Create New Group
</button>
)}
</div>
)}
</div>
)}
{showWorktreeParentSection && (
<>
<div className="my-1 border-t" style={{ borderColor: theme.colors.border }} />
{onQuickCreateWorktree && session.worktreeConfig && (
<button type="button"
onClick={() => {
onQuickCreateWorktree();
onDismiss();
}}
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2"
style={{ color: theme.colors.accent }}
>
<GitBranch className="w-3.5 h-3.5" />
Create Worktree
</button>
)}
{onConfigureWorktrees && (
<button type="button"
onClick={() => {
onConfigureWorktrees();
onDismiss();
}}
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2"
style={{ color: theme.colors.accent }}
>
<Settings className="w-3.5 h-3.5" />
Configure Worktrees
</button>
)}
</>
)}
{showWorktreeChildSection && (
<>
<div className="my-1 border-t" style={{ borderColor: theme.colors.border }} />
{onCreatePR && (
<button type="button"
onClick={() => {
onCreatePR();
onDismiss();
}}
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2"
style={{ color: theme.colors.accent }}
>
<GitPullRequest className="w-3.5 h-3.5" />
Create Pull Request
</button>
)}
{onDeleteWorktree && (
<button type="button"
onClick={() => {
onDeleteWorktree();
onDismiss();
}}
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2"
style={{ color: theme.colors.error }}
>
<Trash2 className="w-3.5 h-3.5" />
Remove Worktree
</button>
)}
</>
)}
{!session.parentSessionId && (
<>
<div className="my-1 border-t" style={{ borderColor: theme.colors.border }} />
<button type="button"
onClick={() => {
onDelete();
onDismiss();
}}
className="w-full text-left px-3 py-1.5 text-xs hover:bg-white/5 transition-colors flex items-center gap-2"
style={{ color: theme.colors.error }}
>
<Trash2 className="w-3.5 h-3.5" />
Remove Agent
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/SessionList/SessionContextMenu.tsx` around lines 149
- 377, The menu's <button> elements in SessionContextMenu.tsx (e.g., the buttons
invoking onRename/onDismiss, onEdit/onDismiss, onDuplicate/onDismiss, the Move
to Group button and submenu buttons that call onMoveToGroup, onCreateGroup,
onQuickCreateWorktree, onConfigureWorktrees, onCreatePR, onDeleteWorktree,
onDelete, onToggleBookmark, etc.) need explicit type="button" attributes to
avoid accidental form submission when nested in a form; update every <button> in
this component (including those inside the moveToGroupRef submenu and
conditional sections controlled by showMoveSubmenu, showWorktreeParentSection,
and showWorktreeChildSection) to include type="button".

@reachrazamair reachrazamair changed the title refactor: decompose SettingsModal into per-tab components refactor: decompose SettingsModal and SessionList components Mar 3, 2026
@reachrazamair
reachrazamair merged commit dabda9d into main Mar 3, 2026
4 checks passed
@chr1syy chr1syy mentioned this pull request Mar 4, 2026
4 tasks
@coderabbitai coderabbitai Bot mentioned this pull request Mar 12, 2026
8 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant