feat(dsh-plugin-browserskill): add the web client half with a browser_screenshot toolview

Make the package dual-face: a dsh.client declaration (platform 'web')
plus an exports["./client"] bundle built in dsh's closure-factory shape
(window.__ModuleLoader__.load handoff, platform modules external,
everything else inlined, CSS Modules via lightningcss).

The client registers a keyed 'tool.call.toolview' view for
browser_screenshot that keeps the terminal block (command + output) and,
when the settled result carries an image block, resolves the durable
attachment through the client session's authorized readAttachment RPC
and renders it with the shared MessageImage thumbnail/lightbox atoms.
Path-only results render unchanged; every other browser_* tool keeps
the stock terminal card.

Tests: view-model derivation (running/ok/error, image vs path-only),
component rendering through @testing-library/react + happy-dom, and the
registration key. biome.json now excludes the package's lib/ build
output (same treatment as dist/).
This commit is contained in:
BB-fat
2026-08-14 02:54:33 +00:00
parent 0771292336
commit 6b96fcca32
12 changed files with 2642 additions and 25 deletions
+1
View File
@@ -39,6 +39,7 @@
"**",
"!**/node_modules/**",
"!**/dist/**",
"!packages/dsh-plugin-browserskill/lib/**",
"!**/target/**",
"!**/.wxt/**",
"!**/.output/**",
@@ -69,6 +69,15 @@ All fields are optional and validated through the plugin's Schemastery `Config`:
- **UI cards**: calls render as terminal cards (command line as title, output as the completed
card). Screenshots additionally attach the image itself when the host mounts an attachment store
and the active model route declares image input; otherwise the PNG path is returned.
- **Web UI toolview (browser half)**: the package is dual-face. `dsh.client` (platform `web`) ships
`lib/client.js`, which registers a keyed `tool.call.toolview` view for `browser_screenshot`. The
custom view keeps the terminal block (command + output) and, when the settled result carries an
image block, resolves the durable attachment through the client session's authorized
`readAttachment` RPC and renders it with the shared `MessageImage` thumbnail/lightbox atoms.
Every other `browser_*` tool keeps the stock terminal card. The bundle follows the dsh client
contract: a CJS closure factory handed to `window.__ModuleLoader__.load`, platform modules
(`react`, `dsh-client-ui-*`) external, everything else inlined, CSS Modules compiled by
lightningcss.
- **Errors**: non-zero bsk exits surface the CLI's JSON error envelope (`code`, `message`, `hint`)
so the model gets the daemon's actionable guidance.
- **Long-running work** (e.g. `bsk record`) is not backgrounded via `ctx.jobs` yet — tracked as a
+41 -1
View File
@@ -11,6 +11,9 @@
"types": "./lib/index.d.mts",
"default": "./lib/index.mjs"
},
"./client": {
"default": "./lib/client.js"
},
"./package.json": "./package.json"
},
"files": [
@@ -20,6 +23,13 @@
"dsh": {
"bundle": {
"patch": "./cordis.patch.yml"
},
"client": {
"platform": "web",
"inject": [
"@deepseek-ai/dsh-client-runtime",
"@deepseek-ai/dsh-client-ui-tool"
]
}
},
"scripts": {
@@ -29,24 +39,54 @@
},
"peerDependencies": {
"@deepseek-ai/cordis": "^4.0.1",
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6"
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
"@deepseek-ai/dsh-client-ui-attachment": "^0.1.0-rc.6",
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
"@deepseek-ai/dsh-client-ui-tool": "^0.1.0-rc.6",
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
"react": "^18.2.0"
},
"peerDependenciesMeta": {
"@deepseek-ai/dsh-attachment": {
"optional": true
},
"@deepseek-ai/dsh-client-runtime": {
"optional": true
},
"@deepseek-ai/dsh-client-ui-attachment": {
"optional": true
},
"@deepseek-ai/dsh-client-ui-primitives": {
"optional": true
},
"@deepseek-ai/dsh-client-ui-tool": {
"optional": true
},
"@deepseek-ai/dsh-llm": {
"optional": true
},
"react": {
"optional": true
}
},
"devDependencies": {
"@deepseek-ai/cordis": "^4.0.1",
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
"@deepseek-ai/dsh-attachment": "^0.1.0-rc.6",
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
"@deepseek-ai/dsh-client-ui-attachment": "^0.1.0-rc.6",
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
"@deepseek-ai/dsh-client-ui-tool": "^0.1.0-rc.6",
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
"@deepseek-ai/schemastery": "^3.18.1",
"@testing-library/react": "^16.1.0",
"@types/node": "^22.10.0",
"@types/react": "~18.3.1",
"happy-dom": "^15.11.7",
"lightningcss": "^1.30.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tsdown": "^0.22.14",
"typescript": "^5.8.0",
"vitest": "^4.1.6"
@@ -0,0 +1,57 @@
/* browser_screenshot toolview: disclosure row over a terminal block plus the
screenshot image. Semantic tokens only (--dsw-alias-*), no literal colors. */
.card {
display: flex;
flex-direction: column;
}
.summary {
min-width: 0;
margin-left: 6px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--dsw-alias-label-secondary);
}
.card[data-state="error"] .summary {
color: var(--dsw-alias-label-error);
}
.body {
display: flex;
flex-direction: column;
gap: 8px;
padding: 4px 0 4px 22px;
}
.image-wrap {
display: flex;
}
.inspect-button {
align-self: flex-start;
border: none;
background: none;
padding: 0;
font: inherit;
font-size: 12px;
color: var(--dsw-alias-label-tertiary);
cursor: pointer;
}
.inspect-button:hover {
color: var(--dsw-alias-label-secondary);
}
.inspect-button:focus-visible {
outline: 2px solid var(--dsw-alias-border-focus);
outline-offset: 2px;
border-radius: 2px;
}
@media (prefers-reduced-motion: reduce) {
.inspect-button {
transition: none;
}
}
@@ -0,0 +1,161 @@
// browser_screenshot keyed toolview: the terminal card's command line and
// output stay exactly as the host renders them, and a result carrying an
// image block additionally renders the screenshot itself (thumbnail with the
// shared click-to-open lightbox). Pure function of the frozen call/result
// slice — live and replay paths render identically.
import type { ImageAttachmentRef } from "@deepseek-ai/dsh-attachment";
import {
type ImageLoader,
MessageImage,
type MessageImageLabels,
} from "@deepseek-ai/dsh-client-ui-attachment";
import {
DisclosureRow,
StateDot,
type StateDotState,
TerminalBlock,
type TerminalBlockLabels,
} from "@deepseek-ai/dsh-client-ui-primitives";
import type { ToolCallViewProps } from "@deepseek-ai/dsh-client-ui-tool/client";
import { useState } from "react";
import css from "./ScreenshotToolView.module.css";
/** Resolved loader bound to the owning session at registration time. */
export type ScreenshotImageLoader = (attachment: ImageAttachmentRef) => Promise<string>;
export type ScreenshotToolViewProps = ToolCallViewProps & {
loadImage: ScreenshotImageLoader;
};
type ViewState = "running" | "ok" | "error";
interface ViewModel {
readonly state: ViewState;
readonly command: string;
readonly output: string | null;
readonly image: ImageAttachmentRef | null;
readonly summary: string;
}
const TERMINAL_LABELS: Partial<TerminalBlockLabels> = {
running: "Running",
failed: "Failed",
done: "Done",
copy: "Copy",
copied: "Copied",
noOutput: "No output",
collapseAria: "Collapse output",
collapse: "Collapse",
expandAria: (hidden) => `Expand the remaining ${hidden} output lines`,
expand: (hidden) => `… ${hidden} more lines`,
};
const IMAGE_LABELS: MessageImageLabels = {
image: "screenshot",
open: "Open the original screenshot",
openNamed: (label) => `Open screenshot ${label}`,
loading: "Loading…",
loadFailed: "Load failed — retry",
lightbox: { dialog: "Screenshot preview", close: "Close preview" },
};
function firstLine(text: string): string {
const newline = text.indexOf("\n");
return newline === -1 ? text : text.slice(0, newline);
}
/** Rebuild the command line from the logged arguments (mirrors the host presenter). */
function commandOf(argsRaw: string, callId: string): string {
try {
const args = JSON.parse(argsRaw) as { session?: unknown; ref?: unknown };
const parts = ["bsk", "screenshot", "--session"];
parts.push(
typeof args.session === "string" && args.session !== "" ? args.session : "(current)",
);
if (typeof args.ref === "string" && args.ref !== "") parts.push("--ref", args.ref);
return parts.join(" ");
} catch {
// Streaming can expose a truncated JSON prefix; it is still the best label.
return argsRaw === "" ? `bsk screenshot (${callId})` : `bsk screenshot ${firstLine(argsRaw)}`;
}
}
/** Derive the display model from the frozen block only. */
export function viewModelOf(block: ToolCallViewProps["block"]): ViewModel {
const settled = "kind" in block;
const argsRaw = (settled ? block.call?.argsRaw : block.argsRaw) ?? "";
const command = commandOf(argsRaw, block.callId);
if (!settled) {
return { state: "running", command, output: null, image: null, summary: command };
}
const textBlock = block.content.find((item) => item.type === "text");
const imageBlock = block.content.find((item) => item.type === "image");
const output = textBlock !== undefined && textBlock.type === "text" ? textBlock.text : null;
const image =
imageBlock !== undefined && imageBlock.type === "image" ? imageBlock.attachment : null;
const state: ViewState = block.isError ? "error" : "ok";
const summary = output !== null ? firstLine(output) : command;
return { state, command, output, image, summary };
}
function dotState(state: ViewState): StateDotState {
switch (state) {
case "running":
return "ongoing";
case "error":
return "error";
default:
return "done";
}
}
/**
* Render one browser_screenshot call: a disclosure row over a terminal block
* (command + output) plus the screenshot image when the result carries one.
*/
export function ScreenshotToolView({ block, cwd, inspect, loadImage }: ScreenshotToolViewProps) {
const model = viewModelOf(block);
const [expanded, setExpanded] = useState(false);
const expandable = model.state === "running" || model.output !== null || model.image !== null;
const open = expanded && expandable;
return (
<div className={css.card} data-tool="browser_screenshot" data-state={model.state}>
<DisclosureRow
icon={<StateDot state={dotState(model.state)} />}
title="Screenshot"
open={open}
expandable={expandable}
onToggle={() => setExpanded((value) => !value)}
expandOnRowClick
previewChevron
collapsedContent={<span className={css.summary}>{model.summary}</span>}
>
<div className={css.body}>
<TerminalBlock
command={model.command}
cwd={cwd ?? undefined}
output={model.output ?? undefined}
running={model.state === "running"}
labels={TERMINAL_LABELS}
/>
{model.image !== null ? (
<div className={css["image-wrap"]}>
<MessageImage
attachment={model.image}
load={loadImage}
variant="single"
labels={IMAGE_LABELS}
/>
</div>
) : null}
{inspect !== undefined ? (
<button type="button" className={css["inspect-button"]} onClick={inspect}>
Inspect
</button>
) : null}
</div>
</DisclosureRow>
</div>
);
}
@@ -0,0 +1,60 @@
/**
* dsh-plugin-browserskill browser half: registers the `browser_screenshot`
* keyed toolview (`tool.call.toolview` slot) so screenshot results render the
* captured image inline in the Web UI instead of a bare JSON block. Every
* other browser_* tool keeps the stock terminal card.
*
* The image bytes never enter the session log: the view resolves the durable
* attachment reference through the client session's authorized
* `readAttachment` RPC and hands a blob URL to the shared MessageImage atom.
*/
import type { ImageAttachmentRef } from "@deepseek-ai/dsh-attachment";
import type { ClientContext, ISessions, SessionId } from "@deepseek-ai/dsh-client-runtime/client";
import type { ToolCallViewProps } from "@deepseek-ai/dsh-client-ui-tool/client";
import { createElement } from "react";
import { ScreenshotToolView } from "./ScreenshotToolView";
/** Required services: the slot registry plus session-scoped attachment reads. */
export const inject = ["slots", "sessions"];
/** Resolve one durable image attachment into a browser blob URL. */
async function loadSessionImage(
sessions: ISessions,
sessionId: SessionId,
attachment: ImageAttachmentRef,
): Promise<string> {
const session = sessions.binding(sessionId)?.session;
if (session === undefined) {
throw new Error(`screenshot toolview: session "${String(sessionId)}" is not bound`);
}
const result = await session.readAttachment(attachment.attachmentId);
if (!result.ok) {
throw new Error(
`screenshot toolview: readAttachment failed: ${result.error.code}: ${result.error.message}`,
);
}
const bytes = Uint8Array.from(result.value.data);
return URL.createObjectURL(
new Blob([bytes.buffer as ArrayBuffer], { type: result.value.attachment.mediaType }),
);
}
/**
* Client plugin body: register the keyed view with its session-bound loader.
* @param ctx - client root context.
*/
export function apply(ctx: ClientContext): void {
const sessions = ctx.get("sessions") as unknown as ISessions;
ctx.slots.inject("tool.call.toolview", () =>
ctx.slots.register(
{ name: "tool.call.toolview", key: "browser_screenshot" },
(props: ToolCallViewProps) =>
createElement(ScreenshotToolView, {
...props,
loadImage: (attachment: ImageAttachmentRef) =>
loadSessionImage(sessions, props.sessionId, attachment),
}),
),
);
}
+6
View File
@@ -0,0 +1,6 @@
declare module "*.module.css" {
const classes: Record<string, string>;
export default classes;
}
declare module "*.css";
@@ -0,0 +1,151 @@
// @vitest-environment happy-dom
// browser_screenshot keyed toolview: view-model derivation from frozen blocks,
// image/path-only/running/error rendering, the registration key, and the
// session-bound attachment loader.
import type { RunningToolCall, ToolResultNode } from "@deepseek-ai/dsh-client-runtime/client";
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { apply } from "../../src/client/index";
import {
ScreenshotToolView,
type ScreenshotToolViewProps,
viewModelOf,
} from "../../src/client/ScreenshotToolView";
const ATTACHMENT = {
attachmentId: "sha256:abc123",
mediaType: "image/png",
bytes: 8,
width: 800,
height: 457,
name: "screenshot-s1.png",
} as never;
function runningBlock(argsRaw: string): RunningToolCall {
return {
callId: "c1",
name: "browser_screenshot",
argsRaw,
turn: 1,
step: 1,
time: 0,
callView: null,
subCalls: [],
} as never;
}
function settledBlock(argsRaw: string, content: unknown[], isError = false): ToolResultNode {
return {
kind: "tool-result",
seq: 1,
time: 0,
callId: "c1",
call: { name: "browser_screenshot", argsRaw },
callTime: 0,
content,
isError,
callView: null,
resultView: null,
subCalls: [],
} as never;
}
const IMAGE_TEXT = "[session s1] screenshot of tab 7 (800x457px)";
const PATH_TEXT = "[session s1] screenshot saved to /tmp/shot.png (800x457px, 8 bytes)";
afterEach(cleanup);
describe("viewModelOf", () => {
it("derives the running model from the call frame", () => {
const model = viewModelOf(runningBlock('{"session":"s1"}'));
expect(model.state).toBe("running");
expect(model.command).toBe("bsk screenshot --session s1");
expect(model.image).toBeNull();
});
it("marks settled results carrying an image block", () => {
const block = settledBlock("{}", [
{ type: "text", text: IMAGE_TEXT },
{ type: "image", attachment: ATTACHMENT },
]);
const model = viewModelOf(block);
expect(model.state).toBe("ok");
expect(model.image).toBe(ATTACHMENT);
expect(model.output).toBe(IMAGE_TEXT);
expect(model.command).toBe("bsk screenshot --session (current)");
});
it("keeps the path-only form image-free", () => {
const model = viewModelOf(settledBlock("{}", [{ type: "text", text: PATH_TEXT }]));
expect(model.state).toBe("ok");
expect(model.image).toBeNull();
expect(model.summary).toBe(PATH_TEXT);
});
it("marks error results", () => {
const model = viewModelOf(settledBlock("{}", [{ type: "text", text: "Error: boom" }], true));
expect(model.state).toBe("error");
});
});
describe("ScreenshotToolView", () => {
const baseProps = { callId: "c1", toolName: "browser_screenshot", openFile: () => {} };
const renderView = (
block: ScreenshotToolViewProps["block"],
loadImage: ScreenshotToolViewProps["loadImage"],
) => {
const props = { ...baseProps, block, loadImage } as unknown as ScreenshotToolViewProps;
return render(<ScreenshotToolView {...props} />);
};
it("renders the screenshot image through the loader", async () => {
const block = settledBlock("{}", [
{ type: "text", text: IMAGE_TEXT },
{ type: "image", attachment: ATTACHMENT },
]);
const loadImage = vi.fn(async () => "blob:mock-url");
renderView(block, loadImage);
// Collapsed row shows the summary; expand to reach the image.
expect(screen.getByText(IMAGE_TEXT)).toBeTruthy();
const toggle = screen.getByRole("button", { name: /screenshot/i });
toggle.click();
await waitFor(() => expect(loadImage).toHaveBeenCalledWith(ATTACHMENT));
await screen.findByRole("img", { name: "screenshot-s1.png" });
});
it("renders the path-only form without touching the loader", async () => {
const block = settledBlock("{}", [{ type: "text", text: PATH_TEXT }]);
const loadImage = vi.fn(async () => "blob:unused");
renderView(block, loadImage);
screen.getByRole("button", { name: /screenshot/i }).click();
await screen.findByText(PATH_TEXT, { exact: false });
expect(loadImage).not.toHaveBeenCalled();
});
it("renders the running form without output", () => {
const loadImage = vi.fn();
renderView(runningBlock("{}"), loadImage);
expect(screen.getByText("bsk screenshot --session (current)")).toBeTruthy();
expect(loadImage).not.toHaveBeenCalled();
});
});
describe("client plugin registration", () => {
it("registers only the browser_screenshot keyed toolview", () => {
const registrations: { name: string; key?: string }[] = [];
const sessions = { binding: () => undefined };
const ctx = {
get: (key: string) => (key === "sessions" ? sessions : undefined),
slots: {
inject: (_name: string, fn: () => unknown) => fn(),
register: (slot: { name: string; key?: string }, view: unknown) => {
registrations.push(slot);
expect(typeof view).toBe("function");
},
},
};
apply(ctx as never);
expect(registrations).toEqual([{ name: "tool.call.toolview", key: "browser_screenshot" }]);
});
});
@@ -3,6 +3,8 @@
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"noEmit": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
@@ -1,10 +1,124 @@
import { defineConfig } from "tsdown";
import { readFile } from "node:fs/promises";
import { basename, dirname, resolve as resolvePath } from "node:path";
import { transform } from "lightningcss";
import { defineConfig, type UserConfig } from "tsdown";
export default defineConfig({
entry: ["src/index.ts"],
format: ["esm"],
dts: true,
/**
* Two build faces of the dual-face package:
* - host: the Cordis tool plugin (lib/index.mjs, ESM, dsh packages external);
* - client: the Web UI bundle (lib/client.js) in dsh's closure-factory shape —
* the artifact registers itself through `window.__ModuleLoader__.load`, with
* platform modules resolved from the loader's frozen module table and every
* other dependency inlined. Mirrors deepseek-harness `packages/client/tsdown.client.ts`.
*/
/** Module specifiers the dsh web shell shares into its frozen module table. */
const CLIENT_EXTERNALS: readonly string[] = [
"react",
"react/jsx-runtime",
"react-dom",
"react-dom/client",
"@deepseek-ai/cordis",
"@deepseek-ai/dsh-client-ui-slots",
"@deepseek-ai/dsh-client-web-react",
"@deepseek-ai/dsh-client-ui-primitives",
"@deepseek-ai/dsh-client-ui-attachment",
"@deepseek-ai/dsh-client-schema-form",
"@deepseek-ai/dsh-client-runtime/client",
];
/** Wire/type layers with no shared runtime identity: safe to inline. */
const INLINE_SAFE = /^@deepseek-ai\/dsh-(host-apiproxy|session|llm|tools|brand|attachment)(\/|$)/;
const VENDORED_LIBRARY = /^@deepseek-ai\/(cosmokit|schemastery)(\/|$)/;
const CSS_VIRTUAL_PREFIX = "\0bsk-css:";
const CSS_VIRTUAL_SUFFIX = ".mjs";
const CLIENT_ID = "dsh-plugin-browserskill";
const client: UserConfig = {
name: `${CLIENT_ID}/client`,
entry: { client: "src/client/index.ts" },
outDir: "lib",
// dsh host packages are provided by the profile the plugin is installed into.
external: [/^@deepseek-ai\//],
});
format: "cjs",
platform: "browser",
dts: false,
clean: false,
external: [...CLIENT_EXTERNALS],
// Anything not in the loader module table must inline; an unanswerable
// require() is a guaranteed runtime throw.
noExternal: (id: string) => (CLIENT_EXTERNALS.includes(id) ? undefined : true),
define: {
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV ?? "production"),
"import.meta.env.MODE": JSON.stringify(process.env.NODE_ENV ?? "production"),
"import.meta.env": JSON.stringify({ MODE: process.env.NODE_ENV ?? "production" }),
},
plugins: [
{
// Bundle purity gate: platform modules stay external, wire layers
// inline, every other @deepseek-ai value import is a build error.
name: "bsk-client-bundle-purity",
resolveId(source: string) {
if (!source.startsWith("@deepseek-ai/")) return null;
if (CLIENT_EXTERNALS.includes(source)) return null;
if (VENDORED_LIBRARY.test(source)) return null;
if (INLINE_SAFE.test(source)) return null;
throw new Error(
`client bundle purity: "${source}" is not a platform module or an inline-safe wire layer — ` +
"collaborate through cordis services (type-only imports are erased and never reach this gate)",
);
},
},
{
name: "bsk-css-modules-inline",
resolveId(source: string, importer: string | undefined) {
if (!source.endsWith(".module.css")) return null;
const abs = importer !== undefined ? resolvePath(dirname(importer), source) : source;
return CSS_VIRTUAL_PREFIX + abs + CSS_VIRTUAL_SUFFIX;
},
async load(virtualId: string) {
if (!virtualId.startsWith(CSS_VIRTUAL_PREFIX)) return null;
const fileId = virtualId.slice(CSS_VIRTUAL_PREFIX.length, -CSS_VIRTUAL_SUFFIX.length);
this.addWatchFile(fileId);
const source = await readFile(fileId);
const { code, exports: cssExports } = transform({
filename: fileId,
code: source,
cssModules: { pattern: "[hash]_[local]" },
minify: true,
});
const classMap: Record<string, string> = {};
for (const [local, exp] of Object.entries(cssExports ?? {})) classMap[local] = exp.name;
return [
`const css = ${JSON.stringify(code.toString())};`,
`const tagId = ${JSON.stringify(`${CLIENT_ID}/${basename(fileId)}`)};`,
"if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css=' + JSON.stringify(tagId) + ']') === null) {",
" const tag = document.createElement('style');",
` tag.dataset.plugin = ${JSON.stringify(CLIENT_ID)};`,
" tag.dataset.pluginCss = tagId;",
" tag.textContent = css;",
" document.head.appendChild(tag);",
"}",
`export default ${JSON.stringify(classMap)};`,
].join("\n");
},
},
],
outputOptions: {
entryFileNames: "client.js",
banner: `window.__ModuleLoader__.load({ id: ${JSON.stringify(CLIENT_ID)}, factory: (require) => {`,
footer: "return module.exports; } });",
intro: "var module = { exports: {} }; var exports = module.exports;",
},
};
export default defineConfig([
{
entry: ["src/index.ts"],
format: ["esm"],
dts: true,
outDir: "lib",
// dsh host packages are provided by the profile the plugin is installed into.
external: [/^@deepseek-ai\//],
},
client,
]);
@@ -3,6 +3,13 @@ import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "node",
include: ["tests/**/*.test.ts"],
include: ["tests/**/*.test.ts", "tests/**/*.test.tsx"],
server: {
deps: {
// dsh client packages ship ESM with CSS imports; run them through the
// transform pipeline (where CSS is stubbed) instead of Node's loader.
inline: [/@deepseek-ai\//],
},
},
},
});
+2024 -15
View File
File diff suppressed because it is too large Load Diff