Skip to content

refactor: migrate to canonical shared hooks (Phase 09)#834

Merged
pedramamini merged 1 commit intoRunMaestro:rcfrom
jSydorowicz21:dedup/phase-09-shared-hooks
Apr 16, 2026
Merged

refactor: migrate to canonical shared hooks (Phase 09)#834
pedramamini merged 1 commit intoRunMaestro:rcfrom
jSydorowicz21:dedup/phase-09-shared-hooks

Conversation

@jSydorowicz21
Copy link
Copy Markdown
Contributor

@jSydorowicz21 jSydorowicz21 commented Apr 13, 2026

Summary

Migrates a conservative subset of hook patterns to canonical shared hooks. This phase is intentionally narrow - most call sites had semantic subtleties that would have caused regressions if blindly replaced.

Net: -19 lines across 4 files

09A - useEventListener migrations (4 sites)

Replaced raw addEventListener / removeEventListener pairs with the canonical useEventListener hook from src/renderer/hooks/utils/useEventListener.ts:

  • useTourActions.ts - tour:action event
  • SessionList.tsx - tour:action event
  • useHandsOnTimeTracker.ts - beforeunload event
  • MarketplaceModal.tsx - keydown event in PlaybookDetailView

09A - useFocusAfterRender migrations (0 sites)

Intentionally zero - the canonical hook uses useLayoutEffect with NO dep array and re-runs every render while its condition is true. Replacing the common useEffect(() => { setTimeout(() => ref.current?.focus(), 50); }, []) pattern would cause the focus to be re-stolen on every subsequent render, breaking the app.

09B - (0 sites migrated)

useActiveSession() semantic gap: selectActiveSession falls back to state.sessions[0] when there's no active session; inline sessions.find returns undefined. Blindly migrating would surface stale-session data instead of the "no active session" branch.

Debounce/throttle misfits: Every hand-rolled debounce/throttle has a reason the canonical hook doesn't fit.

Supersedes #823 (accidentally closed during rebase).

Test plan

  • npm run lint passes (all 3 tsconfigs)
  • npx prettier --check . passes
  • Tour events still fire
  • Marketplace playbook detail keyboard shortcuts work
  • beforeunload handler fires (active-time tracking)

Risk

Very low. 4 behavior-preserving migrations, no component changes.

Summary by CodeRabbit

  • Refactor
    • Centralized event listener handling across the app (preview keyboard shortcuts, tour actions, session menu toggles, and session time tracking). Behavior is unchanged; no visible impact to users.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 13, 2026

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: 6a87c6c7-9429-48db-80f1-19369a804c5b

📥 Commits

Reviewing files that changed from the base of the PR and between 0548426 and 077a44b.

📒 Files selected for processing (4)
  • src/renderer/components/MarketplaceModal.tsx
  • src/renderer/components/SessionList/SessionList.tsx
  • src/renderer/hooks/session/useHandsOnTimeTracker.ts
  • src/renderer/hooks/ui/useTourActions.ts
✅ Files skipped from review due to trivial changes (1)
  • src/renderer/components/SessionList/SessionList.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/renderer/hooks/ui/useTourActions.ts
  • src/renderer/components/MarketplaceModal.tsx

📝 Walkthrough

Walkthrough

Replaced manual useEffect-based window event registrations with a centralized useEventListener hook across four files, preserving existing handler logic and behavior (keyboard shortcuts, custom tour:action events, and beforeunload persistence).

Changes

Cohort / File(s) Summary
Keyboard shortcuts
src/renderer/components/MarketplaceModal.tsx
Replaced manual window.addEventListener('keydown', ...)/cleanup with useEventListener('keydown', ...). Handler now types the param as Event and casts to KeyboardEvent before using metaKey/altKey/shiftKey/key/target/preventDefault() and preserves preview scrolling logic (ignore inputs, pageHeight = clientHeight * 0.9, meta/alt arrow behaviors).
Tour action handlers
src/renderer/components/SessionList/SessionList.tsx, src/renderer/hooks/ui/useTourActions.ts
Replaced useEffect registration of tour:action with useEventListener('tour:action', ...). Handlers still cast to CustomEvent<{ type: string; value?: string }> and perform the same state updates (toggle menuOpen; set right panel tab/open/close; handle ensureAiTab by reading session/tab stores and selecting target tab).
Lifecycle event persistence
src/renderer/hooks/session/useHandsOnTimeTracker.ts
Replaced manual beforeunload listener with useEventListener('beforeunload', ...). Synchronous handler behavior unchanged: if accumulated time > 0, call addTotalActiveTimeMsRef.current(...) and reset accumulator to 0.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I hopped through listeners, neat and light,

Swapped add/remove for a hook's delight,
Keys and tours and unloads all cared for,
Handlers intact — I left logic pure,
Happy hops, the code now bounces more!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.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 title clearly and specifically describes the main change: migrating hook patterns to canonical shared hooks in Phase 09, which aligns directly with the changeset's four useEventListener migrations.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@pedramamini
Copy link
Copy Markdown
Collaborator

Thanks for the contribution, @jSydorowicz21! This is a clean, well-scoped refactor.

The 4 useEventListener migrations are all behavior-preserving — the hook's handlerRef pattern means closures stay fresh without needing deps. The detailed rationale for what was intentionally not migrated (listener options, conditional registration, semantic gaps in useActiveSession, etc.) is especially appreciated — it shows real care in avoiding regressions.

No merge conflicts, net -19 lines, LGTM. Approving.

@jSydorowicz21 jSydorowicz21 marked this pull request as ready for review April 13, 2026 22:40
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 13, 2026

Greptile Summary

This PR migrates 4 raw addEventListener/removeEventListener pairs across 4 files to the canonical useEventListener hook, yielding a net -19 lines while preserving exact runtime behavior. The PR description clearly documents the intentionally skipped cases (useFocusAfterRender, useActiveSession, conditional/document-scoped listeners), all of which are valid exclusions — confirmed by reading the canonical hooks and the event dispatch sites.

Confidence Score: 5/5

Safe to merge — all 4 migrations are behavior-preserving and the exclusion rationale is sound.

All changed call sites correctly target window events, the canonical hook's handlerRef pattern eliminates stale-closure risk, and every intentionally-skipped site (visibilitychange on document, conditional listeners, useFocusAfterRender re-render semantics) is correctly explained. No P0 or P1 findings.

No files require special attention.

Important Files Changed

Filename Overview
src/renderer/hooks/ui/useTourActions.ts Replaced raw useEffect + window.addEventListener('tour:action', ...) with useEventListener; correct because tour:action is dispatched via window.dispatchEvent in useTour.tsx and TourOverlay.tsx.
src/renderer/components/SessionList/SessionList.tsx Replaced raw useEffect + window.addEventListener('tour:action', ...) with useEventListener for hamburger-menu tour actions; the existing conditional document.addEventListener('keydown', ...) for Escape is correctly left as-is.
src/renderer/hooks/session/useHandsOnTimeTracker.ts Migrated beforeunload handler to useEventListener; visibilitychange correctly retained as a raw document.addEventListener since useEventListener only targets window.
src/renderer/components/MarketplaceModal.tsx Migrated keydown handler in PlaybookDetailView sub-component to useEventListener; the conditional mousedown/document listener for the dropdown is correctly left unchanged.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Component Mounts] --> B[useEventListener called]
    B --> C[handlerRef.current = handler]
    C --> D[useEffect runs\neventType dep]
    D --> E[window.addEventListener\neventType, listener]

    E --> F{Event Fires?}
    F -->|yes| G[listener invokes\nhandlerRef.current]
    G --> H[Handler executes\nwith latest closure]

    F -->|re-render| I[handlerRef.current updated\nto latest handler]
    I --> F

    A --> J[Component Unmounts]
    J --> K[useEffect cleanup runs]
    K --> L[window.removeEventListener\neventType, listener]

    subgraph Migration Sites
        M[tour:action — useTourActions.ts]
        N[tour:action — SessionList.tsx]
        O[beforeunload — useHandsOnTimeTracker.ts]
        P[keydown — PlaybookDetailView]
    end

    subgraph Intentionally NOT Migrated
        Q[visibilitychange — document target]
        R[keydown w/ condition — SessionList Escape]
        S[mousedown — conditional dropdown]
    end
Loading

Reviews (1): Last reviewed commit: "refactor: migrate to canonical shared ho..." | Re-trigger Greptile

@jSydorowicz21 jSydorowicz21 added refactor Clean-up needs ready to merge This PR is ready to merge and removed refactor Clean-up needs ready to merge This PR is ready to merge labels Apr 15, 2026
Sub-phase 09A: useEventListener migrations
- useTourActions: replace useEffect + window.addEventListener('tour:action')
- SessionList: replace useEffect + window.addEventListener('tour:action')
- useHandsOnTimeTracker: replace useEffect + window.addEventListener('beforeunload')
- MarketplaceModal (PlaybookDetailView): replace useEffect + window.addEventListener('keydown')

Sub-phase 09B: no migrations
- useActiveSession has semantic drift (falls back to first session vs inline
  `?? null`), unsafe for existing `sessions.find()` call sites
- Hand-rolled debounce sites (useAutoRunUndo, useSessionDebounce, useTabHoverOverlay,
  GitStatusWidget) are per-session keyed, dep-cleanup-scoped, or shared-timer
  patterns that useDebouncedCallback does not express
- Most setTimeout + focus patterns are imperative (inside click/key handlers),
  which useFocusAfterRender (useLayoutEffect + re-runs every render) does not fit

Skipped in 09A
- keydown listeners using capture: true (DirectorNotesModal, SettingsSearch,
  NewInstanceModal, EditAgentModal, PhaseReviewScreen, UsageDashboardModal)
  - hook does not support listener options
- listeners on document or with passive: true (modal visibilitychange handler,
  activityBus, scroll tracker) - hook is window-only
- listeners conditionally registered on isOpen - hook is unconditional
- useRemoteHandlers: async handler + 380-line body + closure capture, too risky
- useMobileLandscape: pairs addEventListener with initial imperative call
@jSydorowicz21 jSydorowicz21 force-pushed the dedup/phase-09-shared-hooks branch from 0548426 to 077a44b Compare April 15, 2026 05:52
@jSydorowicz21 jSydorowicz21 added refactor Clean-up needs ready to merge This PR is ready to merge labels Apr 15, 2026
@pedramamini pedramamini merged commit 4fe4baf into RunMaestro:rc Apr 16, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved ready to merge This PR is ready to merge refactor Clean-up needs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants