diff --git a/.changeset/insights-newest-first.md b/.changeset/insights-newest-first.md
new file mode 100644
index 0000000000..48c649fc3d
--- /dev/null
+++ b/.changeset/insights-newest-first.md
@@ -0,0 +1,7 @@
+---
+"@runfusion/fusion": patch
+---
+
+summary: Insights now list newest first instead of oldest first.
+category: fix
+dev: Ordering is applied in `useInsights` section grouping; `InsightStore.listInsights` keeps its `createdAt ASC, id ASC` contract.
diff --git a/packages/dashboard/app/components/__tests__/InsightsView.test.tsx b/packages/dashboard/app/components/__tests__/InsightsView.test.tsx
index 118821dec0..843111d756 100644
--- a/packages/dashboard/app/components/__tests__/InsightsView.test.tsx
+++ b/packages/dashboard/app/components/__tests__/InsightsView.test.tsx
@@ -271,6 +271,47 @@ describe("InsightsView", () => {
expect(screen.getByTestId("insights-section-features")).toBeInTheDocument();
});
+ it("renders active-section insight titles newest-first in document order", () => {
+ mockUseInsights.mockReturnValue({
+ sections: [
+ {
+ ...mockSections[0],
+ items: [
+ { id: "INS-NEW", projectId: "test", title: "Newest insight", content: "", category: "features", status: "generated", fingerprint: "fp-new", provenance: { trigger: "manual" }, lastRunId: null, createdAt: "2026-03-01T00:00:00Z", updatedAt: "2026-03-01T00:00:00Z" },
+ { id: "INS-MID", projectId: "test", title: "Middle insight", content: "", category: "features", status: "generated", fingerprint: "fp-mid", provenance: { trigger: "manual" }, lastRunId: null, createdAt: "2026-02-01T00:00:00Z", updatedAt: "2026-02-01T00:00:00Z" },
+ { id: "INS-OLD", projectId: "test", title: "Oldest insight", content: "", category: "features", status: "generated", fingerprint: "fp-old", provenance: { trigger: "manual" }, lastRunId: null, createdAt: "2026-01-01T00:00:00Z", updatedAt: "2026-01-01T00:00:00Z" },
+ ],
+ },
+ ...mockSections.slice(1),
+ ],
+ loading: false,
+ error: null,
+ latestRun: null,
+ isRunInFlight: false,
+ runError: null,
+ refresh: vi.fn(),
+ runInsights: vi.fn(),
+ dismiss: vi.fn(),
+ createTask: vi.fn(),
+ archive: vi.fn(),
+ unarchive: vi.fn(),
+ toggleShowArchived: vi.fn(),
+ dismissStates: new Map(),
+ createTaskStates: new Map(),
+ archiveStates: new Map(),
+ unarchiveStates: new Map(),
+ totalCount: 3,
+ dismissedCount: 0,
+ archivedCount: 0,
+ showArchived: false,
+ });
+
+ const { container } = render();
+
+ expect([...container.querySelectorAll(".insight-item-title")].map((node) => node.textContent))
+ .toEqual(["Newest insight", "Middle insight", "Oldest insight"]);
+ });
+
it("should render loading state", () => {
mockUseInsights.mockReturnValue({
...mockUseInsights("test"),
diff --git a/packages/dashboard/app/hooks/__tests__/useInsights.test.ts b/packages/dashboard/app/hooks/__tests__/useInsights.test.ts
index b2a0304f87..7eaebf2e53 100644
--- a/packages/dashboard/app/hooks/__tests__/useInsights.test.ts
+++ b/packages/dashboard/app/hooks/__tests__/useInsights.test.ts
@@ -6,7 +6,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { renderHook, act, waitFor } from "@testing-library/react";
import { useInsights, INSIGHT_CATEGORIES } from "../useInsights";
import { SWR_CACHE_KEYS } from "../../utils/swrCache";
-import type { InsightCategory, InsightStatus } from "@fusion/core";
+import type { Insight, InsightCategory, InsightStatus } from "@fusion/core";
// Mock the API module
vi.mock("../../api", () => ({
@@ -73,6 +73,23 @@ const mockFetchInsightRun = vi.mocked(fetchInsightRun);
const mockFetchInsightRuns = vi.mocked(fetchInsightRuns);
const mockGetInsightCreateTaskData = vi.mocked(getInsightCreateTaskData);
+function makeInsight(overrides: Partial = {}): Insight {
+ return {
+ id: "INS-1",
+ projectId: "project-1",
+ title: "Insight",
+ content: "Content",
+ category: "features",
+ status: "generated",
+ fingerprint: "fp-1",
+ provenance: { trigger: "manual" },
+ lastRunId: null,
+ createdAt: "2026-01-01T00:00:00Z",
+ updatedAt: "2026-01-01T00:00:00Z",
+ ...overrides,
+ };
+}
+
describe("useInsights", () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -197,6 +214,67 @@ describe("useInsights", () => {
expect(cached).not.toHaveProperty("dismissStates");
});
+ it("sorts API insights newest-first and breaks equal timestamps by descending id", async () => {
+ mockFetchInsights.mockResolvedValue({
+ insights: [
+ makeInsight({ id: "INS-OLD", createdAt: "2026-01-01T00:00:00Z" }),
+ makeInsight({ id: "INS-A", createdAt: "2026-02-01T00:00:00Z" }),
+ makeInsight({ id: "INS-Z", createdAt: "2026-02-01T00:00:00Z" }),
+ makeInsight({ id: "INS-NEW", createdAt: "2026-03-01T00:00:00Z" }),
+ ],
+ count: 4,
+ });
+ mockFetchInsightRuns.mockResolvedValue({ runs: [] });
+
+ const { result } = renderHook(() => useInsights("project-1"));
+
+ await waitFor(() => expect(result.current.loading).toBe(false));
+
+ expect(result.current.sections.find((section) => section.category === "features")?.items.map((item) => item.id))
+ .toEqual(["INS-NEW", "INS-Z", "INS-A", "INS-OLD"]);
+ });
+
+ it("sorts cached insights newest-first before the network refresh resolves", async () => {
+ localStorage.setItem(
+ `${SWR_CACHE_KEYS.INSIGHTS_PREFIX}project-1`,
+ JSON.stringify({
+ savedAt: Date.now(),
+ data: [
+ makeInsight({ id: "INS-CACHED-OLD", createdAt: "2026-01-01T00:00:00Z" }),
+ makeInsight({ id: "INS-CACHED-NEW", createdAt: "2026-03-01T00:00:00Z" }),
+ ],
+ }),
+ );
+ mockFetchInsights.mockImplementation(() => new Promise(() => {}));
+ mockFetchInsightRuns.mockImplementation(() => new Promise(() => {}));
+
+ const { result } = renderHook(() => useInsights("project-1"));
+
+ await waitFor(() => {
+ expect(result.current.sections.find((section) => section.category === "features")?.items.map((item) => item.id))
+ .toEqual(["INS-CACHED-NEW", "INS-CACHED-OLD"]);
+ });
+ });
+
+ it("sorts malformed createdAt values last without throwing", async () => {
+ mockFetchInsights.mockResolvedValue({
+ insights: [
+ makeInsight({ id: "INS-INVALID-FIRST", createdAt: "not-a-date" }),
+ makeInsight({ id: "INS-VALID", createdAt: "2026-03-01T00:00:00Z" }),
+ makeInsight({ id: "INS-MISSING", createdAt: undefined as unknown as string }),
+ ],
+ count: 3,
+ });
+ mockFetchInsightRuns.mockResolvedValue({ runs: [] });
+
+ const { result } = renderHook(() => useInsights("project-1"));
+
+ await waitFor(() => expect(result.current.loading).toBe(false));
+
+ expect(result.current.sections.find((section) => section.category === "features")?.items.map((item) => item.id))
+ .toEqual(["INS-VALID", "INS-INVALID-FIRST", "INS-MISSING"]);
+ });
+
it("caps persisted insight cache at 500", async () => {
const oversized = Array.from({ length: 610 }, (_, index) => ({
id: `INS-${index}`,
@@ -750,6 +828,32 @@ describe("useInsights", () => {
expect(result.current.archivedCount).toBe(0);
});
+ it("preserves newest-first order after dismissing and showing archived insights", async () => {
+ const insights = [
+ makeInsight({ id: "INS-OLD", createdAt: "2026-01-01T00:00:00Z" }),
+ makeInsight({ id: "INS-ARCHIVED", status: "archived", createdAt: "2026-02-01T00:00:00Z" }),
+ makeInsight({ id: "INS-NEW", createdAt: "2026-03-01T00:00:00Z" }),
+ ];
+ mockFetchInsights.mockResolvedValue({ insights, count: insights.length });
+ mockFetchInsightRuns.mockResolvedValue({ runs: [] });
+ mockDismissInsight.mockResolvedValue({ ...insights[0], status: "dismissed" });
+
+ const { result } = renderHook(() => useInsights("project-1"));
+ await waitFor(() => expect(result.current.loading).toBe(false));
+
+ await act(async () => {
+ await result.current.dismiss("INS-OLD");
+ });
+ expect(result.current.sections.find((section) => section.category === "features")?.items.map((item) => item.id))
+ .toEqual(["INS-NEW"]);
+
+ act(() => {
+ result.current.toggleShowArchived();
+ });
+ expect(result.current.sections.find((section) => section.category === "features")?.items.map((item) => item.id))
+ .toEqual(["INS-NEW", "INS-ARCHIVED"]);
+ });
+
it("toggleShowArchived hides and shows archived insights", async () => {
const archivedInsight = {
id: "INS-A",
diff --git a/packages/dashboard/app/hooks/useInsights.ts b/packages/dashboard/app/hooks/useInsights.ts
index 11ad3b1edb..cd144c39db 100644
--- a/packages/dashboard/app/hooks/useInsights.ts
+++ b/packages/dashboard/app/hooks/useInsights.ts
@@ -72,6 +72,30 @@ export const STATUS_LABELS: Record = {
archived: "Archived",
};
+/*
+ * FNXC:InsightsView 2026-08-13-21:58:
+ * The Insights list must read newest-first. InsightStore.listInsights is contractually
+ * createdAt ASC for deterministic pagination and dedupe, so presentation order is inverted
+ * at this single section-derivation seam rather than in the store. Tie-break valid equal
+ * timestamps by id DESC; malformed timestamps sort last without changing their source order.
+ */
+function compareInsightsRecentFirst(a: Insight, b: Insight): number {
+ const aCreatedAt = Date.parse(a.createdAt);
+ const bCreatedAt = Date.parse(b.createdAt);
+ const aHasValidCreatedAt = Number.isFinite(aCreatedAt);
+ const bHasValidCreatedAt = Number.isFinite(bCreatedAt);
+
+ if (!aHasValidCreatedAt || !bHasValidCreatedAt) {
+ if (aHasValidCreatedAt) return -1;
+ if (bHasValidCreatedAt) return 1;
+ return 0;
+ }
+
+ if (aCreatedAt !== bCreatedAt) return bCreatedAt - aCreatedAt;
+ if (a.id === b.id) return 0;
+ return a.id < b.id ? 1 : -1;
+}
+
// Section data structure
export interface InsightSection {
category: InsightCategory;
@@ -347,7 +371,7 @@ export function useInsights(projectId?: string): UseInsightsResult {
INSIGHT_CATEGORIES.map((category) => ({
category,
label: getCategoryLabel(category),
- items: grouped.get(category) ?? [],
+ items: [...(grouped.get(category) ?? [])].sort(compareInsightsRecentFirst),
isLoading: false,
error: null,
})),