From cf8762ee70755da8f8f9992e6b83b4a2d6ded4cf Mon Sep 17 00:00:00 2001 From: Wes Date: Sat, 18 Apr 2026 10:44:18 -0600 Subject: [PATCH 1/3] feat: wire up autoscroll for thread sidebar replies Replace the manual scroll-to-bottom effect with useTimelineScrollManager so new replies automatically scroll into view, matching channel page behavior. Adds a "Jump to latest" button when scrolled up. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../messages/ui/MessageThreadPanel.tsx | 178 ++++++++++-------- 1 file changed, 98 insertions(+), 80 deletions(-) diff --git a/desktop/src/features/messages/ui/MessageThreadPanel.tsx b/desktop/src/features/messages/ui/MessageThreadPanel.tsx index f0bd4ff28..d915219e2 100644 --- a/desktop/src/features/messages/ui/MessageThreadPanel.tsx +++ b/desktop/src/features/messages/ui/MessageThreadPanel.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { X } from "lucide-react"; +import { ArrowDown, X } from "lucide-react"; import type { MainTimelineEntry } from "@/features/messages/lib/threadPanel"; import type { TimelineMessage } from "@/features/messages/types"; @@ -10,6 +10,7 @@ import { MessageComposer } from "./MessageComposer"; import { MessageRow } from "./MessageRow"; import { MessageThreadSummaryRow } from "./MessageThreadSummaryRow"; import { TypingIndicatorRow } from "./TypingIndicatorRow"; +import { useTimelineScrollManager } from "./useTimelineScrollManager"; type MessageThreadPanelProps = { canResetWidth: boolean; @@ -98,29 +99,24 @@ export function MessageThreadPanel({ } : null; - React.useEffect(() => { - if (!threadHeadId) { - return; - } - - const threadBody = threadBodyRef.current; - if (!threadBody) { - return; - } - - const scrollToBottom = () => { - threadBody.scrollTop = threadBody.scrollHeight; - }; - const frame = requestAnimationFrame(() => { - scrollToBottom(); - }); - const timeoutId = window.setTimeout(scrollToBottom, 300); + const threadMessages = React.useMemo( + () => threadReplies.map((entry) => entry.message), + [threadReplies], + ); - return () => { - cancelAnimationFrame(frame); - window.clearTimeout(timeoutId); - }; - }, [threadHeadId]); + const { + bottomAnchorRef, + contentRef, + isAtBottom, + newMessageCount, + scrollToBottom, + syncScrollState, + } = useTimelineScrollManager({ + channelId: threadHeadId, + isLoading: false, + messages: threadMessages, + scrollContainerRef: threadBodyRef, + }); React.useEffect(() => { if (!scrollTargetId) { @@ -197,73 +193,95 @@ export function MessageThreadPanel({
-
-
- +
+
+
+ +
-
-
- {threadReplies.length > 0 ? ( -
- {threadReplies.map((entry, index) => { - const nextDepth = threadReplies[index + 1]?.message.depth ?? -1; - const isExpanded = nextDepth > entry.message.depth; +
+ {threadReplies.length > 0 ? ( +
+ {threadReplies.map((entry, index) => { + const nextDepth = + threadReplies[index + 1]?.message.depth ?? -1; + const isExpanded = nextDepth > entry.message.depth; - return ( -
- - {entry.summary && !isExpanded ? ( - + - ) : null} -
- ); - })} -
- ) : ( -
-

- No replies in this branch yet -

-

- Reply in the thread to continue this branch. -

-
- )} + {entry.summary && !isExpanded ? ( + + ) : null} +
+ ); + })} +
+ ) : ( +
+

+ No replies in this branch yet +

+

+ Reply in the thread to continue this branch. +

+
+ )} +
+
+ {!isAtBottom ? ( +
+ +
+ ) : null} +
Date: Sat, 18 Apr 2026 10:48:30 -0600 Subject: [PATCH 2/3] refactor: remove dead totalReplyCount prop and inline scroll-to-target into hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The manual scroll-to-target effect in MessageThreadPanel duplicated built-in targetMessageId/onTargetReached support in useTimelineScrollManager. Wire those through the hook instead and remove the redundant effect. Also remove the unused totalReplyCount prop chain (MessageThreadPanel ← ChannelPane ← ChannelScreen). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/features/channels/ui/ChannelPane.tsx | 3 -- .../features/channels/ui/ChannelScreen.tsx | 3 +- .../messages/ui/MessageThreadPanel.tsx | 33 ++----------------- 3 files changed, 3 insertions(+), 36 deletions(-) diff --git a/desktop/src/features/channels/ui/ChannelPane.tsx b/desktop/src/features/channels/ui/ChannelPane.tsx index a721e847e..83c4a4d5a 100644 --- a/desktop/src/features/channels/ui/ChannelPane.tsx +++ b/desktop/src/features/channels/ui/ChannelPane.tsx @@ -90,7 +90,6 @@ type ChannelPaneProps = { threadHeadMessage: TimelineMessage | null; threadMessages: MainTimelineEntry[]; threadTypingPubkeys: string[]; - threadTotalReplyCount: number; threadReplyTargetId: string | null; threadReplyTargetMessage: TimelineMessage | null; threadScrollTargetId: string | null; @@ -130,7 +129,6 @@ export const ChannelPane = React.memo(function ChannelPane({ threadMessages, threadScrollTargetId, threadTypingPubkeys, - threadTotalReplyCount, threadReplyTargetId, threadReplyTargetMessage, typingPubkeys, @@ -286,7 +284,6 @@ export const ChannelPane = React.memo(function ChannelPane({ widthPx={threadPanelWidthPx} threadReplies={threadMessages} threadTypingPubkeys={threadTypingPubkeys} - totalReplyCount={threadTotalReplyCount} /> ) : null}
diff --git a/desktop/src/features/channels/ui/ChannelScreen.tsx b/desktop/src/features/channels/ui/ChannelScreen.tsx index a122b22ea..d174764b1 100644 --- a/desktop/src/features/channels/ui/ChannelScreen.tsx +++ b/desktop/src/features/channels/ui/ChannelScreen.tsx @@ -274,7 +274,7 @@ export function ChannelScreen({ const openThreadHeadMessage = threadPanelData.threadHead; const threadMessages = threadPanelData.visibleReplies; const threadReplyTargetMessage = threadPanelData.replyTargetMessage; - const threadTotalReplyCount = threadPanelData.totalReplyCount; + const editTargetMessage = React.useMemo( () => timelineMessages.find((message) => message.id === editTargetId) ?? null, @@ -466,7 +466,6 @@ export function ChannelScreen({ threadHeadMessage={openThreadHeadMessage} threadMessages={threadMessages} threadTypingPubkeys={threadTypingPubkeys} - threadTotalReplyCount={threadTotalReplyCount} threadReplyTargetId={threadReplyTargetId} threadReplyTargetMessage={threadReplyTargetMessage} threadScrollTargetId={threadScrollTargetId} diff --git a/desktop/src/features/messages/ui/MessageThreadPanel.tsx b/desktop/src/features/messages/ui/MessageThreadPanel.tsx index d915219e2..b0bec5efb 100644 --- a/desktop/src/features/messages/ui/MessageThreadPanel.tsx +++ b/desktop/src/features/messages/ui/MessageThreadPanel.tsx @@ -45,7 +45,6 @@ type MessageThreadPanelProps = { threadHead: TimelineMessage | null; threadReplies: MainTimelineEntry[]; threadTypingPubkeys: string[]; - totalReplyCount: number; widthPx: number; }; @@ -115,39 +114,11 @@ export function MessageThreadPanel({ channelId: threadHeadId, isLoading: false, messages: threadMessages, + onTargetReached: onScrollTargetResolved, scrollContainerRef: threadBodyRef, + targetMessageId: scrollTargetId, }); - React.useEffect(() => { - if (!scrollTargetId) { - return; - } - - const threadBody = threadBodyRef.current; - if (!threadBody) { - return; - } - - const target = threadBody.querySelector( - `[data-message-id="${scrollTargetId}"]`, - ); - if (!target) { - return; - } - - const frame = requestAnimationFrame(() => { - target.scrollIntoView({ - behavior: "smooth", - block: "start", - }); - }); - onScrollTargetResolved(); - - return () => { - cancelAnimationFrame(frame); - }; - }, [onScrollTargetResolved, scrollTargetId]); - if (!threadHead) { return null; } From b7a3575104567797e9890a9e98973fcea9fdda98 Mon Sep 17 00:00:00 2001 From: Wes Date: Sat, 18 Apr 2026 11:08:25 -0600 Subject: [PATCH 3/3] fix: create sidecar placeholder binaries in `just dev` recipe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tauri validates `externalBin` paths at compile time. The `desktop-tauri-check` recipe already creates placeholder stubs, but the `dev` recipe didn't — causing a build failure when running `just dev` after the sidecar bundling landed in main (#362). Co-Authored-By: Claude Opus 4.6 (1M context) --- justfile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/justfile b/justfile index a1039efd7..0fb873e21 100644 --- a/justfile +++ b/justfile @@ -151,6 +151,11 @@ dev *ARGS: set -euo pipefail cd {{desktop_dir}} [[ -d node_modules ]] || pnpm install + # Ensure sidecar placeholder binaries exist (Tauri validates externalBin at compile time) + TARGET=$(rustc -vV | sed -n 's|host: ||p') + mkdir -p src-tauri/binaries + touch "src-tauri/binaries/sprout-acp-$TARGET" + touch "src-tauri/binaries/sprout-mcp-server-$TARGET" source ../scripts/instance-env.sh echo "Starting on Vite port ${SPROUT_VITE_PORT}, relay ${SPROUT_RELAY_URL}" pnpm exec tauri dev --config "$SPROUT_TAURI_CONFIG" {{ARGS}}