Menu

#150 Add scroll view and fix message list calculation

closed
nobody
None
2025-10-06
2025-10-05
Anonymous
No

Originally created by: endernoke

Fixes [#136]

This PR introduces a new reusable ScrollView component for managing scrollable areas in the terminal UI and refactors the chat view to use this component for message scrolling. It removes the previous manual message offset and hardcoded message length approach, streamlining the UX.

As a side note, we now have more aura than gemini-cli since we implemented a fully functional ScrollView with foundational Ink which they thought was not possible.

New Components

  • Added a new ScrollView component (scroll-view.tsx) that provides a flexible, imperative API for scrolling, including methods to scroll to start/end and to a specific offset, and emits callbacks when reaching the top or bottom.
  • Implemented a useContentSize hook which measures the total inner content of a Ink component, including any overflown content when overflow="hidden. This hook is used in ScrollView to accurately determine boundaries of scrollable content.
  • It calculates content size by recursively accumulating the sizes of the element's children. I was not able to find a more elegant approach since Ink's underlying yoga engine doesn't provide a direct way to get the overflow size of an element.
  • The hook proactively checks content size in short intervals to detect changes. This is not very idiomatic react code but it's the only way since changes deep in the children's tree may not trigger a rerender of the parent component which uses useContentSize.

Chat View

  • Refactored the chat view (chat-view.tsx) to use the new ScrollView for displaying messages,
  • Chat history now utilizes all available space for display regardless of the height of individual messages.
  • Scrolling is now handled imperatively via the component's API.
  • Loads more messages when scrolling to top via binding to the API on-scroll-to-top callback.
  • Removed the visibleMessageOffset property from the ChatState type and all related logic, as scrolling offset is now managed internally by the ScrollView.
  • Adjusted MessageList to remove unnecessary refs and overflow handling, since scrolling is now managed by the parent ScrollView.
  • Preserved the old behavior of the message history scrolling to bottom initially.
  • Sending a message resets scroll to bottom

Commands

  • Updated chat commands (chat-commands.ts) for scrolling up/down (:k and :j) to use the new ScrollViewRef API, replacing the old offset-based logic.
  • Logic for fetching older messages is moved from :k handler to the chat view itself
  • It is also very easy to add logic for scrolling to the very top or bottom of the chat history. This might or might not be implemented.

Misc

  • Disabled the react/require-default-props lint rule.

Related

Tickets: #136
Tickets: #152

Discussion

  • Anonymous

    Anonymous - 2025-10-05

    Originally posted by: endernoke

    I believe it's quite feasible to add mouse scrolling support, see also https://github.com/zenobi-us/ink-mouse

     
  • Anonymous

    Anonymous - 2025-10-05

    Originally posted by: supreme-gg-gg

    @endernoke I also hoped that during the :select command the chat view will ensure that the selectedMessageIndex will always stay inside the viewport created by the scroll view (i.e. autoscroll during select). I believe it is best to just create a useEffect to check if the index is out of bound (invisible), and scroll in the direction up or down to make it visible. However, I don't find a way to do this without modifying the scroll view or message list components, since there's no way to know if a certain index is visible or invisible.

    I tried two alternatives -- scrolling in the direction of movement by either average size of message (container height / msg count) or by a fixed amount, but both caused significantly jittering and it makes sense cuz they sound scuffed lol

    What's your thought on this?

    Currently the scroll view operates based on offset, would it be a good idea to use a map between message index and item height / width instead? (e.g. Message [#0] starts at 0px, [#1] starts at 35px) This will be measured by the message list when it renders each message, replacing the current recursive method to measure the children. This solves two problems:

    1. You can scroll to a certain message by index, not an arbitrary offset
    2. When we use :j :k no messages will be cutoff in the middle (I believe it happens sometimes right now with images)

    My argument is that this proposed design works much better for keyboard based navigation (because we want discrete values), but the current design (offset) works more natively with mouse based navigation (continuous values).

     

    Related

    Tickets: #1

  • Anonymous

    Anonymous - 2025-10-05

    Originally posted by: supreme-gg-gg

    Also regardless if we choose to change the design or now, would be great if you add the design description to designs/ just ask Gemini Cli or whatever to write a concise description so we document how this works in the codebase

     
  • Anonymous

    Anonymous - 2025-10-06

    Originally posted by: endernoke

    @endernoke I also hoped that during the :select command the chat view will ensure that the selectedMessageIndex will always stay inside the viewport created by the scroll view (i.e. autoscroll during select). I believe it is best to just create a useEffect to check if the index is out of bound (invisible), and scroll in the direction up or down to make it visible. However, I don't find a way to do this without modifying the scroll view or message list components, since there's no way to know if a certain index is visible or invisible.

    There might be a way that gets the absolute terminal position of an ink component which I used in ink-picture, perhaps it will work here too:
    https://github.com/endernoke/ink-picture/blob/main/src/hooks/usePosition.ts

    I tried two alternatives -- scrolling in the direction of movement by either average size of message (container height / msg count) or by a fixed amount, but both caused significantly jittering and it makes sense cuz they sound scuffed lol

    What's your thought on this?

    Currently the scroll view operates based on offset, would it be a good idea to use a map between message index and item height / width instead? (e.g. Message [#0] starts at 0px, [#1] starts at 35px) This will be measured by the message list when it renders each message, replacing the current recursive method to measure the children. This solves two problems:

    Lmao bro we need an ex-FANNG ex-react native guy to work on this 😭
    Measuring individual messages is a non-trivial task (unless we hard code how tall each message type is, again back to the beginning). I believe we can employ the same recursive measurement algorithm to do this.
    I'll take a look at how react native handles snap to item in its scrollview hopefully it will give us some insights.

    1. You can scroll to a certain message by index, not an arbitrary offset
    2. When we use :j :k no messages will be cutoff in the middle (I believe it happens sometimes right now with images)

    My argument is that this proposed design works much better for keyboard based navigation (because we want discrete values), but the current design (offset) works more natively with mouse based navigation (continuous values).

    Yes makes a lot of sense

     

    Related

    Tickets: #1

  • Anonymous

    Anonymous - 2025-10-06

    Originally posted by: endernoke

    @endernoke I also hoped that during the :select command the chat view will ensure that the selectedMessageIndex will always stay inside the viewport created by the scroll view (i.e. autoscroll during select). I believe it is best to just create a useEffect to check if the index is out of bound (invisible), and scroll in the direction up or down to make it visible. However, I don't find a way to do this without modifying the scroll view or message list components, since there's no way to know if a certain index is visible or invisible.

    There might be a way that gets the absolute terminal position of an ink component which I used in ink-picture, perhaps it will work here too: https://github.com/endernoke/ink-picture/blob/main/src/hooks/usePosition.ts

    I tried two alternatives -- scrolling in the direction of movement by either average size of message (container height / msg count) or by a fixed amount, but both caused significantly jittering and it makes sense cuz they sound scuffed lol
    What's your thought on this?
    Currently the scroll view operates based on offset, would it be a good idea to use a map between message index and item height / width instead? (e.g. Message [#0] starts at 0px, [#1] starts at 35px) This will be measured by the message list when it renders each message, replacing the current recursive method to measure the children. This solves two problems:

    Lmao bro we need an ex-FANNG ex-react native guy to work on this 😭 Measuring individual messages is a non-trivial task (unless we hard code how tall each message type is, again back to the beginning). I believe we can employ the same recursive measurement algorithm to do this. I'll take a look at how react native handles snap to item in its scrollview hopefully it will give us some insights.

    1. You can scroll to a certain message by index, not an arbitrary offset
    2. When we use :j :k no messages will be cutoff in the middle (I believe it happens sometimes right now with images)

    My argument is that this proposed design works much better for keyboard based navigation (because we want discrete values), but the current design (offset) works more natively with mouse based navigation (continuous values).

    Yes makes a lot of sense

    ~I tried implementing this but turns out both approaches requires complicated logic, shall we keep this as tech debt for now~

    Maybe this will work:

    const ChatView = () => {
      const [scrollIntervals, setScrollIntervals] = useState<Record<number, number>>({});
    
      const renderMessages = (messages) => {
        return (
          {messages.map((message, idx) => (
            <MessageItem
              content={message}
              onLayout={(size) => {
                setScrollIntervals(prev => {...prev, idx: size});
              })
            />
          ))}
        );
      };
    
      return (
        ...
        <ScrollView snapIntervals={scrollIntervals}>
          renderMessageList();
        <ScrollView />
        ...
      );
    }
    
    const MessageItem = (content, onLayout) => {
      const selfRef = useRef(null);
      useEffect() => {
        onLayout(measureElement(selfRef));
      }, []);
      return <Box ref={selfRef}>...</Box>;
    }
    
     

    Related

    Tickets: #1

  • Anonymous

    Anonymous - 2025-10-06

    Originally posted by: supreme-gg-gg

    The scroll intervals seem interesting, but I agree we have enough leverage to borrow some technical debt for this one. Let's put this as one of the feature that will come together with the next version, perhaps mouse interaction in that one too. I think it's best if we figure out the idea for mouse together with this scroll view design to avoid them conflicting.

     
  • Anonymous

    Anonymous - 2025-10-06

    Ticket changed by: supreme-gg-gg

    • status: open --> closed
     

Log in to post a comment.

Monday.com Logo