refactor(dsh-plugin): simplify browser commands

This commit is contained in:
drakezhang
2026-08-29 14:38:48 +08:00
parent 7163f2f9b0
commit 5df93143c5
25 changed files with 934 additions and 590 deletions
@@ -1,4 +1,4 @@
/* browser_screenshot toolview: disclosure row over a terminal block plus the
/* browser_inspect screenshot toolview: disclosure row over a terminal block plus the
screenshot image. Semantic tokens only (--dsw-alias-*), no literal colors. */
.card {
@@ -1,8 +1,6 @@
// 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.
// browser_inspect keyed toolview: all inspect actions retain a terminal card;
// screenshot results additionally render the image attachment. Pure function
// of the frozen call/result slice, so live and replay paths stay identical.
import type { ImageAttachmentRef } from "@deepseek-ai/dsh-attachment";
import {
@@ -19,13 +17,13 @@ import {
} 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";
import css from "./BrowserInspectToolView.module.css";
/** Resolved loader bound to the owning session at registration time. */
export type ScreenshotImageLoader = (attachment: ImageAttachmentRef) => Promise<string>;
export type BrowserInspectImageLoader = (attachment: ImageAttachmentRef) => Promise<string>;
export type ScreenshotToolViewProps = ToolCallViewProps & {
loadImage: ScreenshotImageLoader;
export type BrowserInspectToolViewProps = ToolCallViewProps & {
loadImage: BrowserInspectImageLoader;
};
type ViewState = "running" | "ok" | "error";
@@ -36,6 +34,7 @@ interface ViewModel {
readonly output: string | null;
readonly image: ImageAttachmentRef | null;
readonly summary: string;
readonly title: string;
}
const TERMINAL_LABELS: Partial<TerminalBlockLabels> = {
@@ -66,18 +65,71 @@ function firstLine(text: string): string {
}
/** Rebuild the command line from the logged arguments (mirrors the host presenter). */
function commandOf(argsRaw: string, callId: string): string {
const INSPECT_COMMANDS: Record<string, string> = {
observe: "observe",
snapshot: "snapshot",
html: "get-html",
screenshot: "screenshot",
console: "console",
network: "network",
};
function titleOf(action: string): string {
return action === "html" ? "HTML" : `${action.slice(0, 1).toUpperCase()}${action.slice(1)}`;
}
interface InspectArgs {
action?: unknown;
session?: unknown;
tabId?: unknown;
maxDepth?: unknown;
maxTokens?: unknown;
ref?: unknown;
maxBytes?: unknown;
since?: unknown;
limit?: unknown;
maxTextChars?: unknown;
includeStack?: unknown;
}
function appendNumber(parts: string[], flag: string, value: unknown): void {
if (typeof value === "number" && Number.isFinite(value)) parts.push(flag, String(value));
}
function commandOf(argsRaw: string, callId: string): { command: string; title: string } {
try {
const args = JSON.parse(argsRaw) as { session?: unknown; ref?: unknown };
const parts = ["bsk", "screenshot", "--session"];
const args = JSON.parse(argsRaw) as InspectArgs;
const action = typeof args.action === "string" ? args.action : "inspect";
const command = INSPECT_COMMANDS[action] ?? action;
const parts = ["bsk", command, "--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(" ");
if (action === "observe" || action === "snapshot") {
appendNumber(parts, "--max-depth", args.maxDepth);
appendNumber(parts, "--max-tokens", args.maxTokens);
}
if (action === "html" || action === "console" || action === "network") {
appendNumber(parts, "--tab-id", args.tabId);
}
if ((action === "html" || action === "screenshot") && typeof args.ref === "string") {
if (args.ref !== "") parts.push("--ref", args.ref);
}
if (action === "html") appendNumber(parts, "--max-bytes", args.maxBytes);
if (action === "console" || action === "network") {
appendNumber(parts, "--since", args.since);
appendNumber(parts, "--limit", args.limit);
appendNumber(parts, "--max-text-chars", args.maxTextChars);
}
if (action === "console" && args.includeStack === true) parts.push("--include-stack");
return { command: parts.join(" "), title: titleOf(action) };
} catch {
// Streaming can expose a truncated JSON prefix; it is still the best label.
return argsRaw === "" ? `bsk screenshot (${callId})` : `bsk screenshot ${firstLine(argsRaw)}`;
return {
command:
argsRaw === "" ? `browser_inspect (${callId})` : `browser_inspect ${firstLine(argsRaw)}`,
title: "Inspect",
};
}
}
@@ -85,9 +137,9 @@ function commandOf(argsRaw: string, callId: string): string {
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);
const { command, title } = commandOf(argsRaw, block.callId);
if (!settled) {
return { state: "running", command, output: null, image: null, summary: command };
return { state: "running", command, output: null, image: null, summary: command, title };
}
const textBlock = block.content.find((item) => item.type === "text");
const imageBlock = block.content.find((item) => item.type === "image");
@@ -96,7 +148,7 @@ export function viewModelOf(block: ToolCallViewProps["block"]): ViewModel {
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 };
return { state, command, output, image, summary, title };
}
function dotState(state: ViewState): StateDotState {
@@ -111,19 +163,24 @@ function dotState(state: ViewState): StateDotState {
}
/**
* Render one browser_screenshot call: a disclosure row over a terminal block
* (command + output) plus the screenshot image when the result carries one.
* Render one browser_inspect call: a disclosure row over a terminal block,
* plus the screenshot image when that action returns an image attachment.
*/
export function ScreenshotToolView({ block, cwd, inspect, loadImage }: ScreenshotToolViewProps) {
export function BrowserInspectToolView({
block,
cwd,
inspect,
loadImage,
}: BrowserInspectToolViewProps) {
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}>
<div className={css.card} data-tool="browser_inspect" data-state={model.state}>
<DisclosureRow
icon={<StateDot state={dotState(model.state)} />}
title="Screenshot"
title={model.title}
open={open}
expandable={expandable}
onToggle={() => setExpanded((value) => !value)}
@@ -1,6 +1,6 @@
/**
* dsh-plugin-browserskill browser half: the `browser_screenshot` keyed
* toolview plus the observation overlay (live thumbnails + interrupt). The
* dsh-plugin-browserskill browser half: the `browser_inspect` keyed toolview
* plus the observation overlay (live thumbnails + interrupt). The
* default carrier is a floating card on the `shell.overlay` seat; when the
* dsh-better-sidebar plugin provides its `betterSidebar` service, the view
* moves into a sidebar tab instead (see observation-sidebar.tsx).
@@ -16,10 +16,10 @@ import { createElement } from "react";
// <style> tags; selectors stay unhashed so `cn(..., "bsk-obs")` roots match).
import "./bsk-tokens.nomodule.css";
import "./bsk-ui.nomodule.css";
import { BrowserInspectToolView } from "./BrowserInspectToolView";
import { ObservationOverlay } from "./ObservationOverlay";
import { type BetterSidebarLike, registerObservationSidebar } from "./observation-sidebar";
import { type EventSourceLike, ObservationClientStore } from "./observation-store";
import { ScreenshotToolView } from "./ScreenshotToolView";
/** Required services: slots, session-scoped attachment reads, and the overlay seat. */
export const inject = ["slots", "sessions"];
@@ -65,9 +65,9 @@ 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" },
{ name: "tool.call.toolview", key: "browser_inspect" },
(props: ToolCallViewProps) =>
createElement(ScreenshotToolView, {
createElement(BrowserInspectToolView, {
...props,
loadImage: (attachment: ImageAttachmentRef) =>
loadSessionImage(sessions, props.sessionId, attachment),