diff --git a/app/src/agentworld/pages/FeedSection.test.tsx b/app/src/agentworld/pages/FeedSection.test.tsx index 10f2d3124..b9352efa7 100644 --- a/app/src/agentworld/pages/FeedSection.test.tsx +++ b/app/src/agentworld/pages/FeedSection.test.tsx @@ -171,6 +171,44 @@ describe('Feed list', () => { expect(screen.getByText(/3 comments/i)).toBeInTheDocument(); }); + test('sorts populated feed items newest first', async () => { + const olderItem = { + ...sampleFeedItem, + post: { + ...samplePost, + postId: 'post-old', + body: 'Old update', + createdAt: '2026-06-01T12:00:00Z', + }, + }; + const newerItem = { + ...sampleFeedItem, + post: { + ...samplePost, + postId: 'post-new', + body: 'Newest update', + createdAt: '2026-06-02T12:00:00Z', + }, + }; + + vi.mocked(apiClient.graphql.homeFeed).mockResolvedValue({ + items: [olderItem, newerItem], + count: 2, + }); + + render(); + + await waitFor(() => { + expect(screen.getByText('Newest update')).toBeInTheDocument(); + expect(screen.getByText('Old update')).toBeInTheDocument(); + }); + + expect( + screen.getByText('Newest update').compareDocumentPosition(screen.getByText('Old update')) & + Node.DOCUMENT_POSITION_FOLLOWING + ).toBeTruthy(); + }); + test('shows wallet-locked error when wallet is not configured', async () => { vi.mocked(apiClient.graphql.homeFeed).mockRejectedValue(new Error('wallet is not configured')); render(); diff --git a/app/src/agentworld/pages/FeedSection.tsx b/app/src/agentworld/pages/FeedSection.tsx index 6f087e3fd..aea13e19e 100644 --- a/app/src/agentworld/pages/FeedSection.tsx +++ b/app/src/agentworld/pages/FeedSection.tsx @@ -15,6 +15,7 @@ * Pattern mirrors ExploreSection / MarketplaceSection: useState + useEffect * fetch, PanelScaffold wrapper, StatusBlock for loading/error/empty states. */ +import debug from 'debug'; import { useCallback, useEffect, useRef, useState } from 'react'; import PanelScaffold from '../../components/layout/PanelScaffold'; @@ -28,6 +29,8 @@ import { import { fetchWalletStatus } from '../../services/walletApi'; import { apiClient } from '../AgentWorldShell'; +const log = debug('agentworld:feed'); + // ── State types ─────────────────────────────────────────────────────────────── type FeedState = @@ -57,6 +60,28 @@ function isWalletLocked(message: string): boolean { ); } +function postCreatedAtMillis(item: GqlHomeFeedItem): number { + const millis = Date.parse(item.post.createdAt); + return Number.isFinite(millis) ? millis : 0; +} + +function sortedHomeFeedItems(result: { items?: GqlHomeFeedItem[] } | null | undefined) { + const items = Array.isArray(result?.items) ? [...result.items] : []; + const originalOrder = items.map(item => item.post.postId).join('\0'); + + items.sort((left, right) => postCreatedAtMillis(right) - postCreatedAtMillis(left)); + + if (items.length > 1 && originalOrder !== items.map(item => item.post.postId).join('\0')) { + log('sorted home feed newest-first', { + count: items.length, + newestCreatedAt: items[0]?.post.createdAt, + oldestCreatedAt: items.at(-1)?.post.createdAt, + }); + } + + return items; +} + /** Centered status message for loading / error / info states. */ function StatusBlock({ tone, title, body }: { tone: string; title: string; body?: string }) { return ( @@ -550,7 +575,7 @@ export default function FeedSection() { .homeFeed({ limit: 50 }) .then(result => { if (cancelled) return; - const items = Array.isArray(result?.items) ? result.items : []; + const items = sortedHomeFeedItems(result); setFeedState({ status: 'ok', items }); }) .catch((err: unknown) => { @@ -624,7 +649,7 @@ export default function FeedSection() { .deletePost(post.postId) .then(() => { void apiClient.graphql.homeFeed({ limit: 50 }).then(result => { - const items = Array.isArray(result?.items) ? result.items : []; + const items = sortedHomeFeedItems(result); setFeedState({ status: 'ok', items }); }); }) @@ -635,7 +660,7 @@ export default function FeedSection() { const refetchFeed = () => { void apiClient.graphql.homeFeed({ limit: 50 }).then(result => { - const items = Array.isArray(result?.items) ? result.items : []; + const items = sortedHomeFeedItems(result); setFeedState({ status: 'ok', items }); }); };