From 5147c945e1893ee1c1a04e585952ee79f49b8c19 Mon Sep 17 00:00:00 2001 From: Zhang GH Date: Sun, 20 Sep 2026 17:10:35 +0800 Subject: [PATCH 1/2] fix: bind profile-specific tasks to extension instances --- README.md | 3 + README.zh-CN.md | 3 + apps/extension/src/entrypoints/popup/App.tsx | 2 + .../popup/profile-instructions.test.tsx | 76 +++++++++++++++++++ .../popup/profile-instructions.tsx | 61 +++++++++++++++ crates/bsk-cli/skill/SKILL.md | 30 +++++++- crates/bsk-cli/src/cli/session.rs | 4 +- docs/browser-profiles.md | 49 ++++++++++++ .../i18n/src/locales/en-US/extension.json | 8 +- .../i18n/src/locales/ko-KR/extension.json | 8 +- .../i18n/src/locales/zh-CN/extension.json | 8 +- skill/SKILL.md | 30 +++++++- 12 files changed, 269 insertions(+), 13 deletions(-) create mode 100644 apps/extension/src/entrypoints/popup/profile-instructions.test.tsx create mode 100644 apps/extension/src/entrypoints/popup/profile-instructions.tsx create mode 100644 docs/browser-profiles.md diff --git a/README.md b/README.md index e8d471b5..a8efbbf4 100644 --- a/README.md +++ b/README.md @@ -248,6 +248,9 @@ and finished help requests are not reopened. Allowing help makes `request-help` not require every browser action to ask for permission. Task authorization and host approvals still apply. Start tasks with `bsk session start`; add `--no-focus` to avoid focusing the Agent Window. +For a specific Chrome profile, use **Copy profile instructions** in that profile's extension +popup and send them to your agent. This pins each new session to its instance with `--browser`, +even when only one browser is online. See [browser profile selection](docs/browser-profiles.md). For unattended operation, turn off the corresponding settings in the extension. `--unattended`, `tab borrow --no-confirm`, and `BSK_REQUEST_HELP=off` remain accepted for compatibility but are deprecated and cannot override the switches. The CLI logs a notice when these inputs are used; diff --git a/README.zh-CN.md b/README.zh-CN.md index 2679b361..0da92ee3 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -213,6 +213,9 @@ bsk update --yes 允许人工协助意味着 `request-help` 可用,不代表每个浏览器操作都必须先请求许可;任务授权和宿主审批仍然有效。 正常使用 `bsk session start`;需要后台打开 Agent Window 时添加 `--no-focus`。 +需要指定 Chrome Profile 时,在目标 Profile 的扩展弹窗中点击“复制此 Profile 的指令”, +再发给 Agent。指令通过 `--browser` 为每个新会话固定实例,即使只有一个浏览器在线也不省略。 +详见[浏览器 Profile 选择](docs/browser-profiles.md)。 无人值守由用户在插件中关闭相应开关。`--unattended`、`tab borrow --no-confirm`、 `BSK_REQUEST_HELP=off` 保留兼容识别,但已弃用,不能覆盖插件开关。CLI 使用这些输入时会输出说明, Daemon 也会为自身继承的旧环境设置记录说明。原先只依靠这些输入避免等待的脚本,现在需要遵循浏览器设置。 diff --git a/apps/extension/src/entrypoints/popup/App.tsx b/apps/extension/src/entrypoints/popup/App.tsx index 08cab6fd..d2451963 100644 --- a/apps/extension/src/entrypoints/popup/App.tsx +++ b/apps/extension/src/entrypoints/popup/App.tsx @@ -11,6 +11,7 @@ import { ConnectionStatusIndicator } from "./connection-status-indicator"; import { POPUP_FEATURES, type PopupView } from "./features"; import { InteractionSettings } from "./interaction-settings"; import { LongScreenshot } from "./long-screenshot"; +import { ProfileInstructions } from "./profile-instructions"; import { SettingInfo } from "./setting-info"; import { Switch } from "./switch"; import { type PopupStatusState, useConnectionState } from "./use-connection-state"; @@ -232,6 +233,7 @@ export function App() { connectionEnabled={snapshot.connectionEnabled} disconnected={isDisconnected && !snapshot.lastError} /> +
{ + beforeEach(async () => { + await i18n.changeLanguage("en-US"); + Object.defineProperty(navigator, "clipboard", { + configurable: true, + value: { writeText: vi.fn().mockResolvedValue(undefined) }, + }); + }); + afterEach(async () => { + cleanup(); + await i18n.changeLanguage("zh-CN"); + vi.restoreAllMocks(); + }); + it("copies an instruction that pins every new session to this instance", async () => { + render(); + fireEvent.click(screen.getByRole("button", { name: "Copy profile instructions" })); + const text = vi.mocked(navigator.clipboard.writeText).mock.calls[0]?.[0]; + expect(text).toContain("bsk session start --browser a1234567 --json"); + expect(text).toContain("every new session for this task"); + expect(text).toContain("Do not omit --browser or switch to another instance"); + expect(await screen.findByRole("status")).toBeTruthy(); + }); + it.each([ + { instanceId: "a1234567", connected: false }, + { instanceId: "", connected: true }, + ])("does not copy an unavailable target: %j", (props) => { + render(); + const button = screen.getByRole("button", { name: "Copy profile instructions" }); + expect(button.hasAttribute("disabled")).toBe(true); + fireEvent.click(button); + expect(navigator.clipboard.writeText).not.toHaveBeenCalled(); + }); + it("uses the current instance after a change and hides stale copy feedback", async () => { + let resolveCopy: () => void = () => {}; + vi.mocked(navigator.clipboard.writeText).mockImplementationOnce( + () => + new Promise((resolve) => { + resolveCopy = resolve; + }), + ); + const { rerender } = render(); + fireEvent.click(screen.getByRole("button", { name: "Copy profile instructions" })); + rerender(); + await act(async () => resolveCopy()); + expect(screen.queryByRole("status")).toBeNull(); + fireEvent.click(screen.getByRole("button", { name: "Copy profile instructions" })); + await screen.findByRole("status"); + expect(vi.mocked(navigator.clipboard.writeText).mock.calls[1]?.[0]).toContain( + "bsk session start --browser b1234567 --json", + ); + }); + it("reports clipboard failure and allows retry without claiming success", async () => { + vi.mocked(navigator.clipboard.writeText).mockRejectedValueOnce(new Error("clipboard denied")); + render(); + fireEvent.click(screen.getByRole("button", { name: "Copy profile instructions" })); + expect(await screen.findByRole("alert")).toBeTruthy(); + expect(screen.queryByRole("status")).toBeNull(); + fireEvent.click(screen.getByRole("button", { name: "Copy profile instructions" })); + expect(await screen.findByRole("status")).toBeTruthy(); + expect(screen.queryByRole("alert")).toBeNull(); + }); + it.each(["zh-CN", "ko-KR"])("preserves the exact command in %s instructions", async (locale) => { + await i18n.changeLanguage(locale); + render(); + fireEvent.click(screen.getByRole("button")); + expect(vi.mocked(navigator.clipboard.writeText).mock.calls[0]?.[0]).toContain( + "bsk session start --browser a1234567 --json", + ); + expect(await screen.findByRole("status")).toBeTruthy(); + }); +}); diff --git a/apps/extension/src/entrypoints/popup/profile-instructions.tsx b/apps/extension/src/entrypoints/popup/profile-instructions.tsx new file mode 100644 index 00000000..54fa7936 --- /dev/null +++ b/apps/extension/src/entrypoints/popup/profile-instructions.tsx @@ -0,0 +1,61 @@ +import { useTranslation } from "@browser-skill/i18n/react"; +import { Button } from "@browser-skill/ui"; +import { RiFileCopyLine } from "@remixicon/react"; +import { useState } from "react"; + +export function ProfileInstructions({ + instanceId, + connected, +}: { + instanceId: string; + connected: boolean; +}) { + const { t } = useTranslation("extension"); + const [feedback, setFeedback] = useState<{ + instanceId: string; + kind: "copied" | "failed"; + } | null>(null); + const ready = connected && Boolean(instanceId); + const currentFeedback = ready && feedback?.instanceId === instanceId ? feedback.kind : null; + + const copy = async () => { + if (!ready) return; + setFeedback(null); + try { + await navigator.clipboard.writeText( + t("popup.profile.promptTemplate", { + command: `bsk session start --browser ${instanceId} --json`, + }), + ); + setFeedback({ instanceId, kind: "copied" }); + } catch { + setFeedback({ instanceId, kind: "failed" }); + } + }; + + return ( +
+

{t("popup.profile.hint")}

+ + {currentFeedback && ( +

+ {t(currentFeedback === "failed" ? "popup.profile.copyFailed" : "popup.copied")} +

+ )} +
+ ); +} diff --git a/crates/bsk-cli/skill/SKILL.md b/crates/bsk-cli/skill/SKILL.md index 98768eb8..d8c1110c 100644 --- a/crates/bsk-cli/skill/SKILL.md +++ b/crates/bsk-cli/skill/SKILL.md @@ -45,12 +45,34 @@ environment settings may not persist between shell calls. Keep browser commands sandboxed. For other startup failures, retry once, then use `bsk doctor`. A local process identity warning permits browser commands when IPC works. +## Required browser profiles + +When the user requires a particular browser profile, bind the task to that +profile's extension instance before starting a session, even if only one browser +is connected. A Chrome profile name or directory is not a BrowserSkill instance +ID or an automatically assigned label. + +Use the instance ID from the BrowserSkill popup in the required profile. The user +can choose **Copy profile instructions** there and send the resulting instruction. +If only a profile name/path is supplied and its mapping is unknown, ask the user +to open that profile, verify its Profile Path at `chrome://version`, and copy the +profile instructions. Do not infer the mapping from a single Connected browser +or Chrome process command lines. + +Run `bsk browsers --json` to check that the supplied instance is connected, then +pass `--browser ` on every new session for this task. A previously +verified unique label also works. If the target is missing or ambiguous, stop and +report it; never omit the selector or substitute another instance to recover. +Opening another Chrome profile does not retarget an existing session. After an +extension reinstall or storage reset, obtain the instance mapping again. + ## Task workflow -1. Define success from the user's request. Start `bsk session start --json` and - retain its `session_id`. With multiple browsers, run `bsk browsers` and add - `--browser ` to start. For background work, add `--no-focus` to - `session start` only. +1. Define success from the user's request. For a required browser profile, follow + **Required browser profiles** above and start with its explicit `--browser` + selector. Otherwise start `bsk session start --json`; with multiple browsers, + run `bsk browsers` and choose `--browser `. Retain the returned + `session_id`. For background work, add `--no-focus` to `session start` only. 2. For a new page, navigate; for an existing user tab, follow **Borrowing** below. Read the page before interacting: diff --git a/crates/bsk-cli/src/cli/session.rs b/crates/bsk-cli/src/cli/session.rs index 1ecbdfb7..9bf3ce07 100644 --- a/crates/bsk-cli/src/cli/session.rs +++ b/crates/bsk-cli/src/cli/session.rs @@ -61,8 +61,8 @@ pub struct SessionStartArgs { /// Optional task name displayed in local operation history. #[arg(long)] pub name: Option, - /// Target browser instance id (only required when multiple browsers - /// are connected). + /// Target browser instance ID or unique label. Always set this when + /// a specific browser profile is required. #[arg(long)] pub browser: Option, diff --git a/docs/browser-profiles.md b/docs/browser-profiles.md new file mode 100644 index 00000000..2339911b --- /dev/null +++ b/docs/browser-profiles.md @@ -0,0 +1,49 @@ +# Select a specific browser profile + +BrowserSkill connects to the extension installed in a browser profile. A successful +connection alone does not identify the profile you intended to use. The CLI does +not launch Chrome or accept Chrome's `--profile-directory` argument. + +## Bind a task to the intended profile + +1. Open the required profile in Chrome. If you need to verify its directory, open + `chrome://version` in that window and check **Profile Path**. +2. Open BrowserSkill's popup in that same profile and ensure it is connected. +3. Choose **Copy profile instructions** and send the instructions along with your + task to the agent. The copied instruction includes this profile's extension + instance ID and requires it on every new session for the task. + +You can also copy the **Instance ID** from the popup and use it directly: + +```sh +bsk browsers --json +bsk session start --browser --json +``` + +Replace the placeholder with the popup's instance ID, not Chrome's extension ID, +profile display name or directory name. An existing, verified unique BrowserSkill +label also works; labels are not populated from Chrome profile names automatically. +The instruction only copies text: it does not start a session or change settings. + +If the target is offline, reconnect BrowserSkill in that profile and retry the +same selector. Do not remove `--browser` to get past the error: that could select +another profile. A missing selector is rejected when multiple browsers are online, +but automatically selects the only connected browser when just one is online. + +The session remains bound to its selected instance. Opening or switching Chrome +profiles does not move an existing session. Stop it with `bsk session stop ` +when the task ends. Reinstalling the extension or resetting its storage can change +the instance ID; verify the mapping again instead of substituting another browser. +Copying a whole profile can also copy its extension storage, so instance IDs are +routing identifiers, not independent proof of a filesystem path. + +## Windows and macOS + +The instance-selection workflow is the same on both platforms. The profile path +location differs; use the value displayed by Chrome rather than guessing it. +[Chromium documents this check](https://chromium.googlesource.com/chromium/src/+/main/docs/user_data_dir.md). + +Several profiles can run in one Chrome browser process. Its startup command line +may still name the first profile after a second profile opens, so scanning process +arguments cannot prove which profile owns a connected extension. There is no need +to quit all other Chrome profiles to select a connected BrowserSkill instance. diff --git a/packages/i18n/src/locales/en-US/extension.json b/packages/i18n/src/locales/en-US/extension.json index b79ac448..95602313 100644 --- a/packages/i18n/src/locales/en-US/extension.json +++ b/packages/i18n/src/locales/en-US/extension.json @@ -114,7 +114,13 @@ "remoteSavedDisabled": "Paired successfully. The connection switch is off; turn it on to connect.", "remoteFormatError": "The pairing link is incomplete or invalid. Copy the full link provided by the server.", "remotePermissionsInfo": "About remote connection permissions", - "remotePermissionsHint": "The server can control dedicated task windows. Confirmation for borrowing existing tabs follows the Automation settings below." + "remotePermissionsHint": "The server can control dedicated task windows. Confirmation for borrowing existing tabs follows the Automation settings below.", + "profile": { + "hint": "Check that this is the browser profile you want to use, then send its instructions to your agent.", + "copyButton": "Copy profile instructions", + "copyFailed": "Could not copy the instructions. Please try again.", + "promptTemplate": "Use BrowserSkill in this browser profile for this task. Start every new session for this task with: {{command}}\nIf this instance is not connected, stop and ask me to reconnect this profile. Do not omit --browser or switch to another instance." + } }, "controlOverlay": { "status": "Agent controlling", diff --git a/packages/i18n/src/locales/ko-KR/extension.json b/packages/i18n/src/locales/ko-KR/extension.json index bd87a0da..e74380f5 100644 --- a/packages/i18n/src/locales/ko-KR/extension.json +++ b/packages/i18n/src/locales/ko-KR/extension.json @@ -114,7 +114,13 @@ "remoteSavedDisabled": "페어링이 완료되었습니다. 연결 스위치가 꺼져 있습니다. 연결하려면 스위치를 켜세요.", "remoteFormatError": "페어링 링크가 불완전하거나 올바르지 않습니다. 서버에서 제공한 전체 링크를 복사하세요.", "remotePermissionsInfo": "원격 연결 권한 안내", - "remotePermissionsHint": "서버는 전용 작업 창을 제어할 수 있습니다. 기존 탭을 빌릴 때의 확인 여부는 아래 자동화 설정을 따릅니다." + "remotePermissionsHint": "서버는 전용 작업 창을 제어할 수 있습니다. 기존 탭을 빌릴 때의 확인 여부는 아래 자동화 설정을 따릅니다.", + "profile": { + "hint": "사용하려는 브라우저 프로필이 맞는지 확인한 뒤 에이전트에게 지침을 보내세요.", + "copyButton": "프로필 지침 복사", + "copyFailed": "지침을 복사하지 못했습니다. 다시 시도해 주세요.", + "promptTemplate": "이 작업에는 이 브라우저 프로필의 BrowserSkill을 사용하세요. 이 작업의 새 세션을 시작할 때마다 다음을 실행하세요: {{command}}\n이 인스턴스가 연결되어 있지 않으면 중단하고 이 프로필을 다시 연결해 달라고 요청하세요. --browser를 생략하거나 다른 인스턴스로 전환하지 마세요." + } }, "controlOverlay": { "status": "에이전트가 제어 중", diff --git a/packages/i18n/src/locales/zh-CN/extension.json b/packages/i18n/src/locales/zh-CN/extension.json index 04735e4d..ce127ba0 100644 --- a/packages/i18n/src/locales/zh-CN/extension.json +++ b/packages/i18n/src/locales/zh-CN/extension.json @@ -114,7 +114,13 @@ "remoteSavedDisabled": "配对成功,连接开关当前关闭。开启后即可连接。", "remoteFormatError": "配对链接不完整或格式不正确,请重新复制服务器提供的完整链接。", "remotePermissionsInfo": "远程连接权限说明", - "remotePermissionsHint": "服务器可操作独立任务窗口。借用已有标签页时,是否请求确认取决于下方的“自动化设置”。" + "remotePermissionsHint": "服务器可操作独立任务窗口。借用已有标签页时,是否请求确认取决于下方的“自动化设置”。", + "profile": { + "hint": "确认当前是你要使用的浏览器个人资料,再将此处的指令发给 Agent。", + "copyButton": "复制此 Profile 的指令", + "copyFailed": "复制指令失败,请重试。", + "promptTemplate": "本次任务请使用此浏览器个人资料中的 BrowserSkill。为本次任务创建每个新会话时都执行:{{command}}\n如果此实例未连接,请停止并让我重新连接此 Profile。不要省略 --browser,也不要切换到其他实例。" + } }, "controlOverlay": { "status": "Agent 正在控制", diff --git a/skill/SKILL.md b/skill/SKILL.md index 98768eb8..d8c1110c 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -45,12 +45,34 @@ environment settings may not persist between shell calls. Keep browser commands sandboxed. For other startup failures, retry once, then use `bsk doctor`. A local process identity warning permits browser commands when IPC works. +## Required browser profiles + +When the user requires a particular browser profile, bind the task to that +profile's extension instance before starting a session, even if only one browser +is connected. A Chrome profile name or directory is not a BrowserSkill instance +ID or an automatically assigned label. + +Use the instance ID from the BrowserSkill popup in the required profile. The user +can choose **Copy profile instructions** there and send the resulting instruction. +If only a profile name/path is supplied and its mapping is unknown, ask the user +to open that profile, verify its Profile Path at `chrome://version`, and copy the +profile instructions. Do not infer the mapping from a single Connected browser +or Chrome process command lines. + +Run `bsk browsers --json` to check that the supplied instance is connected, then +pass `--browser ` on every new session for this task. A previously +verified unique label also works. If the target is missing or ambiguous, stop and +report it; never omit the selector or substitute another instance to recover. +Opening another Chrome profile does not retarget an existing session. After an +extension reinstall or storage reset, obtain the instance mapping again. + ## Task workflow -1. Define success from the user's request. Start `bsk session start --json` and - retain its `session_id`. With multiple browsers, run `bsk browsers` and add - `--browser ` to start. For background work, add `--no-focus` to - `session start` only. +1. Define success from the user's request. For a required browser profile, follow + **Required browser profiles** above and start with its explicit `--browser` + selector. Otherwise start `bsk session start --json`; with multiple browsers, + run `bsk browsers` and choose `--browser `. Retain the returned + `session_id`. For background work, add `--no-focus` to `session start` only. 2. For a new page, navigate; for an existing user tab, follow **Borrowing** below. Read the page before interacting: From 88718fd6b9cbfd5903ccc6583d2b4a6924505a00 Mon Sep 17 00:00:00 2001 From: Zhang GH Date: Sun, 20 Sep 2026 20:18:07 +0800 Subject: [PATCH 2/2] fix: align DSH profile selection guidance and coverage --- .../popup/profile-instructions.test.tsx | 10 ++- .../popup/profile-instructions.tsx | 1 + docs/browser-profiles.md | 23 ++++++ .../dsh-plugin-browserskill/skill/SKILL.md | 27 ++++++- .../src/browser-tools.ts | 10 ++- .../src/tool-params.ts | 8 ++ packages/dsh-plugin-browserskill/src/tools.ts | 8 +- .../tests/profile-selection.test.ts | 74 +++++++++++++++++++ .../tests/skill.test.ts | 6 +- .../i18n/src/locales/en-US/extension.json | 2 +- .../i18n/src/locales/ko-KR/extension.json | 2 +- .../i18n/src/locales/zh-CN/extension.json | 2 +- 12 files changed, 157 insertions(+), 16 deletions(-) create mode 100644 packages/dsh-plugin-browserskill/tests/profile-selection.test.ts diff --git a/apps/extension/src/entrypoints/popup/profile-instructions.test.tsx b/apps/extension/src/entrypoints/popup/profile-instructions.test.tsx index b4aa534e..71f5805e 100644 --- a/apps/extension/src/entrypoints/popup/profile-instructions.test.tsx +++ b/apps/extension/src/entrypoints/popup/profile-instructions.test.tsx @@ -21,8 +21,10 @@ describe("profile instructions", () => { fireEvent.click(screen.getByRole("button", { name: "Copy profile instructions" })); const text = vi.mocked(navigator.clipboard.writeText).mock.calls[0]?.[0]; expect(text).toContain("bsk session start --browser a1234567 --json"); + expect(text).toContain('browser_session({ action: "start", browser: "a1234567" })'); + expect(text).toContain("use the tool call instead of running the CLI command separately"); expect(text).toContain("every new session for this task"); - expect(text).toContain("Do not omit --browser or switch to another instance"); + expect(text).toContain("Do not omit --browser / browser or switch to another instance"); expect(await screen.findByRole("status")).toBeTruthy(); }); it.each([ @@ -53,6 +55,9 @@ describe("profile instructions", () => { expect(vi.mocked(navigator.clipboard.writeText).mock.calls[1]?.[0]).toContain( "bsk session start --browser b1234567 --json", ); + expect(vi.mocked(navigator.clipboard.writeText).mock.calls[1]?.[0]).toContain( + 'browser_session({ action: "start", browser: "b1234567" })', + ); }); it("reports clipboard failure and allows retry without claiming success", async () => { vi.mocked(navigator.clipboard.writeText).mockRejectedValueOnce(new Error("clipboard denied")); @@ -71,6 +76,9 @@ describe("profile instructions", () => { expect(vi.mocked(navigator.clipboard.writeText).mock.calls[0]?.[0]).toContain( "bsk session start --browser a1234567 --json", ); + expect(vi.mocked(navigator.clipboard.writeText).mock.calls[0]?.[0]).toContain( + 'browser_session({ action: "start", browser: "a1234567" })', + ); expect(await screen.findByRole("status")).toBeTruthy(); }); }); diff --git a/apps/extension/src/entrypoints/popup/profile-instructions.tsx b/apps/extension/src/entrypoints/popup/profile-instructions.tsx index 54fa7936..545b0047 100644 --- a/apps/extension/src/entrypoints/popup/profile-instructions.tsx +++ b/apps/extension/src/entrypoints/popup/profile-instructions.tsx @@ -25,6 +25,7 @@ export function ProfileInstructions({ await navigator.clipboard.writeText( t("popup.profile.promptTemplate", { command: `bsk session start --browser ${instanceId} --json`, + toolCall: `browser_session({ action: "start", browser: ${JSON.stringify(instanceId)} })`, }), ); setFeedback({ instanceId, kind: "copied" }); diff --git a/docs/browser-profiles.md b/docs/browser-profiles.md index 2339911b..1118eaea 100644 --- a/docs/browser-profiles.md +++ b/docs/browser-profiles.md @@ -37,6 +37,29 @@ the instance ID; verify the mapping again instead of substituting another browse Copying a whole profile can also copy its extension storage, so instance IDs are routing identifiers, not independent proof of a filesystem path. +## DeepSeek Harness + +Use the verified popup instance ID in the plugin's session tool: + +```text +browser_session({ action: "start", browser: "" }) +``` + +**Copy profile instructions** includes this tool call alongside the CLI command. +Use the DeepSeek Harness call instead of running a separate CLI session. The plugin +manages its own sessions. Supply the same selector on every new session for the task, even if +only one browser is connected. If the mapping is unknown or the target is offline, +ask the user to confirm or reconnect it; do not retry without `browser`. + +## Scope and limitations + +This is a manual profile-to-instance workflow, not automatic profile discovery or +a persisted task/workspace requirement. Explicit selectors reject unavailable or +ambiguous targets. Neither the CLI nor the DSH plugin can infer a profile requirement +from the user's conversation if the agent omits the selector: with only one browser +connected, an unqualified start can still select the wrong profile. The skill and +copied instructions guide the agent; they do not enforce a binding on future calls. + ## Windows and macOS The instance-selection workflow is the same on both platforms. The profile path diff --git a/packages/dsh-plugin-browserskill/skill/SKILL.md b/packages/dsh-plugin-browserskill/skill/SKILL.md index 6ccfb579..ebb1452b 100644 --- a/packages/dsh-plugin-browserskill/skill/SKILL.md +++ b/packages/dsh-plugin-browserskill/skill/SKILL.md @@ -10,9 +10,31 @@ Do not control the browser through another process. Use the loaded action schema For remote setup or pairing, follow the [remote guide](https://github.com/Tencent/BrowserSkill/blob/main/docs/remote-extension-connection.md) before using these tools. +## Required browser profiles + +If the user or workspace requires a specific profile, confirm its instance ID before +starting, even with only one connected browser. If unknown, ask the user to open the +intended profile, check **Profile Path** at `chrome://version` if a directory was +specified, and copy the **Instance ID** or **Copy profile instructions** from the +connected BrowserSkill popup in that same profile. Connected alone and Chrome's +process arguments do not prove the profile. + +Use the verified ID (or verified unique BrowserSkill label) on every new session: + +```text +browser_session({ action: "start", browser: "" }) +``` + +If the copied instructions contain a command-line example, use its instance ID in +this tool call; do not run that command separately. A Chrome profile name, directory, +or extension ID is not an instance ID. If the mapping is unclear, ambiguous, or the +target is unavailable, stop and ask the user to confirm or reconnect it. Never omit +`browser` or substitute another instance to recover. + ## Mandatory workflow -1. Define success. Start a session and retain `sessionId`. For a new page: +1. Define success. Start a session and retain `sessionId`. Include `browser` as above + when a profile is required. Otherwise, for a new page: ```text browser_session({ action: "start" }) @@ -83,7 +105,8 @@ unknown effects or switch backends to bypass limits. Borrow confirmation still a again (users can enter `/browser-skill`), then retry the intended browser tool once after its schema appears. If it remains unavailable, report the failure. - Stale ref: observe, then retry the intended action once. -- Unknown tab/session: list owned resources or start a session; never guess IDs. +- Unknown tab/session: list owned resources or start a session with the required + browser selector, if any; never guess IDs. - Failed or interrupted session stop: accepted cleanup continues in the background. Retry the same stop; a completed previous stop returns `alreadyClosed: true`. If several stops are pending, specify `session` or the owned `requestId` from the diff --git a/packages/dsh-plugin-browserskill/src/browser-tools.ts b/packages/dsh-plugin-browserskill/src/browser-tools.ts index 527cfcfe..03432687 100644 --- a/packages/dsh-plugin-browserskill/src/browser-tools.ts +++ b/packages/dsh-plugin-browserskill/src/browser-tools.ts @@ -7,6 +7,7 @@ import { defineTool, type ParameterSchemaSpec, type ToolDefinition } from "@deepseek-ai/dsh-tools"; import { + BROWSER_PARAM, SESSION_PARAM, SESSION_STOP_PARAMS, TAB_ID_PARAM, @@ -104,8 +105,11 @@ 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, specify session or requestId (not both), or omit both to retry an " + + "owned session; list returns owned sessions. For start, url/device/width/height/noFocus are " + + "optional. When a specific profile is required, always set browser to its verified instance " + + "ID or unique label, even with one connected browser; stop if the target is unknown or " + + "unavailable instead of omitting or changing browser. For stop, specify session or requestId " + + "(not both), or omit both to retry an " + "unacknowledged stop before selecting the current owned session. If several stops await " + "acknowledgement, specify a target. Once accepted, cleanup continues if the call is aborted.", actions: { @@ -119,7 +123,7 @@ const BROWSER_TOOL_SPECS: BrowserToolSpec[] = [ 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." }, + browser: BROWSER_PARAM, device: { type: "string", enum: DEVICE_PRESETS, description: "Device preset for start." }, }, }, diff --git a/packages/dsh-plugin-browserskill/src/tool-params.ts b/packages/dsh-plugin-browserskill/src/tool-params.ts index de3f776f..a651b1ac 100644 --- a/packages/dsh-plugin-browserskill/src/tool-params.ts +++ b/packages/dsh-plugin-browserskill/src/tool-params.ts @@ -1,5 +1,13 @@ /** Shared model-facing parameter schemas for browser tools. */ +export const BROWSER_PARAM = { + type: "string", + description: + "Target browser instance ID or verified unique label for start. Always set this when a " + + "specific profile is required, even if only one browser is connected. Confirm the mapping " + + "with the user if unknown; never omit or substitute the selector to recover from an unavailable target.", +} as const; + export const SESSION_PARAM = { type: "string", description: diff --git a/packages/dsh-plugin-browserskill/src/tools.ts b/packages/dsh-plugin-browserskill/src/tools.ts index ad18dc9e..5f9835eb 100644 --- a/packages/dsh-plugin-browserskill/src/tools.ts +++ b/packages/dsh-plugin-browserskill/src/tools.ts @@ -30,7 +30,7 @@ import { } from "./runner"; import { SessionStarts } from "./session-starts"; import type { SessionRegistry } from "./sessions"; -import { SESSION_PARAM, SESSION_STOP_PARAMS } from "./tool-params"; +import { BROWSER_PARAM, SESSION_PARAM, SESSION_STOP_PARAMS } from "./tool-params"; /** Plugin configuration resolved from the Schemastery schema in index.ts. */ export interface PluginConfig { @@ -196,11 +196,7 @@ function defineBrowserOperations(deps: ToolDeps, register: DefinitionRegistrar): type: "boolean", description: "Open the Agent Window in the background without stealing focus.", }, - browser: { - type: "string", - description: - "Target browser instance id (only needed when multiple browsers are connected).", - }, + browser: BROWSER_PARAM, device: { type: "string", enum: DEVICE_PRESETS, diff --git a/packages/dsh-plugin-browserskill/tests/profile-selection.test.ts b/packages/dsh-plugin-browserskill/tests/profile-selection.test.ts new file mode 100644 index 00000000..402142fe --- /dev/null +++ b/packages/dsh-plugin-browserskill/tests/profile-selection.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from "vitest"; +import type { BskRunResult } from "../src/runner"; +import { harness, ok } from "./session-lifecycle-harness"; + +describe("profile selection through browser_session", () => { + it.each([ + "a1b2c3d4", + "Required Profile A", + ])("preserves the verified selector %s in the start command and returned identity", async (browser) => { + const h = harness(async (args) => { + if (args[1] === "start") { + return ok({ session_id: "selected", browser_instance_id: "a1b2c3d4" }); + } + return ok({ state: args.includes("--claim") ? "active" : "closed" }); + }); + await expect(h.session({ action: "start", browser })).resolves.toMatchObject({ + sessionId: "selected", + browserInstanceId: "a1b2c3d4", + }); + const starts = h.calls.filter(({ args }) => args[0] === "session" && args[1] === "start"); + expect(starts).toHaveLength(1); + expect(starts[0].args).toEqual([ + "session", + "start", + "--request-id", + expect.any(String), + "--browser", + browser, + ]); + }); + + it.each([ + ["a1b2c3d4", "not_found"], + ["Shared label", "invalid_params"], + ])("does not fall back or navigate when %s fails with %s", async (browser, code) => { + const h = harness(async (args): Promise => { + if (args[1] === "start") { + if (args.includes("--browser")) { + return { + ...ok({ code, message: "Required browser is unavailable or ambiguous" }), + code: 1, + }; + } + // An unqualified retry would succeed on the wrong, online browser. + return ok({ session_id: "wrong", browser_instance_id: "b1c2d3e4" }); + } + return ok({ state: "closed" }); + }); + await expect( + h.session({ action: "start", browser, url: "https://example.test/" }), + ).rejects.toMatchObject({ code }); + const starts = h.calls.filter(({ args }) => args[0] === "session" && args[1] === "start"); + expect(starts).toHaveLength(1); + expect(starts[0].args.slice(-2)).toEqual(["--browser", browser]); + expect(h.calls.some(({ args }) => args[0] === "navigate")).toBe(false); + expect(h.registry.current()).toBeUndefined(); + expect(h.registry.size()).toBe(0); + }); + + it("keeps an unqualified start available when no profile selector was supplied", async () => { + const h = harness(async (args) => { + if (args[1] === "start") { + return ok({ session_id: "default", browser_instance_id: "b1c2d3e4" }); + } + return ok({ state: args.includes("--claim") ? "active" : "closed" }); + }); + await expect(h.session({ action: "start" })).resolves.toMatchObject({ + browserInstanceId: "b1c2d3e4", + }); + const starts = h.calls.filter(({ args }) => args[0] === "session" && args[1] === "start"); + expect(starts).toHaveLength(1); + expect(starts[0].args).not.toContain("--browser"); + }); +}); diff --git a/packages/dsh-plugin-browserskill/tests/skill.test.ts b/packages/dsh-plugin-browserskill/tests/skill.test.ts index a8943a9e..65b6d433 100644 --- a/packages/dsh-plugin-browserskill/tests/skill.test.ts +++ b/packages/dsh-plugin-browserskill/tests/skill.test.ts @@ -50,7 +50,11 @@ describe("registerBskSkill", () => { // Keep the lazily injected instructions inside a bounded prompt budget, // while the lower bound catches accidental truncation of the guidance. expect(content.length).toBeGreaterThan(3_000); - expect(content.length).toBeLessThan(7_000); + expect(content.length).toBeLessThan(8_000); + expect(content).toContain( + 'browser_session({ action: "start", browser: "" })', + ); + expect(content).toMatch(/Never omit\s+`browser` or substitute another instance/); expect(content).toContain("[visual:screenshot]"); expect(content).toContain("nextCursor"); expect(skill.source).toBe("bundled"); diff --git a/packages/i18n/src/locales/en-US/extension.json b/packages/i18n/src/locales/en-US/extension.json index 95602313..9e76942a 100644 --- a/packages/i18n/src/locales/en-US/extension.json +++ b/packages/i18n/src/locales/en-US/extension.json @@ -119,7 +119,7 @@ "hint": "Check that this is the browser profile you want to use, then send its instructions to your agent.", "copyButton": "Copy profile instructions", "copyFailed": "Could not copy the instructions. Please try again.", - "promptTemplate": "Use BrowserSkill in this browser profile for this task. Start every new session for this task with: {{command}}\nIf this instance is not connected, stop and ask me to reconnect this profile. Do not omit --browser or switch to another instance." + "promptTemplate": "Use BrowserSkill in this browser profile for this task. Start every new session for this task with the appropriate interface:\nCLI: {{command}}\nDeepSeek Harness: {{toolCall}}\nIn DeepSeek Harness, use the tool call instead of running the CLI command separately.\nIf this instance is not connected, stop and ask me to reconnect this profile. Do not omit --browser / browser or switch to another instance." } }, "controlOverlay": { diff --git a/packages/i18n/src/locales/ko-KR/extension.json b/packages/i18n/src/locales/ko-KR/extension.json index e74380f5..352c5bac 100644 --- a/packages/i18n/src/locales/ko-KR/extension.json +++ b/packages/i18n/src/locales/ko-KR/extension.json @@ -119,7 +119,7 @@ "hint": "사용하려는 브라우저 프로필이 맞는지 확인한 뒤 에이전트에게 지침을 보내세요.", "copyButton": "프로필 지침 복사", "copyFailed": "지침을 복사하지 못했습니다. 다시 시도해 주세요.", - "promptTemplate": "이 작업에는 이 브라우저 프로필의 BrowserSkill을 사용하세요. 이 작업의 새 세션을 시작할 때마다 다음을 실행하세요: {{command}}\n이 인스턴스가 연결되어 있지 않으면 중단하고 이 프로필을 다시 연결해 달라고 요청하세요. --browser를 생략하거나 다른 인스턴스로 전환하지 마세요." + "promptTemplate": "이 작업에는 이 브라우저 프로필의 BrowserSkill을 사용하세요. 이 작업의 새 세션을 시작할 때마다 해당 인터페이스를 사용하세요:\nCLI: {{command}}\nDeepSeek Harness: {{toolCall}}\nDeepSeek Harness에서는 CLI 명령을 별도로 실행하지 말고 도구 호출을 사용하세요.\n이 인스턴스가 연결되어 있지 않으면 중단하고 이 프로필을 다시 연결해 달라고 요청하세요. --browser / browser를 생략하거나 다른 인스턴스로 전환하지 마세요." } }, "controlOverlay": { diff --git a/packages/i18n/src/locales/zh-CN/extension.json b/packages/i18n/src/locales/zh-CN/extension.json index ce127ba0..996ad519 100644 --- a/packages/i18n/src/locales/zh-CN/extension.json +++ b/packages/i18n/src/locales/zh-CN/extension.json @@ -119,7 +119,7 @@ "hint": "确认当前是你要使用的浏览器个人资料,再将此处的指令发给 Agent。", "copyButton": "复制此 Profile 的指令", "copyFailed": "复制指令失败,请重试。", - "promptTemplate": "本次任务请使用此浏览器个人资料中的 BrowserSkill。为本次任务创建每个新会话时都执行:{{command}}\n如果此实例未连接,请停止并让我重新连接此 Profile。不要省略 --browser,也不要切换到其他实例。" + "promptTemplate": "本次任务请使用此浏览器个人资料中的 BrowserSkill。为本次任务创建每个新会话时,请使用对应接口:\nCLI:{{command}}\nDeepSeek Harness:{{toolCall}}\n在 DeepSeek Harness 中请使用工具调用,不要另行执行 CLI 命令。\n如果此实例未连接,请停止并让我重新连接此 Profile。不要省略 --browser / browser,也不要切换到其他实例。" } }, "controlOverlay": {