From bc6b8877c3e64b4910276ce3cb956262de87f86d Mon Sep 17 00:00:00 2001 From: Wes Date: Fri, 20 Mar 2026 16:59:25 -0700 Subject: [PATCH 1/6] perf(desktop): virtualize message timeline and memoize Markdown for instant channel switching --- desktop/package.json | 1 + desktop/pnpm-lock.yaml | 20 ++++ .../features/messages/ui/MessageTimeline.tsx | 101 +++++++++++++----- .../messages/ui/useTimelineScrollManager.ts | 12 +++ desktop/src/shared/ui/markdown.tsx | 40 ++++++- 5 files changed, 146 insertions(+), 28 deletions(-) diff --git a/desktop/package.json b/desktop/package.json index 4751fc76..35de06da 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -31,6 +31,7 @@ "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.21", + "@tanstack/react-virtual": "^3.13.0", "@tauri-apps/api": "^2", "@tauri-apps/plugin-notification": "^2.3.3", "@tauri-apps/plugin-opener": "^2", diff --git a/desktop/pnpm-lock.yaml b/desktop/pnpm-lock.yaml index 34e41666..0c9d1c2e 100644 --- a/desktop/pnpm-lock.yaml +++ b/desktop/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: '@tanstack/react-query': specifier: ^5.90.21 version: 5.90.21(react@19.2.4) + '@tanstack/react-virtual': + specifier: ^3.13.0 + version: 3.13.23(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tauri-apps/api': specifier: ^2 version: 2.10.1 @@ -1036,6 +1039,15 @@ packages: peerDependencies: react: ^18 || ^19 + '@tanstack/react-virtual@3.13.23': + resolution: {integrity: sha512-XnMRnHQ23piOVj2bzJqHrRrLg4r+F86fuBcwteKfbIjJrtGxb4z7tIvPVAe4B+4UVwo9G4Giuz5fmapcrnZ0OQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/virtual-core@3.13.23': + resolution: {integrity: sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==} + '@tauri-apps/api@2.10.1': resolution: {integrity: sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==} @@ -2780,6 +2792,14 @@ snapshots: '@tanstack/query-core': 5.90.20 react: 19.2.4 + '@tanstack/react-virtual@3.13.23(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@tanstack/virtual-core': 3.13.23 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@tanstack/virtual-core@3.13.23': {} + '@tauri-apps/api@2.10.1': {} '@tauri-apps/cli-darwin-arm64@2.10.1': diff --git a/desktop/src/features/messages/ui/MessageTimeline.tsx b/desktop/src/features/messages/ui/MessageTimeline.tsx index 560bfb78..6540da52 100644 --- a/desktop/src/features/messages/ui/MessageTimeline.tsx +++ b/desktop/src/features/messages/ui/MessageTimeline.tsx @@ -1,5 +1,6 @@ import * as React from "react"; import { ArrowDown } from "lucide-react"; +import { useVirtualizer } from "@tanstack/react-virtual"; import type { TimelineMessage } from "@/features/messages/types"; import type { UserProfileLookup } from "@/features/profile/lib/identity"; @@ -30,6 +31,9 @@ type MessageTimelineProps = { onTargetReached?: (messageId: string) => void; }; +const ESTIMATED_ROW_HEIGHT = 60; +const OVERSCAN = 5; + export const MessageTimeline = React.memo(function MessageTimeline({ channelId, messages, @@ -44,6 +48,16 @@ export const MessageTimeline = React.memo(function MessageTimeline({ targetMessageId = null, onTargetReached, }: MessageTimelineProps) { + const scrollElementRef = React.useRef(null); + + const virtualizer = useVirtualizer({ + count: messages.length, + getScrollElement: () => scrollElementRef.current, + estimateSize: () => ESTIMATED_ROW_HEIGHT, + overscan: OVERSCAN, + measureElement: (element) => element.getBoundingClientRect().height, + }); + const { bottomAnchorRef, contentRef, @@ -59,15 +73,31 @@ export const MessageTimeline = React.memo(function MessageTimeline({ messages, onTargetReached, targetMessageId, + virtualizer, }); + // Merge the two refs onto the same scroll container element + const setScrollRef = React.useCallback( + (node: HTMLDivElement | null) => { + ( + scrollElementRef as React.MutableRefObject + ).current = node; + (timelineRef as React.MutableRefObject).current = + node; + }, + [timelineRef], + ); + + const virtualItems = virtualizer.getVirtualItems(); + const totalSize = virtualizer.getTotalSize(); + return (
) : null} - {!isLoading - ? messages.map((message) => - message.kind === KIND_SYSTEM_MESSAGE ? ( - - ) : ( - - ), - ) - : null} + {!isLoading && messages.length > 0 ? ( +
+ {virtualItems.map((virtualRow) => { + const message = messages[virtualRow.index]; + return ( +
+ {message.kind === KIND_SYSTEM_MESSAGE ? ( + + ) : ( + + )} +
+ ); + })} +
+ ) : null} +
diff --git a/desktop/src/features/messages/ui/useTimelineScrollManager.ts b/desktop/src/features/messages/ui/useTimelineScrollManager.ts index ff6ca211..e6e77ee5 100644 --- a/desktop/src/features/messages/ui/useTimelineScrollManager.ts +++ b/desktop/src/features/messages/ui/useTimelineScrollManager.ts @@ -1,5 +1,6 @@ import * as React from "react"; +import type { Virtualizer } from "@tanstack/react-virtual"; import type { TimelineMessage } from "@/features/messages/types"; import { isNearBottom } from "./messageTimelineUtils"; @@ -9,12 +10,14 @@ export function useTimelineScrollManager({ messages, onTargetReached, targetMessageId, + virtualizer, }: { channelId?: string | null; isLoading: boolean; messages: TimelineMessage[]; onTargetReached?: (messageId: string) => void; targetMessageId?: string | null; + virtualizer?: Virtualizer; }) { const timelineRef = React.useRef(null); const contentRef = React.useRef(null); @@ -35,6 +38,10 @@ export function useTimelineScrollManager({ >(null); const [newMessageCount, setNewMessageCount] = React.useState(0); + // Keep a ref to the virtualizer so callbacks don't need it as a dependency + const virtualizerRef = React.useRef(virtualizer); + virtualizerRef.current = virtualizer; + // biome-ignore lint/correctness/useExhaustiveDependencies: channelId is intentionally the sole trigger — we reset all scroll state when the channel changes React.useLayoutEffect(() => { hasInitializedRef.current = false; @@ -145,6 +152,11 @@ export function useTimelineScrollManager({ isProgrammaticBottomScrollRef.current = true; + const virt = virtualizerRef.current; + if (virt && virt.options.count > 0) { + virt.scrollToIndex(virt.options.count - 1, { align: "end" }); + } + const alignToBottom = (nextBehavior: ScrollBehavior) => { bottomAnchorRef.current?.scrollIntoView({ block: "end", diff --git a/desktop/src/shared/ui/markdown.tsx b/desktop/src/shared/ui/markdown.tsx index 66d7975f..6da40d61 100644 --- a/desktop/src/shared/ui/markdown.tsx +++ b/desktop/src/shared/ui/markdown.tsx @@ -1,4 +1,4 @@ -import type * as React from "react"; +import * as React from "react"; import ReactMarkdown, { type Components } from "react-markdown"; import remarkBreaks from "remark-breaks"; import remarkGfm from "remark-gfm"; @@ -180,15 +180,35 @@ function createMarkdownComponents( } as Components; } -export function Markdown({ +function shallowArrayEqual(a?: string[], b?: string[]): boolean { + if (a === b) return true; + if (!a || !b) return false; + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false; + } + return true; +} + +function MarkdownInner({ className, compact = false, content, mentionNames, tight = false, }: MarkdownProps) { - const variant = tight ? "tight" : compact ? "compact" : "default"; + const variant: MarkdownVariant = tight + ? "tight" + : compact + ? "compact" + : "default"; const { channels, onOpenChannel } = useChannelNavigation(); + + const components = React.useMemo( + () => createMarkdownComponents(variant, channels, onOpenChannel), + [variant, channels, onOpenChannel], + ); + let processedContent = content; if (/^(?:\s{2}\n)+/.test(content)) { @@ -211,7 +231,7 @@ export function Markdown({ )} > ); } + +export const Markdown = React.memo( + MarkdownInner, + (prev, next) => + prev.content === next.content && + prev.className === next.className && + prev.compact === next.compact && + prev.tight === next.tight && + shallowArrayEqual(prev.mentionNames, next.mentionNames), +); + +Markdown.displayName = "Markdown"; From 80793c6bcbbd8f09629528d8144893e6e8573ae8 Mon Sep 17 00:00:00 2001 From: Wes Date: Fri, 20 Mar 2026 17:01:38 -0700 Subject: [PATCH 2/6] refactor(desktop): polish virtualizer wiring and fix target-message scroll for virtualized rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename scrollElementRef → scrollContainerRef and setScrollRef → mergedScrollRef for clarity - Bump overscan from 5 → 10 for smoother scrolling experience - Remove redundant measureElement config; ref-based measurement on each row suffices - Use message.id as virtual row key for stable identity across re-renders - Move inline positioning styles to Tailwind classes (relative, absolute, etc.) - Move constants above the type block for conventional ordering - Handle target-message highlighting with virtualization: use scrollToIndex to bring off-screen rows into the DOM before querySelector + scrollIntoView - Add fallback path for non-virtualized usage or unknown target IDs - Add messages to the target-scroll effect dependency array (findIndex needs it) --- .../features/messages/ui/MessageTimeline.tsx | 35 ++++----- .../messages/ui/useTimelineScrollManager.ts | 74 ++++++++++++++----- 2 files changed, 68 insertions(+), 41 deletions(-) diff --git a/desktop/src/features/messages/ui/MessageTimeline.tsx b/desktop/src/features/messages/ui/MessageTimeline.tsx index 6540da52..98180013 100644 --- a/desktop/src/features/messages/ui/MessageTimeline.tsx +++ b/desktop/src/features/messages/ui/MessageTimeline.tsx @@ -12,6 +12,9 @@ import { SystemMessageRow } from "./SystemMessageRow"; import { TimelineSkeleton } from "./TimelineSkeleton"; import { useTimelineScrollManager } from "./useTimelineScrollManager"; +const ESTIMATED_ROW_HEIGHT = 60; +const OVERSCAN_COUNT = 10; + type MessageTimelineProps = { channelId?: string | null; messages: TimelineMessage[]; @@ -31,9 +34,6 @@ type MessageTimelineProps = { onTargetReached?: (messageId: string) => void; }; -const ESTIMATED_ROW_HEIGHT = 60; -const OVERSCAN = 5; - export const MessageTimeline = React.memo(function MessageTimeline({ channelId, messages, @@ -48,14 +48,13 @@ export const MessageTimeline = React.memo(function MessageTimeline({ targetMessageId = null, onTargetReached, }: MessageTimelineProps) { - const scrollElementRef = React.useRef(null); + const scrollContainerRef = React.useRef(null); const virtualizer = useVirtualizer({ count: messages.length, - getScrollElement: () => scrollElementRef.current, + getScrollElement: () => scrollContainerRef.current, estimateSize: () => ESTIMATED_ROW_HEIGHT, - overscan: OVERSCAN, - measureElement: (element) => element.getBoundingClientRect().height, + overscan: OVERSCAN_COUNT, }); const { @@ -76,11 +75,11 @@ export const MessageTimeline = React.memo(function MessageTimeline({ virtualizer, }); - // Merge the two refs onto the same scroll container element - const setScrollRef = React.useCallback( + // Merge the scroll container ref with the timeline ref from the scroll manager + const mergedScrollRef = React.useCallback( (node: HTMLDivElement | null) => { ( - scrollElementRef as React.MutableRefObject + scrollContainerRef as React.MutableRefObject ).current = node; (timelineRef as React.MutableRefObject).current = node; @@ -97,7 +96,7 @@ export const MessageTimeline = React.memo(function MessageTimeline({ className="h-full overflow-y-auto overflow-x-hidden overscroll-contain px-4 py-3 [overflow-anchor:none] sm:px-6" data-testid="message-timeline" onScroll={syncScrollState} - ref={setScrollRef} + ref={mergedScrollRef} >
0 ? (
{virtualItems.map((virtualRow) => { const message = messages[virtualRow.index]; return (
diff --git a/desktop/src/features/messages/ui/useTimelineScrollManager.ts b/desktop/src/features/messages/ui/useTimelineScrollManager.ts index e6e77ee5..21728653 100644 --- a/desktop/src/features/messages/ui/useTimelineScrollManager.ts +++ b/desktop/src/features/messages/ui/useTimelineScrollManager.ts @@ -323,26 +323,60 @@ export function useTimelineScrollManager({ return; } - const targetElement = timeline.querySelector( - `[data-message-id="${targetMessageId}"]`, - ); - if (!targetElement) { - return; - } + // With virtualization the target row may not be in the DOM yet. + // Use scrollToIndex to bring it into view first, then highlight. + const virt = virtualizerRef.current; + const targetIndex = messages.findIndex((m) => m.id === targetMessageId); + + if (virt && targetIndex >= 0) { + virt.scrollToIndex(targetIndex, { align: "center" }); + + // Give the virtualizer a frame to render the row before querying the DOM. + requestAnimationFrame(() => { + const targetElement = timeline.querySelector( + `[data-message-id="${targetMessageId}"]`, + ); - handledTargetMessageIdRef.current = targetMessageId; - shouldStickToBottomRef.current = false; - isAtBottomRef.current = false; - isProgrammaticBottomScrollRef.current = false; - targetElement.scrollIntoView({ - block: "center", - behavior: "smooth", - }); - previousScrollTopRef.current = timeline.scrollTop; - setIsAtBottom(false); - setHighlightedMessageId(targetMessageId); - setNewMessageCount(0); - onTargetReached?.(targetMessageId); + if (targetElement) { + targetElement.scrollIntoView({ + block: "center", + behavior: "smooth", + }); + } + + handledTargetMessageIdRef.current = targetMessageId; + shouldStickToBottomRef.current = false; + isAtBottomRef.current = false; + isProgrammaticBottomScrollRef.current = false; + previousScrollTopRef.current = timeline.scrollTop; + setIsAtBottom(false); + setHighlightedMessageId(targetMessageId); + setNewMessageCount(0); + onTargetReached?.(targetMessageId); + }); + } else { + // Fallback for non-virtualized usage or unknown target + const targetElement = timeline.querySelector( + `[data-message-id="${targetMessageId}"]`, + ); + if (!targetElement) { + return; + } + + handledTargetMessageIdRef.current = targetMessageId; + shouldStickToBottomRef.current = false; + isAtBottomRef.current = false; + isProgrammaticBottomScrollRef.current = false; + targetElement.scrollIntoView({ + block: "center", + behavior: "smooth", + }); + previousScrollTopRef.current = timeline.scrollTop; + setIsAtBottom(false); + setHighlightedMessageId(targetMessageId); + setNewMessageCount(0); + onTargetReached?.(targetMessageId); + } const timeout = window.setTimeout(() => { setHighlightedMessageId((current) => @@ -353,7 +387,7 @@ export function useTimelineScrollManager({ return () => { window.clearTimeout(timeout); }; - }, [isLoading, onTargetReached, targetMessageId]); + }, [isLoading, messages, onTargetReached, targetMessageId]); return { bottomAnchorRef, From a38e2d8cc84173c5e7ac89e5f500fe80f07e8f67 Mon Sep 17 00:00:00 2001 From: Wes Date: Fri, 20 Mar 2026 17:13:30 -0700 Subject: [PATCH 3/6] refactor(desktop): clean up scroll manager ref sharing and deduplicate target-scroll logic - Accept scrollContainerRef from parent instead of creating internally, eliminating the merged-ref callback hack in MessageTimeline - Extract settleOnTarget() helper to deduplicate state-setting between virtualizer and fallback target-scroll paths - Fix import ordering (external before internal) - Add biome-ignore comments for stable ref parameter --- .../features/messages/ui/MessageTimeline.tsx | 16 +------ .../messages/ui/useTimelineScrollManager.ts | 44 ++++++++++--------- 2 files changed, 25 insertions(+), 35 deletions(-) diff --git a/desktop/src/features/messages/ui/MessageTimeline.tsx b/desktop/src/features/messages/ui/MessageTimeline.tsx index 98180013..be2fa113 100644 --- a/desktop/src/features/messages/ui/MessageTimeline.tsx +++ b/desktop/src/features/messages/ui/MessageTimeline.tsx @@ -65,28 +65,16 @@ export const MessageTimeline = React.memo(function MessageTimeline({ newMessageCount, scrollToBottom, syncScrollState, - timelineRef, } = useTimelineScrollManager({ channelId, isLoading, messages, onTargetReached, + scrollContainerRef, targetMessageId, virtualizer, }); - // Merge the scroll container ref with the timeline ref from the scroll manager - const mergedScrollRef = React.useCallback( - (node: HTMLDivElement | null) => { - ( - scrollContainerRef as React.MutableRefObject - ).current = node; - (timelineRef as React.MutableRefObject).current = - node; - }, - [timelineRef], - ); - const virtualItems = virtualizer.getVirtualItems(); const totalSize = virtualizer.getTotalSize(); @@ -96,7 +84,7 @@ export const MessageTimeline = React.memo(function MessageTimeline({ className="h-full overflow-y-auto overflow-x-hidden overscroll-contain px-4 py-3 [overflow-anchor:none] sm:px-6" data-testid="message-timeline" onScroll={syncScrollState} - ref={mergedScrollRef} + ref={scrollContainerRef} >
void; + scrollContainerRef: React.RefObject; targetMessageId?: string | null; virtualizer?: Virtualizer; }) { - const timelineRef = React.useRef(null); + const timelineRef = scrollContainerRef; const contentRef = React.useRef(null); const bottomAnchorRef = React.useRef(null); const hasInitializedRef = React.useRef(false); @@ -62,6 +64,7 @@ export function useTimelineScrollManager({ const latestMessage = messages.length > 0 ? messages[messages.length - 1] : undefined; + // biome-ignore lint/correctness/useExhaustiveDependencies: timelineRef is a stable React ref passed from the parent — its identity never changes const syncScrollState = React.useCallback(() => { const timeline = timelineRef.current; if (!timeline) { @@ -111,6 +114,7 @@ export function useTimelineScrollManager({ } }, []); + // biome-ignore lint/correctness/useExhaustiveDependencies: timelineRef is a stable React ref — its identity never changes const restoreScrollPosition = React.useCallback( (scrollTop: number) => { const timeline = timelineRef.current; @@ -142,6 +146,7 @@ export function useTimelineScrollManager({ [syncScrollState], ); + // biome-ignore lint/correctness/useExhaustiveDependencies: timelineRef is a stable React ref — its identity never changes const scrollToBottom = React.useCallback( (behavior: ScrollBehavior) => { const timeline = timelineRef.current; @@ -203,6 +208,7 @@ export function useTimelineScrollManager({ [syncScrollState], ); + // biome-ignore lint/correctness/useExhaustiveDependencies: timelineRef is a stable React ref — its identity never changes React.useEffect(() => { const timeline = timelineRef.current; @@ -307,6 +313,7 @@ export function useTimelineScrollManager({ previousMessageCountRef.current = messages.length; }, [isLoading, latestMessage, messages.length, scrollToBottom]); + // biome-ignore lint/correctness/useExhaustiveDependencies: timelineRef is a stable React ref — its identity never changes React.useEffect(() => { if (!targetMessageId) { handledTargetMessageIdRef.current = null; @@ -323,6 +330,18 @@ export function useTimelineScrollManager({ return; } + const settleOnTarget = () => { + handledTargetMessageIdRef.current = targetMessageId; + shouldStickToBottomRef.current = false; + isAtBottomRef.current = false; + isProgrammaticBottomScrollRef.current = false; + previousScrollTopRef.current = timeline.scrollTop; + setIsAtBottom(false); + setHighlightedMessageId(targetMessageId); + setNewMessageCount(0); + onTargetReached?.(targetMessageId); + }; + // With virtualization the target row may not be in the DOM yet. // Use scrollToIndex to bring it into view first, then highlight. const virt = virtualizerRef.current; @@ -344,15 +363,7 @@ export function useTimelineScrollManager({ }); } - handledTargetMessageIdRef.current = targetMessageId; - shouldStickToBottomRef.current = false; - isAtBottomRef.current = false; - isProgrammaticBottomScrollRef.current = false; - previousScrollTopRef.current = timeline.scrollTop; - setIsAtBottom(false); - setHighlightedMessageId(targetMessageId); - setNewMessageCount(0); - onTargetReached?.(targetMessageId); + settleOnTarget(); }); } else { // Fallback for non-virtualized usage or unknown target @@ -363,19 +374,11 @@ export function useTimelineScrollManager({ return; } - handledTargetMessageIdRef.current = targetMessageId; - shouldStickToBottomRef.current = false; - isAtBottomRef.current = false; - isProgrammaticBottomScrollRef.current = false; targetElement.scrollIntoView({ block: "center", behavior: "smooth", }); - previousScrollTopRef.current = timeline.scrollTop; - setIsAtBottom(false); - setHighlightedMessageId(targetMessageId); - setNewMessageCount(0); - onTargetReached?.(targetMessageId); + settleOnTarget(); } const timeout = window.setTimeout(() => { @@ -397,6 +400,5 @@ export function useTimelineScrollManager({ newMessageCount, scrollToBottom, syncScrollState, - timelineRef, }; } From 41f4a1ea28af4bb6fd0496853ea079126d46e9a8 Mon Sep 17 00:00:00 2001 From: Wes Date: Fri, 20 Mar 2026 17:40:21 -0700 Subject: [PATCH 4/6] perf(desktop): remove contentPaneKey remount and reduce agent polling frequency --- desktop/src/app/AppShell.tsx | 13 +------------ desktop/src/features/agents/hooks.ts | 6 +++--- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/desktop/src/app/AppShell.tsx b/desktop/src/app/AppShell.tsx index 6345ef50..e5104e03 100644 --- a/desktop/src/app/AppShell.tsx +++ b/desktop/src/app/AppShell.tsx @@ -238,14 +238,6 @@ export function AppShell() { .filter((value) => value && value.trim().length > 0) .join(" ") || "Channel details and activity." : "Connect to the relay to browse channels and read messages."; - const contentPaneKey = - selectedView === "home" - ? "home" - : selectedView === "agents" - ? "agents" - : selectedView === "settings" - ? "settings" - : `channel:${activeChannel?.id ?? "none"}`; const shouldLoadTimeline = activeChannel !== null && activeChannel.channelType !== "forum"; const isTimelineLoading = @@ -610,10 +602,7 @@ export function AppShell() { unreadChannelIds={unreadChannelIds} /> - + {selectedView === "home" ? ( { const agents = query.state.data as ManagedAgent[] | undefined; return agents?.some((agent) => agent.status === "running") - ? 2_000 - : 10_000; + ? 5_000 + : 30_000; }, }); } From 497980af89460b87c6af24bdfe583628e068bae4 Mon Sep 17 00:00:00 2001 From: Wes Date: Fri, 20 Mar 2026 18:01:21 -0700 Subject: [PATCH 5/6] perf(desktop): use CSS-based view switching to avoid unmount/remount cycles --- desktop/scripts/check-file-sizes.mjs | 2 +- desktop/src/app/AppShell.tsx | 100 +++++++++++++++++---------- desktop/src/features/agents/hooks.ts | 8 +-- 3 files changed, 70 insertions(+), 40 deletions(-) diff --git a/desktop/scripts/check-file-sizes.mjs b/desktop/scripts/check-file-sizes.mjs index c1354956..2f0f3866 100644 --- a/desktop/scripts/check-file-sizes.mjs +++ b/desktop/scripts/check-file-sizes.mjs @@ -31,7 +31,7 @@ const rules = [ // Exceptions should stay rare and temporary. Prefer splitting files instead. const overrides = new Map([ ["src-tauri/src/managed_agents/persona_card.rs", 700], // PNG/ZIP persona card codec + 21 unit tests (~300 lines of tests) - ["src/app/AppShell.tsx", 750], + ["src/app/AppShell.tsx", 775], ["src/features/agents/ui/AgentsView.tsx", 625], // persona/team orchestration plus import/export wiring ["src/features/channels/hooks.ts", 525], // canvas query + mutation hooks ["src/features/channels/ui/ChannelManagementSheet.tsx", 800], diff --git a/desktop/src/app/AppShell.tsx b/desktop/src/app/AppShell.tsx index e5104e03..a272f2f3 100644 --- a/desktop/src/app/AppShell.tsx +++ b/desktop/src/app/AppShell.tsx @@ -603,19 +603,27 @@ export function AppShell() { /> - {selectedView === "home" ? ( +
- ) : selectedView === "agents" ? ( +
+
- ) : ( +
+
- )} +
- {selectedView === "home" ? ( +
- ) : selectedView === "agents" ? ( +
+
- ) : activeChannel?.channelType === "forum" ? ( - - ) : ( - - )} +
+
+ {activeChannel?.channelType === "forum" ? ( + + ) : ( + + )} +
diff --git a/desktop/src/features/agents/hooks.ts b/desktop/src/features/agents/hooks.ts index 2e9fea01..d97383eb 100644 --- a/desktop/src/features/agents/hooks.ts +++ b/desktop/src/features/agents/hooks.ts @@ -136,8 +136,8 @@ export function useRelayAgentsQuery() { return useQuery({ queryKey: relayAgentsQueryKey, queryFn: listRelayAgents, - staleTime: 15_000, - refetchInterval: 15_000, + staleTime: 30_000, + refetchInterval: 30_000, }); } @@ -421,8 +421,8 @@ export function useManagedAgentLogQuery( queryFn: () => getManagedAgentLog(pubkey!, lineCount), enabled: pubkey !== null, retry: false, - staleTime: 1_000, - refetchInterval: pubkey ? 2_000 : false, + staleTime: 3_000, + refetchInterval: pubkey ? 5_000 : false, }); } From 784120eb1145ea9bdd1e9c8c1b4c01294093f184 Mon Sep 17 00:00:00 2001 From: Wes Date: Sat, 21 Mar 2026 09:37:15 -0700 Subject: [PATCH 6/6] fix(desktop): restore conditional rendering for ChatHeader to fix E2E test selectors --- desktop/src/app/AppShell.tsx | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/desktop/src/app/AppShell.tsx b/desktop/src/app/AppShell.tsx index a272f2f3..7d718ef3 100644 --- a/desktop/src/app/AppShell.tsx +++ b/desktop/src/app/AppShell.tsx @@ -603,27 +603,19 @@ export function AppShell() { /> -
+ {selectedView === "home" ? ( -
-
+ ) : selectedView === "agents" ? ( -
-
+ ) : ( -
+ )}