Files
BrowserSkill/packages/dsh-plugin-browserskill/src/image.ts
drakezhangandCursor 51473c01ff 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>
2026-09-02 16:14:24 +08:00

140 lines
4.8 KiB
TypeScript

/**
* Screenshot delivery. The canonical tool value stays plain JSON (a screenshot
* file path plus pixel metadata); when the host mounts a durable attachment
* store AND the calling route declares image input, the capture bytes are
* additionally committed through `ctx.attachments` so the render step can
* attach the image itself to the tool result. Any uncertainty (no store,
* unknown route, text-only model, unrecognized bytes) falls back to the
* path-only form.
*/
import type { Context } from "@deepseek-ai/cordis";
import type { ImageAttachmentRef } from "@deepseek-ai/dsh-attachment";
import type { ToolExecution } from "@deepseek-ai/dsh-tools";
/** Structural view of the attachment seam (avoids a runtime dependency). */
interface AttachmentLike {
imageLimits: {
mediaTypes: readonly string[];
maxImageBytes: number;
maxMessageImageBytes: number;
};
saveImage(input: {
data: Uint8Array;
mediaType: string;
name?: string;
}): Promise<ImageAttachmentRef>;
}
interface LlmLike {
resolveModelInfo(
provider: string,
model: string,
signal?: AbortSignal,
): Promise<{ inputModalities?: readonly string[] }>;
}
/**
* Sniff the image media type from magic bytes. The capture path can hand back
* JPEG (e.g. `chrome.tabs.captureVisibleTab` on some Chromium/Edge builds
* returning JPEG regardless of the requested format), so the declared type can
* never be trusted from the file extension alone.
* @param data - first bytes of the capture.
* @returns the detected media type, or undefined when unrecognized.
*/
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) {
return "image/png";
}
// JPEG SOI: FF D8 FF
if (data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff) {
return "image/jpeg";
}
// WebP: "RIFF" (52 49 46 46) + "WEBP" at offset 8
if (
data[0] === 0x52 &&
data[1] === 0x49 &&
data[2] === 0x46 &&
data[3] === 0x46 &&
data.length >= 12 &&
data[8] === 0x57 &&
data[9] === 0x45 &&
data[10] === 0x42 &&
data[11] === 0x50
) {
return "image/webp";
}
// GIF: "GIF87a" or "GIF89a"
if (
data[0] === 0x47 &&
data[1] === 0x49 &&
data[2] === 0x46 &&
data[3] === 0x38 &&
data.length >= 6 &&
(data[4] === 0x37 || data[4] === 0x39) &&
data[5] === 0x61
) {
return "image/gif";
}
}
return undefined;
}
/**
* Commit screenshot bytes to the host attachment store when the composition
* supports durable images on the current model route.
* @returns the durable reference, or undefined to stay in path-only mode.
*/
export async function trySaveScreenshot(
ctx: Context,
exec: ToolExecution,
data: Uint8Array,
name: string,
): Promise<ImageAttachmentRef | undefined> {
const attachments = ctx.get("attachments") as AttachmentLike | undefined;
if (attachments === undefined) return undefined;
// Declare the real media type from the bytes, never a hard-coded "image/png".
// Declaring the wrong type trips the store's IMAGE_TYPE_MISMATCH check and
// silently degrades to path-only; sniffing first lets JPEG captures (returned
// by some Chromium builds for `captureVisibleTab({ format: "png" })`) still
// inline correctly.
const mediaType = sniffImageMediaType(data);
if (mediaType === undefined) return undefined;
if (!attachments.imageLimits.mediaTypes.includes(mediaType)) return undefined;
if (
data.byteLength >
Math.min(attachments.imageLimits.maxImageBytes, attachments.imageLimits.maxMessageImageBytes)
) {
return undefined;
}
if (!(await isImageCapableRoute(ctx, exec))) return undefined;
try {
return await attachments.saveImage({ data, mediaType, name });
} catch {
// A store hiccup must not fail the tool call; the path-only form still works.
return undefined;
}
}
/**
* Best-effort check that the calling route's model accepts image input,
* mirroring dsh-tool-fs `read_image`. Unknown route / missing llm service
* answers false (refuse to attach) so history never gains an image block a
* text-only adapter cannot replay.
*/
async function isImageCapableRoute(ctx: Context, exec: ToolExecution): Promise<boolean> {
const llm = ctx.get("llm") as LlmLike | undefined;
const provider =
exec.agent?.session.requestHeader()?.config.provider ?? exec.agent?.options.provider;
const model = exec.agent?.session.requestHeader()?.config.model ?? exec.agent?.options.model;
if (llm === undefined || provider === undefined || model === undefined) return false;
try {
const info = await llm.resolveModelInfo(provider, model, exec.signal);
return info.inputModalities?.includes("image") ?? false;
} catch {
return false;
}
}