Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 20 additions & 0 deletions desktop/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion desktop/scripts/check-file-sizes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
97 changes: 54 additions & 43 deletions desktop/src/app/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -610,10 +602,7 @@ export function AppShell() {
unreadChannelIds={unreadChannelIds}
/>

<SidebarInset
className="min-h-0 min-w-0 overflow-hidden"
key={contentPaneKey}
>
<SidebarInset className="min-h-0 min-w-0 overflow-hidden">
{selectedView === "home" ? (
<ChatHeader
description="Personalized feed for mentions, reminders, channel activity, and agent work."
Expand Down Expand Up @@ -655,7 +644,13 @@ export function AppShell() {
)}

<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
{selectedView === "home" ? (
<div
className={
selectedView === "home"
? "flex min-h-0 flex-1 flex-col"
: "hidden"
}
>
<HomeView
availableChannelIds={availableChannelIds}
currentPubkey={identityQuery.data?.pubkey}
Expand All @@ -671,37 +666,53 @@ export function AppShell() {
void homeFeedQuery.refetch();
}}
/>
) : selectedView === "agents" ? (
</div>
<div
className={
selectedView === "agents"
? "flex min-h-0 flex-1 flex-col"
: "hidden"
}
>
<AgentsView />
) : activeChannel?.channelType === "forum" ? (
<ForumView
channel={activeChannel}
currentPubkey={identityQuery.data?.pubkey}
/>
) : (
<ChannelPane
activeChannel={activeChannel}
currentPubkey={identityQuery.data?.pubkey}
isSending={sendMessageMutation.isPending}
isTimelineLoading={isTimelineLoading}
messages={timelineMessages}
onCancelReply={handleCancelReply}
onReply={handleReply}
onSend={handleSend}
onTargetReached={handleTargetReached}
onToggleReaction={effectiveToggleReaction}
profiles={messageProfiles}
replyTargetId={replyTargetId}
replyTargetMessage={replyTargetMessage}
targetMessageId={
activeChannel &&
searchAnchor?.channelId === activeChannel.id
? searchAnchor.eventId
: null
}
typingPubkeys={typingPubkeys}
/>
)}
</div>
<div
className={
selectedView !== "home" && selectedView !== "agents"
? "flex min-h-0 flex-1 flex-col overflow-hidden"
: "hidden"
}
>
{activeChannel?.channelType === "forum" ? (
<ForumView
channel={activeChannel}
currentPubkey={identityQuery.data?.pubkey}
/>
) : (
<ChannelPane
activeChannel={activeChannel}
currentPubkey={identityQuery.data?.pubkey}
isSending={sendMessageMutation.isPending}
isTimelineLoading={isTimelineLoading}
messages={timelineMessages}
onCancelReply={handleCancelReply}
onReply={handleReply}
onSend={handleSend}
onTargetReached={handleTargetReached}
onToggleReaction={effectiveToggleReaction}
profiles={messageProfiles}
replyTargetId={replyTargetId}
replyTargetMessage={replyTargetMessage}
targetMessageId={
activeChannel &&
searchAnchor?.channelId === activeChannel.id
? searchAnchor.eventId
: null
}
typingPubkeys={typingPubkeys}
/>
)}
</div>
</div>
</SidebarInset>
</React.Fragment>
Expand Down
14 changes: 7 additions & 7 deletions desktop/src/features/agents/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,21 +136,21 @@ export function useRelayAgentsQuery() {
return useQuery({
queryKey: relayAgentsQueryKey,
queryFn: listRelayAgents,
staleTime: 15_000,
refetchInterval: 15_000,
staleTime: 30_000,
refetchInterval: 30_000,
});
}

export function useManagedAgentsQuery() {
return useQuery({
queryKey: managedAgentsQueryKey,
queryFn: listManagedAgents,
staleTime: 1_000,
staleTime: 5_000,
refetchInterval: (query) => {
const agents = query.state.data as ManagedAgent[] | undefined;
return agents?.some((agent) => agent.status === "running")
? 2_000
: 10_000;
? 5_000
: 30_000;
},
});
}
Expand Down Expand Up @@ -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,
});
}

Expand Down
82 changes: 58 additions & 24 deletions desktop/src/features/messages/ui/MessageTimeline.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -11,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[];
Expand Down Expand Up @@ -44,6 +48,15 @@ export const MessageTimeline = React.memo(function MessageTimeline({
targetMessageId = null,
onTargetReached,
}: MessageTimelineProps) {
const scrollContainerRef = React.useRef<HTMLDivElement>(null);

const virtualizer = useVirtualizer({
count: messages.length,
getScrollElement: () => scrollContainerRef.current,
estimateSize: () => ESTIMATED_ROW_HEIGHT,
overscan: OVERSCAN_COUNT,
});

const {
bottomAnchorRef,
contentRef,
Expand All @@ -52,22 +65,26 @@ export const MessageTimeline = React.memo(function MessageTimeline({
newMessageCount,
scrollToBottom,
syncScrollState,
timelineRef,
} = useTimelineScrollManager({
channelId,
isLoading,
messages,
onTargetReached,
scrollContainerRef,
targetMessageId,
virtualizer,
});

const virtualItems = virtualizer.getVirtualItems();
const totalSize = virtualizer.getTotalSize();

return (
<div className="relative min-h-0 flex-1">
<div
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={timelineRef}
ref={scrollContainerRef}
>
<div
className="mx-auto flex w-full max-w-4xl flex-col gap-2"
Expand Down Expand Up @@ -100,29 +117,46 @@ export const MessageTimeline = React.memo(function MessageTimeline({
</div>
) : null}

{!isLoading
? messages.map((message) =>
message.kind === KIND_SYSTEM_MESSAGE ? (
<SystemMessageRow
body={message.body}
currentPubkey={currentPubkey}
key={message.id}
profiles={profiles}
time={message.time}
/>
) : (
<MessageRow
activeReplyTargetId={activeReplyTargetId}
highlighted={message.id === highlightedMessageId}
{!isLoading && messages.length > 0 ? (
<div
className="relative w-full"
style={{ height: `${totalSize}px` }}
>
{virtualItems.map((virtualRow) => {
const message = messages[virtualRow.index];
return (
<div
key={message.id}
message={message}
onToggleReaction={onToggleReaction}
onReply={onReply}
profiles={profiles}
/>
),
)
: null}
data-index={virtualRow.index}
ref={virtualizer.measureElement}
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${virtualRow.start}px)`,
}}
>
{message.kind === KIND_SYSTEM_MESSAGE ? (
<SystemMessageRow
body={message.body}
currentPubkey={currentPubkey}
profiles={profiles}
time={message.time}
/>
) : (
<MessageRow
activeReplyTargetId={activeReplyTargetId}
highlighted={message.id === highlightedMessageId}
message={message}
onToggleReaction={onToggleReaction}
onReply={onReply}
profiles={profiles}
/>
)}
</div>
);
})}
</div>
) : null}

<div aria-hidden className="h-px" ref={bottomAnchorRef} />
</div>
</div>
Expand Down
Loading
Loading