refactor(team): wire scroll owners for MessagesPanel layouts

First step toward virtualizing ActivityTimeline. Makes the real scroll
container observable per layout, without changing behavior.

- `TeamDetailView` forwards `contentRef` to `MessagesPanel` as
  `inlineScrollContainerRef`. `MessagesPanel` accepts it as an optional
  prop (unused in this release) so a follow-up can wire the inline
  viewport to virtualization consumers.
- `MessagesPanel` creates `bottomSheetScrollRef` and passes it to
  `Sheet.Content scrollRef`. react-modal-sheet merges it with its
  internal scroll ref, so the element stays the same DOM node; this
  only exposes it to us.
- Sidebar already owns `sidebarScrollRef`; no change there.

Behavior is unchanged — this only exposes refs for the follow-up that
will thread a viewport contract into ActivityTimeline.
This commit is contained in:
Mike 2026-04-19 23:41:33 +05:00
parent 52677b55d0
commit 49e029163b
2 changed files with 25 additions and 2 deletions

View file

@ -39,8 +39,8 @@ import {
} from '@renderer/store/slices/teamSlice';
import { createChipFromSelection } from '@renderer/utils/chipUtils';
import { sumContextInjectionTokens } from '@renderer/utils/contextMath';
import { formatProjectPath } from '@renderer/utils/pathDisplay';
import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
import { formatProjectPath } from '@renderer/utils/pathDisplay';
import { buildTaskCountsByOwner, normalizePath } from '@renderer/utils/pathNormalize';
import { nameColorSet } from '@renderer/utils/projectColor';
import { resolveProjectIdByPath } from '@renderer/utils/projectLookup';
@ -2011,6 +2011,7 @@ export const TeamDetailView = ({
onReplyToMessage: handleReplyToMessage,
onRestartTeam: handleRestartTeam,
onTaskIdClick: handleTaskIdClick,
inlineScrollContainerRef: contentRef,
}),
[
activeMembers,

View file

@ -1,4 +1,13 @@
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import {
memo,
type RefObject,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react';
import { Sheet, type SheetRef } from 'react-modal-sheet';
import { Badge } from '@renderer/components/ui/badge';
@ -91,6 +100,13 @@ interface MessagesPanelProps {
onRestartTeam?: () => void;
/** Callback when a task ID link is clicked. */
onTaskIdClick?: (taskId: string) => void;
/**
* Scroll container owned by the parent view when `position === 'inline'`.
* MessagesPanel does not own this element the viewport lives in
* TeamDetailView's content scroll area. Plumbed for future viewport
* consumers (virtualization); unused in this release.
*/
inlineScrollContainerRef?: RefObject<HTMLDivElement | null>;
}
export function reconcilePendingRepliesByMember(
@ -214,6 +230,11 @@ export const MessagesPanel = memo(function MessagesPanel({
const sidebarScrollRef = useRef<HTMLDivElement | null>(null);
const bottomSheetRef = useRef<SheetRef>(null);
const bottomSheetStickyTopRef = useRef<HTMLDivElement | null>(null);
// Scroll container inside `Sheet.Content` for the bottom-sheet layout.
// react-modal-sheet merges this ref with its own internal scroll ref.
// Held here so future viewport consumers (virtualization) can observe the
// true scrolling element in bottom-sheet mode.
const bottomSheetScrollRef = useRef<HTMLDivElement | null>(null);
const handleExpandContent = useCallback(() => {
// no-op: user is reading expanded content, not composing
}, []);
@ -1063,6 +1084,7 @@ export const MessagesPanel = memo(function MessagesPanel({
<Sheet.Content
className="min-h-0 bg-[var(--color-surface-sidebar)]"
scrollClassName="flex min-h-full flex-col"
scrollRef={bottomSheetScrollRef}
disableDrag={(state) => state.scrollPosition !== 'top'}
>
<div