From abcee33766f744982eba3b86e64fd5a07afd14af Mon Sep 17 00:00:00 2001 From: brennanb2025 Date: Sun, 5 Jul 2026 21:03:14 -0700 Subject: [PATCH] fix(linear): extract comment inline media before body truncation Comment bodies are capped at 20k chars for output; extracting media from the truncated body dropped any screenshot past the cutoff. Extract from the full body in readComments and carry it on the comment node so aggregation surfaces it regardless of truncation. --- .../linear/issue-context-includes.test.ts | 38 +++++++++++++++++++ src/main/linear/issue-context-includes.ts | 5 +++ src/main/linear/issue-context.ts | 7 +++- src/shared/linear-agent-result-types.ts | 3 ++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/main/linear/issue-context-includes.test.ts b/src/main/linear/issue-context-includes.test.ts index f5e79a8a180..8db6df94026 100644 --- a/src/main/linear/issue-context-includes.test.ts +++ b/src/main/linear/issue-context-includes.test.ts @@ -201,6 +201,44 @@ describe('Linear issue context includes', () => { }) }) + it('extracts comment media past the body truncation cap', async () => { + const url = 'https://uploads.linear.app/w/file/late?sig=1' + const body = `${'x'.repeat(20_001)}\n![late](${url})` + rawRequest.mockResolvedValueOnce({ + data: { + issue: { + comments: { + nodes: [{ id: 'comment-1', body }], + pageInfo: { hasNextPage: false } + } + } + } + }) + const { readOptionalIncludes } = await import('./issue-context-includes') + const output = result() + + await readOptionalIncludes( + resolvedIssue(), + requestWithComments(), + output, + [], + output.meta.sections + ) + + expect(output.comments?.[0]?.bodyTruncated).toBe(true) + expect(output.comments?.[0]?.body).not.toContain(url) + expect(output.comments?.[0]?.inlineMedia).toEqual([ + { + source: 'comment', + sourceId: 'comment-1', + url, + altText: 'late', + fileName: 'late', + linearUpload: true + } + ]) + }) + it('reads comment and child bodies through the signed public-file-url client', async () => { // Guards STA-1246: reverting to the plain entry client would return unsigned // uploads.linear.app URLs that 401 for agents. diff --git a/src/main/linear/issue-context-includes.ts b/src/main/linear/issue-context-includes.ts index 3993a845611..ce79937478a 100644 --- a/src/main/linear/issue-context-includes.ts +++ b/src/main/linear/issue-context-includes.ts @@ -16,6 +16,7 @@ import { LINEAR_RELATIONS_CAP, clampLinearIssueDepth } from '../../shared/linear-agent-access' +import { extractLinearInlineMedia } from '../../shared/linear-inline-media' import type { ResolvedIssue } from './issue-context-client' import { getRequiredEntry, withLinearRead } from './issue-context-client' import { getPublicFileUrlClient } from './client' @@ -130,10 +131,14 @@ async function readComments(resolved: ResolvedIssue): Promise<{ const nodes = response.nodes const items = nodes.slice(0, LINEAR_COMMENTS_CAP).map((comment) => { const body = comment.body ?? '' + // Extract media from the full body before truncating so screenshots past + // the body cap are still surfaced. + const inlineMedia = extractLinearInlineMedia(body, 'comment', comment.id) return { id: comment.id, body: body.slice(0, LINEAR_COMMENT_BODY_CAP), bodyTruncated: body.length > LINEAR_COMMENT_BODY_CAP, + ...(inlineMedia.length > 0 ? { inlineMedia } : {}), createdAt: comment.createdAt, updatedAt: comment.updatedAt, parentId: comment.parent?.id ?? null, diff --git a/src/main/linear/issue-context.ts b/src/main/linear/issue-context.ts index 6d4566317bb..9116b9a591b 100644 --- a/src/main/linear/issue-context.ts +++ b/src/main/linear/issue-context.ts @@ -108,8 +108,11 @@ export function collectInlineMedia( ): LinearIssueContextResult['inlineMedia'] { const media = [ ...extractLinearInlineMedia(result.issue.description, 'description'), - ...(result.comments ?? []).flatMap((comment) => - extractLinearInlineMedia(comment.body, 'comment', comment.id) + ...(result.comments ?? []).flatMap( + // Prefer media pre-extracted from the full (untruncated) comment body; + // fall back to the stored body for callers that did not pre-extract. + (comment) => + comment.inlineMedia ?? extractLinearInlineMedia(comment.body, 'comment', comment.id) ), ...(result.children ?? []).flatMap((child) => collectChildInlineMedia(child)) ] diff --git a/src/shared/linear-agent-result-types.ts b/src/shared/linear-agent-result-types.ts index ad7b5eb46da..97e06c63d01 100644 --- a/src/shared/linear-agent-result-types.ts +++ b/src/shared/linear-agent-result-types.ts @@ -44,6 +44,9 @@ export type LinearIssueCommentNode = { id: string body: string bodyTruncated: boolean + // Extracted from the full comment body before truncation so media past the + // body cap is not lost. + inlineMedia?: LinearInlineMedia[] createdAt?: string | null updatedAt?: string | null parentId?: string | null