From 5848ee0d24ba273dea17daa084e4634ac8bf5b16 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Sat, 19 Sep 2026 09:13:29 +0000 Subject: [PATCH] fix(app): restore attachment classification (#49932) Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com> --- .../session-timeline-projection.spec.ts | 39 +++++++++++++++++++ .../src/components/message-file.test.ts | 19 ++++++++- .../session-ui/src/components/message-file.ts | 7 ++++ .../src/message/message-content.tsx | 4 +- 4 files changed, 66 insertions(+), 3 deletions(-) diff --git a/packages/app/e2e/regression/session-timeline-projection.spec.ts b/packages/app/e2e/regression/session-timeline-projection.spec.ts index bf63067fa52..acdbf87ba51 100644 --- a/packages/app/e2e/regression/session-timeline-projection.spec.ts +++ b/packages/app/e2e/regression/session-timeline-projection.spec.ts @@ -166,6 +166,45 @@ test.describe("session timeline projection", () => { await expect(longNotice.locator("[title]")).toHaveAttribute("title", `Switched to ${longName}`) await expect.poll(() => longNotice.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(true) }) + + test("does not repeat a review comment file as an attachment", async ({ page }) => { + const message = userMessage([ + userText("what's goin on here", { id: "prt_user_review_comment" }), + { + id: "prt_user_review_file", + type: "file", + mime: "text/plain", + filename: "LiveRuntime.ts", + url: "file:///repo/LiveRuntime.ts?start=14&end=32", + }, + { + id: "prt_user_unrelated_file", + type: "file", + mime: "text/plain", + filename: "notes.txt", + url: "data:text/plain;base64,bm90ZXM=", + }, + ]) + message.metadata = { + displayText: "what's goin on here", + comments: [ + { + path: "LiveRuntime.ts", + comment: "what's goin on here", + selection: { startLine: 14, startChar: 0, endLine: 32, endChar: 0 }, + origin: "review", + }, + ], + } + + await setupTimeline(page, { messages: [message, assistantMessage()] }) + + const user = page.locator('[data-component="user-message"]') + await expect(user.getByText("LiveRuntime.ts:14-32", { exact: true })).toBeVisible() + const attachments = user.locator('[data-slot="user-message-attachments"]') + await expect(attachments.getByText("LiveRuntime.ts", { exact: true })).toHaveCount(0) + await expect(attachments.getByText("notes.txt", { exact: true })).toBeVisible() + }) }) function patchFile(file: string, status: "added" | "modified" | "deleted") { diff --git a/packages/session-ui/src/components/message-file.test.ts b/packages/session-ui/src/components/message-file.test.ts index c794d0c93a1..559aa1da586 100644 --- a/packages/session-ui/src/components/message-file.test.ts +++ b/packages/session-ui/src/components/message-file.test.ts @@ -1,7 +1,24 @@ import { describe, expect, test } from "bun:test" -import { typeLabel } from "./message-file" +import type { PromptFileAttachment } from "@opencode/client/promise" +import { attached, typeLabel } from "./message-file" + +function file(source: PromptFileAttachment["source"], mention = false): PromptFileAttachment { + return { + data: "", + mime: "text/plain", + source, + ...(mention ? { mention: { text: "@README.md", start: 0, end: 10 } } : {}), + } +} describe("message-file", () => { + test("only treats data-backed files without mentions as attachments", () => { + expect(attached(file({ type: "inline" }))).toBe(true) + expect(attached(file({ type: "uri", uri: "data:text/plain;base64,SGVsbG8=" }))).toBe(true) + expect(attached(file({ type: "uri", uri: "file:///repo/README.md" }))).toBe(false) + expect(attached(file({ type: "inline" }, true))).toBe(false) + }) + test("labels attachment types from the basename extension", () => { expect(typeLabel("list.md", "text/plain", "File")).toBe("Markdown") expect(typeLabel("/repo/src/main.ts", "text/plain", "File")).toBe("TypeScript") diff --git a/packages/session-ui/src/components/message-file.ts b/packages/session-ui/src/components/message-file.ts index 21e4977fa81..b526b085db8 100644 --- a/packages/session-ui/src/components/message-file.ts +++ b/packages/session-ui/src/components/message-file.ts @@ -1,5 +1,12 @@ import { bundledLanguagesInfo } from "shiki" import { getFilename } from "@opencode/util/path" +import type { PromptFileAttachment } from "@opencode/client/promise" + +export function attached(file: PromptFileAttachment) { + if (file.mention) return false + if (file.source.type === "inline") return true + return file.source.uri.startsWith("data:") +} // language metadata only; grammars stay behind shiki's lazy imports const LANGUAGE_NAMES = new Map( diff --git a/packages/session-ui/src/message/message-content.tsx b/packages/session-ui/src/message/message-content.tsx index 82535dd083f..74809f7d287 100644 --- a/packages/session-ui/src/message/message-content.tsx +++ b/packages/session-ui/src/message/message-content.tsx @@ -27,7 +27,7 @@ import type { SessionMessageUser, } from "@opencode/client/promise" import type { SessionUserActions, SessionUserAttachmentReference, SessionUserComment } from "../actions" -import { typeLabel } from "../components/message-file" +import { attached, typeLabel } from "../components/message-file" export async function writeClipboard(text: string): Promise { const body = typeof document === "undefined" ? undefined : document.body @@ -214,7 +214,7 @@ export function CurrentUserMessageDisplay(props: { const dialog = useDialog() const i18n = useI18n() const [state, setState] = createStore({ copied: false, reverting: false }) - const attachments = createMemo(() => (props.message.files ?? []).filter((file) => !file.mention)) + const attachments = createMemo(() => (props.message.files ?? []).filter(attached)) const references = createMemo(() => props.references ?? []) const inlineFiles = createMemo(() => (props.message.files ?? []).filter((file) => !!file.mention)) const agents = createMemo(() => props.message.agents ?? [])