From b8499b80ff619e887761ef59d9ad04cfe60332e8 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 28 Oct 2026 20:03:06 -0700 Subject: [PATCH 1/2] Add thread minimap inset gallery variants Agent-Session: https://cmux.com/deeplink/agent?id=aid-3d5ed4e77a8f565f77b6 --- .../threadMinimapNarrow.gallery.ts | 77 ++++++++++++++----- .../thread-minimap-inset-variants.test.ts | 28 +++++++ 2 files changed, 86 insertions(+), 19 deletions(-) create mode 100644 webviews/test/thread-minimap-inset-variants.test.ts diff --git a/webviews/src/agent-session/acpmux/threadMinimap/threadMinimapNarrow.gallery.ts b/webviews/src/agent-session/acpmux/threadMinimap/threadMinimapNarrow.gallery.ts index 26c043380dd4..99f55900799a 100644 --- a/webviews/src/agent-session/acpmux/threadMinimap/threadMinimapNarrow.gallery.ts +++ b/webviews/src/agent-session/acpmux/threadMinimap/threadMinimapNarrow.gallery.ts @@ -25,31 +25,70 @@ const withGap = (gap: number) => async (ctx: PlayContext) => { const target = Math.max(0, row.getBoundingClientRect().left - frame.left - rail.getBoundingClientRect().width - gap); rail.style.left = `${target}px`; }; -const gapPlay = withGap(4); + +/// Gallery-only inset comparison. The production placement remains untouched; these plays are +/// exploratory edge positions for the pending railPlacement/useRailPlacement seam. The exact +/// choice is made from rendered gallery captures, not inferred from private-image scale. +const withInset = (inset: number) => async (ctx: PlayContext) => { + await ctx.waitFor(() => ctx.document.querySelector(".acpmux-minimap__rail:not(.acpmux-scrollmap)")); + const rail = ctx.document.querySelector(".acpmux-minimap__rail:not(.acpmux-scrollmap)"); + if (!rail) return; + rail.style.left = `${inset}px`; +}; + +export const railInsetVariants = { + "inset-current": { + note: "Current product placement with no gallery override; this is the baseline for every inset comparison.", + snapshot: chat(rows, { title: "Thread minimap, current placement" }), + }, + "inset-4-lawrence-candidate": { + note: "Exploratory 4 px pane-edge candidate informed by the supplied Lawrence crop; confirm the visual match in the rendered gallery.", + snapshot: chat(rows, { title: "Thread minimap, 4 px inset candidate" }), + play: withInset(4), + }, + "inset-8-reference-candidate": { + note: "Exploratory 8 px pane-edge candidate informed by the four rail references; confirm the visual match in the rendered gallery before selecting a production token.", + snapshot: chat(rows, { title: "Thread minimap, 8 px inset candidate" }), + play: withInset(8), + }, + "inset-12-alternative": { + note: "Exploratory 12 px pane-edge alternative, retained to show a looser edge step in the same harness.", + snapshot: chat(rows, { title: "Thread minimap, 12 px inset alternative" }), + play: withInset(12), + }, + "inset-16-alternative": { + note: "Exploratory 16 px pane-edge alternative, the widest comparison before the rail starts consuming the chat gutter.", + snapshot: chat(rows, { title: "Thread minimap, 16 px inset alternative" }), + play: withInset(16), + }, +} as const; + +export const threadMinimapGalleryVariants = { + "gap-4-chosen": { + note: "Chosen. The rail keeps its full 36 px look and sits 4 px from the transcript at rest; it sheds its spare gutter first as Session Info narrows the chat.", + snapshot: chat(rows, { title: "Thread minimap, 4 px chosen gutter" }), + play: withGap(4), + }, + "gap-8": { + note: "Alternative: an 8 px rail-to-text gutter, compared at wide, Session Info and narrow widths.", + snapshot: chat(rows, { title: "Thread minimap, 8 px gutter alternative" }), + play: withGap(8), + }, + "gap-12": { + note: "Alternative: a 12 px rail-to-text gutter, kept here to show the extra padding Leo rejected.", + snapshot: chat(rows, { title: "Thread minimap, 12 px gutter alternative" }), + play: withGap(12), + }, + ...railInsetVariants, +} as const; export default agentPaneEntry({ id: "agent-pane.thread-minimap-narrow", - title: "Thread minimap gutter variants", + title: "Thread minimap gutter and pane-edge inset variants", area: "Agent pane", height: 640, widths: { narrow: 780, normal: 1040, wide: 1600 }, anchors: [{ selector: ".acpmux-scroll" }], covers: ["agent-session/acpmux/threadMinimap/ThreadMinimap.tsx#ThreadMinimap"], - variants: { - "gap-4-chosen": { - note: "Chosen. The rail keeps its full 36 px look and sits 4 px from the transcript at rest; it sheds its spare gutter first as Session Info narrows the chat.", - snapshot: chat(rows, { title: "Thread minimap, 4 px chosen gutter" }), - play: gapPlay, - }, - "gap-8": { - note: "Alternative: an 8 px rail-to-text gutter, compared at wide, Session Info and narrow widths.", - snapshot: chat(rows, { title: "Thread minimap, 8 px gutter alternative" }), - play: withGap(8), - }, - "gap-12": { - note: "Alternative: a 12 px rail-to-text gutter, kept here to show the extra padding Leo rejected.", - snapshot: chat(rows, { title: "Thread minimap, 12 px gutter alternative" }), - play: withGap(12), - }, - }, + variants: threadMinimapGalleryVariants, }); diff --git a/webviews/test/thread-minimap-inset-variants.test.ts b/webviews/test/thread-minimap-inset-variants.test.ts new file mode 100644 index 000000000000..7b03f9235a27 --- /dev/null +++ b/webviews/test/thread-minimap-inset-variants.test.ts @@ -0,0 +1,28 @@ +import { expect, test } from "bun:test"; +import { + railInsetVariants, + threadMinimapGalleryVariants, +} from "../src/agent-session/acpmux/threadMinimap/threadMinimapNarrow.gallery"; + +test("thread minimap inset gallery keeps existing gaps and adds candidates", () => { + expect(Object.keys(threadMinimapGalleryVariants)).toEqual([ + "gap-4-chosen", + "gap-8", + "gap-12", + "inset-current", + "inset-4-lawrence-candidate", + "inset-8-reference-candidate", + "inset-12-alternative", + "inset-16-alternative", + ]); + expect(Object.keys(railInsetVariants)).toEqual([ + "inset-current", + "inset-4-lawrence-candidate", + "inset-8-reference-candidate", + "inset-12-alternative", + "inset-16-alternative", + ]); + expect("play" in railInsetVariants["inset-current"]).toBe(false); + expect(railInsetVariants["inset-4-lawrence-candidate"].note).toContain("Exploratory"); + expect(railInsetVariants["inset-8-reference-candidate"].note).toContain("four rail references"); +}); From bd6e3d7bfeda439f8d9008ed3a00474261cd3524 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 28 Oct 2026 20:46:20 -0700 Subject: [PATCH 2/2] Exercise minimap gallery placement plays in regression tests Agent-Session: https://cmux.com/deeplink/agent?id=aid-3d5ed4e77a8f565f77b6 --- .../thread-minimap-inset-variants.test.ts | 72 ++++++++++++------- 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/webviews/test/thread-minimap-inset-variants.test.ts b/webviews/test/thread-minimap-inset-variants.test.ts index 7b03f9235a27..93e8bb7cdf78 100644 --- a/webviews/test/thread-minimap-inset-variants.test.ts +++ b/webviews/test/thread-minimap-inset-variants.test.ts @@ -1,28 +1,50 @@ import { expect, test } from "bun:test"; -import { - railInsetVariants, - threadMinimapGalleryVariants, -} from "../src/agent-session/acpmux/threadMinimap/threadMinimapNarrow.gallery"; +import { JSDOM } from "jsdom"; +import gallery from "../src/agent-session/acpmux/threadMinimap/threadMinimapNarrow.gallery"; +import type { PlayContext } from "../src/gallery/play"; -test("thread minimap inset gallery keeps existing gaps and adds candidates", () => { - expect(Object.keys(threadMinimapGalleryVariants)).toEqual([ - "gap-4-chosen", - "gap-8", - "gap-12", - "inset-current", - "inset-4-lawrence-candidate", - "inset-8-reference-candidate", - "inset-12-alternative", - "inset-16-alternative", - ]); - expect(Object.keys(railInsetVariants)).toEqual([ - "inset-current", - "inset-4-lawrence-candidate", - "inset-8-reference-candidate", - "inset-12-alternative", - "inset-16-alternative", - ]); - expect("play" in railInsetVariants["inset-current"]).toBe(false); - expect(railInsetVariants["inset-4-lawrence-candidate"].note).toContain("Exploratory"); - expect(railInsetVariants["inset-8-reference-candidate"].note).toContain("four rail references"); +test.each([ + ["inset-current", 29], + ["inset-4-lawrence-candidate", 4], + ["inset-8-reference-candidate", 8], + ["inset-12-alternative", 12], + ["inset-16-alternative", 16], + ["gap-4-chosen", 40], + ["gap-8", 36], + ["gap-12", 32], +] as const)("%s places the prompt rail at %i px", async (variantName, expectedLeft) => { + const dom = new JSDOM(`
+ + +
+
`); + try { + const document = dom.window.document; + const rail = document.querySelector("#prompt-rail")!; + const scrollMap = document.querySelector(".acpmux-scrollmap")!; + const frame = document.querySelector("#frame")!; + const row = document.querySelector(".acpmux-row")!; + // jsdom has no layout. Keep a known transcript gutter for the pre-existing gap plays. + Object.defineProperty(rail, "offsetParent", { value: frame }); + frame.getBoundingClientRect = () => new dom.window.DOMRect(20, 0, 780, 640); + row.getBoundingClientRect = () => new dom.window.DOMRect(100, 0, 600, 100); + rail.getBoundingClientRect = () => new dom.window.DOMRect(49, 0, 36, 180); + let waits = 0; + const ctx = { + document, + waitFor: async (condition: Parameters[0]) => { + waits += 1; + expect(condition()).toBe(rail); + }, + } as PlayContext; + const variant = gallery.variants[variantName]; + expect(variant).toBeDefined(); + await variant!.play?.(ctx); + + expect(rail.style.left).toBe(`${expectedLeft}px`); + expect(scrollMap.style.left).toBe("73px"); + expect(waits).toBe(variantName === "inset-current" ? 0 : 1); + } finally { + dom.window.close(); + } });