Skip to content

fix(webapp): synchronize run page highlight when hovering spans#2998

Open
mpcgrid wants to merge 5 commits intomainfrom
fix/tri-6744-in-run-page-synchronize-highligh-on-hover
Open

fix(webapp): synchronize run page highlight when hovering spans#2998
mpcgrid wants to merge 5 commits intomainfrom
fix/tri-6744-in-run-page-synchronize-highligh-on-hover

Conversation

@mpcgrid
Copy link
Collaborator

@mpcgrid mpcgrid commented Feb 4, 2026

Closes #

✅ Checklist

  • I have followed every step in the contributing guide
  • The PR title follows the convention.
  • I ran and tested the code works

Testing

Checked in the interface


Changelog

Synchronized row between spans and timeline on hover


Screenshots

Screenshot 2026-02-04 at 17 36 09
Open with Devin

@changeset-bot
Copy link

changeset-bot bot commented Feb 4, 2026

⚠️ No Changeset found

Latest commit: 01d654c

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 4, 2026

Walkthrough

This change adds hover-aware interaction and scroll-aware hover deferral to the runs route. It introduces hoveredNodeId state, an isScrolling flag with refs, handleHoverChange and handleScroll helpers to queue and apply hover changes around scrolling, and onMouseEnter/onMouseLeave handlers on tree and timeline items. Hover state is propagated through TasksTreeView into TimelineView and TreeView via new props and used to compute isHovered for conditional background styling. TimelineViewProps and the TimelineView function signature are extended to accept the new hover/scroll coordination fields.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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 The PR description follows the required template structure with all major sections completed including checklist, testing, changelog, and screenshots.
Title check ✅ Passed The pull request title clearly and specifically describes the main change: synchronizing the highlight/hover state between spans in the run page's tree and timeline views.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/tri-6744-in-run-page-synchronize-highligh-on-hover

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx:
- Around line 724-729: The scroll timeout handler currently only applies
pendingHoverIdRef.current when it is not null, which prevents clearing the hover
when the pointer left during scroll; update the timeout callback in the
component where scrollTimeoutRef, pendingHoverIdRef, setIsScrolling, and
setHoveredNodeId are used so that after setIsScrolling(false) you always call
setHoveredNodeId(pendingHoverIdRef.current) (apply the pending value
unconditionally) and then clear pendingHoverIdRef.current as needed to avoid
stale state.
🧹 Nitpick comments (1)
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx (1)

697-701: Clean up the scroll timeout on unmount.

This avoids a late setState after unmount and keeps the timeout lifecycle tidy.

Suggested fix
   const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
   const pendingHoverIdRef = useRef<string | null>(null);
+
+  useEffect(() => {
+    return () => {
+      if (scrollTimeoutRef.current) {
+        clearTimeout(scrollTimeoutRef.current);
+        scrollTimeoutRef.current = null;
+      }
+    };
+  }, []);

Also applies to: 712-731

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c059570 and b09a80f.

📒 Files selected for processing (1)
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead

**/*.{ts,tsx}: Always import tasks from @trigger.dev/sdk, never use @trigger.dev/sdk/v3 or deprecated client.defineJob pattern
Every Trigger.dev task must be exported and have a unique id property with no timeouts in the run function

Files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
{packages/core,apps/webapp}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use zod for validation in packages/core and apps/webapp

Files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use function declarations instead of default exports

Import from @trigger.dev/core using subpaths only, never import from root

Files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
apps/webapp/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

Access all environment variables through the env export of env.server.ts instead of directly accessing process.env in the Trigger.dev webapp

Files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
apps/webapp/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

apps/webapp/**/*.{ts,tsx}: When importing from @trigger.dev/core in the webapp, use subpath exports from the package.json instead of importing from the root path
Follow the Remix 2.1.0 and Express server conventions when updating the main trigger.dev webapp

Access environment variables via env export from apps/webapp/app/env.server.ts, never use process.env directly

Files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
**/*.{js,ts,jsx,tsx,json,md,yaml,yml}

📄 CodeRabbit inference engine (AGENTS.md)

Format code using Prettier before committing

Files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
🧠 Learnings (2)
📚 Learning: 2025-12-08T15:19:56.823Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2760
File: apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx:278-281
Timestamp: 2025-12-08T15:19:56.823Z
Learning: In apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx, the tableState search parameter uses intentional double-encoding: the parameter value contains a URL-encoded URLSearchParams string, so decodeURIComponent(value("tableState") ?? "") is required to fully decode it before parsing with new URLSearchParams(). This pattern allows bundling multiple filter/pagination params as a single search parameter.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
📚 Learning: 2026-02-03T18:27:40.429Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2994
File: apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx:553-555
Timestamp: 2026-02-03T18:27:40.429Z
Learning: In apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx, the menu buttons (e.g., Edit with PencilSquareIcon) in the TableCellMenu are intentionally icon-only with no text labels as a compact UI pattern. This is a deliberate design choice for this route; preserve the icon-only behavior for consistency in this file.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
🧬 Code graph analysis (1)
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx (1)
apps/webapp/app/utils/cn.ts (1)
  • cn (77-79)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (25)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (5, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (4, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (8, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (7, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (3, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (5, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (6, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (4, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (8, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (2, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (1, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (6, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (7, 8)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - pnpm)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (1, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (3, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (2, 8)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
  • GitHub Check: units / packages / 🧪 Unit Tests: Packages (1, 1)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
  • GitHub Check: typecheck / typecheck
  • GitHub Check: sdk-compat / Node.js 20.20 (ubuntu-latest)
  • GitHub Check: sdk-compat / Bun Runtime
  • GitHub Check: sdk-compat / Node.js 22.12 (ubuntu-latest)
  • GitHub Check: sdk-compat / Cloudflare Workers
🔇 Additional comments (3)
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx (3)

847-867: Hover styling and handlers are consistent across tree and timeline.

Nice synchronization for visual feedback between both panes.

Also applies to: 1199-1219


925-931: Scroll synchronization wiring looks good.

Keeping the two scroll containers in lockstep makes sense here.

Also applies to: 1329-1334


958-962: Hover/scroll prop plumbing is consistent end-to-end.

The added props are wired cleanly through the component tree.

Also applies to: 1027-1054

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@mpcgrid mpcgrid marked this pull request as ready for review February 6, 2026 07:28
Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 3 additional findings.

Open in Devin Review

@nicktrn nicktrn changed the title Fix/tri 6-744 In run page synchronize highlight when hovering spans fix(webapp): synchronize run page highlight when hovering spans Feb 6, 2026
Copy link
Collaborator

@nicktrn nicktrn left a comment

Choose a reason for hiding this comment

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

PR Review: Synchronize run page highlight on hover

Overall: The feature goal is good — syncing hover highlights between the tree panel and timeline panel is a clear UX improvement. However, the current implementation has a high-severity bug, significant performance concerns, and several code quality issues that should be addressed before merging.


HIGH — Stale hover after mousewheel scroll

When scrolling via mousewheel while the cursor hovers over a row, the rows move under the mouse but onMouseEnter/onMouseLeave events do not fire for the new rows appearing beneath the cursor. The pendingHoverIdRef retains the ID of the row that was under the mouse when scrolling started. When the 150ms debounce expires, that stale ID is applied — highlighting a row the user isn't pointing at.

Similarly, when virtualized rows are recycled during scroll (TanStack Virtual unmounts/mounts row components), no onMouseLeave fires for the unmounted row, leaving pendingHoverIdRef stale.

Suggestion: When scroll ends, set hoveredNodeId to null instead of applying the ref value. Let the next natural mouse event re-establish hover:

scrollTimeoutRef.current = setTimeout(() => {
  setIsScrolling(false);
  setHoveredNodeId(null);
  pendingHoverIdRef.current = null;
}, 150);

HIGH — Performance: every hover re-renders the entire tree + timeline

hoveredNodeId is React state in TasksTreeView. Every setHoveredNodeId call re-renders the parent, which re-renders both TreeView components (tree + timeline). Key factors:

  • TreeView is not wrapped in React.memo
  • renderNode callbacks are inline arrow functions, recreated every render
  • Virtualizer uses overscan: 50, so ~70-80 rows re-render per panel per hover event (~140-160 total)
  • Timeline rows are expensive (multiple Timeline.Point, motion.div, Framer Motion animations)

Suggestion: Consider a CSS/DOM-based approach that avoids React re-renders entirely. Add data-node-id attributes to rows and toggle a data-hovered attribute via refs + direct DOM manipulation. The hover highlight is purely visual — it doesn't need to participate in React's render cycle. If that feels too imperative, an intermediate option is to add pointer-events: none to the scroll container during scroll (one state toggle on the container, not per-node), and keep CSS :hover for the actual highlight.


MEDIUM — CSS hover replaced with JS hover adds latency

The PR replaces native CSS :hover pseudo-classes (hover:bg-grid-dimmed) with React state-driven styling. CSS :hover is handled on the compositor thread and is effectively instant. The JS approach adds at minimum one frame of latency (~16ms) through the React event → setState → reconciliation → paint pipeline, and more under load. Users will notice the highlight trailing the mouse when scanning quickly across rows.


LOW — setHoveredNodeId prop passed but unused in TimelineView

TimelineViewProps includes setHoveredNodeId, but TimelineView only uses handleHoverChange (which wraps setHoveredNodeId with scroll-aware logic). The raw setter shouldn't be exposed — it bypasses scroll suppression and is dead code.


LOW — handleHoverChange dependency on isScrolling state destabilizes callback identity

const handleHoverChange = useCallback((nodeId) => {
  pendingHoverIdRef.current = nodeId;
  if (!isScrolling) {
    setHoveredNodeId(nodeId);
  }
}, [isScrolling]); // changes every time isScrolling toggles

When isScrolling toggles, handleHoverChange gets a new identity. This would bust any React.memo on child components receiving it. If isScrolling were tracked as a ref instead of state, the callback would be stable ([] deps).


LOW — Duplicated hover styling logic

The hover class logic is copy-pasted in both the tree renderNode and timeline renderNode:

state.selected
  ? isHovered ? "bg-grid-bright" : "bg-grid-dimmed"
  : isHovered ? "bg-grid-dimmed" : "bg-transparent"

Could be extracted into a helper like getRowBgClass(selected, hovered).


LOW — Indentation bug in handleScroll

scrollTimeoutRef.current = setTimeout(() => {
  setIsScrolling(false);
    setHoveredNodeId(pendingHoverIdRef.current); // 8 spaces, should be 6
}, 150);

LOW — Unused parameter

handleScroll takes scrollTop: number but never reads it. Should be (_scrollTop: number) => void or () => void.


LOW — NodeJS.Timeout type in browser context

useRef<NodeJS.Timeout | null> — in a browser environment this should be ReturnType<typeof setTimeout> for correctness.

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