Skip to content

refactor: decompose useTabHandlers by domain and AgentSessionsBrowser into coordinator pattern - #1057

Merged
reachrazamair merged 5 commits into
rcfrom
cue-polish
Jun 1, 2026
Merged

refactor: decompose useTabHandlers by domain and AgentSessionsBrowser into coordinator pattern#1057
reachrazamair merged 5 commits into
rcfrom
cue-polish

Conversation

@reachrazamair

@reachrazamair reachrazamair commented May 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Two consecutive renderer decompositions that continue the §4.x god-component
reduction campaign. Both follow the established coordinator pattern
(shell + barrel + domain hooks + leaf components), pass the full test suite
with zero regressions, and make no behavior changes.


Commit 1 - useTabHandlers decomposition by domain

src/renderer/hooks/tabs/useTabHandlers.ts was a 2,044-line monolith handling
every tab type (AI, file-preview, browser, terminal, scroll-log) in a single
flat file. It is now a 35-line re-exporting shell backed by 11 focused modules
under src/renderer/hooks/tabs/internal/:

File Lines Responsibility
useAITabHandlers.ts 382 AI tab open/close/rename/star/resume
useFilePreviewTabHandlers.ts 554 File preview open/close/navigation/folder
useUnifiedTabHandlers.ts 232 Cross-type close, move, reorder
useScrollLogHandlers.ts 138 Scroll-log tab lifecycle
useBrowserTabHandlers.ts 132 Browser tab open/close/navigate
unifiedCloseHelpers.ts 101 Close-all / close-others shared logic
types.ts 121 Internal shared types
useTabDerivedState.ts 73 Derived tab state memo
useTerminalTabHandlers.ts 65 Terminal tab open/close
filePreviewTabHelpers.ts 55 File-preview path/kind helpers
browserTabHelpers.ts 42 Browser URL/title helpers

Shell reduction: 2,044 → 35 lines (-98%)

11 new test files added covering all extracted units. All existing
useTabHandlers callers (App.tsx, MainPanel) import through the unchanged
public surface - no call site edits required.


Commit 2 - AgentSessionsBrowser decomposition

src/renderer/components/AgentSessionsBrowser.tsx was a 1,512-line
god-component owning four orthogonal concerns: list view (search, filter,
paginated list, activity graph), detail view (session header, stats grid,
messages), aggregate-stats lifecycle (dual-mode: claude-code progressive IPC
vs other-agent locally computed), and coordination (rename, star, auto-jump,
focus restore, keyboard nav, Cmd+F, modal-layer Escape).

Now a 23-file directory:

AgentSessionsBrowser/

  • AgentSessionsBrowser.tsx (shell, 433 lines)
  • index.ts (barrel)
  • types.ts
  • components/ (9 files)
  • hooks/ (8 files)
  • utils/ (3 files)

Shell reduction: 1,512 → 433 lines (-71%)

Key invariants preserved and guarded by dedicated regression tests:

  • SSH aggregate-stats key - onProjectStatsUpdate subscription is keyed on
    activeSession.projectRoot, NOT projectPathForSessions. Using the remote
    path would silently drop all SSH session stats. Two unit tests in
    useAgentSessionsAggregateStats.test.ts guard this explicitly.
  • Token zeroing in buildUsageStats - resumed tabs receive cost only
    (all 4 token fields zeroed) so they cannot display a misleading 100% context
    bar from stale cumulative totals.
  • Tool-call filtering in messagesToLogEntries - tool-use messages are
    excluded to match live-session behavior (thinking off by default on restore).
  • Cmd+F migrated from a raw document.addEventListener pair to
    useEventListener('keydown', handler, { target: document }) per CLAUDE.md.

85 existing integration tests pass unchanged through the barrel. 170 new unit
tests added across 17 files.


Test plan

  • npm run test - full suite green (29,964 tests, 0 failures)
  • npx tsc --noEmit - 0 type errors
  • npm run lint + npm run lint:eslint - clean
  • npm run format:check - clean

Summary by CodeRabbit

  • New Features

    • New Agent Sessions Browser modal: searchable session list, session detail/messages view, stats panel, and resume/rename/star actions.
    • Search controls: search-mode selector, named/show-all toggles, activity graph with lookback navigation, and load-earlier messages.
  • Tests

    • Extensive new unit/integration tests and test utilities covering session UI, hooks, tab behaviors, and search/usage logic.
  • Refactor

    • Tab management reorganized into modular composable handlers for more reliable tab/session UX.

1,512-line god-component split into shell + barrel + types + 9 components
+ 8 hooks + 3 utils. Shell reduced to ~433 lines (-71%). 170 new unit
tests added across 17 test files; existing 85 integration tests unchanged.

Key invariants preserved: dual-mode aggregate stats (claude-code IPC keyed
on projectRoot NOT projectPathForSessions - SSH regression guard), tool-call
filtering in messagesToLogEntries, token zeroing in buildUsageStats, modal
Escape priority, and Cmd+F gating migrated to useEventListener.
@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7dff565d-6417-4c41-88b0-60a32899b247

📥 Commits

Reviewing files that changed from the base of the PR and between c08bde0 and f2631fd.

📒 Files selected for processing (1)
  • src/renderer/hooks/tabs/internal/useAITabHandlers.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/renderer/hooks/tabs/internal/useAITabHandlers.ts

📝 Walkthrough

Walkthrough

Adds a new Agent Sessions Browser UI (components, hooks, utils, types), comprehensive unit tests, and a refactor of tab-handling into internal helpers and modular hooks plus a thin public composition layer.

Changes

Agent Sessions Browser UI and Hooks

Layer / File(s) Summary
Types and core utilities
src/renderer/components/AgentSessionsBrowser/types.ts, src/renderer/components/AgentSessionsBrowser/utils/*, src/__tests__/renderer/components/AgentSessionsBrowser/utils/*
Introduces types and utilities for usage stats, message→log conversion, and session project path resolution with unit tests.
Agent Sessions hooks and tests
src/renderer/components/AgentSessionsBrowser/hooks/*, src/__tests__/renderer/components/AgentSessionsBrowser/hooks/*
Adds hooks for aggregate stats, activity entries, auto-view, focus-restore, rename, resume, search, and star management, each with targeted tests.
List/detail components and tests
src/renderer/components/AgentSessionsBrowser/components/*, src/__tests__/renderer/components/AgentSessionsBrowser/components/*
Adds SearchModeDropdown, SessionSearchBar, SessionListView/ListHeader/StatsBar, SessionDetailHeader/StatsPanel, SessionMessageBubble/MessagesView and associated tests.
AgentSessionsBrowser container and exports
src/renderer/components/AgentSessionsBrowser/AgentSessionsBrowser.tsx, src/renderer/components/AgentSessionsBrowser/index.ts
Adds a modal container composing the new hooks and components; updates barrel export and removes the legacy top-level file.

Tab Handlers Internal Refactor

Layer / File(s) Summary
Internal types, helpers, and tests
src/renderer/hooks/tabs/internal/types.ts, src/renderer/hooks/tabs/internal/*, src/__tests__/renderer/hooks/tabs/internal/*
Adds internal tab-related types and helpers (browser/file preview/unified closures), test utilities, and unit tests.
Modular internal tab hooks
src/renderer/hooks/tabs/internal/use*.ts
Implements modular hooks for AI tabs, browser tabs, file-preview tabs, scroll/log handlers, terminal tabs, unified tab operations, and derived tab state.
Internal hooks & helpers tests
src/__tests__/renderer/hooks/tabs/internal/*.test.ts
Comprehensive tests validating tab creation/selection/closing, navigation history, reordering, modal confirmations, IPC interactions, and edge cases.
Public composition hook
src/renderer/hooks/tabs/useTabHandlers.ts
Refactors the public hook into a thin composition layer that wires internal hooks and re-exports internal types.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Browser as AgentSessionsBrowser
  participant Tabs as useTabHandlers
  participant Maestro as window.maestro

  User->>Browser: search/select/star/rename/resume
  Browser->>Tabs: open/select/close tabs
  Browser->>Maestro: agentSessions/claude IPC
  Tabs->>Maestro: fs/process/session ops
  Maestro-->>Browser: data/events
  Maestro-->>Tabs: results
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • RunMaestro/Maestro#821: Overlaps refactoring of tab handler logic in src/renderer/hooks/tabs/useTabHandlers.ts.

Suggested labels

refactor

A rabbit taps keys in the twilight glow,
Stitching hooks where the tab-winds blow.
New sessions bloom with stats that sing,
Stars that twinkle, searches spring.
Hop—refactors land just right—burrows tidy, UI bright.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cue-polish

@greptile-apps

greptile-apps Bot commented May 30, 2026

Copy link
Copy Markdown

Greptile Summary

This PR decomposes two large god-components - useTabHandlers.ts (2,044 lines to 35-line barrel + 11 internal modules) and AgentSessionsBrowser.tsx (1,512 lines to 23-file coordinator directory) - following the established coordinator pattern with shell, barrel, domain hooks, and leaf components. No behavior changes are intended and the public API surface is unchanged.

  • useTabHandlers decomposition splits tab handling by domain (AI, file-preview, browser, terminal, scroll-log) into focused internal modules; the 35-line shell composes them via spread and the public export surface is identical.
  • AgentSessionsBrowser decomposition extracts eight domain hooks, nine components, and three utilities from the monolith; key invariants (SSH stats key, token-zeroing on resume, tool-call filtering) are preserved and guarded by dedicated tests.
  • A real defect exists in useAgentSessionsStar: the optimistic star toggle commits local state before the async IPC call and has no rollback on failure, so a transient error leaves the displayed star state out of sync with the backend permanently.

Confidence Score: 3/5

Safe to merge after fixing the star-toggle rollback; the style violations are low-risk but leave behind banned patterns the team explicitly tracks.

The star-toggle optimism in useAgentSessionsStar leaves the displayed star state permanently wrong after any IPC error with no recovery path. Three additional violations of CLAUDE.md conventions add technical debt the team has called out as a maintenance burden.

useAgentSessionsStar.ts needs the error-rollback fix. useAgentSessionsSearch.ts, useAgentSessionsFocusRestore.ts, and the mount-focus block in AgentSessionsBrowser.tsx all deviate from the codebase canonical patterns.

Important Files Changed

Filename Overview
src/renderer/components/AgentSessionsBrowser/AgentSessionsBrowser.tsx New coordinator shell (433 lines); violates CLAUDE.md useEffect + setTimeout focus pattern on mount, and handleKeyDown is not memoized before being passed as a prop.
src/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsStar.ts Optimistic star toggle lacks rollback on IPC failure, leaving UI and backend state out of sync.
src/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsSearch.ts Hand-rolls a 300 ms debounce with useRef + setTimeout instead of using the canonical useDebouncedCallback utility.
src/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsFocusRestore.ts Uses useEffect + setTimeout(() => ref.focus()) pattern prohibited by CLAUDE.md; should use useFocusAfterRender.
src/renderer/hooks/tabs/useTabHandlers.ts Cleanly reduced to 35-line barrel that composes the 11 internal modules; public API unchanged.
src/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsAggregateStats.ts SSH key invariant (subscribe on projectRoot, not projectPathForSessions) correctly preserved and well-commented.
src/renderer/components/AgentSessionsBrowser/utils/buildUsageStats.ts Token-zeroing behavior correctly extracted with clear comment explaining why tokens are zero on resume.
src/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsRename.ts Rename logic cleanly extracted; submitRename has proper error handling but error is only logged, not surfaced to user.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["AgentSessionsBrowser.tsx\n(shell / coordinator, 433 lines)"] --> B["hooks/"]
    A --> C["components/"]
    A --> D["utils/"]

    B --> B1["useAgentSessionsAggregateStats\n(SSH key invariant)"]
    B --> B2["useAgentSessionsSearch\n(debounced IPC)"]
    B --> B3["useAgentSessionsRename"]
    B --> B4["useAgentSessionsStar\n(no error rollback)"]
    B --> B5["useAgentSessionsResume"]
    B --> B6["useAgentSessionsAutoView"]
    B --> B7["useAgentSessionsFocusRestore\n(useEffect+setTimeout)"]
    B --> B8["useAgentSessionsActivityEntries"]

    C --> C1["SessionListView"]
    C --> C2["SessionDetailHeader"]
    C --> C3["SessionDetailStatsPanel"]
    C --> C4["SessionMessagesView"]
    C --> C5["SessionSearchBar"]
    C --> C6["SessionListHeader"]
    C --> C7["SessionListStatsBar"]
    C --> C8["SearchModeDropdown"]
    C --> C9["SessionMessageBubble"]

    D --> D1["buildUsageStats\n(token-zeroing on resume)"]
    D --> D2["messagesToLogEntries\n(tool-call filter)"]
    D --> D3["sessionProjectPath\n(SSH path resolution)"]
Loading

Comments Outside Diff (4)

  1. src/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsStar.ts, line 24-57 (link)

    Optimistic star update is never rolled back on IPC error

    The local starredSessions set is mutated and committed (line 35) before the await calls to persist the change. If either updateSessionStarred or setSessionStarred throws, the hook catches nothing, so the UI permanently shows the wrong star state while the backend holds the original value. Users who lose their connection or hit a transient error will see a starred session that was never actually persisted.

    Wrap the IPC calls in a try/catch and revert setStarredSessions to the original set on failure.

  2. src/renderer/components/AgentSessionsBrowser/AgentSessionsBrowser.tsx, line 196-201 (link)

    useEffect + setTimeout focus pattern violates the CLAUDE.md convention

    CLAUDE.md lists useFocusAfterRender() (src/renderer/hooks/utils/useFocusAfterRender.ts) as the canonical way to focus after render and explicitly says not to use useEffect + setTimeout(() => ref.focus()). The same pattern also appears in useAgentSessionsFocusRestore.ts (lines 20-24). Both sites should use useFocusAfterRender(inputRef, true, 50) (or the equivalent condition).

    Context Used: CLAUDE.md (source)

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

  3. src/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsSearch.ts, line 25-68 (link)

    Manual debounce reimplements useDebouncedCallback from the canonical utility

    CLAUDE.md lists useDebouncedCallback() in src/renderer/hooks/utils/useThrottle.ts as the standard debounce utility and calls out manual reimplementations as the Internal Logging #1 source of maintenance burden. This hook hand-rolls a useRef<NodeJS.Timeout> + setTimeout + cleanup pattern that useDebouncedCallback already provides. The async search body can be lifted into a stable useCallback and passed to useDebouncedCallback with a 300 ms delay.

    Context Used: CLAUDE.md (source)

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

  4. src/renderer/components/AgentSessionsBrowser/AgentSessionsBrowser.tsx, line 252-277 (link)

    handleKeyDown is not memoized but is passed as a prop to child components

    handleKeyDown closes over viewingSession, clearViewingSession, handleResume, onClose, filteredSessions, and selectedIndex, and is passed as onKeyDown to SessionMessagesView. Because it's recreated on every render, SessionMessagesView (and any React.memo wrapping inside it) sees a new prop reference every render, defeating memoization. Wrapping in useCallback with the correct dependency list fixes this.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Reviews (1): Last reviewed commit: "refactor: decompose AgentSessionsBrowser..." | Re-trigger Greptile

@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

🧹 Nitpick comments (3)
src/renderer/components/AgentSessionsBrowser/components/SearchModeDropdown.tsx (1)

15-43: 💤 Low value

Derive getModeIcon from MODES to avoid duplicate mode→icon mapping.

The icon mapping lives in two places (MODES and the getModeIcon switch). They agree today, but they can drift independently if a mode/icon is added or changed. A single lookup keeps them in sync.

♻️ Proposed consolidation
-function getModeIcon(mode: SearchMode) {
-	switch (mode) {
-		case 'title':
-			return Search;
-		case 'user':
-			return User;
-		case 'assistant':
-			return Bot;
-		default:
-			return MessageSquare;
-	}
-}
+function getModeIcon(mode: SearchMode) {
+	return MODES.find((m) => m.mode === mode)?.icon ?? MessageSquare;
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/renderer/components/AgentSessionsBrowser/components/SearchModeDropdown.tsx`
around lines 15 - 43, The getModeIcon switch duplicates the icon mapping already
defined in MODES; replace the switch in getModeIcon with a lookup into MODES
(e.g., find the entry where entry.mode === mode) and return its icon, falling
back to MessageSquare if not found. Update getModeIcon to reference the const
MODES array and preserve the same return type and fallback behavior so
adding/changing modes only needs to be done in MODES.
src/renderer/hooks/tabs/internal/useAITabHandlers.ts (1)

189-203: ⚡ Quick win

Remove leftover [DEBUG renameTab] logging.

These logger.info calls are debug artifacts that will add noise in production. Drop them (or gate behind a debug flag) before merge.

♻️ Proposed cleanup
 	const handleRequestTabRename = useCallback((tabId: string) => {
-		logger.info('[DEBUG renameTab] handleRequestTabRename called', undefined, { tabId });
 		const { setSessions } = useSessionStore.getState();
 		const session = selectActiveSession(useSessionStore.getState());
 		if (!session) {
-			logger.info('[DEBUG renameTab] no session found');
 			return;
 		}
 		const tab = session.aiTabs?.find((t) => t.id === tabId);
-		logger.info('[DEBUG renameTab] tab found:', undefined, [
-			!!tab,
-			{
-				aiTabCount: session.aiTabs?.length,
-				tabId,
-			},
-		]);
 		if (tab) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/hooks/tabs/internal/useAITabHandlers.ts` around lines 189 - 203,
Remove the leftover debug logger.info calls in the handleRequestTabRename flow:
delete the three '[DEBUG renameTab]' logger.info lines (the initial call, the
"no session found" log, and the "tab found" log) in useAITabHandlers.ts, or wrap
them behind an existing debug flag if you prefer conditional logging; keep the
surrounding logic that uses selectActiveSession and setSessions intact
(references: handleRequestTabRename, selectActiveSession,
useSessionStore.getState(), setSessions).
src/__tests__/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsRename.test.ts (1)

1-1: ⚡ Quick win

afterEach is used without an import, but it won’t break this test run

src/__tests__/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsRename.test.ts calls afterEach (line 24) without importing it. The default Vitest config sets test.globals: true in vitest.config.mts, so afterEach is available globally. For consistency with the explicit Vitest imports, consider importing afterEach (or removing the named imports and relying entirely on globals).

Optional consistency fix
-import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/__tests__/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsRename.test.ts`
at line 1, The test file mixes explicit Vitest imports and global usage by
calling afterEach without importing it; update the import statement (the one
that currently imports describe, it, expect, vi, beforeEach from 'vitest') to
also import afterEach so the test consistently uses explicit Vitest imports (or
alternatively remove all named imports and rely on globals—preferred: add
afterEach to the existing import list).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/__tests__/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsFocusRestore.test.ts`:
- Around line 1-29: The test file's vitest import is missing afterEach; update
the import statement that currently reads "import { describe, it, expect, vi,
beforeEach } from 'vitest';" to include afterEach so it becomes "import {
describe, it, expect, vi, beforeEach, afterEach } from 'vitest';" and ensure the
existing afterEach() call in the file uses that imported symbol.

In `@src/renderer/components/AgentSessionsBrowser/AgentSessionsBrowser.tsx`:
- Around line 252-265: The local Escape handling in handleKeyDown (used by
SessionMessagesView and the search input) duplicates the modal-layer Escape
handling provided by useModalLayer and can cause double-close; remove Escape
branches from handleKeyDown so Escape is handled only by the modal layer,
leaving Enter behavior (handleResume) intact for when viewingSession is true,
and ensure clearViewingSession/onClose are only invoked by the modal-layer
listener; update usages of handleKeyDown in SessionMessagesView and the search
input so they no longer rely on it to handle Escape.

In
`@src/renderer/components/AgentSessionsBrowser/components/SessionDetailHeader.tsx`:
- Around line 127-145: The tooltip and hover absolute time are using
viewingSession.timestamp while the visible relative label uses
formatRelativeTime(viewingSession.modifiedAt); update the title and the hover
span to use viewingSession.modifiedAt (e.g., new
Date(viewingSession.modifiedAt).toLocaleString()) so the absolute time matches
the displayed relative time from formatRelativeTime; ensure you reference
viewingSession.modifiedAt consistently in the SessionDetailHeader component.

In
`@src/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsRename.ts`:
- Around line 84-86: In useAgentSessionsRename's catch block (the one that
currently calls logger.error('Failed to rename session:', undefined, error)),
ensure the error is reported to Sentry by calling captureException(error, {
extra: { agentId, sessionId, projectPath: activeSession.projectRoot } }) (or if
you must keep UI state cleanup there, call captureException and then rethrow the
error); update the catch inside the useAgentSessionsRename hook so it both logs
via logger.error and calls captureException with the specified extra context
(agentId, sessionId, projectPath) to ensure failures are captured.

In
`@src/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsSearch.ts`:
- Around line 56-59: In useAgentSessionsSearch.ts update the catch block to
report the caught error to Sentry by importing and calling captureException from
src/utils/sentry.ts (in addition to the existing logger.error and
setSearchResults([])); pass the caught error and a short context object (e.g., {
fn: 'useAgentSessionsSearch', action: 'search' } or relevant search params) to
captureException so failures are tracked in production.

In `@src/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsStar.ts`:
- Around line 37-52: The IPC calls in useAgentSessionsStar (the block that calls
window.maestro.claude.updateSessionStarred and
window.maestro.agentSessions.setSessionStarred using activeSession.projectRoot,
sessionId and isNowStarred) can throw and currently leave local state
inconsistent; wrap those await calls in a try/catch, and on exception revert the
optimistic local state change (e.g., reset whatever state updater you used to
flip the star, such as setIsStarred or the sessions list mutation) so the UI
matches backend, then report the exception to Sentry (e.g.,
Sentry.captureException(err) or your app’s error reporting facade) and rethrow
or return appropriately so the error is tracked.
- Around line 28-35: The current toggle logic reads starredSessions from the
closure causing a race condition; update the handler to use the functional state
updater of setStarredSessions(prev => { ... }) so you operate on the latest
state (use prev to create a new Set, add/delete sessionId, and return it), and
remove starredSessions from the hook's dependency array since it will no longer
be referenced directly in the closure; keep references to setStarredSessions and
sessionId to locate the code.

In `@src/renderer/hooks/tabs/internal/useFilePreviewTabHandlers.ts`:
- Line 417: The handlers in useFilePreviewTabHandlers.ts currently abort
navigation on empty-file content by using the truthy check "if (!content)
return"; update these guards to specifically check for null (use "if (content
=== null) return") so empty strings are allowed and only a true read failure
stops navigation—make this change for the three occurrences flagged (the two
guards near lines 416-417, 464-465 and 510-511) to match the existing behavior
in handleReloadFileTab which already uses content === null.

In `@src/renderer/hooks/tabs/internal/useTerminalTabHandlers.ts`:
- Around line 27-41: The promise catch for
window.maestro.process.isTerminalBusy(ptySessionId) currently swallows errors;
modify the catch to report the error (e.g., call captureException or
captureMessage with the caught error) and then fallback to
closeTerminalTab(tabId). Locate the isTerminalBusy call in
useTerminalTabHandlers (the block that uses useModalStore.getState().openModal
and closeTerminalTab) and add error reporting inside the .catch handler before
invoking closeTerminalTab.

---

Nitpick comments:
In
`@src/__tests__/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsRename.test.ts`:
- Line 1: The test file mixes explicit Vitest imports and global usage by
calling afterEach without importing it; update the import statement (the one
that currently imports describe, it, expect, vi, beforeEach from 'vitest') to
also import afterEach so the test consistently uses explicit Vitest imports (or
alternatively remove all named imports and rely on globals—preferred: add
afterEach to the existing import list).

In
`@src/renderer/components/AgentSessionsBrowser/components/SearchModeDropdown.tsx`:
- Around line 15-43: The getModeIcon switch duplicates the icon mapping already
defined in MODES; replace the switch in getModeIcon with a lookup into MODES
(e.g., find the entry where entry.mode === mode) and return its icon, falling
back to MessageSquare if not found. Update getModeIcon to reference the const
MODES array and preserve the same return type and fallback behavior so
adding/changing modes only needs to be done in MODES.

In `@src/renderer/hooks/tabs/internal/useAITabHandlers.ts`:
- Around line 189-203: Remove the leftover debug logger.info calls in the
handleRequestTabRename flow: delete the three '[DEBUG renameTab]' logger.info
lines (the initial call, the "no session found" log, and the "tab found" log) in
useAITabHandlers.ts, or wrap them behind an existing debug flag if you prefer
conditional logging; keep the surrounding logic that uses selectActiveSession
and setSessions intact (references: handleRequestTabRename, selectActiveSession,
useSessionStore.getState(), setSessions).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e054f7e3-55e5-4310-b2a5-68a5f39c43ea

📥 Commits

Reviewing files that changed from the base of the PR and between 41e4204 and 70a249e.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (64)
  • src/__tests__/renderer/components/AgentSessionsBrowser/components/SearchModeDropdown.test.tsx
  • src/__tests__/renderer/components/AgentSessionsBrowser/components/SessionDetailStatsPanel.test.tsx
  • src/__tests__/renderer/components/AgentSessionsBrowser/components/SessionListHeader.test.tsx
  • src/__tests__/renderer/components/AgentSessionsBrowser/components/SessionListStatsBar.test.tsx
  • src/__tests__/renderer/components/AgentSessionsBrowser/components/SessionMessageBubble.test.tsx
  • src/__tests__/renderer/components/AgentSessionsBrowser/components/SessionMessagesView.test.tsx
  • src/__tests__/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsActivityEntries.test.ts
  • src/__tests__/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsAggregateStats.test.ts
  • src/__tests__/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsAutoView.test.ts
  • src/__tests__/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsFocusRestore.test.ts
  • src/__tests__/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsRename.test.ts
  • src/__tests__/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsResume.test.ts
  • src/__tests__/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsSearch.test.ts
  • src/__tests__/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsStar.test.ts
  • src/__tests__/renderer/components/AgentSessionsBrowser/utils/buildUsageStats.test.ts
  • src/__tests__/renderer/components/AgentSessionsBrowser/utils/messagesToLogEntries.test.ts
  • src/__tests__/renderer/components/AgentSessionsBrowser/utils/sessionProjectPath.test.ts
  • src/__tests__/renderer/hooks/tabs/internal/browserTabHelpers.test.ts
  • src/__tests__/renderer/hooks/tabs/internal/filePreviewTabHelpers.test.ts
  • src/__tests__/renderer/hooks/tabs/internal/testUtils.ts
  • src/__tests__/renderer/hooks/tabs/internal/unifiedCloseHelpers.test.ts
  • src/__tests__/renderer/hooks/tabs/internal/useAITabHandlers.test.ts
  • src/__tests__/renderer/hooks/tabs/internal/useBrowserTabHandlers.test.ts
  • src/__tests__/renderer/hooks/tabs/internal/useFilePreviewTabHandlers.test.ts
  • src/__tests__/renderer/hooks/tabs/internal/useScrollLogHandlers.test.ts
  • src/__tests__/renderer/hooks/tabs/internal/useTabDerivedState.test.ts
  • src/__tests__/renderer/hooks/tabs/internal/useTerminalTabHandlers.test.ts
  • src/__tests__/renderer/hooks/tabs/internal/useUnifiedTabHandlers.test.ts
  • src/renderer/components/AgentSessionsBrowser.tsx
  • src/renderer/components/AgentSessionsBrowser/AgentSessionsBrowser.tsx
  • src/renderer/components/AgentSessionsBrowser/components/SearchModeDropdown.tsx
  • src/renderer/components/AgentSessionsBrowser/components/SessionDetailHeader.tsx
  • src/renderer/components/AgentSessionsBrowser/components/SessionDetailStatsPanel.tsx
  • src/renderer/components/AgentSessionsBrowser/components/SessionListHeader.tsx
  • src/renderer/components/AgentSessionsBrowser/components/SessionListStatsBar.tsx
  • src/renderer/components/AgentSessionsBrowser/components/SessionListView.tsx
  • src/renderer/components/AgentSessionsBrowser/components/SessionMessageBubble.tsx
  • src/renderer/components/AgentSessionsBrowser/components/SessionMessagesView.tsx
  • src/renderer/components/AgentSessionsBrowser/components/SessionSearchBar.tsx
  • src/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsActivityEntries.ts
  • src/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsAggregateStats.ts
  • src/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsAutoView.ts
  • src/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsFocusRestore.ts
  • src/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsRename.ts
  • src/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsResume.ts
  • src/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsSearch.ts
  • src/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsStar.ts
  • src/renderer/components/AgentSessionsBrowser/index.ts
  • src/renderer/components/AgentSessionsBrowser/types.ts
  • src/renderer/components/AgentSessionsBrowser/utils/buildUsageStats.ts
  • src/renderer/components/AgentSessionsBrowser/utils/messagesToLogEntries.ts
  • src/renderer/components/AgentSessionsBrowser/utils/sessionProjectPath.ts
  • src/renderer/hooks/tabs/internal/browserTabHelpers.ts
  • src/renderer/hooks/tabs/internal/filePreviewTabHelpers.ts
  • src/renderer/hooks/tabs/internal/types.ts
  • src/renderer/hooks/tabs/internal/unifiedCloseHelpers.ts
  • src/renderer/hooks/tabs/internal/useAITabHandlers.ts
  • src/renderer/hooks/tabs/internal/useBrowserTabHandlers.ts
  • src/renderer/hooks/tabs/internal/useFilePreviewTabHandlers.ts
  • src/renderer/hooks/tabs/internal/useScrollLogHandlers.ts
  • src/renderer/hooks/tabs/internal/useTabDerivedState.ts
  • src/renderer/hooks/tabs/internal/useTerminalTabHandlers.ts
  • src/renderer/hooks/tabs/internal/useUnifiedTabHandlers.ts
  • src/renderer/hooks/tabs/useTabHandlers.ts
💤 Files with no reviewable changes (1)
  • src/renderer/components/AgentSessionsBrowser.tsx

Comment thread src/renderer/components/AgentSessionsBrowser/AgentSessionsBrowser.tsx Outdated
Comment on lines +28 to +35
const newStarred = new Set(starredSessions);
const isNowStarred = !newStarred.has(sessionId);
if (isNowStarred) {
newStarred.add(sessionId);
} else {
newStarred.delete(sessionId);
}
setStarredSessions(newStarred);

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 | ⚡ Quick win

Fix race condition in state update.

Line 28 reads starredSessions from the closure. If the user rapidly clicks star on multiple sessions before React batches state updates, both callbacks will see the same stale starredSessions value, causing lost updates. Use the functional form of setState to read the latest state.

🔒 Proposed fix
-		const newStarred = new Set(starredSessions);
-		const isNowStarred = !newStarred.has(sessionId);
-		if (isNowStarred) {
-			newStarred.add(sessionId);
-		} else {
-			newStarred.delete(sessionId);
-		}
-		setStarredSessions(newStarred);
+		let isNowStarred = false;
+		setStarredSessions((prev) => {
+			const newStarred = new Set(prev);
+			isNowStarred = !newStarred.has(sessionId);
+			if (isNowStarred) {
+				newStarred.add(sessionId);
+			} else {
+				newStarred.delete(sessionId);
+			}
+			return newStarred;
+		});

Also update the dependency array to remove starredSessions since it's no longer read directly in the closure:

-	[starredSessions, activeSession?.projectRoot, agentId, onUpdateTab]
+	[activeSession?.projectRoot, agentId, onUpdateTab]
📝 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 newStarred = new Set(starredSessions);
const isNowStarred = !newStarred.has(sessionId);
if (isNowStarred) {
newStarred.add(sessionId);
} else {
newStarred.delete(sessionId);
}
setStarredSessions(newStarred);
let isNowStarred = false;
setStarredSessions((prev) => {
const newStarred = new Set(prev);
isNowStarred = !newStarred.has(sessionId);
if (isNowStarred) {
newStarred.add(sessionId);
} else {
newStarred.delete(sessionId);
}
return newStarred;
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsStar.ts`
around lines 28 - 35, The current toggle logic reads starredSessions from the
closure causing a race condition; update the handler to use the functional state
updater of setStarredSessions(prev => { ... }) so you operate on the latest
state (use prev to create a new Set, add/delete sessionId, and return it), and
remove starredSessions from the hook's dependency array since it will no longer
be referenced directly in the closure; keep references to setStarredSessions and
sessionId to locate the code.

Comment thread src/renderer/hooks/tabs/internal/useFilePreviewTabHandlers.ts Outdated
Comment thread src/renderer/hooks/tabs/internal/useTerminalTabHandlers.ts
@pedramamini

Copy link
Copy Markdown
Collaborator

Hi @reachrazamair — thank you for this contribution! 🙏 This is a genuinely high-quality decomposition: collapsing the 2,044-line useTabHandlers and the 1,512-line AgentSessionsBrowser into the coordinator pattern while keeping the public surface unchanged, calling out and regression-testing the tricky invariants (SSH stats keyed on projectRoot, token-zeroing on resume, tool-call filtering), and landing 170+ new tests is exactly the kind of god-component reduction we want.

I reviewed the PR alongside the Greptile and CodeRabbit passes and verified the findings against the code. There's one item I'd like fixed before approval, plus a few smaller things worth a look. No merge conflicts — the branch is clean against rc.

🔴 Please fix before merge

useAgentSessionsStar.toggleStar — optimistic update with no rollback (src/renderer/components/AgentSessionsBrowser/hooks/useAgentSessionsStar.ts)

Both bots flagged this and I confirmed it by reading the code. setStarredSessions(newStarred) commits the local set, then the await window.maestro…SessionStarred(...) IPC call runs with no try/catch. If that call rejects (transient error, dropped SSH connection), the local state is already changed, onUpdateTab never fires, and the promise rejects unhandled — leaving the UI showing a star state the backend never persisted, with no recovery path.

Two small changes resolve it:

  • Wrap the IPC calls in try/catch and revert setStarredSessions to the prior set on failure.
  • Build the next set with the functional updater (setStarredSessions(prev => …)) rather than reading starredSessions from the closure, so rapid successive toggles don't clobber each other.

🟡 Worth addressing (your call)

  • Error surfacing consistencyuseAgentSessionsSearch and useAgentSessionsRename catch IPC errors but only logger.error them (rename also silently runs cancelRename), and useTerminalTabHandlers .catch(() => closeTerminalTab(...)) swallows the isTerminalBusy failure. Per the error-handling guidance in CLAUDE.md, recoverable IPC failures are fine to handle, but consider whether these should surface to the user / be captured for Sentry rather than disappearing.
  • Debounce reuseuseAgentSessionsSearch hand-rolls a useRef<Timeout> + setTimeout debounce. The repo already ships useDebouncedValue / useDebouncedCallback (src/renderer/hooks/utils/useThrottle.ts), and CLAUDE-PERFORMANCE.md recommends them for search. Functionally your version is fine, but reusing the canonical helper avoids the kind of reimplementation that doc explicitly calls out.

⚪ Minor nits

  • SessionDetailHeader.tsx — the tooltip/hover absolute time uses viewingSession.timestamp while the visible relative label uses modifiedAt; they'll disagree when create ≠ last-modified.
  • useFilePreviewTabHandlers.tsif (!content) return treats a legitimately empty file as a read failure and aborts navigation silently.
  • useAgentSessionsFocusRestore.test.tsafterEach is used without importing it (works via Vitest globals, but inconsistent with siblings).

Note on one bot suggestion

Greptile recommends replacing the useEffect + setTimeout(() => ref.focus()) focus pattern with useFocusAfterRender(...). Heads up that no such utility exists in the repo, and I couldn't find that focus convention in any of the CLAUDE*.md files — so please disregard that specific suggestion and use your own judgment on the focus timing. (The handleKeyDown memoization point it raised is a reasonable micro-optimization if you want it, but not required.)

Thanks again — the star rollback is the only real blocker; everything else is polish. Ping me once that's in and I'll take another pass. 🎬

…lers

- Fix missing afterEach imports in focusRestore and rename test files
- Remove duplicate Escape handling from handleKeyDown; modal layer is sole handler
- Fix SessionDetailHeader tooltip to use modifiedAt consistently with the visible label
- Add captureException to useAgentSessionsRename and useAgentSessionsSearch catch blocks
- Fix useAgentSessionsStar: functional state updater prevents stale-closure race, try/catch reverts optimistic star on IPC failure
- Fix useFilePreviewTabHandlers nav guards: !content -> content === null so empty files navigate correctly
- Remove leftover [DEBUG renameTab] logger.info calls from useAITabHandlers
- Add captureMessage(warning) to useTerminalTabHandlers isTerminalBusy catch
- Replace getModeIcon switch with MODES.find lookup in SearchModeDropdown
React schedules state updater callbacks asynchronously, so deriving
isNowStarred as a side-effect inside setStarredSessions caused the IPC
call to always receive false. Read starredSessions synchronously from
the closure to compute isNowStarred, then use the functional updater
only for the state mutation to keep rapid multi-session toggles correct.
# Conflicts:
#	src/renderer/hooks/tabs/useTabHandlers.ts
@reachrazamair

Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@reachrazamair
reachrazamair merged commit 61e5623 into rc Jun 1, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants