mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
Merge pull request #157 from Tencent/refactor/simplify-dsh-commands
Refactor/simplify dsh commands
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
* are hidden from every surface, so their Agent Windows would otherwise
|
||||
* linger with no way back to them.
|
||||
*
|
||||
* Ownership lineage: browser_session_start records the calling agent's
|
||||
* Ownership lineage: browser_session action=start records the calling agent's
|
||||
* session id PLUS every ancestor along `header.parentSession` (a subagent's
|
||||
* browsers are reaped when any ancestor is archived, the root conversation
|
||||
* included). Lineage is resolved through the host session store; an
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
/**
|
||||
* The six model-facing BrowserSkill tools. Each action dispatches to a private
|
||||
* operation handler, so the model receives only six schemas while every action
|
||||
* retains the existing ownership, cancellation, queuing, observation,
|
||||
* screenshot, and presentation behavior.
|
||||
*/
|
||||
|
||||
import { defineTool, type ParameterSchemaSpec, type ToolDefinition } from "@deepseek-ai/dsh-tools";
|
||||
import { SESSION_PARAM, TAB_ID_PARAM, TIMEOUT_MS_PARAM, WAIT_UNTIL_PARAM } from "./tool-params";
|
||||
import { createBrowserOperationDefinitions, type ToolDeps } from "./tools";
|
||||
|
||||
const DEVICE_PRESETS = [
|
||||
"iphone-14",
|
||||
"iphone-14-pro-max",
|
||||
"iphone-se",
|
||||
"pixel-7",
|
||||
"galaxy-s23",
|
||||
"ipad-mini",
|
||||
"galaxy-tab-s8",
|
||||
] as const;
|
||||
|
||||
const TARGET_PARAM = {
|
||||
type: "string",
|
||||
description: "Snapshot ref such as @e3, or a CSS selector.",
|
||||
} as const;
|
||||
|
||||
const DOMAIN_RESULT = { type: "json" } as const;
|
||||
|
||||
interface BrowserToolSpec {
|
||||
name: string;
|
||||
description: string;
|
||||
actions: Record<string, string>;
|
||||
parameters: ParameterSchemaSpec;
|
||||
}
|
||||
|
||||
function operationArgs(args: unknown): Record<string, unknown> {
|
||||
const { action: _action, ...rest } = args as Record<string, unknown>;
|
||||
return rest;
|
||||
}
|
||||
|
||||
function definitionFor(
|
||||
definitions: ReadonlyMap<string, ToolDefinition>,
|
||||
actions: Record<string, string>,
|
||||
action: unknown,
|
||||
): ToolDefinition {
|
||||
const operationName = typeof action === "string" ? actions[action] : undefined;
|
||||
const definition = operationName === undefined ? undefined : definitions.get(operationName);
|
||||
if (definition === undefined) throw new Error(`unsupported browser action: ${String(action)}`);
|
||||
return definition;
|
||||
}
|
||||
|
||||
function defineBrowserTool(
|
||||
spec: BrowserToolSpec,
|
||||
definitions: ReadonlyMap<string, ToolDefinition>,
|
||||
): ToolDefinition {
|
||||
const actionNames = Object.keys(spec.actions);
|
||||
return defineTool({
|
||||
name: spec.name,
|
||||
description: spec.description,
|
||||
parameters: {
|
||||
action: {
|
||||
type: "string",
|
||||
required: true,
|
||||
enum: actionNames,
|
||||
description:
|
||||
"Operation to perform. Other arguments are action-dependent and are validated by that operation.",
|
||||
},
|
||||
...spec.parameters,
|
||||
},
|
||||
output: {
|
||||
schema: DOMAIN_RESULT,
|
||||
render(args, value) {
|
||||
const definition = definitionFor(definitions, spec.actions, args.action);
|
||||
return definition.output.render(operationArgs(args), value);
|
||||
},
|
||||
},
|
||||
isConcurrencySafe(args) {
|
||||
const definition = definitionFor(definitions, spec.actions, args.action);
|
||||
return definition.isConcurrencySafe?.(operationArgs(args)) === true;
|
||||
},
|
||||
async execute(args, exec) {
|
||||
const definition = definitionFor(definitions, spec.actions, args.action);
|
||||
return (await definition.execute(operationArgs(args), exec)) as never;
|
||||
},
|
||||
presentCall(args) {
|
||||
const definition = definitionFor(definitions, spec.actions, args.action);
|
||||
return definition.presentCall?.(operationArgs(args));
|
||||
},
|
||||
presentResult(args, result) {
|
||||
const definition = definitionFor(definitions, spec.actions, args.action);
|
||||
return definition.presentResult?.(operationArgs(args), result);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const BROWSER_TOOL_SPECS: BrowserToolSpec[] = [
|
||||
{
|
||||
name: "browser_session",
|
||||
description:
|
||||
"Manage plugin-owned browser sessions. Actions: start opens an Agent Window; stop closes an " +
|
||||
"owned session; list returns owned sessions. For start, url/device/width/height/noFocus/browser " +
|
||||
"are optional. For stop, session is optional and defaults to the current owned session.",
|
||||
actions: {
|
||||
start: "session.start",
|
||||
stop: "session.stop",
|
||||
list: "session.list",
|
||||
},
|
||||
parameters: {
|
||||
session: SESSION_PARAM,
|
||||
url: { type: "string", description: "Initial URL for start." },
|
||||
width: { type: "integer", description: "Agent Window width; start requires height too." },
|
||||
height: { type: "integer", description: "Agent Window height; start requires width too." },
|
||||
noFocus: { type: "boolean", description: "Start the Agent Window in the background." },
|
||||
browser: { type: "string", description: "Browser instance id for start." },
|
||||
device: { type: "string", enum: DEVICE_PRESETS, description: "Device preset for start." },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "browser_page",
|
||||
description:
|
||||
"Navigate and wait on the active Agent Window tab. Actions: navigate, back, forward, reload, " +
|
||||
"wait. navigate requires url; reload optionally accepts hard; navigation actions accept " +
|
||||
"waitUntil/timeoutMs. Observe again after a meaningful page change before reusing refs.",
|
||||
actions: {
|
||||
navigate: "page.navigate",
|
||||
back: "page.back",
|
||||
forward: "page.forward",
|
||||
reload: "page.reload",
|
||||
wait: "page.wait",
|
||||
},
|
||||
parameters: {
|
||||
session: SESSION_PARAM,
|
||||
tabId: TAB_ID_PARAM,
|
||||
url: { type: "string", description: "Destination URL; required for navigate." },
|
||||
waitUntil: WAIT_UNTIL_PARAM,
|
||||
timeoutMs: TIMEOUT_MS_PARAM,
|
||||
hard: { type: "boolean", description: "Bypass cache for reload." },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "browser_inspect",
|
||||
description:
|
||||
"Read page state without arbitrary script execution. Actions: observe, snapshot, html, " +
|
||||
"screenshot, console, network. Prefer observe, then snapshot, then bounded html; use screenshot " +
|
||||
"for visual evidence. console/network support cursor fields since/limit/maxTextChars.",
|
||||
actions: {
|
||||
observe: "inspect.observe",
|
||||
snapshot: "inspect.snapshot",
|
||||
html: "inspect.html",
|
||||
screenshot: "inspect.screenshot",
|
||||
console: "inspect.console",
|
||||
network: "inspect.network",
|
||||
},
|
||||
parameters: {
|
||||
session: SESSION_PARAM,
|
||||
tabId: TAB_ID_PARAM,
|
||||
maxDepth: { type: "integer", description: "Tree depth cap for observe/snapshot." },
|
||||
maxTokens: { type: "integer", description: "Token cap for observe/snapshot." },
|
||||
ref: { type: "string", description: "Fresh ref for scoped html or cropped screenshot." },
|
||||
maxBytes: { type: "integer", description: "HTML byte cap." },
|
||||
since: { type: "integer", description: "Console/network sequence cursor." },
|
||||
limit: { type: "integer", description: "Console/network entry cap." },
|
||||
maxTextChars: { type: "integer", description: "Console/network per-entry text cap." },
|
||||
includeStack: { type: "boolean", description: "Include console stack frames." },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "browser_interact",
|
||||
description:
|
||||
"Interact with an element in the active Agent Window tab. Actions: click, hover, fill, select, " +
|
||||
"press. click/hover/fill/select require target; fill also requires value; select requires " +
|
||||
"values; press requires key and may optionally focus target first.",
|
||||
actions: {
|
||||
click: "interact.click",
|
||||
hover: "interact.hover",
|
||||
fill: "interact.fill",
|
||||
select: "interact.select",
|
||||
press: "interact.press",
|
||||
},
|
||||
parameters: {
|
||||
session: SESSION_PARAM,
|
||||
tabId: TAB_ID_PARAM,
|
||||
target: TARGET_PARAM,
|
||||
button: { type: "string", enum: ["left", "middle", "right"], description: "Click button." },
|
||||
clickCount: { type: "integer", description: "Click count." },
|
||||
value: { type: "string", description: "Text for fill." },
|
||||
noClear: { type: "boolean", description: "Append instead of clearing for fill." },
|
||||
modifiers: {
|
||||
type: "array",
|
||||
items: { type: "string", enum: ["alt", "ctrl", "meta", "shift"] },
|
||||
description: "Modifiers held during hover.",
|
||||
},
|
||||
settleMs: { type: "integer", description: "Hover settle delay." },
|
||||
timeoutMs: TIMEOUT_MS_PARAM,
|
||||
values: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Option values for select; at least one is required.",
|
||||
},
|
||||
key: { type: "string", description: "Key or combo for press." },
|
||||
holdMs: { type: "integer", description: "Key hold duration for press." },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "browser_tabs",
|
||||
description:
|
||||
"Manage tabs visible to an owned session. Actions: list, create, select, close, borrow, return. " +
|
||||
"select/close/borrow/return require tabId from list/create. Borrow moves a user tab into the " +
|
||||
"Agent Window; return it as soon as the task finishes.",
|
||||
actions: {
|
||||
list: "tabs.list",
|
||||
create: "tabs.create",
|
||||
select: "tabs.select",
|
||||
close: "tabs.close",
|
||||
borrow: "tabs.borrow",
|
||||
return: "tabs.return",
|
||||
},
|
||||
parameters: {
|
||||
session: SESSION_PARAM,
|
||||
tabId: TAB_ID_PARAM,
|
||||
scope: {
|
||||
type: "string",
|
||||
enum: ["user", "agent", "all"],
|
||||
description: "Tab scope for list.",
|
||||
},
|
||||
url: { type: "string", description: "Initial URL for create." },
|
||||
active: { type: "boolean", description: "Focus the created tab (default true)." },
|
||||
index: { type: "integer", description: "Insertion index for create." },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "browser_assist",
|
||||
description:
|
||||
"Display and human-assistance operations. Actions: resize, emulate, request-help. resize " +
|
||||
"requires width/height; emulate accepts device or width/height/mobile, or off alone; " +
|
||||
"request-help requires prompt and can wait for explicit completion criteria.",
|
||||
actions: {
|
||||
resize: "assist.resize",
|
||||
emulate: "assist.emulate",
|
||||
"request-help": "assist.request-help",
|
||||
},
|
||||
parameters: {
|
||||
session: SESSION_PARAM,
|
||||
tabId: TAB_ID_PARAM,
|
||||
width: { type: "integer", description: "Window/viewport width." },
|
||||
height: { type: "integer", description: "Window/viewport height." },
|
||||
device: { type: "string", enum: DEVICE_PRESETS, description: "Emulation device preset." },
|
||||
mobile: { type: "boolean", description: "Enable a mobile viewport with width/height." },
|
||||
off: { type: "boolean", description: "Clear emulation; use alone." },
|
||||
prompt: { type: "string", description: "Instructions shown to the user for request-help." },
|
||||
title: { type: "string", description: "Optional request-help title." },
|
||||
targets: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Refs/selectors highlighted for request-help.",
|
||||
},
|
||||
timeoutMs: TIMEOUT_MS_PARAM,
|
||||
completionCriteria: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
description: "Automatic completion detector for request-help.",
|
||||
properties: {
|
||||
any: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
urlContains: { type: "string" },
|
||||
urlMatches: { type: "string" },
|
||||
selectorExists: { type: "string" },
|
||||
selectorMissing: { type: "string" },
|
||||
textExists: { type: "string" },
|
||||
textMissing: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
all: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
urlContains: { type: "string" },
|
||||
urlMatches: { type: "string" },
|
||||
selectorExists: { type: "string" },
|
||||
selectorMissing: { type: "string" },
|
||||
textExists: { type: "string" },
|
||||
textMissing: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
stableForMs: { type: "integer" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function indexOperations(definitions: ToolDefinition[]): ReadonlyMap<string, ToolDefinition> {
|
||||
const indexed = new Map<string, ToolDefinition>();
|
||||
for (const definition of definitions) {
|
||||
if (indexed.has(definition.name)) {
|
||||
throw new Error(`duplicate browser operation definition: ${definition.name}`);
|
||||
}
|
||||
indexed.set(definition.name, definition);
|
||||
}
|
||||
|
||||
const routed = new Set(BROWSER_TOOL_SPECS.flatMap((spec) => Object.values(spec.actions)));
|
||||
const missing = [...routed].filter((name) => !indexed.has(name));
|
||||
const unreachable = [...indexed.keys()].filter((name) => !routed.has(name));
|
||||
if (missing.length > 0 || unreachable.length > 0) {
|
||||
throw new Error(
|
||||
[
|
||||
missing.length > 0 ? `missing operations: ${missing.join(", ")}` : undefined,
|
||||
unreachable.length > 0 ? `unreachable operations: ${unreachable.join(", ")}` : undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("; "),
|
||||
);
|
||||
}
|
||||
return indexed;
|
||||
}
|
||||
|
||||
/** Register the complete six-tool browser suite; returns its combined disposer. */
|
||||
export function registerBrowserTools(deps: ToolDeps): () => void {
|
||||
const definitions = indexOperations(createBrowserOperationDefinitions(deps));
|
||||
const disposers = BROWSER_TOOL_SPECS.map((spec) =>
|
||||
deps.ctx.tools.register(defineBrowserTool(spec, definitions)),
|
||||
).filter((dispose): dispose is () => void => typeof dispose === "function");
|
||||
return () => {
|
||||
for (const dispose of disposers.splice(0)) dispose();
|
||||
};
|
||||
}
|
||||
+1
-1
@@ -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 {
|
||||
+80
-23
@@ -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),
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
import type { Context } from "@deepseek-ai/cordis";
|
||||
import Schema from "@deepseek-ai/schemastery";
|
||||
import { armArchiveCleanup } from "./archive-cleanup";
|
||||
import { registerBrowserTools } from "./browser-tools";
|
||||
import { armLazyTools } from "./lazy-tools";
|
||||
import { ObservationService } from "./observation";
|
||||
import { registerObservationRoutes } from "./observation-http";
|
||||
@@ -20,7 +21,7 @@ import { KeyedExecutor } from "./queue";
|
||||
import { type BskRunner, createBskRunner } from "./runner";
|
||||
import { SessionRegistry } from "./sessions";
|
||||
import { armAgentScopedBskSkill, registerBskSkill } from "./skill";
|
||||
import { type PluginConfig, registerTools } from "./tools";
|
||||
import type { PluginConfig } from "./tools";
|
||||
|
||||
export const name = "@wxg-prc-cpg/browser-skill-dsh-plugin";
|
||||
export const inject = ["tools"];
|
||||
@@ -100,11 +101,9 @@ export function apply(
|
||||
// exact agent context at startup so DSH always loads the browser_* protocol
|
||||
// instructions; the shared CLI skill remains untouched for other agents.
|
||||
const disarmAgentSkill = armAgentScopedBskSkill(ctx);
|
||||
const removeSuite = resolved.lazyTools
|
||||
? armLazyTools(ctx, () =>
|
||||
registerTools({ ctx, runner, registry, config: resolved, observation, queue }),
|
||||
)
|
||||
: registerTools({ ctx, runner, registry, config: resolved, observation, queue });
|
||||
const registerSuite = () =>
|
||||
registerBrowserTools({ ctx, runner, registry, config: resolved, observation, queue });
|
||||
const removeSuite = resolved.lazyTools ? armLazyTools(ctx, registerSuite) : registerSuite();
|
||||
// Route registration rides ctx.inject: the webServer service may be provided
|
||||
// AFTER this plugin loads, and in headless compositions it never appears (the
|
||||
// callback simply never runs, leaving the rest of the plugin unaffected).
|
||||
@@ -133,7 +132,7 @@ export function apply(
|
||||
);
|
||||
|
||||
// Unload cleanup: kill in-flight children, then stop every session this
|
||||
// plugin OWNS (created via browser_session_start). Referenced or unknown
|
||||
// plugin OWNS (created via browser_session action=start). Referenced or unknown
|
||||
// sessions belonging to other programs on the shared daemon are never
|
||||
// touched; per-stop failures (already stopped externally, daemon restart)
|
||||
// are swallowed so one stale handle cannot abort the rest.
|
||||
@@ -154,6 +153,7 @@ export function apply(
|
||||
}
|
||||
|
||||
export { armArchiveCleanup, ownerSessionIds } from "./archive-cleanup";
|
||||
export { registerBrowserTools } from "./browser-tools";
|
||||
export type { ObservationEvent, ObservationOptions, SessionObservation } from "./observation";
|
||||
export { ObservationService } from "./observation";
|
||||
export { registerObservationRoutes } from "./observation-http";
|
||||
@@ -162,4 +162,3 @@ export type { BskRunner, BskRunResult, SpawnImpl } from "./runner";
|
||||
export { BskError, createBskRunner } from "./runner";
|
||||
export { SessionRegistry } from "./sessions";
|
||||
export type { PluginConfig, ToolDeps } from "./tools";
|
||||
export { registerTools } from "./tools";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Lazy tool-schema injection ("progressive disclosure", final stage): with
|
||||
* `lazyTools` on (the default), the 28 browser_* tool schemas stay OUT of the
|
||||
* `lazyTools` on (the default), the six browser tool schemas stay OUT of the
|
||||
* system prompt until the `browser-skill` skill has actually been invoked —
|
||||
* the skill catalog entry is the only advertisement. One successful
|
||||
* invocation (model tool call, or a user's `/browser-skill` gesture) reveals
|
||||
@@ -96,7 +96,7 @@ export function hasSuccessfulSkillInvocation(events: readonly SessionEventLike[]
|
||||
/**
|
||||
* Arm the lazy reveal. Returns a disposer tearing down listeners and — when
|
||||
* the reveal already happened — the tool suite itself.
|
||||
* @param registerSuite - registers the 28 browser_* tools, returns their disposer.
|
||||
* @param registerSuite - registers the six browser tools and returns their disposer.
|
||||
*/
|
||||
export function armLazyTools(ctx: Context, registerSuite: () => () => void): () => void {
|
||||
let suiteDisposer: (() => void) | undefined;
|
||||
|
||||
@@ -161,7 +161,7 @@ export class ObservationService {
|
||||
this.emit({ type: "upsert", session: { ...entry } });
|
||||
}
|
||||
|
||||
/** Register a fresh owned session (called from browser_session_start). */
|
||||
/** Register a fresh owned session (called from browser_session action=start). */
|
||||
addSession(sessionId: string, url?: string): void {
|
||||
if (!this.deps.options.enabled || this.disposed) return;
|
||||
const dshSessionIds = this.deps.registry.dshOwnersOf(sessionId);
|
||||
@@ -176,7 +176,7 @@ export class ObservationService {
|
||||
this.scheduleCapture(sessionId, 0);
|
||||
}
|
||||
|
||||
/** Drop a session (called from browser_session_stop). */
|
||||
/** Drop a session (called from browser_session action=stop). */
|
||||
removeSession(sessionId: string): void {
|
||||
this.cancelCapture(sessionId);
|
||||
this.foregroundDepth.delete(sessionId);
|
||||
@@ -276,7 +276,7 @@ export class ObservationService {
|
||||
|
||||
/**
|
||||
* Stop one owned session and close its Agent Window (the overlay's stop
|
||||
* button — same end state as `browser_session_stop`). Never waits behind a
|
||||
* button — same end state as `browser_session` action=stop). Never waits behind a
|
||||
* hung in-flight command: tool children are killed first so the session's
|
||||
* keyed queue drains immediately, and no further captures queue up. A
|
||||
* session the daemon already forgot stops idempotently — the goal state
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ToolDefinition, ToolResult, ToolRunContext } from "@deepseek-ai/dsh-tools";
|
||||
import type { ToolDeps } from "./tools";
|
||||
|
||||
export type ToolRegistrar = (definition: ToolDefinition) => () => void;
|
||||
export type ToolRegistrar = (definition: ToolDefinition) => void;
|
||||
|
||||
type TerminalPresentation = { card: "terminal"; output: string; exitCode: number } | undefined;
|
||||
|
||||
|
||||
@@ -23,10 +23,10 @@ export function registerPhaseOneInteractionTools(
|
||||
|
||||
register(
|
||||
defineTool({
|
||||
name: "browser_hover",
|
||||
name: "interact.hover",
|
||||
description:
|
||||
"Move the mouse over a snapshot ref or CSS selector to reveal hover-triggered UI. " +
|
||||
"Run browser_observe or browser_snapshot again afterwards to discover newly visible refs.",
|
||||
"Run browser_inspect action=observe or snapshot afterwards to discover newly visible refs.",
|
||||
parameters: {
|
||||
target: {
|
||||
type: "string",
|
||||
@@ -68,7 +68,7 @@ export function registerPhaseOneInteractionTools(
|
||||
requireNonEmpty(args.target, "target");
|
||||
requirePositive(args.timeoutMs, "timeoutMs");
|
||||
requirePositive(args.settleMs, "settleMs");
|
||||
const sessionId = registry.resolve(args.session, "browser_hover");
|
||||
const sessionId = registry.resolve(args.session, "browser_interact(action=hover)");
|
||||
const cmdArgs = ["hover", "--session", sessionId];
|
||||
appendTabId(cmdArgs, args.tabId);
|
||||
if (args.modifiers !== undefined && args.modifiers.length > 0) {
|
||||
@@ -102,7 +102,7 @@ export function registerPhaseOneInteractionTools(
|
||||
|
||||
register(
|
||||
defineTool({
|
||||
name: "browser_select",
|
||||
name: "interact.select",
|
||||
description:
|
||||
"Set a select element's option values. Pass one value for a normal select or multiple " +
|
||||
"values for a multi-select; values replace the current selection.",
|
||||
@@ -155,7 +155,7 @@ export function registerPhaseOneInteractionTools(
|
||||
requireNonEmpty(args.target, "target");
|
||||
requirePositive(args.timeoutMs, "timeoutMs");
|
||||
if (args.values.length === 0) throw new Error("values must contain at least one option");
|
||||
const sessionId = registry.resolve(args.session, "browser_select");
|
||||
const sessionId = registry.resolve(args.session, "browser_interact(action=select)");
|
||||
const cmdArgs = ["select", "--session", sessionId];
|
||||
appendTabId(cmdArgs, args.tabId);
|
||||
for (const value of args.values) cmdArgs.push("--value", value);
|
||||
|
||||
@@ -51,7 +51,7 @@ export function registerPhaseOneNavigationTools(
|
||||
const { registry } = deps;
|
||||
|
||||
const registerHistory = (direction: "back" | "forward") => {
|
||||
const toolName = `browser_navigate_${direction}` as const;
|
||||
const toolName = `page.${direction}` as const;
|
||||
const command = `navigate-${direction}`;
|
||||
register(
|
||||
defineTool({
|
||||
@@ -106,7 +106,7 @@ export function registerPhaseOneNavigationTools(
|
||||
|
||||
register(
|
||||
defineTool({
|
||||
name: "browser_reload",
|
||||
name: "page.reload",
|
||||
description:
|
||||
"Reload the active Agent Window tab and wait for a lifecycle phase. Set hard=true to " +
|
||||
"bypass the HTTP cache.",
|
||||
@@ -132,7 +132,7 @@ export function registerPhaseOneNavigationTools(
|
||||
},
|
||||
async execute(args, exec) {
|
||||
requirePositive(args.timeoutMs, "timeoutMs");
|
||||
const sessionId = registry.resolve(args.session, "browser_reload");
|
||||
const sessionId = registry.resolve(args.session, "browser_page(action=reload)");
|
||||
const cmdArgs = ["reload", "--session", sessionId];
|
||||
appendTabId(cmdArgs, args.tabId);
|
||||
appendWaitOptions(cmdArgs, args.waitUntil, args.timeoutMs);
|
||||
@@ -163,7 +163,7 @@ export function registerPhaseOneNavigationTools(
|
||||
|
||||
register(
|
||||
defineTool({
|
||||
name: "browser_wait_for_navigation",
|
||||
name: "page.wait",
|
||||
description:
|
||||
"Wait for a page lifecycle event on the active Agent Window tab. Use after an action " +
|
||||
"that may navigate when the action result itself does not wait for navigation.",
|
||||
@@ -195,7 +195,7 @@ export function registerPhaseOneNavigationTools(
|
||||
},
|
||||
async execute(args, exec) {
|
||||
requirePositive(args.timeoutMs, "timeoutMs");
|
||||
const sessionId = registry.resolve(args.session, "browser_wait_for_navigation");
|
||||
const sessionId = registry.resolve(args.session, "browser_page(action=wait)");
|
||||
const timeoutMs = args.timeoutMs ?? 30_000;
|
||||
const cmdArgs = ["wait-for-navigation", "--session", sessionId];
|
||||
appendTabId(cmdArgs, args.tabId);
|
||||
|
||||
@@ -143,7 +143,7 @@ export function registerPhaseOneSupportTools(
|
||||
|
||||
register(
|
||||
defineTool({
|
||||
name: "browser_request_help",
|
||||
name: "assist.request-help",
|
||||
description:
|
||||
"Pause browser automation and ask the user to complete an in-page step such as login, " +
|
||||
"captcha, OTP, or confirmation. After continued/completed, observe again before using refs.",
|
||||
@@ -233,7 +233,7 @@ export function registerPhaseOneSupportTools(
|
||||
if (criteria?.stableForMs !== undefined && criteria.stableForMs < 0) {
|
||||
throw new Error("completionCriteria.stableForMs must be zero or greater");
|
||||
}
|
||||
const sessionId = registry.resolve(args.session, "browser_request_help");
|
||||
const sessionId = registry.resolve(args.session, "browser_assist(action=request-help)");
|
||||
const timeoutMs = args.timeoutMs ?? 300_000;
|
||||
const cmdArgs = [
|
||||
"request-help",
|
||||
@@ -285,10 +285,10 @@ export function registerPhaseOneSupportTools(
|
||||
|
||||
register(
|
||||
defineTool({
|
||||
name: "browser_get_html",
|
||||
name: "inspect.html",
|
||||
description:
|
||||
"Read raw DOM HTML from the active Agent Window tab, optionally scoped to a fresh " +
|
||||
"snapshot ref. Use only when browser_observe/browser_snapshot cannot answer the question.",
|
||||
"snapshot ref. Use only when browser_inspect observe/snapshot cannot answer the question.",
|
||||
parameters: {
|
||||
session: SESSION_PARAM,
|
||||
tabId: TAB_ID_PARAM,
|
||||
@@ -326,7 +326,7 @@ export function registerPhaseOneSupportTools(
|
||||
if (!isSnapshotRef(args.ref)) throw new Error("ref must be a snapshot ref such as @e3");
|
||||
}
|
||||
requirePositive(args.maxBytes, "maxBytes");
|
||||
const sessionId = registry.resolve(args.session, "browser_get_html");
|
||||
const sessionId = registry.resolve(args.session, "browser_inspect(action=html)");
|
||||
const cmdArgs = ["get-html", "--session", sessionId];
|
||||
appendTabId(cmdArgs, args.tabId);
|
||||
if (args.ref !== undefined) cmdArgs.push("--ref", args.ref);
|
||||
@@ -361,7 +361,7 @@ export function registerPhaseOneSupportTools(
|
||||
|
||||
register(
|
||||
defineTool({
|
||||
name: "browser_console",
|
||||
name: "inspect.console",
|
||||
description:
|
||||
"Read buffered console messages, browser log entries, and JavaScript exceptions from a tab. " +
|
||||
"This is read-only and does not evaluate JavaScript.",
|
||||
@@ -448,7 +448,7 @@ export function registerPhaseOneSupportTools(
|
||||
isConcurrencySafe: () => true,
|
||||
async execute(args, exec) {
|
||||
validateDebugArgs(args);
|
||||
const sessionId = registry.resolve(args.session, "browser_console");
|
||||
const sessionId = registry.resolve(args.session, "browser_inspect(action=console)");
|
||||
const cmdArgs = ["console", "--session", sessionId];
|
||||
appendDebugOptions(cmdArgs, args);
|
||||
if (args.includeStack === true) cmdArgs.push("--include-stack");
|
||||
@@ -493,7 +493,7 @@ export function registerPhaseOneSupportTools(
|
||||
|
||||
register(
|
||||
defineTool({
|
||||
name: "browser_network",
|
||||
name: "inspect.network",
|
||||
description:
|
||||
"Read buffered network responses and failures from a tab. Returns metadata only; request " +
|
||||
"and response headers and bodies are not captured.",
|
||||
@@ -552,7 +552,7 @@ export function registerPhaseOneSupportTools(
|
||||
isConcurrencySafe: () => true,
|
||||
async execute(args, exec) {
|
||||
validateDebugArgs(args);
|
||||
const sessionId = registry.resolve(args.session, "browser_network");
|
||||
const sessionId = registry.resolve(args.session, "browser_inspect(action=network)");
|
||||
const cmdArgs = ["network", "--session", sessionId];
|
||||
appendDebugOptions(cmdArgs, args);
|
||||
const reply = (await runtime.run(exec, cmdArgs, "network", sessionId)) as {
|
||||
@@ -592,7 +592,7 @@ export function registerPhaseOneSupportTools(
|
||||
|
||||
register(
|
||||
defineTool({
|
||||
name: "browser_window_resize",
|
||||
name: "assist.resize",
|
||||
description: "Resize the owned session's Agent Window using outer CSS-pixel dimensions.",
|
||||
parameters: {
|
||||
width: {
|
||||
@@ -629,7 +629,7 @@ export function registerPhaseOneSupportTools(
|
||||
if (args.width < 100 || args.width > 7680 || args.height < 100 || args.height > 7680) {
|
||||
throw new Error("width and height must each be in the range 100..=7680");
|
||||
}
|
||||
const sessionId = registry.resolve(args.session, "browser_window_resize");
|
||||
const sessionId = registry.resolve(args.session, "browser_assist(action=resize)");
|
||||
const reply = (await runtime.run(
|
||||
exec,
|
||||
[
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { ToolDeps } from "./tools";
|
||||
const TAB_ID_REQUIRED = {
|
||||
type: "integer",
|
||||
required: true,
|
||||
description: "Chrome tab id returned by browser_tab_list or browser_tab_create.",
|
||||
description: "Chrome tab id returned by browser_tabs list or create.",
|
||||
} as const;
|
||||
|
||||
interface RawTabInfo {
|
||||
@@ -29,7 +29,7 @@ export function registerPhaseOneTabTools(
|
||||
|
||||
register(
|
||||
defineTool({
|
||||
name: "browser_tab_list",
|
||||
name: "tabs.list",
|
||||
description:
|
||||
"List tabs visible to an owned browser session. Other sessions' Agent Windows remain " +
|
||||
"hidden; use scope=user to find a user tab before borrowing it.",
|
||||
@@ -83,7 +83,7 @@ export function registerPhaseOneTabTools(
|
||||
},
|
||||
isConcurrencySafe: () => true,
|
||||
async execute(args, exec) {
|
||||
const sessionId = registry.resolve(args.session, "browser_tab_list");
|
||||
const sessionId = registry.resolve(args.session, "browser_tabs(action=list)");
|
||||
const cmdArgs = ["tab", "list", "--session", sessionId];
|
||||
if (args.scope !== undefined) cmdArgs.push("--scope", args.scope);
|
||||
const reply = (await runtime.run(exec, cmdArgs, "tab list", sessionId)) as {
|
||||
@@ -118,7 +118,7 @@ export function registerPhaseOneTabTools(
|
||||
|
||||
register(
|
||||
defineTool({
|
||||
name: "browser_tab_create",
|
||||
name: "tabs.create",
|
||||
description:
|
||||
"Create a tab inside the owned session's Agent Window. The tab is focused by default; " +
|
||||
"set active=false to open it in the background.",
|
||||
@@ -148,7 +148,7 @@ export function registerPhaseOneTabTools(
|
||||
},
|
||||
async execute(args, exec) {
|
||||
if (args.url !== undefined) requireNonEmpty(args.url, "url");
|
||||
const sessionId = registry.resolve(args.session, "browser_tab_create");
|
||||
const sessionId = registry.resolve(args.session, "browser_tabs(action=create)");
|
||||
const cmdArgs = ["tab", "create", "--session", sessionId];
|
||||
if (args.url !== undefined) cmdArgs.push("--url", args.url);
|
||||
if (args.active === false) cmdArgs.push("--no-active");
|
||||
@@ -185,7 +185,7 @@ export function registerPhaseOneTabTools(
|
||||
|
||||
register(
|
||||
defineTool({
|
||||
name: "browser_tab_close",
|
||||
name: "tabs.close",
|
||||
description: "Close a tab in the owned session's Agent Window.",
|
||||
parameters: { tabId: TAB_ID_REQUIRED, session: SESSION_PARAM },
|
||||
output: {
|
||||
@@ -202,7 +202,7 @@ export function registerPhaseOneTabTools(
|
||||
],
|
||||
},
|
||||
async execute(args, exec) {
|
||||
const sessionId = registry.resolve(args.session, "browser_tab_close");
|
||||
const sessionId = registry.resolve(args.session, "browser_tabs(action=close)");
|
||||
const reply = (await runtime.run(
|
||||
exec,
|
||||
["tab", "close", String(args.tabId), "--session", sessionId],
|
||||
@@ -228,7 +228,7 @@ export function registerPhaseOneTabTools(
|
||||
|
||||
register(
|
||||
defineTool({
|
||||
name: "browser_tab_select",
|
||||
name: "tabs.select",
|
||||
description: "Focus a tab in the owned session's Agent Window.",
|
||||
parameters: { tabId: TAB_ID_REQUIRED, session: SESSION_PARAM },
|
||||
output: {
|
||||
@@ -249,7 +249,7 @@ export function registerPhaseOneTabTools(
|
||||
],
|
||||
},
|
||||
async execute(args, exec) {
|
||||
const sessionId = registry.resolve(args.session, "browser_tab_select");
|
||||
const sessionId = registry.resolve(args.session, "browser_tabs(action=select)");
|
||||
const reply = (await runtime.run(
|
||||
exec,
|
||||
["tab", "select", String(args.tabId), "--session", sessionId],
|
||||
@@ -275,10 +275,10 @@ export function registerPhaseOneTabTools(
|
||||
|
||||
register(
|
||||
defineTool({
|
||||
name: "browser_tab_borrow",
|
||||
name: "tabs.borrow",
|
||||
description:
|
||||
"Move a user-window tab into the owned session's Agent Window for controlled interaction. " +
|
||||
"Return it with browser_tab_return when finished; stopping the session also auto-returns it.",
|
||||
"Return it with browser_tabs action=return; stopping the session also auto-returns it.",
|
||||
parameters: { tabId: TAB_ID_REQUIRED, session: SESSION_PARAM },
|
||||
output: {
|
||||
schema: {
|
||||
@@ -302,7 +302,7 @@ export function registerPhaseOneTabTools(
|
||||
],
|
||||
},
|
||||
async execute(args, exec) {
|
||||
const sessionId = registry.resolve(args.session, "browser_tab_borrow");
|
||||
const sessionId = registry.resolve(args.session, "browser_tabs(action=borrow)");
|
||||
const reply = (await runtime.run(
|
||||
exec,
|
||||
["tab", "borrow", String(args.tabId), "--session", sessionId],
|
||||
@@ -339,7 +339,7 @@ export function registerPhaseOneTabTools(
|
||||
|
||||
register(
|
||||
defineTool({
|
||||
name: "browser_tab_return",
|
||||
name: "tabs.return",
|
||||
description: "Return a borrowed tab to its original user window and tab-strip position.",
|
||||
parameters: { tabId: TAB_ID_REQUIRED, session: SESSION_PARAM },
|
||||
output: {
|
||||
@@ -365,7 +365,7 @@ export function registerPhaseOneTabTools(
|
||||
],
|
||||
},
|
||||
async execute(args, exec) {
|
||||
const sessionId = registry.resolve(args.session, "browser_tab_return");
|
||||
const sessionId = registry.resolve(args.session, "browser_tabs(action=return)");
|
||||
const reply = (await runtime.run(
|
||||
exec,
|
||||
["tab", "return", String(args.tabId), "--session", sessionId],
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*
|
||||
* Strict ownership boundary (the bsk daemon may be shared with other agents,
|
||||
* terminals, or dsh instances): the registry ONLY ever holds sessions created
|
||||
* by this plugin's browser_session_start. Tools cannot see or act on any
|
||||
* by this plugin's browser_session start action. Tools cannot see or act on any
|
||||
* other session — an explicit `session` argument naming a foreign id is an
|
||||
* error, the list tool shows owned sessions only, and stop/unload cleanup
|
||||
* can never touch a session this plugin did not create.
|
||||
@@ -48,7 +48,7 @@ export class SessionRegistry {
|
||||
if (this.sessions.size + this.pendingStarts >= this.maxSessions) {
|
||||
throw new Error(
|
||||
`session limit reached (${this.maxSessions} concurrent sessions); ` +
|
||||
"stop one with browser_session_stop before starting another",
|
||||
"stop one with browser_session action=stop before starting another",
|
||||
);
|
||||
}
|
||||
this.pendingStarts += 1;
|
||||
@@ -69,7 +69,7 @@ export class SessionRegistry {
|
||||
if (!this.sessions.has(session.sessionId) && this.sessions.size >= this.maxSessions) {
|
||||
throw new Error(
|
||||
`session limit reached (${this.maxSessions} concurrent sessions); ` +
|
||||
"stop one with browser_session_stop before starting another",
|
||||
"stop one with browser_session action=stop before starting another",
|
||||
);
|
||||
}
|
||||
this.sessions.set(session.sessionId, { ...session, owned: true });
|
||||
@@ -149,7 +149,7 @@ export class SessionRegistry {
|
||||
private foreignError(sessionId: string, toolName: string): Error {
|
||||
return new Error(
|
||||
`${toolName}: session "${sessionId}" does not belong to this plugin — only sessions ` +
|
||||
"created by browser_session_start are visible and operable here",
|
||||
"created by browser_session action=start are visible and operable here",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ export class SessionRegistry {
|
||||
const current = this.current();
|
||||
if (current === undefined) {
|
||||
throw new Error(
|
||||
`${toolName} needs a session but none is active — start one with browser_session_start`,
|
||||
`${toolName} needs a session but none is active — use browser_session action=start`,
|
||||
);
|
||||
}
|
||||
this.touch(current);
|
||||
@@ -184,10 +184,11 @@ export class SessionRegistry {
|
||||
explicit !== undefined && explicit.trim().length > 0 ? explicit : this.current();
|
||||
if (candidate === undefined) {
|
||||
throw new Error(
|
||||
"browser_session_stop needs a session but none is active — start one with browser_session_start",
|
||||
"browser_session action=stop needs a session but none is active — use action=start first",
|
||||
);
|
||||
}
|
||||
if (!this.isOwned(candidate)) throw this.foreignError(candidate, "browser_session_stop");
|
||||
if (!this.isOwned(candidate))
|
||||
throw this.foreignError(candidate, "browser_session(action=stop)");
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
export const SESSION_PARAM = {
|
||||
type: "string",
|
||||
description:
|
||||
"bsk session id to act on; must be one created by this plugin's browser_session_start. " +
|
||||
"bsk session id to act on; must be one created by browser_session with action=start. " +
|
||||
"Omit to use the current session (the one most recently started or used).",
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
/**
|
||||
* Model-facing tool definitions. Each tool maps to one `bsk <cmd> --json`
|
||||
* invocation: spawn via the runner, parse the JSON envelope, and return a
|
||||
* canonical value whose `output.render` produces the model-facing text.
|
||||
* UI cards reuse the terminal style: the pending card is the command line,
|
||||
* the completed card carries the output.
|
||||
* Internal browser operation definitions shared by the six model-facing tools.
|
||||
* These definitions are never registered with `ctx.tools`: they provide the
|
||||
* action-level validation, execution, rendering, and UI presentation reused by
|
||||
* the domain tools without exposing one schema per operation to the model.
|
||||
*/
|
||||
|
||||
import { readFile, unlink } from "node:fs/promises";
|
||||
@@ -158,22 +157,15 @@ function abortError(): Error {
|
||||
return error;
|
||||
}
|
||||
|
||||
/** Register the full browser tool suite; returns the combined unregister disposer. */
|
||||
export function registerTools(deps: ToolDeps): () => void {
|
||||
const { ctx, registry } = deps;
|
||||
// Track every registration without touching the cordis context itself
|
||||
// (contexts are fiber-owned; the lazy path registers from a listener fiber).
|
||||
const disposers: (() => void)[] = [];
|
||||
const register = (definition: ToolDefinition): (() => void) => {
|
||||
const dispose = ctx.tools.register(definition);
|
||||
// Test doubles occasionally return the registry map instead of a disposer.
|
||||
if (typeof dispose === "function") disposers.push(dispose);
|
||||
return dispose;
|
||||
};
|
||||
type DefinitionRegistrar = (definition: ToolDefinition) => void;
|
||||
|
||||
/** Define the private action handlers through an injected collector. */
|
||||
function defineBrowserOperations(deps: ToolDeps, register: DefinitionRegistrar): void {
|
||||
const { registry } = deps;
|
||||
|
||||
register(
|
||||
defineTool({
|
||||
name: "browser_session_start",
|
||||
name: "session.start",
|
||||
description:
|
||||
"Start a new browser session: opens an Agent Window in the connected browser and returns " +
|
||||
"its session id. The new session becomes the current session for subsequent browser_* calls. " +
|
||||
@@ -314,10 +306,10 @@ export function registerTools(deps: ToolDeps): () => void {
|
||||
|
||||
register(
|
||||
defineTool({
|
||||
name: "browser_session_stop",
|
||||
name: "session.stop",
|
||||
description:
|
||||
"Stop a browser session and close its Agent Window. Stops the given session, or the " +
|
||||
"current session when `session` is omitted. Only sessions created by browser_session_start " +
|
||||
"current session when `session` is omitted. Only plugin-created sessions " +
|
||||
"can be stopped — sessions owned by other programs sharing the bsk daemon are refused.",
|
||||
parameters: { session: SESSION_PARAM },
|
||||
output: {
|
||||
@@ -354,7 +346,7 @@ export function registerTools(deps: ToolDeps): () => void {
|
||||
|
||||
register(
|
||||
defineTool({
|
||||
name: "browser_session_list",
|
||||
name: "session.list",
|
||||
description:
|
||||
"List the browser sessions created by this plugin. Sessions owned by other programs on the " +
|
||||
"shared bsk daemon are not visible here. The session marked `current` is the one browser_* " +
|
||||
@@ -419,7 +411,7 @@ export function registerTools(deps: ToolDeps): () => void {
|
||||
|
||||
register(
|
||||
defineTool({
|
||||
name: "browser_navigate",
|
||||
name: "page.navigate",
|
||||
description:
|
||||
"Navigate the session's active tab to a URL and wait for a page lifecycle phase " +
|
||||
"(default: load). Returns the final URL after redirects.",
|
||||
@@ -461,7 +453,7 @@ export function registerTools(deps: ToolDeps): () => void {
|
||||
},
|
||||
async execute(args, exec) {
|
||||
if (args.url.trim().length === 0) throw new Error("url must be a non-empty string");
|
||||
const sessionId = registry.resolve(args.session, "browser_navigate");
|
||||
const sessionId = registry.resolve(args.session, "browser_page(action=navigate)");
|
||||
const cmdArgs = ["navigate", "--session", sessionId];
|
||||
if (args.waitUntil !== undefined) cmdArgs.push("--wait-until", args.waitUntil);
|
||||
if (args.timeoutMs !== undefined) cmdArgs.push("--timeout", `${args.timeoutMs}ms`);
|
||||
@@ -492,12 +484,12 @@ export function registerTools(deps: ToolDeps): () => void {
|
||||
|
||||
const registerObservationTool = (kind: "snapshot" | "observe") => {
|
||||
const isSnapshot = kind === "snapshot";
|
||||
const name = isSnapshot ? "browser_snapshot" : "browser_observe";
|
||||
const name = isSnapshot ? "inspect.snapshot" : "inspect.observe";
|
||||
const description = isSnapshot
|
||||
? "Capture an indented aria-tree snapshot of the session's active tab. Interactive elements " +
|
||||
"carry @eN refs for browser_click / browser_fill. Prefer browser_observe for a richer semantic view."
|
||||
"carry @eN refs for browser_interact actions. Prefer browser_inspect action=observe for a richer semantic view."
|
||||
: "Produce a semantic VOM observation of the session's active tab (roles, states, perception " +
|
||||
"probes) with @eN refs for browser_click / browser_fill. Read-only: never submits input.";
|
||||
"probes) with @eN refs for browser_interact actions. Read-only: never submits input.";
|
||||
register(
|
||||
defineTool({
|
||||
name,
|
||||
@@ -566,10 +558,10 @@ export function registerTools(deps: ToolDeps): () => void {
|
||||
|
||||
register(
|
||||
defineTool({
|
||||
name: "browser_click",
|
||||
name: "interact.click",
|
||||
description:
|
||||
"Click an element in the session's active tab. Target is a snapshot ref (@e3) from the " +
|
||||
"last browser_snapshot/browser_observe, or a CSS selector.",
|
||||
"last browser_inspect observation, or a CSS selector.",
|
||||
parameters: {
|
||||
target: {
|
||||
type: "string",
|
||||
@@ -607,7 +599,7 @@ export function registerTools(deps: ToolDeps): () => void {
|
||||
},
|
||||
async execute(args, exec) {
|
||||
if (args.target.trim().length === 0) throw new Error("target must be a non-empty string");
|
||||
const sessionId = registry.resolve(args.session, "browser_click");
|
||||
const sessionId = registry.resolve(args.session, "browser_interact(action=click)");
|
||||
const cmdArgs = ["click", "--session", sessionId];
|
||||
if (args.button !== undefined) cmdArgs.push("--button", args.button);
|
||||
if (args.clickCount !== undefined) cmdArgs.push("--click-count", String(args.clickCount));
|
||||
@@ -630,7 +622,7 @@ export function registerTools(deps: ToolDeps): () => void {
|
||||
|
||||
register(
|
||||
defineTool({
|
||||
name: "browser_fill",
|
||||
name: "interact.fill",
|
||||
description:
|
||||
"Fill an input / textarea / contenteditable element, clearing it first by default. " +
|
||||
"Target is a snapshot ref (@e3) or a CSS selector.",
|
||||
@@ -666,7 +658,7 @@ export function registerTools(deps: ToolDeps): () => void {
|
||||
},
|
||||
async execute(args, exec) {
|
||||
if (args.target.trim().length === 0) throw new Error("target must be a non-empty string");
|
||||
const sessionId = registry.resolve(args.session, "browser_fill");
|
||||
const sessionId = registry.resolve(args.session, "browser_interact(action=fill)");
|
||||
const cmdArgs = ["fill", "--session", sessionId, "--value", args.value];
|
||||
if (args.noClear === true) cmdArgs.push("--no-clear");
|
||||
cmdArgs.push(args.target);
|
||||
@@ -687,7 +679,7 @@ export function registerTools(deps: ToolDeps): () => void {
|
||||
|
||||
register(
|
||||
defineTool({
|
||||
name: "browser_press",
|
||||
name: "interact.press",
|
||||
description:
|
||||
"Dispatch a keyboard key or combo (Enter, Escape, ArrowDown, Ctrl+A, …) to the session's " +
|
||||
"active tab, optionally focusing a target element first.",
|
||||
@@ -726,7 +718,7 @@ export function registerTools(deps: ToolDeps): () => void {
|
||||
},
|
||||
async execute(args, exec) {
|
||||
if (args.key.trim().length === 0) throw new Error("key must be a non-empty string");
|
||||
const sessionId = registry.resolve(args.session, "browser_press");
|
||||
const sessionId = registry.resolve(args.session, "browser_interact(action=press)");
|
||||
const cmdArgs = ["press", "--session", sessionId];
|
||||
if (args.target !== undefined) {
|
||||
if (args.target.trim().length === 0) throw new Error("target must be a non-empty string");
|
||||
@@ -757,7 +749,7 @@ export function registerTools(deps: ToolDeps): () => void {
|
||||
|
||||
register(
|
||||
defineTool({
|
||||
name: "browser_screenshot",
|
||||
name: "inspect.screenshot",
|
||||
description:
|
||||
"Capture a PNG screenshot of the session's active tab, or crop to a snapshot ref element. " +
|
||||
"Returns the image itself when the deployment supports image input, otherwise a file path.",
|
||||
@@ -822,7 +814,7 @@ export function registerTools(deps: ToolDeps): () => void {
|
||||
},
|
||||
isConcurrencySafe: () => true,
|
||||
async execute(args, exec) {
|
||||
const sessionId = registry.resolve(args.session, "browser_screenshot");
|
||||
const sessionId = registry.resolve(args.session, "browser_inspect(action=screenshot)");
|
||||
const outPath = join(
|
||||
tmpdir(),
|
||||
`bsk-screenshot-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.png`,
|
||||
@@ -887,7 +879,7 @@ export function registerTools(deps: ToolDeps): () => void {
|
||||
|
||||
register(
|
||||
defineTool({
|
||||
name: "browser_emulate",
|
||||
name: "assist.emulate",
|
||||
description:
|
||||
"Apply (or clear) mobile device emulation on the session's active tab: viewport metrics, " +
|
||||
"user agent, and touch. Overrides are per-tab and not inherited by new tabs.",
|
||||
@@ -928,7 +920,7 @@ export function registerTools(deps: ToolDeps): () => void {
|
||||
],
|
||||
},
|
||||
async execute(args, exec) {
|
||||
const sessionId = registry.resolve(args.session, "browser_emulate");
|
||||
const sessionId = registry.resolve(args.session, "browser_assist(action=emulate)");
|
||||
if (args.off === true) {
|
||||
if (args.device !== undefined || args.width !== undefined || args.height !== undefined) {
|
||||
throw new Error("off is mutually exclusive with device/width/height");
|
||||
@@ -977,7 +969,17 @@ export function registerTools(deps: ToolDeps): () => void {
|
||||
commandLine: (args) => cmdline(deps, args),
|
||||
presentTerminalResult,
|
||||
});
|
||||
return () => {
|
||||
for (const dispose of disposers.splice(0)) dispose();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the private action handlers without publishing them to the model. The
|
||||
* six browser tools dispatch through these handlers, preserving validation,
|
||||
* rendering, cancellation, ownership, UI presentation, and attachments.
|
||||
*/
|
||||
export function createBrowserOperationDefinitions(deps: ToolDeps): ToolDefinition[] {
|
||||
const definitions: ToolDefinition[] = [];
|
||||
defineBrowserOperations(deps, (definition) => {
|
||||
definitions.push(definition);
|
||||
});
|
||||
return definitions;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user