fix(dsh-plugin): add PiP close button and sniff observation thumbnail media type

The Document PiP window had no close/return entry once popped out —
users could not dismiss it from inside the window. Add an onClosePip
callback and render a close button in the PiP header; clicking it
calls pipWindow.close() and the content returns to its carrier.

The observation thumbnail publisher hardcoded mediaType as "image/png"
regardless of the actual capture bytes. Reuse the existing
sniffImageMediaType helper (exported from image.ts) so the HTTP
thumbnail endpoint declares the correct Content-Type when Chromium
returns JPEG captures.

Closes #153 (Bug 2 fully; Bug 1 was fixed in #135, this covers the
remaining observation-thumbnail path).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
drakezhang
2026-09-02 16:14:24 +08:00
co-authored by Cursor
parent 49744233f7
commit 51473c01ff
6 changed files with 58 additions and 2 deletions
@@ -14,6 +14,7 @@ import { cn } from "@browser-skill/ui";
import {
RiArrowDownSLine,
RiCheckLine,
RiCloseLine,
RiCloseCircleLine,
RiErrorWarningLine,
RiPictureInPicture2Line,
@@ -28,6 +29,7 @@ const asIcon = (component: unknown): IconComponent => component as IconComponent
const IconStop = asIcon(RiStopCircleLine);
const IconPip = asIcon(RiPictureInPicture2Line);
const IconDown = asIcon(RiArrowDownSLine);
const IconClose = asIcon(RiCloseLine);
const IconWarn = asIcon(RiErrorWarningLine);
const IconPin = asIcon(RiPushpinFill);
const IconCloseSession = asIcon(RiCloseCircleLine);
@@ -320,6 +322,7 @@ export function OverlayBody(props: {
now: number;
onPopOut?: (() => void) | undefined;
onCollapse?: (() => void) | undefined;
onClosePip?: (() => void) | undefined;
inPip: boolean;
onHeaderPointerDown?: (event: ReactPointerEvent<HTMLDivElement>) => void;
}) {
@@ -333,6 +336,7 @@ export function OverlayBody(props: {
now,
onPopOut,
onCollapse,
onClosePip,
inPip,
onHeaderPointerDown,
} = props;
@@ -389,6 +393,16 @@ export function OverlayBody(props: {
<IconDown size={14} />
</button>
) : null}
{onClosePip !== undefined ? (
<button
type="button"
className={css["icon-button"]}
aria-label="Close mini window"
onClick={onClosePip}
>
<IconClose size={14} />
</button>
) : null}
</div>
<div className={css.stage}>
{displayUrl !== undefined ? (
@@ -572,6 +586,7 @@ export function ObservationOverlay({ store }: { store: ObservationClientStore })
: undefined
}
onCollapse={pipWindow === null ? () => setCollapsed(true) : undefined}
onClosePip={pipWindow !== null ? () => pipWindow.close() : undefined}
onHeaderPointerDown={pipWindow === null ? beginMove : undefined}
/>
);
@@ -149,6 +149,7 @@ export function ObservationSidebarTab({
now={now}
inPip={pipWindow !== null}
onPopOut={pipWindow === null && pipSupported ? () => popOut() : undefined}
onClosePip={pipWindow !== null ? () => pipWindow.close() : undefined}
/>
);
@@ -42,7 +42,7 @@ interface LlmLike {
* @param data - first bytes of the capture.
* @returns the detected media type, or undefined when unrecognized.
*/
function sniffImageMediaType(data: Uint8Array): string | undefined {
export function sniffImageMediaType(data: Uint8Array): string | undefined {
if (data.length >= 4) {
// PNG signature: 89 50 4E 47 (0D 0A 1A 0A)
if (data[0] === 0x89 && data[1] === 0x50 && data[2] === 0x4e && data[3] === 0x47) {
@@ -17,6 +17,7 @@ import { readFile, unlink } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { Context } from "@deepseek-ai/cordis";
import { sniffImageMediaType } from "./image";
import type { KeyedExecutor } from "./queue";
import { BskError, type BskRunner, parseBskJson, runWithSessionBusyRetry } from "./runner";
import type { SessionRegistry } from "./sessions";
@@ -467,7 +468,7 @@ export class ObservationService {
const seq = (this.seqs.get(sessionId) ?? 0) + 1;
this.seqs.set(sessionId, seq);
const id = `obs-${sessionId}-${seq}`;
this.frames.set(id, { data, mediaType: "image/png" });
this.frames.set(id, { data, mediaType: sniffImageMediaType(data) ?? "image/png" });
this.hashes.set(sessionId, hash);
const ring = this.rings.get(sessionId) ?? [];
ring.push(id);
@@ -298,6 +298,33 @@ describe("ObservationOverlay", () => {
expect(close).toHaveBeenCalledTimes(1);
});
it("shows a close button inside the PiP window that closes it", async () => {
const h = makeHarness([BUSY]);
const pipDoc = document.implementation.createHTMLDocument("pip");
const close = vi.fn();
const listeners = new Map<string, (() => void)[]>();
const requestWindow = vi.fn(async () => {
return {
document: pipDoc,
addEventListener: (name: string, fn: () => void) => {
listeners.set(name, [...(listeners.get(name) ?? []), fn]);
},
close,
} as unknown as Window;
});
(window as unknown as Record<string, unknown>).documentPictureInPicture = { requestWindow };
render(<ObservationOverlay store={h.store} />);
const popout = await screen.findByRole("button", { name: /Pop out/ });
fireEvent.click(popout);
await waitFor(() => expect(pipDoc.body.textContent).toContain("s1"));
const closeBtn = pipDoc.body.querySelector<HTMLButtonElement>(
'button[aria-label="Close mini window"]',
);
expect(closeBtn).not.toBeNull();
closeBtn!.click();
expect(close).toHaveBeenCalledTimes(1);
});
it("renders the strip for two sessions and pins focus on click", async () => {
const older: SessionObservation = {
sessionId: "s1",
@@ -335,6 +335,18 @@ describe("thumbnail cadence", () => {
await waitFor(() => service.getState()[0]?.thumbnailAttachmentId === "obs-s1-1");
expect((await service.readThumbnail("obs-s1-1"))?.mediaType).toBe("image/png");
});
it("detects JPEG thumbnail bytes instead of hardcoding image/png", async () => {
const scheduler = fakeScheduler();
const runner = fakeRunner({
screenshotBytes: () => new Uint8Array([0xff, 0xd8, 0xff, 0xdb, 0x00, 0x01]),
});
const { service } = setup({ runner, scheduler });
service.addSession("s1");
scheduler.runNext();
await waitFor(() => service.getState()[0]?.thumbnailAttachmentId === "obs-s1-1");
expect((await service.readThumbnail("obs-s1-1"))?.mediaType).toBe("image/jpeg");
});
});
describe("observation traffic isolation", () => {