diff --git a/.gitignore b/.gitignore index e30f788..146838a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .DS_Store node_modules/ coverage/ +.otto/ *.tgz !tooling/vendor/*.tgz diff --git a/README.md b/README.md index b21b452..0fec1b5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # bb plugins -Five bb plugins I use for product design work, kept together with the few build and repository tools they share. [![CI](https://github.com/brsbl/bb-plugins/actions/workflows/ci.yml/badge.svg)](https://github.com/brsbl/bb-plugins/actions/workflows/ci.yml) +Six bb plugins I use for product design work, kept together with the few build and repository tools they share. [![CI](https://github.com/brsbl/bb-plugins/actions/workflows/ci.yml/badge.svg)](https://github.com/brsbl/bb-plugins/actions/workflows/ci.yml) [bb](https://getbb.app) is an agentic IDE for running coding agents across projects, threads, and environments. Its plugins can add UI, commands, skills, and server capabilities; this repository is where I build and maintain mine. @@ -8,6 +8,16 @@ Five bb plugins I use for product design work, kept together with the few build Each plugin has its own workspace under `plugins/` and a short README with the story behind it. +### Browser Context + +Selects and annotates an element or region in BB's Browser, then stages its screenshot and editable Markdown context in the current thread composer. + +![Browser Context capture preview in bb](plugins/browser-context/docs/screenshot.png) + +[Source](plugins/browser-context) · [README](plugins/browser-context/README.md) + +Install: `bb plugin install git:https://github.com/brsbl/bb-plugins.git@plugin/browser-context --yes` + ### Design Doctrine Keeps the design lessons that come up again and again in reviews as reusable rules. diff --git a/package-lock.json b/package-lock.json index 0af981c..2d7f379 100644 --- a/package-lock.json +++ b/package-lock.json @@ -113,7 +113,7 @@ "node_modules/@bb/plugin-sdk": { "version": "0.5.0", "resolved": "file:tooling/vendor/bb-plugin-sdk-0.5.0.tgz", - "integrity": "sha512-0oDLD4lugQD/QQlRABqxxMeLZJbwLA+GiKcXiN8VUTsoqrZzTppUxWpf53/iLYupfF+hGuwOLtQN50oM0YiCEA==", + "integrity": "sha512-avMbN5Go1w8QfvSF7/QuexaSXlgYnBzx1B58C/S1F4l07JVqG4BacXY/PaKZzXbA77HQTHNQlEWULHn9JEnYJA==", "dev": true, "peerDependencies": { "@testing-library/react": "^16.3.2", @@ -2980,6 +2980,10 @@ ], "license": "MIT" }, + "node_modules/bb-plugin-browser-context": { + "resolved": "plugins/browser-context", + "link": true + }, "node_modules/bb-plugin-design-doctrine": { "resolved": "plugins/design-doctrine", "link": true @@ -5055,6 +5059,34 @@ "vitest": "^4.0.0" } }, + "plugins/browser-context": { + "name": "bb-plugin-browser-context", + "version": "0.1.0", + "license": "UNLICENSED", + "dependencies": { + "zod": "^4.3.6" + }, + "devDependencies": { + "@bb/plugin-sdk": "file:../../tooling/vendor/bb-plugin-sdk-0.5.0.tgz", + "@testing-library/react": "^16.3.2", + "@types/better-sqlite3": "^7.6.12", + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "better-sqlite3": "^12.10.0", + "cron-parser": "^5.5.0", + "hono": "^4.11.9", + "jsdom": "^29.0.1", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "typescript": "^5.7.0", + "vitest": "^4.1.8" + }, + "engines": { + "bb": ">=0.0.34", + "bbPluginSdk": "^0.5.0" + } + }, "plugins/design-doctrine": { "name": "bb-plugin-design-doctrine", "version": "0.3.0", @@ -5118,6 +5150,52 @@ "bbPluginSdk": "^0.5.0" } }, + "plugins/improve-prompt/node_modules/@bb/plugin-sdk": { + "version": "0.5.0", + "resolved": "file:plugins/improve-prompt/vendor/bb-plugin-sdk-0.5.0.tgz", + "integrity": "sha512-avMbN5Go1w8QfvSF7/QuexaSXlgYnBzx1B58C/S1F4l07JVqG4BacXY/PaKZzXbA77HQTHNQlEWULHn9JEnYJA==", + "dev": true, + "peerDependencies": { + "@testing-library/react": "^16.3.2", + "@types/better-sqlite3": "^7.6.12", + "@types/react": "^19.0.0", + "better-sqlite3": ">=12", + "cron-parser": "^5.5.0", + "hono": "^4.11.9", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "zod": "^4.3.6" + }, + "peerDependenciesMeta": { + "@testing-library/react": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "cron-parser": { + "optional": true + }, + "hono": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "plugins/thread-hover-cards": { "name": "bb-plugin-thread-hover-cards", "version": "0.1.0", @@ -5193,6 +5271,52 @@ "bb": ">=0.0.34", "bbPluginSdk": "^0.5.0" } + }, + "plugins/timeline-comments/node_modules/@bb/plugin-sdk": { + "version": "0.5.0", + "resolved": "file:plugins/timeline-comments/vendor/bb-plugin-sdk-0.5.0.tgz", + "integrity": "sha512-avMbN5Go1w8QfvSF7/QuexaSXlgYnBzx1B58C/S1F4l07JVqG4BacXY/PaKZzXbA77HQTHNQlEWULHn9JEnYJA==", + "dev": true, + "peerDependencies": { + "@testing-library/react": "^16.3.2", + "@types/better-sqlite3": "^7.6.12", + "@types/react": "^19.0.0", + "better-sqlite3": ">=12", + "cron-parser": "^5.5.0", + "hono": "^4.11.9", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "zod": "^4.3.6" + }, + "peerDependenciesMeta": { + "@testing-library/react": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "cron-parser": { + "optional": true + }, + "hono": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "zod": { + "optional": true + } + } } } } diff --git a/plugins/browser-context/README.md b/plugins/browser-context/README.md new file mode 100644 index 0000000..efeffe8 --- /dev/null +++ b/plugins/browser-context/README.md @@ -0,0 +1,25 @@ +# Browser Context + +Select an element or drag over a region in BB's Browser, annotate the captured preview, then add it to the current thread composer. + +![Browser context staged in the current bb composer](docs/screenshot.png) + +## Install + +```bash +bb plugin install git:https://github.com/brsbl/bb-plugins.git@plugin/browser-context --yes +``` + +## Use + +Open a Browser tab and click the Browser Context action to enter selection mode. Click an element or drag over a region, then add an optional comment. Choose **Add to prompt** to preserve the existing draft, attach the screenshot, and append the comment followed by a compact quoted summary of the page, target, DOM, relevant styles, and available framework or accessibility hints. The user request stays primary while supporting context can collapse naturally, and the standard Send or Queue flow remains in control. + +## Develop + +From the monorepo root: + +```bash +npm ci +npm run check --workspace=bb-plugin-browser-context +bb plugin install "path:$PWD/plugins/browser-context" --yes +``` diff --git a/plugins/browser-context/app-registration.test.ts b/plugins/browser-context/app-registration.test.ts new file mode 100644 index 0000000..578d256 --- /dev/null +++ b/plugins/browser-context/app-registration.test.ts @@ -0,0 +1,22 @@ +// @vitest-environment jsdom + +import { installTestPluginRuntime } from "@bb/plugin-sdk/testing/app"; +import { describe, expect, it, vi } from "vitest"; + +describe("Browser Context registration", () => { + it("registers on current hosts and remains inert on older hosts", async () => { + installTestPluginRuntime(); + const { registerBrowserContextApp } = await import("./app.js"); + const register = vi.fn(); + + registerBrowserContextApp({ experimental_browserAction: register }); + expect(register).toHaveBeenCalledWith( + expect.objectContaining({ + id: "capture", + title: "Select page context", + component: expect.any(Function), + }), + ); + expect(() => registerBrowserContextApp({})).not.toThrow(); + }); +}); diff --git a/plugins/browser-context/app.css b/plugins/browser-context/app.css new file mode 100644 index 0000000..f6b9886 --- /dev/null +++ b/plugins/browser-context/app.css @@ -0,0 +1,365 @@ +@scope ([data-bb-plugin="browser-context"]) { + .bb-browser-context-action { + display: inline-flex; + width: 28px; + height: 28px; + align-items: center; + justify-content: center; + border: 0; + border-radius: 7px; + background: transparent; + color: var(--muted-foreground, #667085); + cursor: pointer; + transition: + background-color 140ms ease, + box-shadow 140ms ease, + color 140ms ease; + } + + .bb-browser-context-action:hover:not(:disabled), + .bb-browser-context-action[aria-pressed="true"] { + background: var(--state-hover, rgb(0 0 0 / 6%)); + color: var(--foreground, #101828); + } + + .bb-browser-context-action[aria-pressed="true"] { + box-shadow: inset 0 0 0 1px + color-mix(in srgb, var(--ring, #4c76e8) 68%, transparent); + color: var(--ring, #4c76e8); + } + + .bb-browser-context-action:disabled { + cursor: not-allowed; + opacity: 0.42; + } + + .bb-browser-context-action:focus-visible { + outline: 2px solid var(--ring, #4c76e8); + outline-offset: 2px; + } + + .bb-browser-context-action svg { + width: 17px; + height: 17px; + } + + .bb-browser-context-action[aria-pressed="true"] svg { + animation: bb-browser-context-activate 220ms ease-out; + transform-origin: center; + } + + .bb-browser-context-status { + position: fixed; + z-index: 2147483102; + right: 12px; + top: 92px; + max-width: 320px; + padding: 9px 11px; + border: 1px solid var(--destructive, #b42318); + border-radius: 9px; + background: var(--popover, #fff); + box-shadow: 0 8px 24px rgb(16 24 40 / 14%); + color: var(--destructive, #b42318); + font: + 500 12px/1.4 Inter, + system-ui, + sans-serif; + } + + .bb-browser-context-review { + position: absolute; + inset: 0; + overflow: hidden; + pointer-events: auto; + background: var(--background, #fff); + color: var(--foreground, #101828); + font: + 400 13px/1.45 Inter, + system-ui, + sans-serif; + } + + .bb-browser-context-canvas { + position: absolute; + inset: 0; + overflow: hidden; + background: #f2f4f7; + } + + .bb-browser-context-canvas > img { + display: block; + width: 100%; + height: 100%; + object-fit: fill; + user-select: none; + } + + .bb-browser-context-target { + position: absolute; + min-width: 8px; + min-height: 8px; + border: 2px solid #3867e8; + border-radius: 3px; + background: rgb(56 103 232 / 12%); + box-shadow: + 0 0 0 1px rgb(255 255 255 / 78%), + 0 3px 12px rgb(16 24 40 / 24%); + cursor: help; + outline: none; + } + + .bb-browser-context-target:hover, + .bb-browser-context-target:focus-visible { + border-color: #1747c8; + background: rgb(56 103 232 / 23%); + box-shadow: + 0 0 0 2px rgb(255 255 255 / 92%), + 0 4px 16px rgb(16 24 40 / 30%); + } + + .bb-browser-context-target-badge, + .bb-browser-context-comment-index { + display: inline-flex; + width: 20px; + height: 20px; + align-items: center; + justify-content: center; + border-radius: 999px; + background: #3867e8; + color: #fff; + font-size: 11px; + font-weight: 700; + } + + .bb-browser-context-target-badge { + position: absolute; + top: -12px; + left: -12px; + box-shadow: 0 1px 4px rgb(16 24 40 / 24%); + } + + .bb-browser-context-target-tooltip { + position: absolute; + left: 0; + top: calc(100% + 8px); + z-index: 2; + display: grid; + width: max-content; + max-width: 280px; + gap: 2px; + padding: 7px 9px; + border: 1px solid rgb(255 255 255 / 30%); + border-radius: 7px; + background: rgb(16 24 40 / 92%); + color: #fff; + box-shadow: 0 6px 20px rgb(16 24 40 / 28%); + text-align: left; + white-space: normal; + } + + .bb-browser-context-target-tooltip strong { + font-size: 11px; + font-weight: 650; + } + + .bb-browser-context-target-tooltip span { + color: rgb(255 255 255 / 78%); + font-size: 11px; + } + + .bb-browser-context-comment-card { + position: absolute; + z-index: 3; + top: 10px; + right: 10px; + display: grid; + width: min(300px, calc(100% - 20px)); + gap: 7px; + padding: 10px; + border: 1px solid var(--border, #d0d5dd); + border-radius: 12px; + background: color-mix(in srgb, var(--popover, #fff) 96%, transparent); + box-shadow: 0 14px 36px rgb(16 24 40 / 20%); + backdrop-filter: blur(12px); + } + + .bb-browser-context-comment-card-left { + right: auto; + left: 10px; + } + + .bb-browser-context-comment-toolbar { + display: flex; + min-width: 0; + align-items: flex-start; + justify-content: space-between; + gap: 6px; + } + + .bb-browser-context-comment-heading { + display: flex; + flex: 1; + min-width: 0; + align-items: center; + gap: 7px; + cursor: grab; + touch-action: none; + user-select: none; + } + + .bb-browser-context-comment-heading:active { + cursor: grabbing; + } + + .bb-browser-context-comment-heading > svg { + width: 12px; + height: 16px; + flex: none; + color: var(--muted-foreground, #667085); + } + + .bb-browser-context-comment-heading > span:last-child { + display: grid; + min-width: 0; + } + + .bb-browser-context-comment-heading strong { + font-size: 12px; + font-weight: 650; + } + + .bb-browser-context-comment-heading small { + overflow: hidden; + color: var(--muted-foreground, #667085); + font-size: 11px; + text-overflow: ellipsis; + white-space: nowrap; + } + + .bb-browser-context-cancel { + display: inline-flex; + width: 26px; + height: 26px; + flex: none; + align-items: center; + justify-content: center; + border: 0; + border-radius: 6px; + background: transparent; + color: var(--muted-foreground, #667085); + cursor: pointer; + } + + .bb-browser-context-cancel:hover:not(:disabled) { + background: var(--state-hover, #f2f4f7); + color: var(--foreground, #101828); + } + + .bb-browser-context-cancel:focus-visible { + outline: 2px solid var(--ring, #4c76e8); + outline-offset: 1px; + } + + .bb-browser-context-cancel:disabled { + cursor: wait; + opacity: 0.5; + } + + .bb-browser-context-cancel svg { + width: 14px; + height: 14px; + } + + .bb-browser-context-comment-card textarea { + min-height: 72px; + resize: vertical; + padding: 9px 10px; + border: 1px solid var(--border, #d0d5dd); + border-radius: 8px; + background: var(--background, #fff); + color: var(--foreground, #101828); + font: inherit; + outline: none; + } + + .bb-browser-context-comment-card textarea:focus-visible { + border-color: var(--ring, #4c76e8); + box-shadow: 0 0 0 2px + color-mix(in srgb, var(--ring, #4c76e8) 24%, transparent); + } + + .bb-browser-context-error { + margin: 0; + font-size: 11px; + } + + .bb-browser-context-error { + color: var(--destructive, #b42318); + } + + .bb-browser-context-review-actions { + display: flex; + justify-content: flex-end; + gap: 6px; + margin-top: 2px; + } + + .bb-browser-context-review-actions button { + height: 30px; + padding: 0 10px; + border: 1px solid var(--border, #d0d5dd); + border-radius: 7px; + background: var(--background, #fff); + color: var(--foreground, #101828); + cursor: pointer; + font: + 600 11px/1 Inter, + system-ui, + sans-serif; + } + + .bb-browser-context-review-actions button:hover:not(:disabled) { + background: var(--state-hover, #f2f4f7); + } + + .bb-browser-context-review-actions button:focus-visible { + outline: 2px solid var(--ring, #4c76e8); + outline-offset: 2px; + } + + .bb-browser-context-review-actions button:disabled { + cursor: wait; + opacity: 0.55; + } + + .bb-browser-context-review-actions .bb-browser-context-primary { + border-color: #3867e8; + background: #3867e8; + color: #fff; + } + + .bb-browser-context-review-actions + .bb-browser-context-primary:hover:not(:disabled) { + background: #2d58cf; + } +} + +@keyframes bb-browser-context-activate { + 0% { + opacity: 0.62; + transform: scale(0.88); + } + + 100% { + opacity: 1; + transform: scale(1); + } +} + +@media (prefers-reduced-motion: reduce) { + [data-bb-plugin="browser-context"] + .bb-browser-context-action[aria-pressed="true"] + svg { + animation: none; + } +} diff --git a/plugins/browser-context/app.test.tsx b/plugins/browser-context/app.test.tsx new file mode 100644 index 0000000..a627884 --- /dev/null +++ b/plugins/browser-context/app.test.tsx @@ -0,0 +1,395 @@ +// @vitest-environment jsdom + +import { cleanup, fireEvent, screen, waitFor } from "@testing-library/react"; +import type { + ExperimentalBrowserInspectionResult, + PluginBrowserActionProps, +} from "@bb/plugin-sdk/app"; +import { + loadPluginApp, + renderSlot, + type PluginRpcTestHandlers, +} from "@bb/plugin-sdk/testing/app"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import type { rpcContract } from "./server.js"; + +const capture: ExperimentalBrowserInspectionResult = { + version: 1, + kind: "element", + page: { + url: "https://example.com/settings", + title: "Settings", + viewport: { width: 800, height: 600 }, + scroll: { x: 0, y: 0 }, + }, + rect: { x: 40, y: 50, width: 180, height: 36 }, + element: { + selector: "button#save", + tag: "button", + id: "save", + classNames: ["primary"], + rect: { x: 40, y: 50, width: 180, height: 36 }, + dom: '', + text: "Save", + styles: { display: "inline-flex" }, + accessibility: { + source: "dom-hint", + roleHint: "button", + nameHint: "Save", + attributes: {}, + }, + reactComponentStack: ["SaveButton"], + }, + region: null, + screenshot: { + dataUrl: "data:image/png;base64,aQ==", + pixelSize: { width: 1600, height: 1200 }, + deviceScaleFactor: 2, + pageZoom: 1, + cssToImageScale: { x: 2, y: 2 }, + }, +}; + +const regionCapture: ExperimentalBrowserInspectionResult = { + ...capture, + kind: "region", + element: null, + region: { + elements: [ + { + selector: "main > section", + tag: "section", + id: null, + classNames: ["settings"], + text: "Settings", + rect: { x: 20, y: 30, width: 420, height: 260 }, + }, + ], + }, +}; + +function preparedCapture(comment: string) { + return { + promptText: [ + comment, + "", + '> Browser context · + + +