From ad4c25cda2e68fa89a8ae7c11c0a3a88bf34bfd8 Mon Sep 17 00:00:00 2001 From: Suat Kocar Date: Sat, 24 Oct 2026 03:20:34 +0100 Subject: [PATCH] fix(linear): list issue comments oldest first (#25622) Linear returns an issue's comments newest first and getIssueComments passed that order through, so the activity feed showed the newest comment on top while comments posted from Orca were appended at the bottom and jumped to the top on reload. Sort by createdAt, oldest first, like the GitLab notes path does. Fixes #25573 Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> --- src/main/linear/linear-issue-comments.test.ts | 53 +++++++++++++++++++ src/main/linear/linear-issue-comments.ts | 4 +- 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 src/main/linear/linear-issue-comments.test.ts diff --git a/src/main/linear/linear-issue-comments.test.ts b/src/main/linear/linear-issue-comments.test.ts new file mode 100644 index 00000000000..89427dcdf43 --- /dev/null +++ b/src/main/linear/linear-issue-comments.test.ts @@ -0,0 +1,53 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const rawRequest = vi.fn() +const getClients = vi.fn() + +vi.mock('./linear-request-concurrency', () => ({ + acquire: vi.fn().mockResolvedValue(undefined), + release: vi.fn() +})) + +vi.mock('./linear-token-store', () => ({ + clearToken: vi.fn() +})) + +vi.mock('./client', () => ({ + getClients: (...args: unknown[]) => getClients(...args), + getPublicFileUrlClient: () => ({ client: { rawRequest } }), + isAuthError: () => false +})) + +function rawComment(id: string, body: string, createdAt: string) { + return { id, body, createdAt, user: { displayName: 'Ada', avatarUrl: null } } +} + +describe('getIssueComments', () => { + beforeEach(() => { + vi.clearAllMocks() + getClients.mockReturnValue([ + { workspace: { id: 'workspace-1' }, client: { client: { rawRequest } } } + ]) + }) + + it('returns comments oldest first, although Linear sends them newest first', async () => { + rawRequest.mockResolvedValueOnce({ + data: { + issue: { + comments: { + nodes: [ + rawComment('comment-3', 'Third', '2026-01-02T03:06:05.000Z'), + rawComment('comment-2', 'Second', '2026-01-02T03:05:05.000Z'), + rawComment('comment-1', 'First', '2026-01-02T03:04:05.000Z') + ] + } + } + } + }) + const { getIssueComments } = await import('./linear-issue-comments') + + const comments = await getIssueComments('issue-uuid', 'workspace-1') + + expect(comments.map((comment) => comment.body)).toEqual(['First', 'Second', 'Third']) + }) +}) diff --git a/src/main/linear/linear-issue-comments.ts b/src/main/linear/linear-issue-comments.ts index 5cba1ec0db5..69051e09c72 100644 --- a/src/main/linear/linear-issue-comments.ts +++ b/src/main/linear/linear-issue-comments.ts @@ -243,7 +243,7 @@ export async function getIssueComments( LinearRawVariables >(ISSUE_COMMENTS_QUERY, { id: issueId }) const nodes = result.data?.issue?.comments?.nodes ?? [] - return nodes.map((node) => ({ + const comments = nodes.map((node) => ({ id: node.id, body: node.body ?? '', createdAt: node.createdAt ?? '', @@ -254,6 +254,8 @@ export async function getIssueComments( } : undefined })) + // Why: Linear sends newest first; oldest-first matches linear.app and where new comments land. + return comments.sort((a, b) => a.createdAt.localeCompare(b.createdAt)) } catch (error) { if (isAuthError(error)) { clearToken(entry.workspace.id)