Compare commits

...

6 Commits

Author SHA1 Message Date
Brendonovich fef487bdb3 fix(desktop): align file and directory text 2026-08-21 03:32:23 +00:00
Brendonovich adbac6a96a fix(desktop): omit removed patch count 2026-08-21 03:24:06 +00:00
Brendonovich 3dfc594cca fix(desktop): refine patch counts and alignment 2026-08-21 03:20:42 +00:00
Brendonovich f882c0b3e5 fix(desktop): hide create and delete diff counts 2026-08-21 03:10:38 +00:00
Brendonovich c58d4fdf87 test(app): assert patch labels semantically 2026-08-21 02:57:53 +00:00
Brendonovich 7e5df47700 fix(desktop): render prompt images and patch actions 2026-08-21 02:49:02 +00:00
7 changed files with 83 additions and 14 deletions
@@ -2,6 +2,59 @@ 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}"]`).getByLabel(item.title, { exact: true })).toBeVisible()
}
const created = page.locator('[data-timeline-part-id="prt_created_patch"] [data-slot="message-part-actions"]')
const removed = page.locator('[data-timeline-part-id="prt_removed_patch"] [data-slot="message-part-actions"]')
const modified = page.locator('[data-timeline-part-id="prt_modified_patch"] [data-slot="message-part-actions"]')
await expect(created.locator('[data-slot="diff-changes-additions"]')).toHaveText("+4")
await expect(created.locator('[data-slot="diff-changes-deletions"]')).toHaveCount(0)
await expect(removed.locator('[data-slot="diff-changes-additions"]')).toHaveCount(0)
await expect(removed.locator('[data-slot="diff-changes-deletions"]')).toHaveCount(0)
await expect(modified.locator('[data-slot="diff-changes-additions"]')).toHaveText("+4")
await expect(modified.locator('[data-slot="diff-changes-deletions"]')).toHaveText("-3")
const alignment = await page.locator('[data-timeline-part-id="prt_created_patch"]').evaluate((element) => {
const bounds = (selector: string) => {
const target = element.querySelector(selector)
if (!target) throw new Error(`Missing ${selector}`)
const range = document.createRange()
range.selectNodeContents(target)
const rect = range.getBoundingClientRect()
return { top: rect.top, bottom: rect.bottom }
}
return {
filename: bounds('[data-slot="message-part-title-filename"]'),
directory: bounds('[data-slot="message-part-directory"]'),
}
})
expect(alignment.directory).toEqual(alignment.filename)
})
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")]
@@ -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()
@@ -438,20 +438,20 @@
align-items: center;
gap: 8px;
min-width: 0;
font-family: var(--font-family-sans);
font-size: 14px;
font-style: normal;
line-height: var(--line-height-large);
letter-spacing: var(--letter-spacing-normal);
}
[data-slot="message-part-title"] {
flex: 1 1 auto;
display: flex;
align-items: center;
align-items: baseline;
gap: 8px;
min-width: 0;
font-family: var(--font-family-sans);
font-size: 14px;
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: var(--line-height-large);
letter-spacing: var(--letter-spacing-normal);
color: var(--v2-text-text-muted);
}
@@ -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 ?? [])
@@ -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<string[]>([])
let seeded = false
@@ -1525,7 +1530,7 @@ ToolRegistry.register({
<div data-slot="message-part-title-area">
<div data-slot="message-part-title">
<span data-slot="message-part-title-text">
<TextShimmer text={i18n.t("ui.tool.patch")} active={pending()} />
<TextShimmer text={title()} active={pending()} />
</span>
<Show when={!pending()}>
<span data-slot="message-part-title-filename">{getFilename(single()!.path)}</span>
@@ -1538,9 +1543,10 @@ ToolRegistry.register({
</Show>
</div>
<div data-slot="message-part-actions">
<Show when={!pending()}>
<Show when={!pending() && single()!.type !== "delete"}>
<DiffChanges
appearance="standard"
hideZero
changes={{ additions: single()!.additions, deletions: single()!.deletions }}
/>
</Show>
@@ -4,6 +4,7 @@ import "./diff-changes.css"
export function DiffChanges(props: {
class?: string
appearance?: "standard" | "compact"
hideZero?: boolean
changes: { additions: number; deletions: number } | { additions: number; deletions: number }[]
}) {
const additions = createMemo(() =>
@@ -25,8 +26,12 @@ export function DiffChanges(props: {
data-appearance={props.appearance ?? "compact"}
classList={{ [props.class ?? ""]: true }}
>
<span data-slot="diff-changes-additions">{`+${additions()}`}</span>
<span data-slot="diff-changes-deletions">{`-${deletions()}`}</span>
<Show when={!props.hideZero || additions() > 0}>
<span data-slot="diff-changes-additions">{`+${additions()}`}</span>
</Show>
<Show when={!props.hideZero || deletions() > 0}>
<span data-slot="diff-changes-deletions">{`-${deletions()}`}</span>
</Show>
</div>
</Show>
)
+1
View File
@@ -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",