Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/web/src/components/chat/MessagesTimeline.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,12 @@ export function resolveTimelineMinimapHasPersistentGutter(viewportWidth: number)
return sideGutter >= TIMELINE_MINIMAP_PERSISTENT_GUTTER;
}

export const TIMELINE_MINIMAP_HIT_STRIP_LEFT = 12;
export const TIMELINE_MINIMAP_HIT_STRIP_INSET = 12;
export const TIMELINE_MINIMAP_HIT_STRIP_MAX_WIDTH = 40;
export const TIMELINE_MINIMAP_EXPANDED_HIT_STRIP_WIDTH = "22rem";

/**
* The minimap overlays the viewport's left edge while the content column is
* The minimap overlays the viewport's right edge while the content column is
* centered, so the side gutter between them shrinks under browser zoom or a
* narrow pane. A fixed-width hover strip would then sit on top of the message
* text and swallow its pointer events. Cap the strip's width so it never
Expand All @@ -130,7 +130,7 @@ export function resolveTimelineMinimapHitStripWidth(viewportWidth: number): numb
0,
Math.min(
TIMELINE_MINIMAP_HIT_STRIP_MAX_WIDTH,
Math.floor(sideGutter) - TIMELINE_MINIMAP_HIT_STRIP_LEFT,
Math.floor(sideGutter) - TIMELINE_MINIMAP_HIT_STRIP_INSET,
),
);
}
Expand Down
19 changes: 19 additions & 0 deletions apps/web/src/components/chat/MessagesTimeline.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,25 @@ describe("MessagesTimeline", () => {
expect(fadedMarkup).toContain("topbar-scroll-fade");
});

it("renders the user-message minimap on the right side", () => {
const firstEntry = buildUserTimelineEntry("First prompt.");
const secondEntry = {
...buildUserTimelineEntry("Second prompt."),
id: "entry-2",
message: {
...buildUserTimelineEntry("Second prompt.").message,
id: MessageId.make("message-2"),
},
};
const markup = renderToStaticMarkup(
<MessagesTimeline {...buildProps()} timelineEntries={[firstEntry, secondEntry]} />,
);

expect(markup).toContain('data-testid="timeline-minimap"');
expect(markup).toContain("absolute inset-y-0 right-0");
expect(markup).not.toContain("absolute inset-y-0 left-0");
});

it("keeps assistant changed-files headers sticky below the thread header", () => {
const assistantMessageId = MessageId.make("message-assistant-with-files");
const turnId = TurnId.make("turn-with-files");
Expand Down
10 changes: 5 additions & 5 deletions apps/web/src/components/chat/MessagesTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -789,7 +789,7 @@ function TimelineMinimap({
return (
<div
className={cn(
"group/minimap pointer-events-none absolute inset-y-0 left-0 z-40 hidden w-18 [@media(pointer:fine)]:block",
"group/minimap pointer-events-none absolute inset-y-0 right-0 z-40 hidden w-18 [@media(pointer:fine)]:block",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anchoring the overlay to right-0 puts it on top of the floating preview mini player, which defaults to the chat column's top-right (right: PREVIEW_MINI_PLAYER_EDGE_GAP = 12px, 320x200, and is freely draggable — preview/ThreadPreviewMiniPlayer.tsx). Both live in the same chat-column stacking context (ChatView.tsx renders the mini player as a sibling of the messages wrapper, and neither relative wrapper sets a z-index), so this z-40 root paints above the player's drag/close cluster (z-[34]) and resize grip (z-[33]). Wherever the two overlap vertically — long threads, where the strip height saturates at calc(100vh - 18rem), or any position the player is dragged to — the minimap's pointer-events-auto strip wins the hit test: hovering the player's right 40px opens the minimap preview and a click jumps the timeline instead of resizing or dragging the player. While the preview is open the strip is 22rem wide, i.e. wider than the player itself, so the whole player is shadowed in that band. Smallest fix: keep the minimap on the left and inset it past SidebarRail (the element that actually causes the sidebar hover conflict), or, if it must move right, suppress the strip's pointer events while a mini player occupies the right edge.

Posted via Macroscope — UI Consistency

hasPersistentGutter
? "opacity-100"
: "opacity-0 transition-opacity duration-150 hover:opacity-100 focus-within:opacity-100",
Expand All @@ -801,7 +801,7 @@ function TimelineMinimap({
<button
aria-label={`Jump to message: ${activeItem?.userText ?? "User message"}`}
className={cn(
"absolute top-1/2 left-3 -translate-y-1/2 cursor-pointer bg-transparent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/70",
"absolute top-1/2 right-3 -translate-y-1/2 cursor-pointer bg-transparent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/70",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right-3 reuses the left-edge inset (TIMELINE_MINIMAP_HIT_STRIP_INSET = 12) in a lane that already belongs to the native scrollbar: the list is scrollbar-gutter-both and --app-scrollbar-width is 6px, so the scroll thumb occupies the rightmost 6px and only 6px of clearance remains before a 40px jump target that expands to 22rem on hover. Reaching for the thumb now crosses the strip, so the 20rem preview card pops over the message text, and a mousedown that misses the thumb by a few pixels is swallowed (onMouseDown calls preventDefault) and turns into a jump-to-message on click — the scroll thumb stops being reliably grabbable. If the minimap stays on the right, the inset should clear the scrollbar lane explicitly (scrollbar width + the current 12px, applied both to the right-* class and to the value resolveTimelineMinimapHitStripWidth subtracts) instead of inheriting the left-edge number.

Posted via Macroscope — UI Consistency

// The strip is width-capped to the side gutter so it never overlays
// the centered content column; with no usable gutter it goes inert.
hitStripWidth > 0 ? "pointer-events-auto" : "pointer-events-none",
Expand Down Expand Up @@ -853,7 +853,7 @@ function TimelineMinimap({
}}
type="button"
>
<div className="absolute top-0 left-3 h-full w-px bg-border/15" />
<div className="absolute top-0 right-3 h-full w-px bg-border/15" />
{items.map((item, index) => {
const top = `${resolveTimelineMinimapTopPercent(index, items.length)}%`;
const activeDistance =
Expand All @@ -862,7 +862,7 @@ function TimelineMinimap({
<span
aria-hidden="true"
className={cn(
"pointer-events-none absolute left-0 h-0.5 -translate-y-1/2 rounded-full bg-muted-foreground/35 transition-[background-color,width] duration-150 data-[in-view=true]:bg-foreground/90",
"pointer-events-none absolute right-0 h-0.5 -translate-y-1/2 rounded-full bg-muted-foreground/35 transition-[background-color,width] duration-150 data-[in-view=true]:bg-foreground/90",
activeDistance === 0
? "w-6 bg-muted-foreground/75"
: activeDistance === 1
Expand All @@ -887,7 +887,7 @@ function TimelineMinimap({
})}
{activeItem ? (
<span
className="pointer-events-auto absolute left-8 w-80 cursor-text select-text"
className="pointer-events-auto absolute right-8 w-80 cursor-text select-text"
data-minimap-preview
onMouseMove={(event) => event.stopPropagation()}
style={{
Expand Down
Loading