Skip to content

feat(dashboard): scrollable, autocomplete, bookmarkable project selector#1467

Merged
gsxdsm merged 7 commits into
Runfusion:mainfrom
buihongduc132:upstream-pr/project-selector-enhancements
Jun 8, 2026
Merged

feat(dashboard): scrollable, autocomplete, bookmarkable project selector#1467
gsxdsm merged 7 commits into
Runfusion:mainfrom
buihongduc132:upstream-pr/project-selector-enhancements

Conversation

@buihongduc132

@buihongduc132 buihongduc132 commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Enhances the Fusion dashboard project selector dropdown with 3 features:

1. Scrollable dropdown

  • max-height: min(480px, calc(100vh - 120px)) with overflow-y: auto
  • scrollIntoView({ block: "nearest" }) on keyboard navigation
  • Thin scrollbar styling for Firefox and WebKit

2. Autocomplete / type-ahead

  • Always-visible search input (removed 5+ project threshold)
  • HighlightMatch component renders matched substrings with <mark> (bold + accent underline)
  • Exact match detection (case-insensitive) with "Exact" badge + "press Enter" indicator
  • Auto-highlight first result when filtering; auto-select exact match on Enter
  • No-results state with search icon and query display
  • Filters by both project name and path

3. Project bookmarking (localStorage)

  • useProjectBookmarks hook: fusion_project_bookmarks key, JSON array of IDs
  • Star toggle (lucide Star) on each item — opacity-on-hover pattern, filled when bookmarked
  • "Bookmarked" section at top of dropdown (above Recent)
  • stopPropagation on star click — toggling does not trigger selection or close dropdown
  • Handles corrupt JSON, non-string filter, localStorage quota

Refactor

  • Header.tsx: Removed inline ProjectSelector (~130 lines) and now imports the standalone component from ./ProjectSelector with all 3 enhancements

Files changed

File Change
ProjectSelector.tsx +335 lines: autocomplete, bookmarks, scroll logic
ProjectSelector.css +104 lines: scroll, highlight, bookmark, exact-match styles
Header.tsx -128 lines: removed inline copy, imports standalone
ProjectSelector.test.tsx +446 lines: scroll, autocomplete, bookmark tests
useProjectBookmarks.ts NEW: localStorage bookmark hook
useProjectBookmarks.test.ts NEW: 10 hook tests

Test plan

  • 131 tests pass (ProjectSelector.test.tsx + useProjectBookmarks.test.ts)
  • tsc --noEmit clean — zero errors
  • pnpm build:client succeeds
  • Deployed and verified in browser — dropdown scrolls, autocomplete works, bookmarks persist

Summary by CodeRabbit

  • New Features

    • Project bookmarks: star favorites with bookmark-first ordering and bookmark toggles
    • Shared project selector: replaces inline selector with a standalone component and always-visible search input
  • Bug Fixes / Reliability

    • Improved dropdown scrolling, exact-match indicator, keyboard/type-ahead behavior, and clear-on-close search
    • Robust retry/backoff and typed engine error handling for background operations
  • Tests

    • Expanded coverage for project selector, bookmarks, search/autocomplete, retry/backoff, and error classification

Fusion Worker and others added 4 commits June 7, 2026 00:15
…th exponential backoff

Add domain-specific error classes (engine-errors.ts) replacing generic
catch blocks with typed, classifiable errors:
- EngineError base with code/retryable/details fields
- TransientError hierarchy: NetworkError, ServiceUnavailableError, TimeoutError
- PermanentError hierarchy: ConfigurationError, ValidationError
- RateLimitError (triggers global pause, not local retry)
- classifyThrownError() bridge from legacy string-based detection

Add general-purpose retry with exponential backoff (retry-with-backoff.ts):
- withRetry() wraps async ops with configurable retries for transient errors
- Exponential backoff: delay = min(baseMs * 2^attempt, maxMs)
- Three jitter strategies: full (default), equal, none
- Per-attempt timeout support via timeoutMs option
- AbortSignal integration for task pause/cancel/shutdown
- Custom isRetryable check for domain-specific retry logic
- withRetryResult() variant returning retry metadata
- Never retries rate-limit errors (delegates to global pause)

53 new tests covering error hierarchy, backoff math, cancellable sleep,
retry on transient errors, non-retryable fail-fast, rate-limit bypass,
abort cancellation, per-attempt timeout, custom predicates.
- Fix: ProjectSelector dropdown now scrolls (max-height + overflow-y)
  when project list exceeds visible area, with scrollIntoView on
  keyboard navigation
- Feat: Always-visible search input with type-ahead filtering,
  HighlightMatch text highlighting, exact match detection + auto-select
  on Enter
- Feat: Project bookmarking via localStorage (useProjectBookmarks hook),
  star toggle on each item, bookmarked section shown at top of dropdown
- Refactor: Header.tsx now imports standalone ProjectSelector instead
  of using an inline copy that lacked these features
- Tests: 131 tests passing across ProjectSelector + useProjectBookmarks
… match

- Make onSelect optional in ProjectSelectorProps to match Header usage
  where onSelectProject may be undefined (fixes runtime crash)
- Exclude currentProject from exact match detection so typing the
  current project name doesn't show misleading exact-match banner
- Addresses Gemini Code Assist review findings
…ements

feat(dashboard): scrollable, autocomplete, bookmarkable project selector
@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e6f857ed-9061-45d0-b8e4-e60c1a491a3e

📥 Commits

Reviewing files that changed from the base of the PR and between 266b18e and a27921a.

📒 Files selected for processing (7)
  • .changeset/fn-1467-pr-feedback-fixes.md
  • packages/dashboard/app/components/Header.tsx
  • packages/dashboard/app/components/ProjectSelector.tsx
  • packages/dashboard/app/components/__tests__/ProjectSelector.test.tsx
  • packages/engine/src/__tests__/retry-with-backoff.test.ts
  • packages/engine/src/engine-errors.ts
  • packages/engine/src/retry-with-backoff.ts
✅ Files skipped from review due to trivial changes (1)
  • .changeset/fn-1467-pr-feedback-fixes.md
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/engine/src/engine-errors.ts
  • packages/engine/src/retry-with-backoff.ts
  • packages/dashboard/app/components/Header.tsx
  • packages/engine/src/tests/retry-with-backoff.test.ts
  • packages/dashboard/app/components/tests/ProjectSelector.test.tsx
  • packages/dashboard/app/components/ProjectSelector.tsx

📝 Walkthrough

Walkthrough

Adds bookmarking and exact-match autocomplete to the dashboard ProjectSelector (now a standalone component) and introduces a typed EngineError hierarchy plus exponential-backoff retry utilities with cancellable delays and per-attempt timeouts.

Changes

Dashboard Project Selector Enhancement

Layer / File(s) Summary
Project Bookmark Hook
packages/dashboard/app/hooks/useProjectBookmarks.ts, packages/dashboard/app/hooks/__tests__/useProjectBookmarks.test.ts
useProjectBookmarks React hook manages a Set of bookmarked project IDs persisted to localStorage (fusion_project_bookmarks). Exposes toggleBookmark(projectId) and isBookmarked(projectId). Tests validate persistence, toggle behavior, and robustness to corrupted storage.
ProjectSelector Component Enhancement
packages/dashboard/app/components/ProjectSelector.tsx, packages/dashboard/app/components/__tests__/ProjectSelector.test.tsx
Component integrates bookmark toggles, exact-match computation for autocomplete, bookmarked-first/recent/others ordering, ref-based scroll-into-view for keyboard navigation, always-visible search input, search-clearing on close/select, Enter-selection for exact matches, and HighlightMatch for matched substrings. Tests cover bookmark interactions, highlighting, exact-match behavior, and search/reset flows.
ProjectSelector Styling
packages/dashboard/app/components/ProjectSelector.css, packages/dashboard/app/components/AgentsView.css, packages/dashboard/app/components/ChatView.css
Adds dropdown max-height with overflow/overscroll behavior and cross-browser scrollbar styling; exact-match banner/badge and matched-text highlight rules; bookmark-star toggle visibility/hover/focus styles; minor layout fixes (flex: 1 / width adjustments).
Header Component Refactoring
packages/dashboard/app/components/Header.tsx, .changeset/fn-1467-pr-feedback-fixes.md
Removes inline ProjectSelector implementation, imports the standalone ProjectSelector and CSS, updates render call and remaps callback prop from onSelectProject to onSelect. Changeset records the patch release entry.

Engine Retry Utilities with Error Hierarchy

Layer / File(s) Summary
Typed Engine Error Hierarchy
packages/engine/src/engine-errors.ts
Adds abstract EngineError with code, retryable, optional details, and cause. Adds Transient/Network/Timeout/ServiceUnavailable subclasses and Permanent/Configuration/Validation/RateLimit subclasses. Implements classifyThrownError(err) and isRetryableError type guard.
Retry Logic Implementation
packages/engine/src/retry-with-backoff.ts
Adds JitterStrategy type and RetryOptions/RetryResult<T> interfaces. Implements computeBackoff with jitter modes, cancellableSleep for abort-aware delays, internal withTimeout, withRetry loop (error classification, non-retry on usage-limit, optional custom isRetryable, onRetry callback), and withRetryResult returning value with retries and elapsed time.
Retry Test Coverage
packages/engine/src/__tests__/retry-with-backoff.test.ts
Large suite validating error types/classification, backoff jitter strategies and capping, cancellableSleep abort behavior, withRetry success/retry/exhaustion/abort/custom-predicate scenarios with fake timers, and withRetryResult metadata.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • gsxdsm

Poem

A rabbit hops through bookmarked skies,
Stars for projects twinkle in its eyes;
Exact matches glow, highlights sing,
Retries step back with measured spring.
Code and UI dance—both take flight! 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: adding scrollability, autocomplete/type-ahead, and bookmarking features to the project selector component.
Docstring Coverage ✅ Passed Docstring coverage is 91.67% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

@greptile-apps

greptile-apps Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR enhances the dashboard project selector with scrollable dropdown, autocomplete/type-ahead with HighlightMatch, and localStorage-backed bookmarks, while also extracting the inline component from Header.tsx into a standalone ProjectSelector and adding a structured error-type hierarchy (engine-errors.ts) with a general-purpose withRetry utility. The iteration addresses all previously-flagged regressions — onSelect null guards, bookmarked projects disappearing during search, and the contradictory exact-match/no-results banners are all resolved.

  • ProjectSelector: onSelect is now safely optional throughout (onSelect?.()); during search bookmarkedAndRecentIds is empty so all matching projects surface in others, eliminating the hide-but-also-exclude bug.
  • engine-errors / retry-with-backoff: withTimeout now throws a typed TimeoutError directly; RateLimitError is unconditionally short-circuited before any custom isRetryable check; the dead if (retries > 0) scaffold is gone.

Confidence Score: 5/5

Safe to merge — all previously flagged regressions are addressed and no new blocking issues introduced.

The three dashboard defects and two engine defects from prior review rounds are all corrected. The one remaining finding is a minor cleanup pattern in cancellableSleep where the resolve reassignment is dead code — cosmetic and no correctness impact under normal usage.

No files require special attention.

Important Files Changed

Filename Overview
packages/dashboard/app/components/ProjectSelector.tsx Adds autocomplete, bookmarks, and scroll; previous P1s (null-guard on onSelect, bookmarked projects vanishing during search, contradictory exact-match/no-results banners) are all resolved.
packages/dashboard/app/hooks/useProjectBookmarks.ts New localStorage bookmark hook; handles corrupt JSON, non-array values, non-string items, and quota errors gracefully.
packages/engine/src/retry-with-backoff.ts New retry-with-backoff utility; well-structured with abort support, configurable jitter, and RateLimitError guard. Minor: abort listener cleanup in cancellableSleep doesn't work as intended (resolve reassignment is dead code).
packages/engine/src/engine-errors.ts New structured error hierarchy; classifyThrownError bridges legacy string-based detection; withTimeout now correctly throws TimeoutError instead of a raw Error.
packages/dashboard/app/components/Header.tsx Removes ~130-line inline ProjectSelector and delegates to the standalone component; render site guarded with onSelectProject && to avoid passing undefined.

Reviews (4): Last reviewed commit: "fix: address PR review feedback (#1467)" | Re-trigger Greptile

Comment thread packages/dashboard/app/components/ProjectSelector.tsx
Comment thread packages/dashboard/app/components/ProjectSelector.tsx
Comment thread packages/engine/src/retry-with-backoff.ts Outdated
Comment thread packages/engine/src/retry-with-backoff.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

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

⚠️ Outside diff range comments (1)
packages/dashboard/app/components/ProjectSelector.tsx (1)

297-305: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Add optional chaining for onSelect call.

Same issue as the keyboard handler: onSelect is optional but called directly at line 300. If undefined, clicking a project will crash.

🛡️ Proposed fix
   const handleSelectProject = useCallback(
     (project: ProjectInfo) => {
-      onSelect(project);
+      onSelect?.(project);
       setIsOpen(false);
       setSearchQuery("");
     },
     [onSelect]
   );
🤖 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 `@packages/dashboard/app/components/ProjectSelector.tsx` around lines 297 -
305, handleSelectProject currently calls the potentially-undefined callback
onSelect directly, which can crash; change the invocation to use optional
chaining (call onSelect?.(project)) so it only executes when provided, leaving
the rest of the logic (setIsOpen(false), setSearchQuery("")) intact; update the
useCallback body for handleSelectProject to use onSelect?.(project) and keep the
dependency array as [onSelect].
🤖 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 `@packages/dashboard/app/components/Header.tsx`:
- Around line 839-846: The desktop/tablet rendering of StandaloneProjectSelector
should also guard that onSelectProject is provided; update the conditional that
currently checks {!isMobile && projects.length >= 1 && onViewAllProjects} to
additionally require onSelectProject, so StandaloneProjectSelector is only
rendered when onSelectProject is defined; ensure the props passed (projects,
currentProject, onViewAll, onSelect) remain unchanged but that onSelect receives
a defined function to avoid crashes in StandaloneProjectSelector /
ProjectSelector.

In `@packages/dashboard/app/components/ProjectSelector.tsx`:
- Around line 222-250: In ProjectSelector.tsx update the Enter key handling so
the optional onSelect prop is invoked with optional chaining for highlighted
items: replace direct calls to onSelect(displayProjects.bookmarked[...]),
onSelect(displayProjects.recent[...]) and onSelect(displayProjects.others[...])
with onSelect?.(...) respectively; leave the View All path (onViewAll()),
setIsOpen(false) and setSearchQuery("") behavior unchanged so Enter no longer
throws when onSelect is undefined.

In `@packages/engine/src/engine-errors.ts`:
- Line 247: The regex in the conditional is using unnecessary backslash-escaped
quotes which triggers the no-useless-escape lint rule; update the regex in the
if condition that tests message (the /.../i.test(message) expression) to remove
the redundant backslashes and use a plain alternation like
"type":"server_error"|"code":"server_error" (keep the /.../i literal and
.test(message) unchanged) so the pattern reads correctly without escaped quotes.

In `@packages/engine/src/retry-with-backoff.ts`:
- Around line 43-45: Short-circuit typed RateLimitError before running custom
retry checks by detecting the typed error with classifyThrownError or the
EngineError/RateLimitError type before inspecting message text: in the retry
logic where you call the isRetryable callback (and similarly in the block around
where isUsageLimitError is used), first call classifyThrownError(thrown) (or
check thrown instanceof RateLimitError if available) and immediately treat
classified "RateLimitError" as non-retryable, returning/throwing early so a
broad isRetryable implementation cannot override the global rate-limit pause.
- Around line 246-248: Remove the unused retry bookkeeping variables to satisfy
the linter: delete the declarations of startTime and retryCount in
retry-with-backoff.ts (the const startTime = Date.now(); and let retryCount = 0;
lines) and remove any other stray retryCount/startTime declarations (e.g., the
occurrence around line 301). Keep lastError (let lastError: EngineError |
undefined;) if it is referenced elsewhere; otherwise remove it as well. Ensure
no remaining references to these removed symbols remain in functions such as the
retryWithBackoff logic so TypeScript no-unused-vars errors are resolved.
- Around line 170-210: withTimeout currently aborts an internal AbortController
but never passes its signal to the attempt function, so timed-out attempts keep
running; change withTimeout<T>(fn: () => Promise<T>, ...) to withTimeout<T>(fn:
(signal?: AbortSignal) => Promise<T>, ...) and invoke fn(ac.signal) so the
attempt can cooperatively abort; keep the existing parentSignal linking and
timer logic but ensure ac.abort(...) is called on parent abort or timeout (as it
already is) and that you update all call sites (including the withRetry usage
around lines 231-260) to accept/pass an AbortSignal into their attempt
functions.

---

Outside diff comments:
In `@packages/dashboard/app/components/ProjectSelector.tsx`:
- Around line 297-305: handleSelectProject currently calls the
potentially-undefined callback onSelect directly, which can crash; change the
invocation to use optional chaining (call onSelect?.(project)) so it only
executes when provided, leaving the rest of the logic (setIsOpen(false),
setSearchQuery("")) intact; update the useCallback body for handleSelectProject
to use onSelect?.(project) and keep the dependency array as [onSelect].
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 418cc0d4-95ce-438c-afd1-ee9113299f9a

📥 Commits

Reviewing files that changed from the base of the PR and between ba16805 and 4b870c2.

📒 Files selected for processing (9)
  • packages/dashboard/app/components/Header.tsx
  • packages/dashboard/app/components/ProjectSelector.css
  • packages/dashboard/app/components/ProjectSelector.tsx
  • packages/dashboard/app/components/__tests__/ProjectSelector.test.tsx
  • packages/dashboard/app/hooks/__tests__/useProjectBookmarks.test.ts
  • packages/dashboard/app/hooks/useProjectBookmarks.ts
  • packages/engine/src/__tests__/retry-with-backoff.test.ts
  • packages/engine/src/engine-errors.ts
  • packages/engine/src/retry-with-backoff.ts

Comment thread packages/dashboard/app/components/Header.tsx Outdated
Comment thread packages/dashboard/app/components/ProjectSelector.tsx
Comment thread packages/engine/src/engine-errors.ts Outdated
Comment thread packages/engine/src/retry-with-backoff.ts Outdated
Comment thread packages/engine/src/retry-with-backoff.ts
Comment thread packages/engine/src/retry-with-backoff.ts Outdated
Added flex: 1 to .agents-view so it expands to fill the
project-content parent instead of shrinking to content width.
@gsxdsm

gsxdsm commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator

Thanks! Please address comments and will get it merged in

Same pattern as agents-view fix — .chat-view had display:flex + height:100%
but no flex or width, so it shrank to content width (~270px) instead of
filling the .project-content parent.
Comment thread packages/dashboard/app/components/ProjectSelector.tsx
- guard optional project selection callbacks and keep bookmarked search matches visible
- use structured timeout/rate-limit handling in retry backoff
- add focused regressions for selector and retry behavior

Note: pnpm test passed the merge gate but hit pre-existing non-blocking engine mock failures in changed-package tests.
@gsxdsm

gsxdsm commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Same issue as the keyboard handler: onSelect is optional but called directly at line 300.

Not safe to merge without addressing the null-call and bookmarked-during-search bugs.

Addressed in a27921a: click and keyboard selection now guard optional onSelect, desktop rendering requires onSelectProject, bookmarked/recent search matches remain visible, the exact-match/no-results contradiction is covered, and the retry/backoff comments are fixed. Validation run: targeted selector/retry tests passed, pnpm lint passed, pnpm typecheck passed, and pnpm test passed the merge-gate section; the later non-blocking changed-package engine suite hit unrelated pre-existing node:child_process mock failures importing @fusion/core/src/git-repository.ts.

@gsxdsm
gsxdsm merged commit 340b2ef into Runfusion:main Jun 8, 2026
9 of 10 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