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