diff --git a/packages/app/e2e/regression/session-timeline-file-state.spec.ts b/packages/app/e2e/regression/session-timeline-file-state.spec.ts index f36bd7628a8..8f9bbbdccc2 100644 --- a/packages/app/e2e/regression/session-timeline-file-state.spec.ts +++ b/packages/app/e2e/regression/session-timeline-file-state.spec.ts @@ -2,6 +2,36 @@ import { expect, test } from "@playwright/test" import { assistantMessage, setupTimeline, toolPart, userMessage } from "../performance/timeline-stability/fixture" import { createTwoFilesPatch } from "diff" +test("labels single-file patches by operation", async ({ page }) => { + const cases = [ + { id: "prt_created_patch", file: "src/new.ts", status: "added" as const, title: "Created" }, + { id: "prt_removed_patch", file: "src/old.ts", status: "deleted" as const, title: "Removed" }, + { id: "prt_modified_patch", file: "src/current.ts", status: "modified" as const, title: "Patch" }, + ] + await setupTimeline(page, { + messages: [ + userMessage(), + assistantMessage( + cases.map((item) => + toolPart( + item.id, + "patch", + "completed", + { patchText: `Update ${item.file}` }, + { metadata: { files: [patchFile(item.file, item.status)] } }, + ), + ), + ), + ], + }) + + for (const item of cases) { + await expect(page.locator(`[data-timeline-part-id="${item.id}"] [data-slot="message-part-title-text"]`)).toHaveText( + item.title, + ) + } +}) + test("preserves nested patch file state through outer collapse and reopen", async ({ page }) => { const patchID = "prt_nested_patch" const files = [patchFile("src/a.ts", "modified"), patchFile("src/b.ts", "added"), patchFile("src/old.ts", "deleted")] diff --git a/packages/app/e2e/regression/session-timeline-projection.spec.ts b/packages/app/e2e/regression/session-timeline-projection.spec.ts index 500a169dbbb..be3608f9ca1 100644 --- a/packages/app/e2e/regression/session-timeline-projection.spec.ts +++ b/packages/app/e2e/regression/session-timeline-projection.spec.ts @@ -133,7 +133,7 @@ test.describe("session timeline projection", () => { }) test("renders user image, file attachment, file reference, and agent reference", async ({ page }) => { - const text = "Use @explore with @src/a.ts and inspect the attachments" + const text = "[Image 1] Use @explore with @src/a.ts and inspect the attachments" const parts: PartSeed<"user">[] = [ userText(text, { id: "prt_user_rich" }), { @@ -142,6 +142,7 @@ test.describe("session timeline projection", () => { mime: "image/png", filename: "pixel.png", url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", + source: { type: "file", path: "pixel.png", text: { value: "[Image 1]", start: 0, end: 9 } }, }, { id: "prt_user_attachment", @@ -156,18 +157,19 @@ test.describe("session timeline projection", () => { mime: "text/plain", filename: "a.ts", url: "src/a.ts", - source: { type: "file", path: "src/a.ts", text: { value: "@src/a.ts", start: 18, end: 27 } }, + source: { type: "file", path: "src/a.ts", text: { value: "@src/a.ts", start: 28, end: 37 } }, }, { id: "prt_user_agent", type: "agent", name: "explore", - source: { value: "@explore", start: 4, end: 12 }, + source: { value: "@explore", start: 14, end: 22 }, }, ] await setupTimeline(page, { messages: [userMessage(parts), assistantMessage()] }) await expect(page.getByAltText("pixel.png")).toBeVisible() + await expect(page.getByText("[Image 1]", { exact: true })).toBeVisible() await expect(page.getByText("tsconfig.json")).toBeVisible() await expect(page.getByText("@src/a.ts", { exact: true })).toBeVisible() await expect(page.getByText("@explore", { exact: true })).toBeVisible() diff --git a/packages/session-ui/src/message/message-content.tsx b/packages/session-ui/src/message/message-content.tsx index ee383267262..f136c12fa30 100644 --- a/packages/session-ui/src/message/message-content.tsx +++ b/packages/session-ui/src/message/message-content.tsx @@ -206,7 +206,9 @@ 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((file) => !file.mention || file.mime.startsWith("image/")), + ) const inlineFiles = createMemo(() => (props.message.files ?? []).filter((file) => !!file.mention)) const agents = createMemo(() => props.message.agents ?? []) const comments = createMemo(() => props.comments ?? []) diff --git a/packages/session-ui/src/tools/tool-renderer.tsx b/packages/session-ui/src/tools/tool-renderer.tsx index 701f534fb58..5070b3e3fc6 100644 --- a/packages/session-ui/src/tools/tool-renderer.tsx +++ b/packages/session-ui/src/tools/tool-renderer.tsx @@ -1397,6 +1397,11 @@ ToolRegistry.register({ if (list.length !== 1) return undefined return list[0] }) + const title = createMemo(() => { + if (pending() || !single() || single()!.type === "update") return i18n.t("ui.tool.patch") + if (single()?.type === "add") return i18n.t("ui.patch.action.created") + return i18n.t("ui.patch.action.removed") + }) const [expanded, setExpanded] = createSignal([]) let seeded = false @@ -1525,7 +1530,7 @@ ToolRegistry.register({
- + {getFilename(single()!.path)} diff --git a/packages/ui/src/i18n/en.ts b/packages/ui/src/i18n/en.ts index 4f8ad2863b6..b9b3ba880f0 100644 --- a/packages/ui/src/i18n/en.ts +++ b/packages/ui/src/i18n/en.ts @@ -213,6 +213,7 @@ const source = { "ui.patch.action.deleted": "Deleted", "ui.patch.action.created": "Created", + "ui.patch.action.removed": "Removed", "ui.patch.action.moved": "Moved", "ui.patch.action.patched": "Patched",