diff --git a/desktop/src/features/channels/ui/ChannelScreen.tsx b/desktop/src/features/channels/ui/ChannelScreen.tsx index efc0bbb88..2e9601ff7 100644 --- a/desktop/src/features/channels/ui/ChannelScreen.tsx +++ b/desktop/src/features/channels/ui/ChannelScreen.tsx @@ -78,6 +78,7 @@ import { mergeAgentNamesIntoProfiles, useChannelActivityTyping, } from "./useChannelActivityTyping"; +import { useAgentConversationRouteTarget } from "./useAgentConversationRouteTarget"; import { useChannelAgentSessions } from "./useChannelAgentSessions"; import { useChannelPanelHistoryState } from "./useChannelPanelHistoryState"; import { useChannelProfilePanel } from "./useChannelProfilePanel"; @@ -166,12 +167,21 @@ export function ChannelScreen({ const mainInsetRef = useMainInsetRef(); const currentPubkey = currentIdentity?.pubkey; const activeChannelId = activeChannel?.id ?? null; + const canShowTasksSurface = activeChannel?.channelType === "stream"; + const effectiveSurfaceTab = canShowTasksSurface + ? activeSurfaceTab + : "messages"; useResetChannelSurfaceTabOnRouteOpen({ activeChannelId, openThreadHeadId, setActiveSurfaceTab, targetMessageId, }); + React.useEffect(() => { + if (!canShowTasksSurface && activeSurfaceTab === "tasks") { + setActiveSurfaceTab("messages"); + } + }, [activeSurfaceTab, canShowTasksSurface]); const effectiveOpenThreadHeadId = optimisticOpenThreadHeadId === undefined ? openThreadHeadId @@ -325,6 +335,11 @@ export function ChannelScreen({ const messageProfilesQuery = useUsersBatchQuery(messageProfilePubkeys, { enabled: messageProfilePubkeys.length > 0, }); + const messageProfilesReady = + messageProfilePubkeys.length === 0 || + (!messageProfilesQuery.isPending && + !messageProfilesQuery.isFetching && + !messageProfilesQuery.isPlaceholderData); const channelMembersQuery = useChannelMembersQuery(activeChannel?.id ?? null); const channelMembers = channelMembersQuery.data; const managedAgentsQuery = useManagedAgentsQuery(); @@ -744,71 +759,15 @@ export function ChannelScreen({ setProfilePanelPubkey, ], ); - const handledAgentConversationRouteTargetRef = React.useRef( - null, - ); - React.useEffect(() => { - if (!targetAgentConversationReplyId) { - handledAgentConversationRouteTargetRef.current = null; - return; - } - - const targetKey = `${activeChannelId ?? "none"}:${targetAgentConversationReplyId}`; - if (handledAgentConversationRouteTargetRef.current === targetKey) { - return; - } - if (!activeChannel || activeChannel.channelType === "forum") { - return; - } - - const agentReply = - timelineMessages.find( - (message) => message.id === targetAgentConversationReplyId, - ) ?? null; - const agentReplyPubkey = agentReply?.pubkey; - if (!agentReply || !agentReplyPubkey) { - return; - } - - const rootId = agentReply.rootId ?? agentReply.parentId ?? agentReply.id; - const contextMessages = timelineMessages.filter( - (candidate) => - candidate.id === rootId || - candidate.id === agentReply.id || - candidate.rootId === rootId || - candidate.parentId === rootId, - ); - const parentMessage = agentReply.parentId - ? (timelineMessages.find( - (candidate) => candidate.id === agentReply.parentId, - ) ?? null) - : null; - const threadRootMessage = - timelineMessages.find((candidate) => candidate.id === rootId) ?? null; - - handledAgentConversationRouteTargetRef.current = targetKey; - void goChannel(activeChannel.id, { replace: true }).then(() => { - openAgentConversation( - { - agentName: agentReply.author, - agentPubkey: agentReplyPubkey, - agentReply, - channel: activeChannel, - contextMessages, - parentMessage, - threadRootMessage, - }, - { publishMarker: false }, - ); - }); - }, [ + useAgentConversationRouteTarget({ activeChannel, activeChannelId, goChannel, + messageProfilesReady, openAgentConversation, targetAgentConversationReplyId, timelineMessages, - ]); + }); const mainTimelineTargetMessageId = useChannelRouteTarget({ activeChannel, activeChannelId, @@ -938,7 +897,7 @@ export function ChannelScreen({ activeChannel={activeChannel} activeChannelEphemeralDisplay={activeChannelEphemeralDisplay} activeChannelTitle={activeChannelTitle} - activeSurfaceTab={activeSurfaceTab} + activeSurfaceTab={effectiveSurfaceTab} actionsVariant={shouldCompactHeaderActions ? "compact" : "inline"} activeDmAvatarUrl={activeDmAvatarUrl} activeDmHeaderParticipants={activeDmHeaderParticipants} @@ -964,9 +923,9 @@ export function ChannelScreen({ activeDmAvatarUrl, activeDmHeaderParticipants, activeDmPresenceStatus, - activeSurfaceTab, channelHeaderChromeRef, currentPubkey, + effectiveSurfaceTab, handleSurfaceTabChange, isAddBotOpen, joinChannelMutation.isPending, @@ -1098,7 +1057,7 @@ export function ChannelScreen({ profilePanelView={profilePanelView} personaLookup={personaLookup} profiles={messageProfiles} - surfaceTab={activeSurfaceTab} + surfaceTab={effectiveSurfaceTab} onSurfaceTabChange={handleSurfaceTabChange} firstUnreadMessageId={firstUnreadMessageId} unreadCount={unreadCount} diff --git a/desktop/src/features/channels/ui/useAgentConversationRouteTarget.ts b/desktop/src/features/channels/ui/useAgentConversationRouteTarget.ts new file mode 100644 index 000000000..5637d3968 --- /dev/null +++ b/desktop/src/features/channels/ui/useAgentConversationRouteTarget.ts @@ -0,0 +1,101 @@ +import * as React from "react"; + +import type { useAppNavigation } from "@/app/navigation/useAppNavigation"; +import type { OpenAgentConversationInput } from "@/features/agents/agentConversations"; +import type { TimelineMessage } from "@/features/messages/types"; +import type { Channel } from "@/shared/api/types"; + +type GoChannel = ReturnType["goChannel"]; +type OpenAgentConversation = ( + input: OpenAgentConversationInput, + options?: { publishMarker?: boolean }, +) => void; + +type UseAgentConversationRouteTargetOptions = { + activeChannel: Channel | null; + activeChannelId: string | null; + goChannel: GoChannel; + messageProfilesReady: boolean; + openAgentConversation: OpenAgentConversation; + targetAgentConversationReplyId: string | null; + timelineMessages: readonly TimelineMessage[]; +}; + +export function useAgentConversationRouteTarget({ + activeChannel, + activeChannelId, + goChannel, + messageProfilesReady, + openAgentConversation, + targetAgentConversationReplyId, + timelineMessages, +}: UseAgentConversationRouteTargetOptions) { + const handledRouteTargetRef = React.useRef(null); + + React.useEffect(() => { + if (!targetAgentConversationReplyId) { + handledRouteTargetRef.current = null; + return; + } + + const targetKey = `${activeChannelId ?? "none"}:${targetAgentConversationReplyId}`; + if (handledRouteTargetRef.current === targetKey) { + return; + } + if (!activeChannel || activeChannel.channelType === "forum") { + return; + } + if (!messageProfilesReady) { + return; + } + + const agentReply = + timelineMessages.find( + (message) => message.id === targetAgentConversationReplyId, + ) ?? null; + const agentReplyPubkey = agentReply?.pubkey; + if (!agentReply || !agentReplyPubkey) { + return; + } + + const rootId = agentReply.rootId ?? agentReply.parentId ?? agentReply.id; + const contextMessages = timelineMessages.filter( + (candidate) => + candidate.id === rootId || + candidate.id === agentReply.id || + candidate.rootId === rootId || + candidate.parentId === rootId, + ); + const parentMessage = agentReply.parentId + ? (timelineMessages.find( + (candidate) => candidate.id === agentReply.parentId, + ) ?? null) + : null; + const threadRootMessage = + timelineMessages.find((candidate) => candidate.id === rootId) ?? null; + + handledRouteTargetRef.current = targetKey; + void goChannel(activeChannel.id, { replace: true }).then(() => { + openAgentConversation( + { + agentName: agentReply.author, + agentPubkey: agentReplyPubkey, + agentReply, + channel: activeChannel, + contextMessages, + parentMessage, + threadRootMessage, + }, + { publishMarker: false }, + ); + }); + }, [ + activeChannel, + activeChannelId, + goChannel, + messageProfilesReady, + openAgentConversation, + targetAgentConversationReplyId, + timelineMessages, + ]); +}