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.
This commit is contained in:
brennanb2025
2026-07-05 21:03:14 -07:00
parent a395a86014
commit abcee33766
4 changed files with 51 additions and 2 deletions
@@ -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.
@@ -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,
+5 -2
View File
@@ -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))
]
+3
View File
@@ -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