From cd636b305e77861b806441a5986a14bc3c132d1e Mon Sep 17 00:00:00 2001 From: klopez4212 Date: Sun, 28 Jun 2026 09:03:07 +0100 Subject: [PATCH] Address task list review feedback --- .../src/features/channels/ui/ChannelPane.tsx | 247 +-------------- .../features/channels/ui/ChannelScreen.tsx | 2 + .../features/channels/ui/ChannelTasksView.tsx | 293 ++++++++++++++++++ 3 files changed, 307 insertions(+), 235 deletions(-) create mode 100644 desktop/src/features/channels/ui/ChannelTasksView.tsx diff --git a/desktop/src/features/channels/ui/ChannelPane.tsx b/desktop/src/features/channels/ui/ChannelPane.tsx index 7423ac2e6..9f23ea74b 100644 --- a/desktop/src/features/channels/ui/ChannelPane.tsx +++ b/desktop/src/features/channels/ui/ChannelPane.tsx @@ -1,13 +1,5 @@ import * as React from "react"; -import { - Bot, - ClipboardPlus, - Hash, - LogIn, - Plus, - Sparkles, - UserPlus, -} from "lucide-react"; +import { Bot, Hash, LogIn, Plus, Sparkles, UserPlus } from "lucide-react"; import { useMediaUpload } from "@/features/messages/lib/useMediaUpload"; import { MessageComposer } from "@/features/messages/ui/MessageComposer"; import { DropZoneOverlay } from "@/features/messages/ui/ComposerAttachments"; @@ -40,6 +32,7 @@ import { AgentSessionThreadPanel } from "@/features/channels/ui/AgentSessionThre import { ChannelManagementAuxiliaryPanel } from "@/features/channels/ui/ChannelManagementAuxiliaryPanel"; import { RightAuxiliaryPane } from "@/features/channels/ui/RightAuxiliaryPane"; import { BotActivityComposerAction } from "@/features/channels/ui/BotActivityBar"; +import { ChannelTasksView } from "@/features/channels/ui/ChannelTasksView"; import { containsWelcomePersonaMention, WelcomeComposerBanner, @@ -60,236 +53,15 @@ import type { ChannelPaneProps } from "@/features/channels/ui/ChannelPane.types" import * as agentSessionSelection from "@/features/channels/ui/agentSessionSelection"; import { Button } from "@/shared/ui/button"; import { buildMainTimelineEntries } from "@/features/messages/lib/threadPanel"; -import { - formatDayHeading, - formatTime, -} from "@/features/messages/lib/dateFormatters"; +import { isBroadcastReply } from "@/features/messages/lib/threading"; import { useRenderScopedReactionHydration } from "@/features/messages/lib/useRenderScopedReactionHydration"; import type { TimelineMessage } from "@/features/messages/types"; -import { - resolveUserLabel, - type UserProfileLookup, -} from "@/features/profile/lib/identity"; import { isWelcomeChannel } from "@/features/onboarding/welcome"; -import type { Channel } from "@/shared/api/types"; import { KIND_SYSTEM_MESSAGE } from "@/shared/constants/kinds"; import { useAppShell } from "@/app/AppShellContext"; import { useIsThreadPanelOverlay } from "@/shared/hooks/use-mobile"; import { channelChrome } from "@/shared/layout/chromeLayout"; import { cn } from "@/shared/lib/cn"; - -type ChannelTaskItem = { - marker: AgentConversationMarker; - message: TimelineMessage | null; - threadMessage: TimelineMessage | null; -}; - -function formatTaskStartedAt(unixSeconds: number): string { - return `${formatDayHeading(unixSeconds)} at ${formatTime(unixSeconds)}`; -} - -function ChannelTaskRow({ - currentPubkey, - marker, - message, - onOpenAgentConversation, - onGoToTaskMessage, - profiles, - threadMessage, -}: { - currentPubkey?: string; - marker: AgentConversationMarker; - message: TimelineMessage | null; - onOpenAgentConversation?: ( - message: TimelineMessage, - options?: { publishMarker?: boolean }, - ) => void; - onGoToTaskMessage?: ( - marker: AgentConversationMarker, - message: TimelineMessage, - threadMessage: TimelineMessage, - ) => void; - profiles?: UserProfileLookup; - threadMessage: TimelineMessage | null; -}) { - const startedAt = marker.startedAt || marker.createdAt; - const starterName = resolveUserLabel({ - currentPubkey, - profiles, - pubkey: marker.starterPubkey, - }); - - return ( -
-
-
- -
-
-

- {marker.title} -

-

- {starterName} · {formatTaskStartedAt(startedAt)} -

-
-
- - -
-
-
- ); -} - -function ChannelTasksView({ - activeChannel, - agentConversationMarkers, - currentPubkey, - messages, - onOpenAgentConversation, - onGoToTaskMessage, - profiles, - scrollContainerRef, -}: { - activeChannel: Channel | null; - agentConversationMarkers?: readonly AgentConversationMarker[]; - currentPubkey?: string; - messages: readonly TimelineMessage[]; - onOpenAgentConversation?: ( - message: TimelineMessage, - options?: { publishMarker?: boolean }, - ) => void; - onGoToTaskMessage?: ( - marker: AgentConversationMarker, - message: TimelineMessage, - threadMessage: TimelineMessage, - ) => void; - profiles?: UserProfileLookup; - scrollContainerRef: React.RefObject; -}) { - const messageById = React.useMemo( - () => new Map(messages.map((message) => [message.id, message])), - [messages], - ); - const taskItems = React.useMemo(() => { - const channelId = activeChannel?.id ?? null; - - return (agentConversationMarkers ?? []) - .filter((marker) => !channelId || marker.channelId === channelId) - .map((marker) => { - const message = messageById.get(marker.agentReplyId) ?? null; - const resolvedThreadMessage = - messageById.get(marker.threadRootMessageId ?? "") ?? - messageById.get(marker.threadRootId) ?? - messageById.get(marker.parentMessageId ?? "") ?? - null; - const threadMessage = - resolvedThreadMessage ?? - (marker.threadRootId === marker.agentReplyId ? message : null); - return { - marker, - message, - threadMessage, - }; - }) - .sort( - (left, right) => - (right.marker.startedAt || right.marker.createdAt) - - (left.marker.startedAt || left.marker.createdAt) || - right.marker.eventId.localeCompare(left.marker.eventId), - ); - }, [activeChannel?.id, agentConversationMarkers, messageById]); - - return ( -
-
-
- {taskItems.length === 0 ? ( -
-
- -
-

- No tasks yet -

-

- New tasks will appear here when an agent conversation is opened - from this channel. -

-
- ) : ( -
- {taskItems.map(({ marker, message, threadMessage }) => ( - - ))} -
- )} -
-
-
- ); -} export const ChannelPane = React.memo(function ChannelPane({ activeChannel, agentConversationMarkers, @@ -615,11 +387,13 @@ export const ChannelPane = React.memo(function ChannelPane({ threadMessage: TimelineMessage, ) => { onSurfaceTabChange?.("messages"); + const isBroadcastTask = isBroadcastReply(message.tags ?? []); const isThreadedTask = - Boolean(marker.parentMessageId) || - Boolean(message.parentId) || - Boolean(message.rootId && message.rootId !== message.id) || - threadMessage.id !== message.id; + !isBroadcastTask && + (Boolean(marker.parentMessageId) || + Boolean(message.parentId) || + Boolean(message.rootId && message.rootId !== message.id) || + threadMessage.id !== message.id); if (isThreadedTask) { onOpenThread(threadMessage); @@ -996,6 +770,9 @@ export const ChannelPane = React.memo(function ChannelPane({ activeChannel={activeChannel} agentConversationMarkers={agentConversationMarkers} currentPubkey={currentPubkey} + fetchOlder={fetchOlder} + hasOlderMessages={hasOlderMessages} + isFetchingOlder={isFetchingOlder} messages={messages} onOpenAgentConversation={handleOpenAgentConversation} onGoToTaskMessage={handleGoToTaskMessage} diff --git a/desktop/src/features/channels/ui/ChannelScreen.tsx b/desktop/src/features/channels/ui/ChannelScreen.tsx index 2e9601ff7..0edae3d60 100644 --- a/desktop/src/features/channels/ui/ChannelScreen.tsx +++ b/desktop/src/features/channels/ui/ChannelScreen.tsx @@ -876,10 +876,12 @@ export function ChannelScreen({ setThreadReplyTargetId(null); handleCloseAgentSession(); setProfilePanelPubkey(null); + handleSurfaceTabChange("messages"); setChannelManagementOpen(true); }, [ activeChannel?.channelType, channelManagementOpen, + handleSurfaceTabChange, openGlobalChannelManagement, setChannelManagementOpen, setOpenThreadHeadId, diff --git a/desktop/src/features/channels/ui/ChannelTasksView.tsx b/desktop/src/features/channels/ui/ChannelTasksView.tsx new file mode 100644 index 000000000..682956477 --- /dev/null +++ b/desktop/src/features/channels/ui/ChannelTasksView.tsx @@ -0,0 +1,293 @@ +import * as React from "react"; +import { ClipboardPlus } from "lucide-react"; + +import type { AgentConversationMarker } from "@/features/agents/agentConversations"; +import { + formatDayHeading, + formatTime, +} from "@/features/messages/lib/dateFormatters"; +import type { TimelineMessage } from "@/features/messages/types"; +import { + resolveUserLabel, + type UserProfileLookup, +} from "@/features/profile/lib/identity"; +import type { Channel } from "@/shared/api/types"; +import { channelChrome } from "@/shared/layout/chromeLayout"; +import { cn } from "@/shared/lib/cn"; +import { Button } from "@/shared/ui/button"; + +type ChannelTaskItem = { + marker: AgentConversationMarker; + message: TimelineMessage | null; + threadMessage: TimelineMessage | null; +}; + +type ChannelTasksViewProps = { + activeChannel: Channel | null; + agentConversationMarkers?: readonly AgentConversationMarker[]; + currentPubkey?: string; + fetchOlder?: () => Promise; + hasOlderMessages?: boolean; + isFetchingOlder?: boolean; + messages: readonly TimelineMessage[]; + onOpenAgentConversation?: ( + message: TimelineMessage, + options?: { publishMarker?: boolean }, + ) => void; + onGoToTaskMessage?: ( + marker: AgentConversationMarker, + message: TimelineMessage, + threadMessage: TimelineMessage, + ) => void; + profiles?: UserProfileLookup; + scrollContainerRef: React.RefObject; +}; + +function formatTaskStartedAt(unixSeconds: number): string { + return `${formatDayHeading(unixSeconds)} at ${formatTime(unixSeconds)}`; +} + +function ChannelTaskRow({ + currentPubkey, + marker, + message, + onOpenAgentConversation, + onGoToTaskMessage, + profiles, + threadMessage, +}: { + currentPubkey?: string; + marker: AgentConversationMarker; + message: TimelineMessage | null; + onOpenAgentConversation?: ( + message: TimelineMessage, + options?: { publishMarker?: boolean }, + ) => void; + onGoToTaskMessage?: ( + marker: AgentConversationMarker, + message: TimelineMessage, + threadMessage: TimelineMessage, + ) => void; + profiles?: UserProfileLookup; + threadMessage: TimelineMessage | null; +}) { + const startedAt = marker.startedAt || marker.createdAt; + const starterName = resolveUserLabel({ + currentPubkey, + profiles, + pubkey: marker.starterPubkey, + }); + + return ( +
+
+
+ +
+
+

+ {marker.title} +

+

+ {starterName} · {formatTaskStartedAt(startedAt)} +

+
+
+ + +
+
+
+ ); +} + +export function ChannelTasksView({ + activeChannel, + agentConversationMarkers, + currentPubkey, + messages, + fetchOlder, + hasOlderMessages, + isFetchingOlder, + onOpenAgentConversation, + onGoToTaskMessage, + profiles, + scrollContainerRef, +}: ChannelTasksViewProps) { + const loadOlderRef = React.useRef(null); + const messageById = React.useMemo( + () => new Map(messages.map((message) => [message.id, message])), + [messages], + ); + const canLoadOlderTasks = Boolean(fetchOlder && hasOlderMessages); + const handleLoadOlderTasks = React.useCallback(() => { + if (!fetchOlder || isFetchingOlder) { + return; + } + + void fetchOlder(); + }, [fetchOlder, isFetchingOlder]); + + React.useEffect(() => { + if (!canLoadOlderTasks || isFetchingOlder) { + return; + } + + const root = scrollContainerRef.current; + const target = loadOlderRef.current; + if (!root || !target) { + return; + } + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry?.isIntersecting) { + handleLoadOlderTasks(); + } + }, + { root, rootMargin: "160px 0px" }, + ); + observer.observe(target); + return () => observer.disconnect(); + }, [ + canLoadOlderTasks, + handleLoadOlderTasks, + isFetchingOlder, + scrollContainerRef, + ]); + + const taskItems = React.useMemo(() => { + const channelId = activeChannel?.id ?? null; + + return (agentConversationMarkers ?? []) + .filter((marker) => !channelId || marker.channelId === channelId) + .map((marker) => { + const message = messageById.get(marker.agentReplyId) ?? null; + const resolvedThreadMessage = + messageById.get(marker.threadRootMessageId ?? "") ?? + messageById.get(marker.threadRootId) ?? + messageById.get(marker.parentMessageId ?? "") ?? + null; + const threadMessage = + resolvedThreadMessage ?? + (marker.threadRootId === marker.agentReplyId ? message : null); + return { + marker, + message, + threadMessage, + }; + }) + .sort( + (left, right) => + (right.marker.startedAt || right.marker.createdAt) - + (left.marker.startedAt || left.marker.createdAt) || + right.marker.eventId.localeCompare(left.marker.eventId), + ); + }, [activeChannel?.id, agentConversationMarkers, messageById]); + const olderTasksLoader = canLoadOlderTasks ? ( +
+ +
+ ) : null; + + return ( +
+
+
+ {taskItems.length === 0 ? ( + <> +
+
+ +
+

+ No tasks yet +

+

+ New tasks will appear here when an agent conversation is + opened from this channel. +

+
+ {olderTasksLoader} + + ) : ( +
+ {taskItems.map(({ marker, message, threadMessage }) => ( + + ))} + {olderTasksLoader} +
+ )} +
+
+
+ ); +}