From 1234dccc25812727b752a3f8e4849ac230111425 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sun, 25 Oct 2026 21:56:19 +0000 Subject: [PATCH 1/3] fix(web): keep floating preview placement stable after resizing Co-authored-by: Alex <5401039+PixPMusic@users.noreply.github.com> --- .../components/chat/chatCanvasLayout.test.ts | 129 ++++++++++++++---- .../src/components/chat/chatCanvasLayout.ts | 15 +- .../preview/previewMiniPlayerLayout.test.ts | 59 ++++++++ .../preview/previewMiniPlayerLayout.ts | 7 +- apps/web/src/previewMiniPlayerStore.test.ts | 39 ++++++ 5 files changed, 216 insertions(+), 33 deletions(-) diff --git a/apps/web/src/components/chat/chatCanvasLayout.test.ts b/apps/web/src/components/chat/chatCanvasLayout.test.ts index 748d8abb5d1ac..c016861225e08 100644 --- a/apps/web/src/components/chat/chatCanvasLayout.test.ts +++ b/apps/web/src/components/chat/chatCanvasLayout.test.ts @@ -8,7 +8,7 @@ import { const preview: ChatCanvasPreview = { key: "browser:one", - width: 320, + width: null, position: null, source: { width: 1600, height: 1000 }, }; @@ -64,13 +64,94 @@ describe("chat canvas layout", () => { } } }); + it.each([ + { canvasWidth: 744, previewWidth: 240 }, + { canvasWidth: 900, previewWidth: null }, + { canvasWidth: 900, previewWidth: 240 }, + { canvasWidth: 952, previewWidth: 400 }, + ])( + "keeps chat fixed with a $previewWidth preview in a compact $canvasWidth pixel canvas", + ({ canvasWidth, previewWidth }) => { + const original = resolve(canvasWidth, null).chat; + for (const x of [12, canvasWidth / 2, canvasWidth - 252]) { + const result = resolve(canvasWidth, { + ...preview, + width: previewWidth, + position: { x, y: 200 }, + source: { width: 390, height: 844 }, + }); + expect(result.chat).toEqual(original); + expect(result.overlapsChat).toBe(true); + } + }, + ); + + it("reflows chat once the canvas has room for the minimum side-by-side layout", () => { + const result = resolve(953, { + ...preview, + width: 240, + position: { x: 701, y: 200 }, + }); + expect(result.chat).toMatchObject({ left: 48, width: 641 }); + expectClear(result); + }); + + it.each([1012, 1040, 1100])( + "overlays a preview when details already constrain chat in a %spx canvas", + (width) => { + const layout = (preview: ChatCanvasPreview | null) => + resolveChatCanvasLayout({ + container: { width, height: 900 }, + maxChatWidth: 736, + composerHeight: 180, + detailsCard: { left: width - 292, right: width - 12, bottom: 400 }, + preview, + }); + const original = layout(null).chat; + for (const x of [12, width / 2, width - 412]) { + const result = layout({ + ...preview, + width: 400, + position: { x, y: 200 }, + source: { width: 768, height: 1024 }, + }); + expect(result.chat).toEqual(original); + expect(result.overlapsChat).toBe(true); + } + }, + ); + + it("folds details around a manually dragged preview and expands them after moving away", () => { + const container = { width: 1584, height: 988 }; + const detailsCard = { left: 1292, right: 1572, bottom: 400 }; + const layout = (position: ChatCanvasPreview["position"]) => + resolveChatCanvasLayout({ + container, + detailsCard, + preview: { ...preview, width: null, position }, + }); + const density = (result: ReturnType) => { + const card = resolveThreadDetailsCardLayout({ + container, + lane: { padding: 48, minChatWidth: 640 }, + frame: result.frame, + overlapsDetailsCard: result.overlapsDetailsCard, + })!; + return resolveThreadDetailsCardDensity(card.height, { full: 388, compact: 182 }); + }; + const dragged = layout({ x: 1252, y: 275 }); + expect(dragged.frame).toEqual({ x: 1252, y: 275, width: 320, height: 200 }); + expect(density(dragged)).toBe("compact"); + expect(density(layout({ x: 1252, y: 500 }))).toBe("full"); + expect(layout(null).overlapsDetailsCard).toBe(false); + }); + it("lifts a growing preview above the composer without snapping at the chat boundary", () => { let previous: ReturnType | undefined; for (let width = 480; width <= 1100; width++) { const result = resolve(1344, { ...preview, width, - lastInteraction: "resize", position: { x: 1332 - width, y: 888 - Math.round(width / 1.6) }, }); const frame = result.frame!; @@ -94,13 +175,11 @@ describe("chat canvas layout", () => { const moved = resolve(width, { ...preview, position: { x: start.x - 300, y: start.y }, - lastInteraction: "drag", }); expect(moved.frame).toEqual({ ...start, x: start.x - 300 }); const returned = resolve(width, { ...preview, position: { x: start.x, y: start.y }, - lastInteraction: "drag", }); expect(returned.frame).toEqual(start); }); @@ -128,7 +207,7 @@ describe("chat canvas layout", () => { expect(resolve(1800, player).chat.width).toBe(768); expect(player.width).toBe(480); }); - it("constrains a dragged preview below the full top-right card instead of folding it", () => { + it("folds the top-right card when a dragged preview crosses it", () => { const result = resolveChatCanvasLayout({ container: { width: 1584, height: 988 }, maxChatWidth: 736, @@ -141,8 +220,8 @@ describe("chat canvas layout", () => { position: { x: 1260, y: 240 }, }, }); - expect(result.frame).toEqual({ x: 1260, y: 351, width: 240, height: 365 }); - expect(result.overlapsDetailsCard).toBe(false); + expect(result.frame).toEqual({ x: 1260, y: 240, width: 240, height: 365 }); + expect(result.overlapsDetailsCard).toBe(true); expect(result.chat).toEqual({ left: 424, width: 736, insetStart: 0, insetEnd: 0 }); }); it("moves the chat left only as far as a details card it would crowd requires", () => { @@ -175,9 +254,9 @@ describe("chat canvas layout", () => { expect(Math.abs(leftAt(width + 1) - leftAt(width))).toBeLessThanOrEqual(1); } }); - it("keeps a dragged or resized preview below the open find bar", () => { + it("keeps default-sized and explicitly sized previews below the open find bar", () => { const findBar = { left: 1292, right: 1572, bottom: 48 }; - const layout = (lastInteraction: "drag" | "resize") => + const layout = (width: number | null) => resolveChatCanvasLayout({ container: { width: 1584, height: 988 }, maxChatWidth: 736, @@ -185,20 +264,23 @@ describe("chat canvas layout", () => { findBar, preview: { ...preview, - width: 240, + width, source: { width: 240, height: 365 }, position: { x: 1332, y: 12 }, - lastInteraction, }, }); - for (const lastInteraction of ["drag", "resize"] as const) { - const result = layout(lastInteraction); - expect(result.frame).toEqual({ x: 1332, y: 60, width: 240, height: 365 }); + for (const width of [null, 240]) { + const result = layout(width); + expect(result.frame).toEqual({ + x: 1332, + y: 60, + ...(width === null ? { width: 210, height: 320 } : { width: 240, height: 365 }), + }); expect(result.overlapsDetailsCard).toBe(false); expect(result.chat).toEqual({ left: 424, width: 736, insetStart: 0, insetEnd: 0 }); } }); - it("uses space beside the full card if a tall preview cannot fit below it", () => { + it("opens beside the full card if a tall preview cannot fit below it", () => { const result = resolveChatCanvasLayout({ container: { width: 1584, height: 988 }, maxChatWidth: 736, @@ -208,14 +290,14 @@ describe("chat canvas layout", () => { ...preview, width: 240, source: { width: 240, height: 700 }, - position: { x: 1260, y: 100 }, + position: null, }, }); - expect(result.frame).toEqual({ x: 1008, y: 100, width: 240, height: 700 }); + expect(result.frame).toEqual({ x: 1008, y: 276, width: 240, height: 700 }); expect(result.overlapsDetailsCard).toBe(false); expectClear(result); }); - it("keeps a large player when clearing the full card would leave no readable chat", () => { + it("preserves the chosen size while folding the card and keeping chat readable", () => { const container = { width: 1584, height: 988 }; const source = { width: 1000, height: 1523 }; const layout = (width: number) => { @@ -237,13 +319,13 @@ describe("chat canvas layout", () => { const shifted = layout(460); expect(shifted.frame!.width).toBe(460); expect(shifted.chat.left).toBeLessThan(424); - expect(shifted.overlapsDetailsCard).toBe(false); + expect(shifted.overlapsDetailsCard).toBe(true); expectClear(shifted); const narrowed = layout(532); expect(narrowed.frame!.width).toBe(532); - expect(narrowed.chat.width).toBe(656); - expect(narrowed.overlapsDetailsCard).toBe(false); + expect(narrowed.chat.width).toBe(736); + expect(narrowed.overlapsDetailsCard).toBe(true); expectClear(narrowed); const grown = layout(600); @@ -276,7 +358,6 @@ describe("chat canvas layout", () => { width: resized.width, position: resized, source, - lastInteraction: "resize", }, }); }; @@ -307,7 +388,7 @@ describe("chat canvas layout", () => { expect(density(layout(400))).toBe("full"); }, ); - it("requests card folding only when no full-card slot fits and keeps the composer clear", () => { + it("folds details around an oversized preview while keeping the composer clear", () => { const result = resolveChatCanvasLayout({ container: { width: 1584, height: 988 }, maxChatWidth: 736, @@ -317,7 +398,7 @@ describe("chat canvas layout", () => { ...preview, width: 1300, source: { width: 1300, height: 400 }, - position: { x: 12, y: 12 }, + position: { x: 12, y: 700 }, }, }); expect(result.overlapsDetailsCard).toBe(true); diff --git a/apps/web/src/components/chat/chatCanvasLayout.ts b/apps/web/src/components/chat/chatCanvasLayout.ts index 77e276ee817f4..1be526a256145 100644 --- a/apps/web/src/components/chat/chatCanvasLayout.ts +++ b/apps/web/src/components/chat/chatCanvasLayout.ts @@ -1,5 +1,6 @@ import { clampPreviewMiniPlayerPosition, + PREVIEW_MINI_PLAYER_MIN_SIZE, resolvePreviewMiniPlayerFrame, type PreviewMiniPlayerFrame, type PreviewMiniPlayerObstacles, @@ -56,15 +57,20 @@ export function resolveChatCanvasLayout({ let frame: PreviewMiniPlayerFrame | null = null; let overlapsChat = false; if (preview && container.width > 0 && container.height > 0) { + const explicitlySized = preview.width !== null; + const canReflowChat = + normalWidth === centeredWidth && + container.width > padding + minChatWidth + PREVIEW_MINI_PLAYER_MIN_SIZE.width + GAP * 2; frame = resolvePreviewMiniPlayerFrame({ ...preview, container }); // Reserve a chat lane only if the preview still has room to drag. const minimumPreviewX = - preview.lastInteraction === "resize" || + explicitlySized || + !canReflowChat || container.width <= padding + minChatWidth + frame.width + GAP * 2 ? GAP : padding + minChatWidth + GAP; if (preview.position === null) frame = { ...frame, y: container.height - frame.height - GAP }; - if (preview.lastInteraction === "resize" && composerHeight > 0) { + if (explicitlySized && composerHeight > 0) { const laneWidth = Math.min(normalWidth, minChatWidth); const transitionWidth = Math.max(GAP, normalWidth - laneWidth); const lift = Math.min( @@ -96,7 +102,7 @@ export function resolveChatCanvasLayout({ bottom: Math.max(detailsCard.bottom, findBar.bottom), } : (detailsCard ?? findBar); - const cardObstacle = preview.lastInteraction === "resize" ? findBar : dragObstacle; + const cardObstacle = preview.position === null ? dragObstacle : findBar; frame = { ...frame, ...clampPreviewMiniPlayerPosition( @@ -110,6 +116,7 @@ export function resolveChatCanvasLayout({ const chatBeside = (player: PreviewMiniPlayerFrame) => { const normalRight = normalLeft + normalWidth; if (player.x >= normalRight + GAP) return chat; + if (!canReflowChat) return null; const width = Math.min(maxChatWidth, player.x - GAP - padding); if (width < minChatWidth) return null; const left = Math.min(normalLeft, player.x - GAP - width); @@ -127,7 +134,7 @@ export function resolveChatCanvasLayout({ } if (nextChat) chat = nextChat; else overlapsChat = true; - if (overlapsChat && preview.lastInteraction === "resize") { + if (overlapsChat && explicitlySized && canReflowChat) { const width = Math.min(normalWidth, minChatWidth); chat = { left: padding, diff --git a/apps/web/src/components/preview/previewMiniPlayerLayout.test.ts b/apps/web/src/components/preview/previewMiniPlayerLayout.test.ts index 4d9eff507ed37..3dc7e633e2177 100644 --- a/apps/web/src/components/preview/previewMiniPlayerLayout.test.ts +++ b/apps/web/src/components/preview/previewMiniPlayerLayout.test.ts @@ -35,6 +35,65 @@ describe("resolvePreviewMiniPlayerSourceSize", () => { }); describe("resolvePreviewMiniPlayerFrame", () => { + it("fits a portrait phone's default preview inside the default box", () => { + expect( + resolvePreviewMiniPlayerFrame({ + width: null, + position: null, + source: { width: 390, height: 844 }, + container, + }), + ).toMatchObject({ width: 148, height: 320 }); + }); + + it.each([ + { width: 390, height: 844 }, + { width: 768, height: 1024 }, + { width: 1024, height: 768 }, + ])("lets a $width×$height device shrink below 240 pixels wide", (source) => { + const requestedWidth = source.width > source.height ? 210 : 140; + const expected = { + width: requestedWidth, + height: Math.round((requestedWidth * source.height) / source.width), + }; + expect( + resolvePreviewMiniPlayerFrame({ width: requestedWidth, position: null, source, container }), + ).toMatchObject(expected); + expect( + resizePreviewMiniPlayer({ + start: { x: 12, y: 12, width: 240, height: (240 * source.height) / source.width }, + direction: "east", + delta: { x: requestedWidth - 240, y: 0 }, + source, + container, + }), + ).toMatchObject(expected); + }); + + it("retains the 240×150 minimum for a 16:10 preview", () => { + expect( + resolvePreviewMiniPlayerFrame({ width: 1, position: null, source, container }), + ).toMatchObject({ width: 240, height: 150 }); + }); + + it.each([ + { source: { width: 1600, height: 900 }, width: 267 }, + { source: { width: 2100, height: 900 }, width: 350 }, + ])("keeps wide previews at least 150 pixels tall", ({ source, width }) => { + expect( + resolvePreviewMiniPlayerFrame({ width: 1, position: null, source, container }), + ).toMatchObject({ width, height: 150 }); + expect( + resizePreviewMiniPlayer({ + start: { x: 12, y: 12, width: 400, height: (400 * source.height) / source.width }, + direction: "east", + delta: { x: -399, y: 0 }, + source, + container, + }), + ).toMatchObject({ width, height: 150 }); + }); + it("shrinks to the space above the composer without losing the stored width", () => { const frame = resolvePreviewMiniPlayerFrame({ width: 800, diff --git a/apps/web/src/components/preview/previewMiniPlayerLayout.ts b/apps/web/src/components/preview/previewMiniPlayerLayout.ts index f66183c9fcb12..3675d568802f2 100644 --- a/apps/web/src/components/preview/previewMiniPlayerLayout.ts +++ b/apps/web/src/components/preview/previewMiniPlayerLayout.ts @@ -14,7 +14,7 @@ export const PREVIEW_MINI_PLAYER_CORNER_RADIUS = 12; // The mini-player shell straddles this webview at 47 and 49: above --z-sheet, under dialogs (50). export const PREVIEW_MINI_PLAYER_WEBVIEW_Z_INDEX = 48; const PREVIEW_MINI_PLAYER_DEFAULT_BOX = { width: 320, height: 320 } as const; -const PREVIEW_MINI_PLAYER_MIN_SIZE = { width: 240, height: 150 } as const; +export const PREVIEW_MINI_PLAYER_MIN_SIZE = { width: 240, height: 150 } as const; export interface PreviewMiniPlayerFrame extends PreviewMiniPlayerPosition, PreviewMiniPlayerSize {} @@ -108,10 +108,7 @@ const availableArea = ( }); const minimumPreviewMiniPlayerWidth = (source: PreviewMiniPlayerSize) => - Math.max( - PREVIEW_MINI_PLAYER_MIN_SIZE.width, - (PREVIEW_MINI_PLAYER_MIN_SIZE.height * source.width) / source.height, - ); + (PREVIEW_MINI_PLAYER_MIN_SIZE.height * source.width) / source.height; function fitPreviewMiniPlayerWidth( desiredWidth: number, diff --git a/apps/web/src/previewMiniPlayerStore.test.ts b/apps/web/src/previewMiniPlayerStore.test.ts index 538fdc11e6602..66e891315b5b2 100644 --- a/apps/web/src/previewMiniPlayerStore.test.ts +++ b/apps/web/src/previewMiniPlayerStore.test.ts @@ -7,6 +7,11 @@ import { selectThreadPreviewMiniPlayer, usePreviewMiniPlayerStore, } from "./previewMiniPlayerStore"; +import { resolveChatCanvasLayout } from "./components/chat/chatCanvasLayout"; +import { + resolveThreadDetailsCardDensity, + resolveThreadDetailsCardLayout, +} from "./components/chat/threadDetailsCardLayout"; const refA = scopeThreadRef("env-1" as EnvironmentId, ThreadId.make("thread-A")); const tabA = browserMiniPlayerSource("tab-a"); @@ -17,6 +22,40 @@ beforeEach(() => { }); describe("previewMiniPlayerStore", () => { + it("keeps a resized preview and folded details card in place when dragging starts", () => { + const store = usePreviewMiniPlayerStore.getState(); + store.open(refA, tabA); + store.resize(refA, "browser:tab-a", 460, { x: 1112, y: 275 }); + const container = { width: 1584, height: 988 }; + const layout = () => { + const player = selectThreadPreviewMiniPlayer( + usePreviewMiniPlayerStore.getState().byThreadKey, + refA, + )!; + return resolveChatCanvasLayout({ + container, + maxChatWidth: 736, + composerHeight: 206, + detailsCard: { left: 1260, right: 1572, bottom: 339 }, + preview: { ...player, key: "browser:tab-a", source: { width: 1000, height: 1523 } }, + }); + }; + const resized = layout(); + expect(resized.frame).toEqual({ x: 1112, y: 275, width: 460, height: 701 }); + store.move(refA, "browser:tab-a", { x: 1106, y: 275 }); + const dragged = layout(); + expect(dragged.frame).toEqual({ ...resized.frame, x: 1106 }); + const card = resolveThreadDetailsCardLayout({ + container, + lane: { padding: 48, minChatWidth: 640 }, + frame: dragged.frame, + overlapsDetailsCard: dragged.overlapsDetailsCard, + })!; + expect(resolveThreadDetailsCardDensity(card.height, { full: 327, compact: 182 })).toBe( + "compact", + ); + }); + it("ignores stale drag updates after the floating tab changes", () => { usePreviewMiniPlayerStore.getState().open(refA, tabA); usePreviewMiniPlayerStore.getState().open(refA, tabB); From 88b89b44bf6ddb2250eedf6c57dfd0eda3a24ec2 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Fri, 6 Nov 2026 04:31:35 +0000 Subject: [PATCH 2/3] test(web): check preview minimums through resizes --- .../components/preview/previewMiniPlayerLayout.test.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/apps/web/src/components/preview/previewMiniPlayerLayout.test.ts b/apps/web/src/components/preview/previewMiniPlayerLayout.test.ts index 3dc7e633e2177..5181d3d9d92b5 100644 --- a/apps/web/src/components/preview/previewMiniPlayerLayout.test.ts +++ b/apps/web/src/components/preview/previewMiniPlayerLayout.test.ts @@ -70,19 +70,11 @@ describe("resolvePreviewMiniPlayerFrame", () => { ).toMatchObject(expected); }); - it("retains the 240×150 minimum for a 16:10 preview", () => { - expect( - resolvePreviewMiniPlayerFrame({ width: 1, position: null, source, container }), - ).toMatchObject({ width: 240, height: 150 }); - }); - it.each([ + { source, width: 240 }, { source: { width: 1600, height: 900 }, width: 267 }, { source: { width: 2100, height: 900 }, width: 350 }, ])("keeps wide previews at least 150 pixels tall", ({ source, width }) => { - expect( - resolvePreviewMiniPlayerFrame({ width: 1, position: null, source, container }), - ).toMatchObject({ width, height: 150 }); expect( resizePreviewMiniPlayer({ start: { x: 12, y: 12, width: 400, height: (400 * source.height) / source.width }, From 43a5120838156da97ddc4359b8cf6d7ecaccaa22 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 14 Nov 2026 08:59:04 +0000 Subject: [PATCH 3/3] refactor(web): drop the unused preview lastInteraction state Placement now follows the preview's explicit size and position, so nothing reads which gesture came last. Remove the field from the store and the canvas preview, and drop tests the layout cases already cover. Co-Authored-By: Claude Opus 5.5 --- .../src/components/chat/canvasStore.test.ts | 4 +- .../components/chat/chatCanvasLayout.test.ts | 2 - .../src/components/chat/chatCanvasLayout.ts | 7 +--- .../preview/ThreadPreviewMiniPlayer.tsx | 1 - .../preview/previewMiniPlayerGesture.test.ts | 1 - .../preview/previewMiniPlayerLayout.test.ts | 16 -------- apps/web/src/previewMiniPlayerStore.test.ts | 40 ------------------- apps/web/src/previewMiniPlayerStore.ts | 13 +----- 8 files changed, 5 insertions(+), 79 deletions(-) diff --git a/apps/web/src/components/chat/canvasStore.test.ts b/apps/web/src/components/chat/canvasStore.test.ts index ad2fe38b044ba..a639c8836d8f1 100644 --- a/apps/web/src/components/chat/canvasStore.test.ts +++ b/apps/web/src/components/chat/canvasStore.test.ts @@ -49,7 +49,7 @@ describe("chat canvas model", () => { measurements: measurements(1100, 300), detailsCardMeasurement: { threadRef, width: 280, fullContentHeight }, threadRef, - preview: { ...preview, position: { x: 760, y: 150 }, lastInteraction: "drag" }, + preview: { ...preview, position: { x: 760, y: 150 } }, detailsCardInlineOpen: true, }); const model = resolve(400); @@ -62,7 +62,7 @@ describe("chat canvas model", () => { measurements: measurements(1100, 300), detailsCardMeasurement: { threadRef: otherThreadRef, width: 280, fullContentHeight: 400 }, threadRef, - preview: { ...preview, position: { x: 760, y: 150 }, lastInteraction: "drag" }, + preview: { ...preview, position: { x: 760, y: 150 } }, detailsCardInlineOpen: true, }); expect(model.layout.overlapsDetailsCard).toBe(false); diff --git a/apps/web/src/components/chat/chatCanvasLayout.test.ts b/apps/web/src/components/chat/chatCanvasLayout.test.ts index c016861225e08..85ae8431ad660 100644 --- a/apps/web/src/components/chat/chatCanvasLayout.test.ts +++ b/apps/web/src/components/chat/chatCanvasLayout.test.ts @@ -43,7 +43,6 @@ describe("chat canvas layout", () => { ...preview, width: resized.width, position: resized, - lastInteraction: "resize", }, height, ).frame!; @@ -58,7 +57,6 @@ describe("chat canvas layout", () => { ...preview, width: resized.width, position: resized, - lastInteraction: "resize", }).frame!.width, ).toBe(resized.width); } diff --git a/apps/web/src/components/chat/chatCanvasLayout.ts b/apps/web/src/components/chat/chatCanvasLayout.ts index 1be526a256145..13c0cbe558ac4 100644 --- a/apps/web/src/components/chat/chatCanvasLayout.ts +++ b/apps/web/src/components/chat/chatCanvasLayout.ts @@ -5,18 +5,13 @@ import { type PreviewMiniPlayerFrame, type PreviewMiniPlayerObstacles, } from "../preview/previewMiniPlayerLayout"; -import type { - PreviewMiniPlayerPosition, - PreviewMiniPlayerSize, - PreviewMiniPlayerState, -} from "~/previewMiniPlayerStore"; +import type { PreviewMiniPlayerPosition, PreviewMiniPlayerSize } from "~/previewMiniPlayerStore"; export interface ChatCanvasPreview { readonly key: string; readonly width: number | null; readonly position: PreviewMiniPlayerPosition | null; readonly source: PreviewMiniPlayerSize; - readonly lastInteraction?: PreviewMiniPlayerState["lastInteraction"]; } const GAP = 12; diff --git a/apps/web/src/components/preview/ThreadPreviewMiniPlayer.tsx b/apps/web/src/components/preview/ThreadPreviewMiniPlayer.tsx index 3e4dba8d879ca..d913587953d87 100644 --- a/apps/web/src/components/preview/ThreadPreviewMiniPlayer.tsx +++ b/apps/web/src/components/preview/ThreadPreviewMiniPlayer.tsx @@ -188,7 +188,6 @@ export function useThreadPreviewCanvasSource( key: sourceKey, width: miniPlayer.width, position: miniPlayer.position, - lastInteraction: miniPlayer.lastInteraction, source: heldSource ?? (source.kind === "browser" diff --git a/apps/web/src/components/preview/previewMiniPlayerGesture.test.ts b/apps/web/src/components/preview/previewMiniPlayerGesture.test.ts index 1d9f9035ebf23..fec863a8be3af 100644 --- a/apps/web/src/components/preview/previewMiniPlayerGesture.test.ts +++ b/apps/web/src/components/preview/previewMiniPlayerGesture.test.ts @@ -60,7 +60,6 @@ describe("settlePreviewMiniPlayerResize", () => { width: settled!.width, position: settled, source, - lastInteraction: "resize", }, }).frame, ).toEqual(settled); diff --git a/apps/web/src/components/preview/previewMiniPlayerLayout.test.ts b/apps/web/src/components/preview/previewMiniPlayerLayout.test.ts index 5181d3d9d92b5..47190250362b0 100644 --- a/apps/web/src/components/preview/previewMiniPlayerLayout.test.ts +++ b/apps/web/src/components/preview/previewMiniPlayerLayout.test.ts @@ -70,22 +70,6 @@ describe("resolvePreviewMiniPlayerFrame", () => { ).toMatchObject(expected); }); - it.each([ - { source, width: 240 }, - { source: { width: 1600, height: 900 }, width: 267 }, - { source: { width: 2100, height: 900 }, width: 350 }, - ])("keeps wide previews at least 150 pixels tall", ({ source, width }) => { - expect( - resizePreviewMiniPlayer({ - start: { x: 12, y: 12, width: 400, height: (400 * source.height) / source.width }, - direction: "east", - delta: { x: -399, y: 0 }, - source, - container, - }), - ).toMatchObject({ width, height: 150 }); - }); - it("shrinks to the space above the composer without losing the stored width", () => { const frame = resolvePreviewMiniPlayerFrame({ width: 800, diff --git a/apps/web/src/previewMiniPlayerStore.test.ts b/apps/web/src/previewMiniPlayerStore.test.ts index 66e891315b5b2..1fe8feea51085 100644 --- a/apps/web/src/previewMiniPlayerStore.test.ts +++ b/apps/web/src/previewMiniPlayerStore.test.ts @@ -7,11 +7,6 @@ import { selectThreadPreviewMiniPlayer, usePreviewMiniPlayerStore, } from "./previewMiniPlayerStore"; -import { resolveChatCanvasLayout } from "./components/chat/chatCanvasLayout"; -import { - resolveThreadDetailsCardDensity, - resolveThreadDetailsCardLayout, -} from "./components/chat/threadDetailsCardLayout"; const refA = scopeThreadRef("env-1" as EnvironmentId, ThreadId.make("thread-A")); const tabA = browserMiniPlayerSource("tab-a"); @@ -22,40 +17,6 @@ beforeEach(() => { }); describe("previewMiniPlayerStore", () => { - it("keeps a resized preview and folded details card in place when dragging starts", () => { - const store = usePreviewMiniPlayerStore.getState(); - store.open(refA, tabA); - store.resize(refA, "browser:tab-a", 460, { x: 1112, y: 275 }); - const container = { width: 1584, height: 988 }; - const layout = () => { - const player = selectThreadPreviewMiniPlayer( - usePreviewMiniPlayerStore.getState().byThreadKey, - refA, - )!; - return resolveChatCanvasLayout({ - container, - maxChatWidth: 736, - composerHeight: 206, - detailsCard: { left: 1260, right: 1572, bottom: 339 }, - preview: { ...player, key: "browser:tab-a", source: { width: 1000, height: 1523 } }, - }); - }; - const resized = layout(); - expect(resized.frame).toEqual({ x: 1112, y: 275, width: 460, height: 701 }); - store.move(refA, "browser:tab-a", { x: 1106, y: 275 }); - const dragged = layout(); - expect(dragged.frame).toEqual({ ...resized.frame, x: 1106 }); - const card = resolveThreadDetailsCardLayout({ - container, - lane: { padding: 48, minChatWidth: 640 }, - frame: dragged.frame, - overlapsDetailsCard: dragged.overlapsDetailsCard, - })!; - expect(resolveThreadDetailsCardDensity(card.height, { full: 327, compact: 182 })).toBe( - "compact", - ); - }); - it("ignores stale drag updates after the floating tab changes", () => { usePreviewMiniPlayerStore.getState().open(refA, tabA); usePreviewMiniPlayerStore.getState().open(refA, tabB); @@ -67,7 +28,6 @@ describe("previewMiniPlayerStore", () => { source: tabB, position: null, width: null, - lastInteraction: "drag", }); }); }); diff --git a/apps/web/src/previewMiniPlayerStore.ts b/apps/web/src/previewMiniPlayerStore.ts index bdbb9b1ca876f..d28a03237b63a 100644 --- a/apps/web/src/previewMiniPlayerStore.ts +++ b/apps/web/src/previewMiniPlayerStore.ts @@ -26,7 +26,6 @@ export interface PreviewMiniPlayerState { readonly source: PreviewMiniPlayerSource; readonly position: PreviewMiniPlayerPosition | null; readonly width: number | null; - readonly lastInteraction: "drag" | "resize"; } interface PreviewMiniPlayerStoreState { @@ -78,7 +77,6 @@ export const usePreviewMiniPlayerStore = create()(( source, position: current?.position ?? null, width: current?.width ?? null, - lastInteraction: current?.lastInteraction ?? "drag", }, }, }; @@ -95,16 +93,11 @@ export const usePreviewMiniPlayerStore = create()(( const threadKey = scopedThreadKey(ref); const current = state.byThreadKey[threadKey]; if (!current || previewMiniPlayerSourceKey(current.source) !== sourceKey) return state; - if ( - current.position?.x === position.x && - current.position.y === position.y && - current.lastInteraction === "drag" - ) - return state; + if (current.position?.x === position.x && current.position.y === position.y) return state; return { byThreadKey: { ...state.byThreadKey, - [threadKey]: { ...current, position, lastInteraction: "drag" }, + [threadKey]: { ...current, position }, }, }; }), @@ -116,7 +109,6 @@ export const usePreviewMiniPlayerStore = create()(( !current || previewMiniPlayerSourceKey(current.source) !== sourceKey || (current.width === width && - current.lastInteraction === "resize" && (!position || (current.position?.x === position.x && current.position.y === position.y))) ) { return state; @@ -128,7 +120,6 @@ export const usePreviewMiniPlayerStore = create()(( ...current, width, position: position ?? current.position, - lastInteraction: "resize", }, }, };