diff --git a/.bb/plugins.json b/.bb/plugins.json index bdce92d..3f59590 100644 --- a/.bb/plugins.json +++ b/.bb/plugins.json @@ -3,7 +3,7 @@ "schemaVersion": 1, "name": "phosphor-bb-plugins", "plugins": [ - { "name": "agentation", "source": "./plugins/agentation" }, + { "name": "agentation-mentions", "source": "./plugins/agentation-mentions" }, { "name": "perspectives", "source": "./plugins/perspectives" }, { "name": "sticky-notes", "source": "./plugins/sticky-notes" }, { "name": "bb-ui-reference", "source": "./plugins/bb-ui-reference" } diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 31e0e1f..6e41d07 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,13 +8,13 @@ on: required: true type: choice options: - - agentation + - agentation-mentions - perspectives - sticky-notes - bb-ui-reference push: tags: - - "agentation/v*" + - "agentation-mentions/v*" - "perspectives/v*" - "sticky-notes/v*" - "bb-ui-reference/v*" @@ -52,7 +52,7 @@ jobs: run: | plugin="${REQUESTED_PLUGIN:-${GITHUB_REF_NAME%%/*}}" case "$plugin" in - agentation) workspace="@phosphorco/bb-plugin-agentation" ;; + agentation-mentions) workspace="@phosphorco/bb-plugin-agentation-mentions" ;; perspectives) workspace="@phosphorco/bb-plugin-perspectives" ;; sticky-notes) workspace="@phosphorco/bb-plugin-sticky-notes" ;; bb-ui-reference) workspace="@phosphorco/bb-plugin-bb-ui-reference" ;; diff --git a/.github/workflows/validate-pull-request.yml b/.github/workflows/validate-pull-request.yml new file mode 100644 index 0000000..464c7d1 --- /dev/null +++ b/.github/workflows/validate-pull-request.yml @@ -0,0 +1,44 @@ +name: Validate pull request + +on: + pull_request: + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Check out the candidate + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + + - name: Set up Node and npm + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: "24" + package-manager-cache: false + + - name: Install locked dependencies + run: npm ci + + - name: Test + run: npm run test + + - name: Typecheck + run: npm run typecheck + + - name: Build BB artifacts + run: npm run build + + - name: Inspect Agentation Mentions package + run: npm pack --dry-run --ignore-scripts --workspace @phosphorco/bb-plugin-agentation-mentions + + - name: Inspect Perspectives package + run: npm pack --dry-run --ignore-scripts --workspace @phosphorco/bb-plugin-perspectives + + - name: Inspect Sticky Notes package + run: npm pack --dry-run --ignore-scripts --workspace @phosphorco/bb-plugin-sticky-notes + + - name: Inspect BB UI Reference package + run: npm pack --dry-run --ignore-scripts --workspace @phosphorco/bb-plugin-bb-ui-reference diff --git a/README.md b/README.md index c10475a..f55d218 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ | Plugin | Purpose | |---|---| -| [Agentation](plugins/agentation/) | Turn visual feedback on any bb surface into structured annotations an agent can act on and resolve. | +| [Agentation → Mentions](plugins/agentation-mentions/) | Based on Agentation by Scott Sunarto; adds native mentions, queued delivery, and verified identity tags. | | [Perspectives](plugins/perspectives/) | Consult independent expert agents and synthesize evidence across caller-selected lenses. | | [Sticky Notes](plugins/sticky-notes/) | Leave shared, movable notes directly on bb threads. | | [BB UI Reference](plugins/bb-ui-reference/) | Explore BB's plugin surfaces and active semantic theme palette. | @@ -18,10 +18,10 @@ Each plugin is published independently to npm by its prefixed release tag: ```sh -bb plugin install npm:@phosphorco/bb-plugin-agentation@^0.1.0 -bb plugin install npm:@phosphorco/bb-plugin-perspectives@^0.1.0 -bb plugin install npm:@phosphorco/bb-plugin-sticky-notes@^0.1.0 -bb plugin install npm:@phosphorco/bb-plugin-bb-ui-reference@^0.1.0 +bb plugin install npm:@phosphorco/bb-plugin-agentation-mentions@^0.1.0 +bb plugin install npm:@phosphorco/bb-plugin-perspectives@^0.2.0 +bb plugin install npm:@phosphorco/bb-plugin-sticky-notes@^0.1.1 +bb plugin install npm:@phosphorco/bb-plugin-bb-ui-reference@^0.1.1 ``` The plugins are also submitted to the BB Community marketplace for installation from bb. @@ -46,12 +46,12 @@ npm run build Install a development checkout by name: ```sh -bb plugin install path:. --plugin agentation --yes +bb plugin install path:. --plugin agentation-mentions --yes ``` ## Licensing and provenance Phosphor-authored plugins are MIT licensed. Agentation preserves its upstream attribution and plugin-scoped third-party notices, including the PolyForm -Shield 1.0.0 terms of its `agentation` dependency. See each plugin directory +Shield 1.0.0 terms of its vendored `agentation` dependency. See each plugin directory for its exact license and notices. diff --git a/package-lock.json b/package-lock.json index ed147a8..adf0ea1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "plugins/*" ], "devDependencies": { - "bb-app": "0.38.0" + "bb-app": "0.39.0" } }, "node_modules/@anthropic-ai/sdk": { @@ -2989,8 +2989,8 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@phosphorco/bb-plugin-agentation": { - "resolved": "plugins/agentation", + "node_modules/@phosphorco/bb-plugin-agentation-mentions": { + "resolved": "plugins/agentation-mentions", "link": true }, "node_modules/@phosphorco/bb-plugin-bb-ui-reference": { @@ -3577,24 +3577,6 @@ "node": ">= 14" } }, - "node_modules/agentation": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/agentation/-/agentation-3.0.2.tgz", - "integrity": "sha512-iGzBxFVTuZEIKzLY6AExSLAQH6i6SwxV4pAu7v7m3X6bInZ7qlZXAwrEqyc4+EfP4gM7z2RXBF6SF4DeH0f2lA==", - "license": "PolyForm-Shield-1.0.0", - "peerDependencies": { - "react": ">=18.0.0", - "react-dom": ">=18.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, "node_modules/aria-hidden": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", @@ -3639,9 +3621,9 @@ "license": "MIT" }, "node_modules/bb-app": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/bb-app/-/bb-app-0.38.0.tgz", - "integrity": "sha512-Gz56Qkr+dZxivXE+RAQXmQkLb77jUDuFzfnk00PS8a2/0FffmfHuhrjgnwaMryHox8Y+weOz7lqPT9vD0xqR9Q==", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/bb-app/-/bb-app-0.39.0.tgz", + "integrity": "sha512-9hiKByqN+NlcoM31yNbDTB+K7s5dxAd9aSM3pUnxTQONuHWU+3cvfCxQIaXfuB85YRstDwEeFNXpGFyVbwnhYg==", "dev": true, "os": [ "darwin", @@ -5234,8 +5216,8 @@ "zod": "^3.25.28 || ^4" } }, - "plugins/agentation": { - "name": "@phosphorco/bb-plugin-agentation", + "plugins/agentation-mentions": { + "name": "@phosphorco/bb-plugin-agentation-mentions", "version": "0.1.0", "license": "MIT AND PolyForm-Shield-1.0.0", "dependencies": { @@ -5243,7 +5225,6 @@ "@hugeicons/react": "^1.1.6", "@radix-ui/react-dialog": "^1.1.19", "@radix-ui/react-slot": "^1.3.0", - "agentation": "^3.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "sonner": "^2.0.7", @@ -5263,13 +5244,13 @@ "typescript": "^5.9.3" }, "engines": { - "bb": ">=0.37", - "bbPluginSdk": "^0.4.1" + "bb": ">=0.39.0 <1.0.0", + "bbPluginSdk": ">=0.4.8" } }, "plugins/bb-ui-reference": { "name": "@phosphorco/bb-plugin-bb-ui-reference", - "version": "0.1.0", + "version": "0.1.1", "license": "MIT", "devDependencies": { "@types/node": "^22", @@ -5284,7 +5265,7 @@ }, "plugins/perspectives": { "name": "@phosphorco/bb-plugin-perspectives", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "dependencies": { "zod": "4.3.6" @@ -5294,13 +5275,13 @@ "typescript": "^5.9.3" }, "engines": { - "bb": ">=0.37", - "bbPluginSdk": "^0.4.1" + "bb": ">=0.39.0 <1.0.0", + "bbPluginSdk": ">=0.4.8" } }, "plugins/sticky-notes": { "name": "@phosphorco/bb-plugin-sticky-notes", - "version": "0.1.0", + "version": "0.1.1", "license": "MIT", "dependencies": { "zod": "4.3.6" diff --git a/package.json b/package.json index 21adb5f..70838e9 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,6 @@ "typecheck": "npm run typecheck --workspaces --if-present" }, "devDependencies": { - "bb-app": "0.38.0" + "bb-app": "0.39.0" } } diff --git a/plugins/agentation-mentions/CHANGELOG.md b/plugins/agentation-mentions/CHANGELOG.md new file mode 100644 index 0000000..ca2a0b7 --- /dev/null +++ b/plugins/agentation-mentions/CHANGELOG.md @@ -0,0 +1,41 @@ +# Upstream Agentation changelog + +Imported from Agentation by Scott Sunarto through version 0.2.2; derivative +releases use the separate `agentation-mentions` package and tag prefix. + +## 0.2.2 + +### Patch Changes + +- 1432728: Support bb 0.39. The engines range is no longer pinned to one minor: it now floors at the tested bb release and excludes only the next major (`>=0.39.0 <1.0.0`), so future bb minors load without a plugin update. Built against plugin SDK 0.4.8. + +## 0.2.1 + +### Patch Changes + +- 186c131: Make the release tag installable. Every import the server bundle pulls in at + runtime is now a real `dependencies` entry, so `bb plugin install` from a git + tag resolves it. The previous tags built only inside this workspace, where a + hoisted `node_modules` supplied what the manifests had left out as devDependencies — + a fresh checkout of the tag failed the build with `Could not resolve "zod"`. +- 186c131: Vendor the patched upstream instead of relying on a Bun patch. `patchedDependencies` + is a workspace-install feature; a consumer installing the tag got the unpatched + package. The modified copy now lives at `vendor/agentation` with its changes + recorded in `vendor/agentation.patch` and its PolyForm Shield licence beside it. + +## 0.2.0 + +### Minor Changes + +- b3ed493: Require bb 0.38 and take the SDK types from the published `@get-bb/plugin-sdk` + package. `engines.bb` is now `>=0.38.0 <0.39.0`, so an older bb no longer + installs these plugins. + + Agent Proxy gains a `routingStrategy` setting (`round-robin`, `fill-first`, or + `weighted-round-robin`) that it writes to the core `config.yaml`. Pick + `fill-first` to keep several Claude OAuth accounts from rotating away the + upstream prompt cache. + +### Patch Changes + +- 65ececd: Release the runtime, presentation, notification, theme, and thread workflow updates. diff --git a/plugins/agentation/LICENSE b/plugins/agentation-mentions/LICENSE similarity index 100% rename from plugins/agentation/LICENSE rename to plugins/agentation-mentions/LICENSE diff --git a/plugins/agentation-mentions/README.md b/plugins/agentation-mentions/README.md new file mode 100644 index 0000000..dbfbc0d --- /dev/null +++ b/plugins/agentation-mentions/README.md @@ -0,0 +1,107 @@ +# Agentation → Mentions + +**Based on Agentation by Scott Sunarto.** This derivative starts from +[`@smsunarto/bb-plugin-agentation` 0.2.2](https://github.com/smsunarto/bb-plugins/tree/8bc27b91333e2228607b137d09b195d90e6aecfa/plugins/agentation) +at exact revision `8bc27b91333e2228607b137d09b195d90e6aecfa`. Scott's canonical +**Agentation** marketplace listing is the maintained visual-annotation plugin. +This separately named package preserves that behavior and adds a bb-native +delivery workflow; `agentation-mentions` is a namespace distinction, not a +claim that this is an unrelated product. + +The original MIT license, the modified `agentation@3.0.2` PolyForm Shield +license, the vendor patch, and complete third-party notices ship with this +plugin. + +## What the derivative adds + +- A composer action attaches staged annotations as a native bb mention. The + mention resolves the current annotation bodies when the prompt is submitted. +- `bb agentation-mentions send --queue` uses bb's durable queued-message API; + omitting `--queue` sends immediately. +- When Identity Boundaries is present, capture stores the author's identity ID + and delivery verifies its current profile before adding the standard + `[from=] ... [/from=]` identity frame. Feedback still works without + that optional plugin and is then left unattributed. +- Every agent tool and CLI surface uses the distinct `agentation_mentions_*` + / `bb agentation-mentions` identity, so it can coexist with canonical + Agentation without tool or command collisions. + +Everything else remains recognizably Agentation: a toolbar overlays bb and +plugin surfaces, captures AFS 1.1 annotations with bb route/plugin context, +stores them in the plugin database, exposes a review panel, updates open +windows in real time, and lets agents acknowledge, reply, resolve, or dismiss +feedback. + +## Install + +```sh +bb plugin install npm:@phosphorco/bb-plugin-agentation-mentions@^0.1.0 +``` + +From this repository: + +```sh +npm ci +npm run build --workspace @phosphorco/bb-plugin-agentation-mentions +bb plugin install path:. --plugin agentation-mentions --yes +``` + +## Use it + +1. Open the toolbar at the bottom-right of bb, select an element, and write the + feedback. The annotation records its DOM selector, available React/source + path, bb route, and owning plugin. +2. In a thread composer, use the Agentation → Mentions action to insert the + staged batch, then submit or queue the prompt normally. Deleting the mention + leaves the annotations staged. +3. An agent reads the supplied batch with the `agentation_mentions_*` tools and + resolves each item after fixing it. The marker disappears from open windows. + +The review panel retains cross-page history and reply threads. Staged feedback +can also be assigned from the CLI: + +```text +bb agentation-mentions pending [--plugin ] [--json] +bb agentation-mentions staged [--json] +bb agentation-mentions send [--queue] [annotationId…] +bb agentation-mentions restage +bb agentation-mentions sessions +bb agentation-mentions show +bb agentation-mentions acknowledge +bb agentation-mentions resolve [summary…] +bb agentation-mentions dismiss +bb agentation-mentions reply +bb agentation-mentions toolbar [on|off] +``` + +## Data, network, and authority + +- Annotation bodies, routing, replies, identity IDs, and retention state are + stored locally in this plugin's bb database/KV namespace. +- The toolbar and review panel use bb's plugin RPC, realtime, and same-origin + event stream. The plugin contacts no third-party service of its own. +- Delivery mutates only the target bb thread: immediate mode sends a prompt; + queue mode adds a queued prompt. Identity lookup is read-only and optional. +- Agent tools can change annotation status and replies. They do not edit source + code themselves; the calling agent's normal permissions govern any fix. + +Resolved annotations are retained for seven days by default. Toolbar visibility +is live state and can be changed with `bb agentation-mentions toolbar on|off`. + +## Artwork + +The rich in-plugin logo and marketplace icon are Cole-approved AM02-A. The +package preserves the generated 1254px RGB source and the verified RGB24 +nearest-neighbor 16/24/32px derivatives byte-for-byte under `assets/`. + +## Develop + +```sh +bb plugin dev plugins/agentation-mentions +npm run test --workspace @phosphorco/bb-plugin-agentation-mentions +npm run typecheck --workspace @phosphorco/bb-plugin-agentation-mentions +``` + +See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) and +[vendor/README.md](vendor/README.md) for the canonical reduction and React 19 +component-detection patch. diff --git a/plugins/agentation/THIRD_PARTY_NOTICES.md b/plugins/agentation-mentions/THIRD_PARTY_NOTICES.md similarity index 84% rename from plugins/agentation/THIRD_PARTY_NOTICES.md rename to plugins/agentation-mentions/THIRD_PARTY_NOTICES.md index 13389c9..2e73853 100644 --- a/plugins/agentation/THIRD_PARTY_NOTICES.md +++ b/plugins/agentation-mentions/THIRD_PARTY_NOTICES.md @@ -1,23 +1,26 @@ # Third-party notices +Agentation → Mentions is based on Agentation by Scott Sunarto, exact upstream +revision `8bc27b91333e2228607b137d09b195d90e6aecfa` (Agentation 0.2.2). + This plugin ships bundled builds (`dist/server.js`, `dist/app.js`, `dist/app.css`) and generated icon artwork in `assets/`. The upstream works below are included in those files. Their licences apply to those parts, in addition to the plugin's own [MIT licence](LICENSE). -| Component | Where it is included | Licence | -|---|---|---| -| [agentation](https://www.npmjs.com/package/agentation) | `dist/app.js` | PolyForm Shield 1.0.0 | -| [class-variance-authority](https://www.npmjs.com/package/class-variance-authority) | `dist/app.js` | Apache-2.0 | -| [zod](https://www.npmjs.com/package/zod) | `dist/server.js` | MIT | -| [clsx](https://www.npmjs.com/package/clsx) | `dist/app.js` | MIT | -| [tailwind-merge](https://www.npmjs.com/package/tailwind-merge) | `dist/app.js` | MIT | -| [@radix-ui/react-slot](https://www.npmjs.com/package/@radix-ui/react-slot), [@radix-ui/react-dialog](https://www.npmjs.com/package/@radix-ui/react-dialog) | `dist/app.js` | MIT | -| [vaul](https://www.npmjs.com/package/vaul) | `dist/app.js` | MIT | -| [sonner](https://www.npmjs.com/package/sonner) | `dist/app.js` | MIT | -| [@hugeicons/react](https://www.npmjs.com/package/@hugeicons/react), [@hugeicons/core-free-icons](https://www.npmjs.com/package/@hugeicons/core-free-icons) | `dist/app.js`, `assets/*.svg` | MIT | -| [shadcn/ui](https://github.com/shadcn-ui/ui) | `dist/app.js`, `dist/app.css` | MIT | -| [bb](https://github.com/get-bb/bb) plugin-registry components | `dist/app.js`, `dist/app.css` | MIT | +| Component | Where it is included | Licence | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | --------------------- | +| [agentation](https://www.npmjs.com/package/agentation), modified — see [`vendor/README.md`](vendor/README.md) | `dist/app.js`, `vendor/agentation/` | PolyForm Shield 1.0.0 | +| [class-variance-authority](https://www.npmjs.com/package/class-variance-authority) | `dist/app.js` | Apache-2.0 | +| [zod](https://www.npmjs.com/package/zod) | `dist/server.js` | MIT | +| [clsx](https://www.npmjs.com/package/clsx) | `dist/app.js` | MIT | +| [tailwind-merge](https://www.npmjs.com/package/tailwind-merge) | `dist/app.js` | MIT | +| [@radix-ui/react-slot](https://www.npmjs.com/package/@radix-ui/react-slot), [@radix-ui/react-dialog](https://www.npmjs.com/package/@radix-ui/react-dialog) | `dist/app.js` | MIT | +| [vaul](https://www.npmjs.com/package/vaul) | `dist/app.js` | MIT | +| [sonner](https://www.npmjs.com/package/sonner) | `dist/app.js` | MIT | +| [@hugeicons/react](https://www.npmjs.com/package/@hugeicons/react), [@hugeicons/core-free-icons](https://www.npmjs.com/package/@hugeicons/core-free-icons) | `dist/app.js` | MIT | +| [shadcn/ui](https://github.com/shadcn-ui/ui) | `dist/app.js`, `dist/app.css` | MIT | +| [bb](https://github.com/get-bb/bb) plugin-registry components | `dist/app.js`, `dist/app.css` | MIT | ## PolyForm Shield License 1.0.0 @@ -80,8 +83,8 @@ bb Copyright (c) 2026 Michael Yong ``` The `components/ui/` sources of this plugin are derived from shadcn/ui and from -the bb app's own plugin-registry components. The icons in `assets/` are generated -from `@hugeicons/core-free-icons`. +the bb app's own plugin-registry components. The plugin identity PNGs are +original approved artwork and are not derived from Hugeicons. ```text MIT License diff --git a/plugins/agentation/app.css b/plugins/agentation-mentions/app.css similarity index 100% rename from plugins/agentation/app.css rename to plugins/agentation-mentions/app.css diff --git a/plugins/agentation/app.tsx b/plugins/agentation-mentions/app.tsx similarity index 87% rename from plugins/agentation/app.tsx rename to plugins/agentation-mentions/app.tsx index a8c8098..85c98fc 100644 --- a/plugins/agentation/app.tsx +++ b/plugins/agentation-mentions/app.tsx @@ -1,10 +1,10 @@ -// @smsunarto/bb-plugin-agentation — frontend entry. +// Agentation → Mentions — based on Agentation by Scott Sunarto at 8bc27b91333e2228607b137d09b195d90e6aecfa. // -// Two surfaces, one job: +// Three surfaces, one job: // - a content script that mounts the Agentation toolbar over the bb app shell, // so every route and every plugin-drawn element can be annotated; -// - a prompt action that shows the live staged count and attaches that feedback -// as a native mention, resolved into durable agent context at submission. +// - a prompt action that attaches staged feedback as a native mention; +// - the current upstream review panel for triage, replies, and outcomes. import "./app.css"; import { useCallback, useEffect, useState, useSyncExternalStore } from "react"; import { @@ -17,6 +17,10 @@ import { import { toast } from "sonner"; import { AgentationSettingsSection } from "@/components/settings-section.tsx"; +import { + AnnotationPanel, + AnnotationPanelHeader, +} from "@/components/annotation-panel.tsx"; import { Button } from "@/components/ui/button.tsx"; import { Icon } from "@/components/ui/icon.tsx"; import { @@ -125,7 +129,7 @@ export default definePluginApp((app) => { }); app.composer.customize({ - id: "agentation", + id: "agentation-mentions", scopes: ["thread"], actions: [{ id: "add-annotations", component: AgentationPromptAction }], richText: { @@ -146,6 +150,15 @@ export default definePluginApp((app) => { }, }); + app.slots.navPanel({ + id: "annotations", + title: "Agentation → Mentions", + icon: "ChatFeedback", + path: "annotations", + component: AnnotationPanel, + headerContent: AnnotationPanelHeader, + }); + app.slots.settingsSection({ id: "about", title: "How this works", diff --git a/plugins/agentation-mentions/assets/icon-16.png b/plugins/agentation-mentions/assets/icon-16.png new file mode 100644 index 0000000..b363b55 Binary files /dev/null and b/plugins/agentation-mentions/assets/icon-16.png differ diff --git a/plugins/agentation-mentions/assets/icon-24.png b/plugins/agentation-mentions/assets/icon-24.png new file mode 100644 index 0000000..eb89b51 Binary files /dev/null and b/plugins/agentation-mentions/assets/icon-24.png differ diff --git a/plugins/agentation-mentions/assets/icon-32.png b/plugins/agentation-mentions/assets/icon-32.png new file mode 100644 index 0000000..53e8fbc Binary files /dev/null and b/plugins/agentation-mentions/assets/icon-32.png differ diff --git a/plugins/agentation-mentions/assets/icon-source.png b/plugins/agentation-mentions/assets/icon-source.png new file mode 100644 index 0000000..ae8b559 Binary files /dev/null and b/plugins/agentation-mentions/assets/icon-source.png differ diff --git a/plugins/agentation/components.json b/plugins/agentation-mentions/components.json similarity index 90% rename from plugins/agentation/components.json rename to plugins/agentation-mentions/components.json index 66918b1..35397b2 100644 --- a/plugins/agentation/components.json +++ b/plugins/agentation-mentions/components.json @@ -16,6 +16,6 @@ "hooks": "@/hooks" }, "registries": { - "@bb": "https://raw.githubusercontent.com/get-bb/bb/desktop-v0.37.0/packages/plugin-registry/r/{name}.json" + "@bb": "https://raw.githubusercontent.com/get-bb/bb/desktop-v0.39.0/packages/plugin-registry/r/{name}.json" } } diff --git a/plugins/agentation-mentions/components/annotation-panel.tsx b/plugins/agentation-mentions/components/annotation-panel.tsx new file mode 100644 index 0000000..9e22fdc --- /dev/null +++ b/plugins/agentation-mentions/components/annotation-panel.tsx @@ -0,0 +1,486 @@ +// The review panel: everything the toolbar collected, in one place. +// +// The toolbar is good at pointing at a thing and bad at reading a backlog, so +// the panel owns the other half — triage across pages, assignment state, and +// the reply thread associated with each annotation. + +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useBbNavigate, useRealtime, useRpc } from "@bb/plugin-sdk/app"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { cn } from "@/lib/utils"; +import type { rpcContract } from "@/server.ts"; +import type { + AnnotationRouting, + AnnotationStatus, + SessionSummary, + StoredAnnotation, +} from "@/lib/afs.ts"; + +type Rpc = ReturnType>; + +const statusFilters = [ + { id: "open", label: "Open", statuses: ["pending", "acknowledged"] }, + { id: "resolved", label: "Resolved", statuses: ["resolved"] }, + { id: "dismissed", label: "Dismissed", statuses: ["dismissed"] }, + { id: "all", label: "All", statuses: null }, +] as const; + +type FilterId = (typeof statusFilters)[number]["id"]; + +const statusTone: Record = { + pending: "text-foreground", + acknowledged: "text-primary", + resolved: "text-muted-foreground", + dismissed: "text-muted-foreground", +}; + +function Pill({ children, className }: { children: React.ReactNode; className?: string }) { + return ( + + {children} + + ); +} + +function useAnnotations(filter: FilterId) { + const rpc = useRpc(); + const [annotations, setAnnotations] = useState([]); + const [routings, setRoutings] = useState>({}); + const [sessions, setSessions] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const statuses = useMemo( + () => statusFilters.find((entry) => entry.id === filter)?.statuses ?? null, + [filter], + ); + + const refresh = useCallback(async () => { + try { + const [annotationResult, sessionResult] = await Promise.all([ + rpc.call("listAnnotations", { + sessionId: null, + statuses: statuses ? [...statuses] : null, + pluginId: null, + }), + rpc.call("listSessions", { status: null }), + ]); + setAnnotations(annotationResult.annotations); + setRoutings(annotationResult.routings); + setSessions(sessionResult.sessions); + setError(null); + } catch (cause) { + setError(cause instanceof Error ? cause.message : String(cause)); + } finally { + setIsLoading(false); + } + }, [rpc, statuses]); + + useEffect(() => { + void refresh(); + }, [refresh]); + + useRealtime( + "annotations", + useCallback(() => { + void refresh(); + }, [refresh]), + ); + + return { annotations, routings, sessions, isLoading, error, refresh }; +} + +function AnnotationCard({ + annotation, + routing, + rpc, + onChanged, +}: { + annotation: StoredAnnotation; + routing: AnnotationRouting | undefined; + rpc: Rpc; + onChanged: () => void; +}) { + const navigate = useBbNavigate(); + const [reply, setReply] = useState(""); + const [isBusy, setIsBusy] = useState(false); + + const run = async (work: () => Promise, failure: string) => { + setIsBusy(true); + try { + await work(); + onChanged(); + } catch (cause) { + toast.error(failure, { + description: cause instanceof Error ? cause.message : String(cause), + }); + } finally { + setIsBusy(false); + } + }; + + const mutate = (action: "acknowledge" | "resolve" | "dismiss" | "delete") => + run( + () => + rpc.call("mutateAnnotation", { + annotationId: annotation.id, + action, + note: null, + }), + `Could not ${action} the annotation`, + ); + + const isClosed = annotation.status === "resolved" || annotation.status === "dismissed"; + const isAssigned = routing?.state === "assigned" && routing.assignedThreadId !== null; + + return ( +
+
+ {annotation.element} + {annotation.status} + {routing ? ( + + {routing.state === "assigned" ? `assigned: ${routing.assignedThreadId}` : routing.state} + + ) : null} + {annotation.severity ? {annotation.severity} : null} + {annotation.intent ? {annotation.intent} : null} + {annotation.bb.pluginId ? `plugin: ${annotation.bb.pluginId}` : "bb shell"} + {annotation.bb.surface ? {annotation.bb.surface} : null} +
+ +

{annotation.comment}

+ +
+
+
Selector:
+
{annotation.elementPath}
+
+ {annotation.reactComponents ? ( +
+
React:
+
{annotation.reactComponents}
+
+ ) : null} + {annotation.sourceFile ? ( +
+
Source:
+
{annotation.sourceFile}
+
+ ) : null} + {annotation.selectedText ? ( +
+
Selected:
+
“{annotation.selectedText}”
+
+ ) : null} +
+ + {annotation.thread.length > 0 ? ( +
    + {annotation.thread.map((message) => ( +
  • + + {message.role} + + {message.content} +
  • + ))} +
+ ) : null} + + {annotation.resolution ? ( +

+ Outcome: {annotation.resolution} +

+ ) : null} + +
+ {!isClosed ? ( + <> + + + + ) : ( + + )} + + {routing?.assignedThreadId ? ( + + ) : null} + {annotation.bb.threadId && annotation.bb.threadId !== routing?.assignedThreadId ? ( + + ) : null} + {!isClosed && isAssigned ? ( + + ) : null} +
+ +
{ + event.preventDefault(); + const message = reply.trim(); + if (message.length === 0) return; + void run( + () => + rpc + .call("replyToAnnotation", { + annotationId: annotation.id, + message, + }) + .then(() => setReply("")), + "Could not post the reply", + ); + }} + > + setReply(event.target.value)} + /> + +
+
+ ); +} + +export function AnnotationPanel() { + const rpc = useRpc(); + const [filter, setFilter] = useState("open"); + const { annotations, routings, sessions, isLoading, error, refresh } = useAnnotations(filter); + + const grouped = useMemo(() => { + const bySession = new Map(); + for (const annotation of annotations) { + const bucket = bySession.get(annotation.sessionId); + if (bucket) bucket.push(annotation); + else bySession.set(annotation.sessionId, [annotation]); + } + return [...bySession.entries()]; + }, [annotations]); + + const sessionsById = useMemo( + () => new Map(sessions.map((session) => [session.id, session])), + [sessions], + ); + + return ( +
+
+
+ {statusFilters.map((entry) => ( + + ))} +
+ + {error ? ( +

{error}

+ ) : isLoading ? ( +

Loading annotations…

+ ) : grouped.length === 0 ? ( +
+

No annotations here yet.

+

+ Open the Agentation toolbar in the bottom-right corner of bb, click any element — + including one drawn by a plugin — and describe what should change. +

+
+ ) : ( + grouped.map(([sessionId, sessionAnnotations]) => { + const session = sessionsById.get(sessionId); + return ( +
+
+

+ {session?.route ?? sessionId} +

+ + {sessionAnnotations.length} annotation + {sessionAnnotations.length === 1 ? "" : "s"} + +
+
+ {sessionAnnotations.map((annotation) => ( + void refresh()} + /> + ))} +
+
+ ); + }) + )} +
+
+ ); +} + +/** Rendered by the host into the shared panel title bar. */ +export function AnnotationPanelHeader() { + const rpc = useRpc(); + const [enabled, setEnabled] = useState(null); + const [pending, setPending] = useState(0); + + const load = useCallback(async () => { + try { + const result = await rpc.call("getConfig"); + setEnabled(result.config.toolbarEnabled); + setPending(result.counts.pending); + } catch { + setEnabled(null); + } + }, [rpc]); + + useEffect(() => { + void load(); + }, [load]); + + useRealtime( + "annotations", + useCallback(() => { + void load(); + }, [load]), + ); + + return ( +
+ {pending > 0 ? {pending} open : null} + +
+ ); +} + +/** Rendered under the declarative settings form on the plugin's detail page. */ +export function AgentationSettingsSection() { + return ( +
+

+ The toolbar mounts over the whole bb app, so it can annotate the shell and any plugin + surface. Elements drawn by a plugin are attributed to that plugin automatically. +

+

+ New annotations enter a shared staging area. Every open thread shows the staged batch above + its composer, where you can assign the batch to that thread. +

+

+ Agents read assigned feedback with the agentation_mentions_* tools or{" "} + bb agentation-mentions pending. Resolving an annotation removes its marker from every + open bb window. +

+
+ ); +} diff --git a/plugins/agentation/components/settings-section.tsx b/plugins/agentation-mentions/components/settings-section.tsx similarity index 89% rename from plugins/agentation/components/settings-section.tsx rename to plugins/agentation-mentions/components/settings-section.tsx index 066b148..6fb51cb 100644 --- a/plugins/agentation/components/settings-section.tsx +++ b/plugins/agentation-mentions/components/settings-section.tsx @@ -13,7 +13,7 @@ export function AgentationSettingsSection() { and send the native prompt.

- Agents use the agentation_* tools to read, reply to, and + Agents use the agentation_mentions_* tools to read, reply to, and resolve feedback. Resolving an annotation removes its marker from every open bb window.

diff --git a/plugins/agentation/components/ui/button.tsx b/plugins/agentation-mentions/components/ui/button.tsx similarity index 80% rename from plugins/agentation/components/ui/button.tsx rename to plugins/agentation-mentions/components/ui/button.tsx index c1e423b..82ab5f5 100644 --- a/plugins/agentation/components/ui/button.tsx +++ b/plugins/agentation-mentions/components/ui/button.tsx @@ -12,12 +12,9 @@ const buttonVariants = cva( variants: { variant: { default: "bg-foreground text-background hover:bg-foreground/90", - destructive: - "bg-destructive text-destructive-foreground hover:bg-destructive/90", - outline: - "border border-input bg-transparent hover:bg-state-hover hover:text-foreground", - secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: "border border-input bg-transparent hover:bg-state-hover hover:text-foreground", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", ghost: "hover:bg-state-hover hover:text-foreground aria-pressed:bg-state-active aria-pressed:text-foreground aria-pressed:hover:bg-state-active data-[state=open]:bg-state-active data-[state=open]:text-foreground data-[state=open]:hover:bg-state-active", link: "text-primary underline-offset-4 hover:underline", @@ -47,11 +44,7 @@ const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : "button"; return ( - + ); }, ); diff --git a/plugins/agentation-mentions/components/ui/card.tsx b/plugins/agentation-mentions/components/ui/card.tsx new file mode 100644 index 0000000..b0f9890 --- /dev/null +++ b/plugins/agentation-mentions/components/ui/card.tsx @@ -0,0 +1,56 @@ +/* shadcn/ui-derived */ +import * as React from "react"; + +import { cn } from "../../lib/utils"; + +const Card = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardDescription.displayName = "CardDescription"; + +const CardContent = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardContent.displayName = "CardContent"; + +const CardFooter = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardFooter.displayName = "CardFooter"; + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; diff --git a/plugins/agentation/components/ui/coarse-pointer-sizing.ts b/plugins/agentation-mentions/components/ui/coarse-pointer-sizing.ts similarity index 81% rename from plugins/agentation/components/ui/coarse-pointer-sizing.ts rename to plugins/agentation-mentions/components/ui/coarse-pointer-sizing.ts index a896ee0..5b94651 100644 --- a/plugins/agentation/components/ui/coarse-pointer-sizing.ts +++ b/plugins/agentation-mentions/components/ui/coarse-pointer-sizing.ts @@ -1,23 +1,17 @@ -export const COARSE_POINTER_TEXT_BASE_CLASS = - "text-sm max-md:pointer-coarse:text-base"; +export const COARSE_POINTER_TEXT_BASE_CLASS = "text-sm max-md:pointer-coarse:text-base"; -export const COARSE_POINTER_TEXT_SM_CLASS = - "text-xs max-md:pointer-coarse:text-sm"; +export const COARSE_POINTER_TEXT_SM_CLASS = "text-xs max-md:pointer-coarse:text-sm"; -export const COARSE_POINTER_ICON_SIZE_CLASS = - "size-4 max-md:pointer-coarse:size-5"; +export const COARSE_POINTER_ICON_SIZE_CLASS = "size-4 max-md:pointer-coarse:size-5"; -export const COARSE_POINTER_ICON_SIZE_SHRINK_CLASS = - "size-4 shrink-0 max-md:pointer-coarse:size-5"; +export const COARSE_POINTER_ICON_SIZE_SHRINK_CLASS = "size-4 shrink-0 max-md:pointer-coarse:size-5"; -export const COARSE_POINTER_COMPACT_ICON_SIZE_CLASS = - "size-3.5 max-md:pointer-coarse:size-5"; +export const COARSE_POINTER_COMPACT_ICON_SIZE_CLASS = "size-3.5 max-md:pointer-coarse:size-5"; export const COARSE_POINTER_COMPACT_ICON_SIZE_SHRINK_CLASS = "size-3.5 shrink-0 max-md:pointer-coarse:size-5"; -export const COARSE_POINTER_DOT_SIZE_CLASS = - "size-1.5 max-md:pointer-coarse:size-2"; +export const COARSE_POINTER_DOT_SIZE_CLASS = "size-1.5 max-md:pointer-coarse:size-2"; export const COARSE_POINTER_GLYPH_BOX_CLASS = "h-4 w-4 max-md:pointer-coarse:h-5 max-md:pointer-coarse:w-5"; @@ -60,11 +54,9 @@ export const COARSE_POINTER_PROMPT_ICON_ACTION_BUTTON_CLASS = export const COARSE_POINTER_PROMPT_COMBO_BUTTON_CLASS = "h-8 w-8 rounded-l-none border-l border-l-primary-foreground/20 px-0 transition-all hover:border-l-primary-foreground/30 max-md:pointer-coarse:h-10 max-md:pointer-coarse:w-10"; -export const COARSE_POINTER_INPUT_HEIGHT_CLASS = - "h-9 max-md:pointer-coarse:h-10"; +export const COARSE_POINTER_INPUT_HEIGHT_CLASS = "h-9 max-md:pointer-coarse:h-10"; -export const COARSE_POINTER_COMPACT_ROW_HEIGHT_CLASS = - "h-7 max-md:pointer-coarse:h-9"; +export const COARSE_POINTER_COMPACT_ROW_HEIGHT_CLASS = "h-7 max-md:pointer-coarse:h-9"; export const COARSE_POINTER_ROW_HEIGHT_CLASS = "h-[var(--bb-sidebar-row-height)] max-md:pointer-coarse:h-[var(--bb-sidebar-row-height-coarse)]"; diff --git a/plugins/agentation/components/ui/dialog.tsx b/plugins/agentation-mentions/components/ui/dialog.tsx similarity index 86% rename from plugins/agentation/components/ui/dialog.tsx rename to plugins/agentation-mentions/components/ui/dialog.tsx index b039dcd..a7df61e 100644 --- a/plugins/agentation/components/ui/dialog.tsx +++ b/plugins/agentation-mentions/components/ui/dialog.tsx @@ -28,12 +28,11 @@ import { Icon } from "../../components/ui/icon.js"; // Context — separate instance from DropdownMenu / Popover. // --------------------------------------------------------------------------- -const ResponsiveDialogContext = - React.createContext({ - isCompactViewport: false, - open: false, - onOpenChange: () => {}, - }); +const ResponsiveDialogContext = React.createContext({ + isCompactViewport: false, + open: false, + onOpenChange: () => {}, +}); function useResponsiveDialog() { return React.useContext(ResponsiveDialogContext); @@ -54,20 +53,12 @@ function Dialog({ const body = ctx.isCompactViewport ? ( children ) : ( - + {children} ); - return ( - - {body} - - ); + return {body}; } // --------------------------------------------------------------------------- @@ -129,9 +120,7 @@ const DialogClose = React.forwardRef( if (isCompactViewport) { const Comp = asChild ? Slot : "button"; - const handleClick: React.MouseEventHandler = ( - event, - ) => { + const handleClick: React.MouseEventHandler = (event) => { onClick?.(event); if (!event.defaultPrevented) { onOpenChange(false); @@ -145,12 +134,7 @@ const DialogClose = React.forwardRef( } return ( - + {children} ); @@ -185,9 +169,7 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; // Content // --------------------------------------------------------------------------- -type DialogContentProps = React.ComponentPropsWithoutRef< - typeof DialogPrimitive.Content ->; +type DialogContentProps = React.ComponentPropsWithoutRef; const DialogContent = React.forwardRef( ({ className, children, ...props }, ref) => { @@ -246,26 +228,14 @@ DialogContent.displayName = "DialogContent"; // Header / Footer — layout primitives, unchanged. // --------------------------------------------------------------------------- -const DialogHeader = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
+const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( +
); DialogHeader.displayName = "DialogHeader"; -const DialogFooter = ({ - className, - ...props -}: React.HTMLAttributes) => ( +const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
); @@ -285,10 +255,7 @@ const DialogTitle = React.forwardRef< return ( ); @@ -300,16 +267,8 @@ const DialogDescription = React.forwardRef< React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => { const { isCompactViewport } = useResponsiveDialog(); - const Comp = isCompactViewport - ? DrawerDescriptionPrimitive - : DialogPrimitive.Description; - return ( - - ); + const Comp = isCompactViewport ? DrawerDescriptionPrimitive : DialogPrimitive.Description; + return ; }); DialogDescription.displayName = DialogPrimitive.Description.displayName; diff --git a/plugins/agentation/components/ui/drawer.tsx b/plugins/agentation-mentions/components/ui/drawer.tsx similarity index 84% rename from plugins/agentation/components/ui/drawer.tsx rename to plugins/agentation-mentions/components/ui/drawer.tsx index bdcf3d3..5a77d60 100644 --- a/plugins/agentation/components/ui/drawer.tsx +++ b/plugins/agentation-mentions/components/ui/drawer.tsx @@ -4,19 +4,13 @@ import { Drawer as DrawerPrimitive } from "vaul"; import { cn } from "../../lib/utils"; import { usePortalScopeProps } from "../../lib/portal-scope"; -import { - getOverlayTriggerClassName, - preventOverlayTriggerSelection, -} from "./overlay-trigger.js"; +import { getOverlayTriggerClassName, preventOverlayTriggerSelection } from "./overlay-trigger.js"; const Drawer = ({ shouldScaleBackground = false, ...props }: React.ComponentProps) => ( - + ); Drawer.displayName = "Drawer"; @@ -46,10 +40,7 @@ const DrawerOverlay = React.forwardRef< // Portaled outside every plugin mount; re-attach the plugin CSS scope // when rendered from a plugin slot (see portal-scope.ts). {...usePortalScopeProps()} - className={cn( - "fixed inset-0 z-50 bg-black/40 backdrop-blur-[1px]", - className, - )} + className={cn("fixed inset-0 z-50 bg-black/40 backdrop-blur-[1px]", className)} {...props} /> )); @@ -70,9 +61,7 @@ const DrawerContent = React.forwardRef< )} {...props} > - + {children} @@ -85,10 +74,7 @@ const DrawerTitle = React.forwardRef< >(({ className, ...props }, ref) => ( )); diff --git a/plugins/agentation/components/ui/hooks/use-compact-viewport.tsx b/plugins/agentation-mentions/components/ui/hooks/use-compact-viewport.tsx similarity index 89% rename from plugins/agentation/components/ui/hooks/use-compact-viewport.tsx rename to plugins/agentation-mentions/components/ui/hooks/use-compact-viewport.tsx index e2adfae..767cce5 100644 --- a/plugins/agentation/components/ui/hooks/use-compact-viewport.tsx +++ b/plugins/agentation-mentions/components/ui/hooks/use-compact-viewport.tsx @@ -1,9 +1,4 @@ -import { - createContext, - createElement, - useContext, - type ReactNode, -} from "react"; +import { createContext, createElement, useContext, type ReactNode } from "react"; import { useMediaQuery } from "./use-media-query.js"; diff --git a/plugins/agentation/components/ui/hooks/use-media-query.ts b/plugins/agentation-mentions/components/ui/hooks/use-media-query.ts similarity index 89% rename from plugins/agentation/components/ui/hooks/use-media-query.ts rename to plugins/agentation-mentions/components/ui/hooks/use-media-query.ts index 11076ad..9ff4768 100644 --- a/plugins/agentation/components/ui/hooks/use-media-query.ts +++ b/plugins/agentation-mentions/components/ui/hooks/use-media-query.ts @@ -42,19 +42,14 @@ function createMediaQueryRef(query: string): MediaQueryRef | null { return ref; } -function subscribeMediaQuery( - query: string, - notify: () => void, -): () => void { +function subscribeMediaQuery(query: string, notify: () => void): () => void { return createMediaQueryRef(query)?.subscribe(notify) ?? (() => {}); } function getMediaQuerySnapshot(query: string): boolean { if (typeof window === "undefined") return false; - return ( - mediaQueryCache.get(query)?.mql.matches ?? window.matchMedia(query).matches - ); + return mediaQueryCache.get(query)?.mql.matches ?? window.matchMedia(query).matches; } export function useMediaQuery(query: string): boolean { diff --git a/plugins/agentation/components/ui/hooks/use-pointer-coarse.ts b/plugins/agentation-mentions/components/ui/hooks/use-pointer-coarse.ts similarity index 100% rename from plugins/agentation/components/ui/hooks/use-pointer-coarse.ts rename to plugins/agentation-mentions/components/ui/hooks/use-pointer-coarse.ts diff --git a/plugins/agentation/components/ui/icon.tsx b/plugins/agentation-mentions/components/ui/icon.tsx similarity index 100% rename from plugins/agentation/components/ui/icon.tsx rename to plugins/agentation-mentions/components/ui/icon.tsx diff --git a/plugins/agentation/components/ui/input.tsx b/plugins/agentation-mentions/components/ui/input.tsx similarity index 100% rename from plugins/agentation/components/ui/input.tsx rename to plugins/agentation-mentions/components/ui/input.tsx diff --git a/plugins/agentation/components/ui/motion.ts b/plugins/agentation-mentions/components/ui/motion.ts similarity index 91% rename from plugins/agentation/components/ui/motion.ts rename to plugins/agentation-mentions/components/ui/motion.ts index e40d4c6..b448472 100644 --- a/plugins/agentation/components/ui/motion.ts +++ b/plugins/agentation-mentions/components/ui/motion.ts @@ -15,7 +15,6 @@ * Reach for one of these rather than a bare `transition-colors` on anything with * a hover/active state. */ -export const CONTROL_HOVER_TRANSITION = - "transition-colors duration-150 hover:duration-0"; +export const CONTROL_HOVER_TRANSITION = "transition-colors duration-150 hover:duration-0"; export const LIST_HOVER_TRANSITION = "transition-none"; diff --git a/plugins/agentation/components/ui/overlay-trigger.ts b/plugins/agentation-mentions/components/ui/overlay-trigger.ts similarity index 92% rename from plugins/agentation/components/ui/overlay-trigger.ts rename to plugins/agentation-mentions/components/ui/overlay-trigger.ts index 09ecb64..758b7c9 100644 --- a/plugins/agentation/components/ui/overlay-trigger.ts +++ b/plugins/agentation-mentions/components/ui/overlay-trigger.ts @@ -17,25 +17,17 @@ const NON_TEXT_INPUT_TYPES = new Set([ "submit", ]); -export const getOverlayTriggerClassName: OverlayTriggerClassNameResolver = ( - className, -) => cn(OVERLAY_TRIGGER_CLASS_NAME, className); +export const getOverlayTriggerClassName: OverlayTriggerClassNameResolver = (className) => + cn(OVERLAY_TRIGGER_CLASS_NAME, className); function isKeyboardInputElement(element: Element): element is HTMLElement { if (element instanceof HTMLTextAreaElement) return true; if (element instanceof HTMLInputElement) { - return ( - !element.disabled && - !element.readOnly && - !NON_TEXT_INPUT_TYPES.has(element.type) - ); + return !element.disabled && !element.readOnly && !NON_TEXT_INPUT_TYPES.has(element.type); } if (!(element instanceof HTMLElement)) return false; - return ( - element.isContentEditable || - element.closest("[contenteditable='true']") !== null - ); + return element.isContentEditable || element.closest("[contenteditable='true']") !== null; } /** diff --git a/plugins/agentation/components/ui/responsive-overlay.tsx b/plugins/agentation-mentions/components/ui/responsive-overlay.tsx similarity index 82% rename from plugins/agentation/components/ui/responsive-overlay.tsx rename to plugins/agentation-mentions/components/ui/responsive-overlay.tsx index ebeb9fd..1c7864f 100644 --- a/plugins/agentation/components/ui/responsive-overlay.tsx +++ b/plugins/agentation-mentions/components/ui/responsive-overlay.tsx @@ -26,9 +26,7 @@ const ResponsiveDrawerDepthContext = React.createContext(0); const SONNER_TOASTER_SELECTOR = "[data-sonner-toaster]"; type DrawerContentPointerDownOutsideEvent = Parameters< - NonNullable< - React.ComponentPropsWithoutRef["onPointerDownOutside"] - > + NonNullable["onPointerDownOutside"]> >[0]; function resetDrawerKeyboardStyles(drawerElement: HTMLElement | null): void { @@ -39,10 +37,7 @@ function resetDrawerKeyboardStyles(drawerElement: HTMLElement | null): void { } function isSonnerToasterPointerTarget(target: EventTarget | null): boolean { - return ( - target instanceof Element && - target.closest(SONNER_TOASTER_SELECTOR) !== null - ); + return target instanceof Element && target.closest(SONNER_TOASTER_SELECTOR) !== null; } // --------------------------------------------------------------------------- @@ -96,61 +91,29 @@ interface MobileTriggerProps { export const MobileTrigger = React.forwardRef< HTMLButtonElement, - MobileTriggerProps & - Omit< - React.ButtonHTMLAttributes, - keyof MobileTriggerProps - > ->( - ( - { - asChild, - open, - onOpenChange, - haspopup, - onClick, - children, - className, - ...domProps - }, - ref, - ) => { - const triggerClassName = getOverlayTriggerClassName(className); - const handleClick: React.MouseEventHandler = (e) => { - onClick?.(e); - if (!e.defaultPrevented) { - if (!open) { - blurActiveKeyboardInputBeforeOverlayOpen(); - } - onOpenChange(!open); + MobileTriggerProps & Omit, keyof MobileTriggerProps> +>(({ asChild, open, onOpenChange, haspopup, onClick, children, className, ...domProps }, ref) => { + const triggerClassName = getOverlayTriggerClassName(className); + const handleClick: React.MouseEventHandler = (e) => { + onClick?.(e); + if (!e.defaultPrevented) { + if (!open) { + blurActiveKeyboardInputBeforeOverlayOpen(); } - }; - - const ariaProps = { - "aria-expanded": open, - "aria-haspopup": haspopup, - "data-state": open ? "open" : "closed", - } as const; - - if (asChild) { - return ( - - {children} - - ); + onOpenChange(!open); } + }; + const ariaProps = { + "aria-expanded": open, + "aria-haspopup": haspopup, + "data-state": open ? "open" : "closed", + } as const; + + if (asChild) { return ( - + ); - }, -); + } + + return ( + + ); +}); MobileTrigger.displayName = "MobileTrigger"; // --------------------------------------------------------------------------- @@ -191,9 +168,7 @@ const RADIX_CONTENT_PROP_NAMES = [ type RadixContentPropName = (typeof RADIX_CONTENT_PROP_NAMES)[number]; -const RADIX_CONTENT_KEYS: ReadonlySet = new Set( - RADIX_CONTENT_PROP_NAMES, -); +const RADIX_CONTENT_KEYS: ReadonlySet = new Set(RADIX_CONTENT_PROP_NAMES); export function stripRadixContentProps>( props: T, @@ -271,16 +246,15 @@ export function ResponsiveDrawerShell({ }, [onOpenChange, resetClosingKeyboardState], ); - const handleContentAnimationEnd = - React.useCallback>( - (event) => { - if (event.currentTarget !== event.target) { - return; - } - onContentAnimationEnd?.(open); - }, - [onContentAnimationEnd, open], - ); + const handleContentAnimationEnd = React.useCallback>( + (event) => { + if (event.currentTarget !== event.target) { + return; + } + onContentAnimationEnd?.(open); + }, + [onContentAnimationEnd, open], + ); const handleOpenAutoFocus = React.useCallback( (event: Event) => { if (isPointerCoarse) { @@ -322,9 +296,7 @@ export function ResponsiveDrawerShell({ onPointerDownOutside={handlePointerDownOutside} > - {srLabel !== undefined ? ( - {srLabel} - ) : null} + {srLabel !== undefined ? {srLabel} : null} {children} diff --git a/plugins/agentation-mentions/docs/media/capture.png b/plugins/agentation-mentions/docs/media/capture.png new file mode 100644 index 0000000..ed7a1d9 Binary files /dev/null and b/plugins/agentation-mentions/docs/media/capture.png differ diff --git a/plugins/agentation-mentions/docs/media/hero.png b/plugins/agentation-mentions/docs/media/hero.png new file mode 100644 index 0000000..abcefa8 Binary files /dev/null and b/plugins/agentation-mentions/docs/media/hero.png differ diff --git a/plugins/agentation-mentions/docs/media/staging.png b/plugins/agentation-mentions/docs/media/staging.png new file mode 100644 index 0000000..4fded33 Binary files /dev/null and b/plugins/agentation-mentions/docs/media/staging.png differ diff --git a/plugins/agentation/hooks/useBrowserDimmingModal.ts b/plugins/agentation-mentions/hooks/useBrowserDimmingModal.ts similarity index 100% rename from plugins/agentation/hooks/useBrowserDimmingModal.ts rename to plugins/agentation-mentions/hooks/useBrowserDimmingModal.ts diff --git a/plugins/agentation/lib/afs.ts b/plugins/agentation-mentions/lib/afs.ts similarity index 92% rename from plugins/agentation/lib/afs.ts rename to plugins/agentation-mentions/lib/afs.ts index 2062ad2..a06e70d 100644 --- a/plugins/agentation/lib/afs.ts +++ b/plugins/agentation-mentions/lib/afs.ts @@ -10,23 +10,9 @@ import { z } from "zod"; export const AFS_VERSION = "1.1"; -export const annotationIntents = [ - "fix", - "change", - "question", - "approve", -] as const; -export const annotationSeverities = [ - "blocking", - "important", - "suggestion", -] as const; -export const annotationStatuses = [ - "pending", - "acknowledged", - "resolved", - "dismissed", -] as const; +export const annotationIntents = ["fix", "change", "question", "approve"] as const; +export const annotationSeverities = ["blocking", "important", "suggestion"] as const; +export const annotationStatuses = ["pending", "acknowledged", "resolved", "dismissed"] as const; export const annotationKinds = ["feedback", "placement", "rearrange"] as const; export type AnnotationIntent = (typeof annotationIntents)[number]; @@ -35,10 +21,7 @@ export type AnnotationStatus = (typeof annotationStatuses)[number]; export type AnnotationKind = (typeof annotationKinds)[number]; /** Statuses the browser toolbar stops drawing a marker for. */ -export const closedStatuses: readonly AnnotationStatus[] = [ - "resolved", - "dismissed", -]; +export const closedStatuses: readonly AnnotationStatus[] = ["resolved", "dismissed"]; const rectSchema = z.object({ x: z.number(), @@ -152,11 +135,7 @@ export const storedAnnotationSchema = z.looseObject({ export type StoredAnnotation = z.infer; -export const annotationRoutingStates = [ - "staged", - "sending", - "assigned", -] as const; +export const annotationRoutingStates = ["staged", "sending", "assigned"] as const; export const annotationRoutingSchema = z.object({ annotationId: z.string(), diff --git a/plugins/agentation/lib/annotation-hygiene.ts b/plugins/agentation-mentions/lib/annotation-hygiene.ts similarity index 95% rename from plugins/agentation/lib/annotation-hygiene.ts rename to plugins/agentation-mentions/lib/annotation-hygiene.ts index bda2792..533fa63 100644 --- a/plugins/agentation/lib/annotation-hygiene.ts +++ b/plugins/agentation-mentions/lib/annotation-hygiene.ts @@ -48,7 +48,5 @@ export function selectOrphans( knownToServer: ReadonlySet, synced: ReadonlySet, ): T[] { - return local.filter( - (item) => !knownToServer.has(item.id) && !synced.has(item.id), - ); + return local.filter((item) => !knownToServer.has(item.id) && !synced.has(item.id)); } diff --git a/plugins/agentation/lib/attachment.ts b/plugins/agentation-mentions/lib/attachment.ts similarity index 100% rename from plugins/agentation/lib/attachment.ts rename to plugins/agentation-mentions/lib/attachment.ts diff --git a/plugins/agentation/lib/delivery.ts b/plugins/agentation-mentions/lib/delivery.ts similarity index 100% rename from plugins/agentation/lib/delivery.ts rename to plugins/agentation-mentions/lib/delivery.ts diff --git a/plugins/agentation/lib/identity.ts b/plugins/agentation-mentions/lib/identity.ts similarity index 100% rename from plugins/agentation/lib/identity.ts rename to plugins/agentation-mentions/lib/identity.ts diff --git a/plugins/agentation/lib/markdown.ts b/plugins/agentation-mentions/lib/markdown.ts similarity index 78% rename from plugins/agentation/lib/markdown.ts rename to plugins/agentation-mentions/lib/markdown.ts index 2f0c566..fe0737d 100644 --- a/plugins/agentation/lib/markdown.ts +++ b/plugins/agentation-mentions/lib/markdown.ts @@ -34,10 +34,7 @@ function describeKind(annotation: StoredAnnotation): string | null { } /** One annotation as a self-contained markdown section. */ -export function renderAnnotation( - annotation: StoredAnnotation, - index?: number, -): string { +export function renderAnnotation(annotation: StoredAnnotation, index?: number): string { const heading = index === undefined ? `### ${annotation.element} — ${annotation.id}` @@ -49,14 +46,8 @@ export function renderAnnotation( out += line("React", annotation.reactComponents); out += line("Source", annotation.sourceFile); out += line("Classes", annotation.cssClasses); - out += line( - "Selected text", - annotation.selectedText ? `"${annotation.selectedText}"` : null, - ); - out += line( - "Nearby text", - annotation.selectedText ? null : annotation.nearbyText?.slice(0, 160), - ); + out += line("Selected text", annotation.selectedText ? `"${annotation.selectedText}"` : null); + out += line("Nearby text", annotation.selectedText ? null : annotation.nearbyText?.slice(0, 160)); out += line("Intent", annotation.intent); out += line("Severity", annotation.severity); out += line("Status", annotation.status); @@ -94,9 +85,7 @@ export function renderAnnotations( else bySession.set(annotation.sessionId, [annotation]); } - const sessionsById = new Map( - (options.sessions ?? []).map((session) => [session.id, session]), - ); + const sessionsById = new Map((options.sessions ?? []).map((session) => [session.id, session])); let out = `## ${options.title ?? "bb UI feedback"}\n\n`; out += `${annotations.length} annotation${annotations.length === 1 ? "" : "s"} across ${bySession.size} page${bySession.size === 1 ? "" : "s"}. Element selectors are live bb DOM paths — pair them with the owning plugin or the bb app source to find the code.\n`; @@ -113,11 +102,22 @@ export function renderAnnotations( return out.trimEnd(); } +/** A self-contained assignment sent directly to one bb thread. */ +export function renderAnnotationAssignment( + annotations: StoredAnnotation[], + sessions: Session[], +): string { + const markdown = renderAnnotations(annotations, { + title: "bb UI feedback from Agentation", + sessions, + }); + + return `${markdown}\n\nThe annotations above are the complete batch assigned to this thread. Work only on these annotation IDs. Do not call \`agentation_mentions_get_all_pending\`; it can include feedback assigned to other threads. Resolve each item with the \`agentation_mentions_resolve\` tool once it is fixed, or use \`agentation_mentions_reply\` if you need a decision from me.`; +} + /** One line per annotation, for CLI listings and tool summaries. */ export function renderAnnotationLine(annotation: StoredAnnotation): string { - const owner = annotation.bb.pluginId - ? `plugin:${annotation.bb.pluginId}` - : "bb-shell"; + const owner = annotation.bb.pluginId ? `plugin:${annotation.bb.pluginId}` : "bb-shell"; const severity = annotation.severity ? ` [${annotation.severity}]` : ""; const comment = annotation.comment.replace(/\s+/g, " ").slice(0, 100); return `${annotation.id} ${annotation.status.padEnd(12)} ${owner.padEnd(24)} ${annotation.element.padEnd(10)}${severity} ${comment}`; diff --git a/plugins/agentation/lib/plugin-rpc.ts b/plugins/agentation-mentions/lib/plugin-rpc.ts similarity index 97% rename from plugins/agentation/lib/plugin-rpc.ts rename to plugins/agentation-mentions/lib/plugin-rpc.ts index cfaeb5e..d374be3 100644 --- a/plugins/agentation/lib/plugin-rpc.ts +++ b/plugins/agentation-mentions/lib/plugin-rpc.ts @@ -49,9 +49,7 @@ export function createRpcClient( }, ); - const envelope = (await response.json().catch(() => null)) as - | RpcEnvelope - | null; + const envelope = (await response.json().catch(() => null)) as RpcEnvelope | null; if (!envelope?.ok) { throw new PluginRpcCallError( diff --git a/plugins/agentation/lib/portal-scope.ts b/plugins/agentation-mentions/lib/portal-scope.ts similarity index 93% rename from plugins/agentation/lib/portal-scope.ts rename to plugins/agentation-mentions/lib/portal-scope.ts index 88c1102..11fff4c 100644 --- a/plugins/agentation/lib/portal-scope.ts +++ b/plugins/agentation-mentions/lib/portal-scope.ts @@ -21,8 +21,7 @@ export function usePortalScopeProps(): { "data-bb-plugin-root"?: ""; "data-bb-plugin"?: string; } { - const pluginId = - typeof __BB_PLUGIN_ID__ === "string" ? __BB_PLUGIN_ID__ : undefined; + const pluginId = typeof __BB_PLUGIN_ID__ === "string" ? __BB_PLUGIN_ID__ : undefined; return { "data-bb-portaled-overlay": "", "data-bb-plugin-root": "", diff --git a/plugins/agentation/lib/route.ts b/plugins/agentation-mentions/lib/route.ts similarity index 100% rename from plugins/agentation/lib/route.ts rename to plugins/agentation-mentions/lib/route.ts diff --git a/plugins/agentation/lib/staging.ts b/plugins/agentation-mentions/lib/staging.ts similarity index 95% rename from plugins/agentation/lib/staging.ts rename to plugins/agentation-mentions/lib/staging.ts index a903dbc..384d5f4 100644 --- a/plugins/agentation/lib/staging.ts +++ b/plugins/agentation-mentions/lib/staging.ts @@ -115,10 +115,7 @@ export function discardStagedAnnotations( })(); } -export function getAnnotationRouting( - db: Database, - annotationId: string, -): AnnotationRouting | null { +export function getAnnotationRouting(db: Database, annotationId: string): AnnotationRouting | null { const row = db .prepare(`SELECT * FROM annotation_routing WHERE annotation_id = ?`) .get(annotationId) as RoutingRow | undefined; @@ -138,9 +135,7 @@ export function listAnnotationRoutings( WHERE annotation_id IN (${placeholders})`, ) .all(...uniqueIds) as RoutingRow[]; - return Object.fromEntries( - rows.map((row) => [row.annotation_id, toRouting(row)]), - ); + return Object.fromEntries(rows.map((row) => [row.annotation_id, toRouting(row)])); } /** @@ -221,11 +216,7 @@ export function completeDispatch(db: Database, dispatchId: string): number { })(); } -export function failDispatch( - db: Database, - dispatchId: string, - error: string, -): number { +export function failDispatch(db: Database, dispatchId: string, error: string): number { return db.transaction(() => { const timestamp = nowIso(); const result = db @@ -281,10 +272,7 @@ export function recoverInterruptedDispatches(db: Database): number { return recovered; } -export function restageAnnotation( - db: Database, - annotationId: string, -): AnnotationRouting | null { +export function restageAnnotation(db: Database, annotationId: string): AnnotationRouting | null { const timestamp = nowIso(); const result = db .prepare( diff --git a/plugins/agentation/lib/store.ts b/plugins/agentation-mentions/lib/store.ts similarity index 92% rename from plugins/agentation/lib/store.ts rename to plugins/agentation-mentions/lib/store.ts index b074969..c7d3a0a 100644 --- a/plugins/agentation/lib/store.ts +++ b/plugins/agentation-mentions/lib/store.ts @@ -205,14 +205,7 @@ export function openSession(db: Database, input: OpenSessionInput): Session { `UPDATE sessions SET url = ?, title = COALESCE(?, title), thread_id = ?, project_id = ?, updated_at = ? WHERE id = ?`, - ).run( - input.url, - input.title, - input.threadId, - input.projectId, - timestamp, - existing.id, - ); + ).run(input.url, input.title, input.threadId, input.projectId, timestamp, existing.id); return toSession({ ...existing, url: input.url, @@ -262,9 +255,11 @@ export function getSession(db: Database, sessionId: string): Session | null { function touchSession(db: Database, sessionId: string): number { const seq = nextSeq(db); - db.prepare( - `UPDATE sessions SET mutation_seq = ?, updated_at = ? WHERE id = ?`, - ).run(seq, nowIso(), sessionId); + db.prepare(`UPDATE sessions SET mutation_seq = ?, updated_at = ? WHERE id = ?`).run( + seq, + nowIso(), + sessionId, + ); return seq; } @@ -384,13 +379,10 @@ function writeAnnotation(db: Database, annotation: StoredAnnotation): void { }); } -export function getAnnotation( - db: Database, - annotationId: string, -): StoredAnnotation | null { - const row = db - .prepare(`SELECT payload FROM annotations WHERE id = ?`) - .get(annotationId) as { payload: string } | undefined; +export function getAnnotation(db: Database, annotationId: string): StoredAnnotation | null { + const row = db.prepare(`SELECT payload FROM annotations WHERE id = ?`).get(annotationId) as + | { payload: string } + | undefined; return row ? readAnnotation(row.payload) : null; } @@ -409,10 +401,7 @@ export interface UpsertAnnotationInput { * never rewinds a status the agent already advanced, and never drops the reply * thread. */ -export function upsertAnnotation( - db: Database, - input: UpsertAnnotationInput, -): StoredAnnotation { +export function upsertAnnotation(db: Database, input: UpsertAnnotationInput): StoredAnnotation { const existing = getAnnotation(db, input.annotation.id); const timestamp = nowIso(); const incoming = sanitizeJson(input.annotation); @@ -452,10 +441,7 @@ export interface SetStatusInput { resolution?: string | null; } -export function setAnnotationStatus( - db: Database, - input: SetStatusInput, -): StoredAnnotation | null { +export function setAnnotationStatus(db: Database, input: SetStatusInput): StoredAnnotation | null { const existing = getAnnotation(db, input.annotationId); if (!existing) return null; @@ -463,8 +449,7 @@ export function setAnnotationStatus( const stored: StoredAnnotation = { ...existing, status: input.status, - resolution: - input.resolution === undefined ? existing.resolution : input.resolution, + resolution: input.resolution === undefined ? existing.resolution : input.resolution, updatedAt: timestamp, seq: nextSeq(db), }; @@ -508,16 +493,13 @@ export function appendThreadMessage( return stored; } -export function deleteAnnotations( - db: Database, - annotationIds: string[], -): number { +export function deleteAnnotations(db: Database, annotationIds: string[]): number { if (annotationIds.length === 0) return 0; const sessionIds = new Set(); for (const id of annotationIds) { - const row = db - .prepare(`SELECT session_id FROM annotations WHERE id = ?`) - .get(id) as { session_id: string } | undefined; + const row = db.prepare(`SELECT session_id FROM annotations WHERE id = ?`).get(id) as + | { session_id: string } + | undefined; if (row) sessionIds.add(row.session_id); } @@ -533,9 +515,7 @@ export function deleteAnnotations( } export function clearSession(db: Database, sessionId: string): number { - const result = db - .prepare(`DELETE FROM annotations WHERE session_id = ?`) - .run(sessionId); + const result = db.prepare(`DELETE FROM annotations WHERE session_id = ?`).run(sessionId); touchSession(db, sessionId); return result.changes; } @@ -562,9 +542,7 @@ export function listAnnotations( params.push(filter.sessionId); } if (filter.statuses && filter.statuses.length > 0) { - clauses.push( - `status IN (${filter.statuses.map(() => "?").join(", ")})`, - ); + clauses.push(`status IN (${filter.statuses.map(() => "?").join(", ")})`); params.push(...filter.statuses); } if (filter.pluginId) { @@ -593,9 +571,7 @@ export function listAnnotations( return rows.map((row) => readAnnotation(row.payload)); } -export function countByStatus( - db: Database, -): Record & { total: number } { +export function countByStatus(db: Database): Record & { total: number } { const rows = db .prepare(`SELECT status, COUNT(*) AS count FROM annotations GROUP BY status`) .all() as { status: AnnotationStatus; count: number }[]; @@ -621,9 +597,7 @@ export function countByStatus( * client can still see. */ export function pruneClosed(db: Database, retentionDays: number): number { - const cutoff = new Date( - Date.now() - retentionDays * 24 * 60 * 60 * 1000, - ).toISOString(); + const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000).toISOString(); const result = db .prepare( `DELETE FROM annotations diff --git a/plugins/agentation/lib/theme.ts b/plugins/agentation-mentions/lib/theme.ts similarity index 100% rename from plugins/agentation/lib/theme.ts rename to plugins/agentation-mentions/lib/theme.ts diff --git a/plugins/agentation/lib/toolbar-sync.ts b/plugins/agentation-mentions/lib/toolbar-sync.ts similarity index 94% rename from plugins/agentation/lib/toolbar-sync.ts rename to plugins/agentation-mentions/lib/toolbar-sync.ts index 438543e..67cc7cd 100644 --- a/plugins/agentation/lib/toolbar-sync.ts +++ b/plugins/agentation-mentions/lib/toolbar-sync.ts @@ -15,9 +15,7 @@ export interface ToolbarTextFieldState { * hidden. A saved webhook URL is durable settings state, not an annotation * draft, so it only blocks a remount while that field has the caret. */ -export function toolbarTextFieldIsBusy( - field: ToolbarTextFieldState, -): boolean { +export function toolbarTextFieldIsBusy(field: ToolbarTextFieldState): boolean { if (field.width === 0 || field.height === 0) return false; if (field.settingsField) return field.focused; return field.focused || field.value.trim() !== ""; @@ -92,9 +90,7 @@ export function isCurrentRouteRequest( currentRoute: string, currentRevision: number, ): boolean { - return ( - requestRoute === currentRoute && requestRevision === currentRevision - ); + return requestRoute === currentRoute && requestRevision === currentRevision; } /** Run mutations in call order, even when an earlier one is still in flight. */ @@ -155,9 +151,7 @@ export function upsertLocalAnnotation( ): T[] { const index = current.findIndex((item) => item.id === annotation.id); if (index === -1) return [...current, annotation]; - return current.map((item, itemIndex) => - itemIndex === index ? annotation : item, - ); + return current.map((item, itemIndex) => (itemIndex === index ? annotation : item)); } /** Remove Agentation's callback delta from its persisted local projection. */ @@ -187,9 +181,7 @@ export function recordPushAcknowledgement( } /** Every id in a complete server snapshot is known to be durable there. */ -export function recordSnapshotAcknowledgement( - knownToServer: ReadonlySet, -): Set { +export function recordSnapshotAcknowledgement(knownToServer: ReadonlySet): Set { return new Set(knownToServer); } diff --git a/plugins/agentation/lib/toolbar.ts b/plugins/agentation-mentions/lib/toolbar.ts similarity index 94% rename from plugins/agentation/lib/toolbar.ts rename to plugins/agentation-mentions/lib/toolbar.ts index 12fa73b..56db25a 100644 --- a/plugins/agentation/lib/toolbar.ts +++ b/plugins/agentation-mentions/lib/toolbar.ts @@ -13,13 +13,17 @@ import { createElement, type FunctionComponent } from "react"; import { createRoot, type Root } from "react-dom/client"; +// Imported by path, not by package name: the copy under vendor/ carries changes +// upstream does not ship (see vendor/README.md), and a path import is the only +// form both Bun and the npm install bb runs for a `git:` source resolve the same +// way. import { Agentation, loadAnnotations, saveAnnotations, type AgentationProps, type Annotation, -} from "agentation"; +} from "../vendor/agentation/dist/index.mjs"; import type { PluginContentScriptContext, PluginContentScriptDisposer, @@ -191,10 +195,7 @@ function loadSynced(route: string): Set { function saveSynced(route: string, ids: Iterable): void { try { - localStorage.setItem( - `${SYNCED_KEY_PREFIX}${route}`, - JSON.stringify([...ids]), - ); + localStorage.setItem(`${SYNCED_KEY_PREFIX}${route}`, JSON.stringify([...ids])); } catch { // Storage full or disabled. Losing the ledger costs a resurrected // annotation at worst, never a lost one. @@ -209,10 +210,7 @@ export async function mountAnnotationToolbar( // bb resolves system/custom themes before plugin content scripts mount. // Seed once; Agentation owns and persists every user change after this. - seedAgentationThemeDefault( - localStorage, - document.documentElement.classList.contains("dark"), - ); + seedAgentationThemeDefault(localStorage, document.documentElement.classList.contains("dark")); const host = document.createElement("div"); host.setAttribute("data-bb-agentation-host", ""); @@ -268,10 +266,7 @@ export async function mountAnnotationToolbar( // delta now so a fast server echo cannot reconcile against the old row. saveAnnotations( meta.route, - upsertLocalAnnotation( - loadAnnotations(meta.route), - annotation, - ), + upsertLocalAnnotation(loadAnnotations(meta.route), annotation), ); localMutationRevision += 1; latestRevisionById.set(annotation.id, localMutationRevision); @@ -291,10 +286,7 @@ export async function mountAnnotationToolbar( // that intent before any RPC response can trigger reconciliation. saveAnnotations( meta.route, - deleteLocalAnnotation( - loadAnnotations(meta.route), - annotation.id, - ), + deleteLocalAnnotation(loadAnnotations(meta.route), annotation.id), ); localMutationRevision += 1; latestRevisionById.set(annotation.id, localMutationRevision); @@ -404,11 +396,7 @@ export async function mountAnnotationToolbar( for (const item of group.upserts) { const id = item.annotation.id; if ( - !shouldRequeueOperation( - item.revision, - clearedThrough, - latestRevisionById.get(id) ?? null, - ) + !shouldRequeueOperation(item.revision, clearedThrough, latestRevisionById.get(id) ?? null) ) { continue; } @@ -440,12 +428,7 @@ export async function mountAnnotationToolbar( if (known) { if ( activationRevision !== null && - isCurrentRouteRequest( - page.route, - activationRevision, - meta.route, - routeRevision, - ) + isCurrentRouteRequest(page.route, activationRevision, meta.route, routeRevision) ) { sessionId = known; } @@ -475,12 +458,7 @@ export async function mountAnnotationToolbar( // A -> B -> A from the original, now-obsolete A request. if ( activationRevision !== null && - isCurrentRouteRequest( - page.route, - activationRevision, - meta.route, - routeRevision, - ) + isCurrentRouteRequest(page.route, activationRevision, meta.route, routeRevision) ) { sessionId = id; if (openedAnnotations !== null) { @@ -501,9 +479,7 @@ export async function mountAnnotationToolbar( } } - async function ensureSession( - revision = routeRevision, - ): Promise { + async function ensureSession(revision = routeRevision): Promise { return sessionFor(meta, revision); } @@ -529,9 +505,7 @@ export async function mountAnnotationToolbar( focused: document.activeElement === field, width: rect.width, height: rect.height, - settingsField: Boolean( - field.closest("[data-agentation-settings-panel]"), - ), + settingsField: Boolean(field.closest("[data-agentation-settings-panel]")), }) ) { return true; @@ -566,8 +540,7 @@ export async function mountAnnotationToolbar( if (toolbarIsBusy()) return "deferred"; const open = annotations.filter( - (annotation) => - annotation.status !== "resolved" && annotation.status !== "dismissed", + (annotation) => annotation.status !== "resolved" && annotation.status !== "dismissed", ); const local = loadAnnotations(route); @@ -676,12 +649,7 @@ export async function mountAnnotationToolbar( }); if ( !clearSucceeded && - isCurrentRouteRequest( - page.route, - activationRevision, - meta.route, - routeRevision, - ) + isCurrentRouteRequest(page.route, activationRevision, meta.route, routeRevision) ) { // Agentation cleared its local state before calling us. The server did // not change, so an ordinary pull at the old cursor would say unchanged @@ -735,9 +703,7 @@ export async function mountAnnotationToolbar( // A held-back change has no second event coming — the stream already fired // for it — so keep checking often until it can be applied. const delay = - streamHealthy && !reconcileDeferred && sessionId !== null - ? IDLE_POLL_MS - : FALLBACK_POLL_MS; + streamHealthy && !reconcileDeferred && sessionId !== null ? IDLE_POLL_MS : FALLBACK_POLL_MS; pollTimer = setTimeout(() => { pollTimer = null; void pullQueue.request(); @@ -803,15 +769,7 @@ export async function mountAnnotationToolbar( // drained before the switch — it will still reach the right session. void flush(); const id = await sessionFor(page, revision); - if ( - !id || - !isCurrentRouteRequest( - page.route, - revision, - meta.route, - routeRevision, - ) - ) { + if (!id || !isCurrentRouteRequest(page.route, revision, meta.route, routeRevision)) { return; } // A cached session has no fresh snapshot attached. Pull now instead of diff --git a/plugins/agentation/lib/utils.ts b/plugins/agentation-mentions/lib/utils.ts similarity index 89% rename from plugins/agentation/lib/utils.ts rename to plugins/agentation-mentions/lib/utils.ts index eb4120b..8510ace 100644 --- a/plugins/agentation/lib/utils.ts +++ b/plugins/agentation-mentions/lib/utils.ts @@ -18,7 +18,5 @@ export function formatHomePathForDisplay(pathValue: string): string { pathValue.match(/^\/home\/[^/]+(?=\/|$)/)?.[0] ?? pathValue.match(/^\/root(?=\/|$)/)?.[0] ?? pathValue.match(/^[A-Za-z]:[\\/]Users[\\/][^\\/]+(?=[\\/]|$)/i)?.[0]; - return homePrefix === undefined - ? pathValue - : `~${pathValue.slice(homePrefix.length)}`; + return homePrefix === undefined ? pathValue : `~${pathValue.slice(homePrefix.length)}`; } diff --git a/plugins/agentation/package.json b/plugins/agentation-mentions/package.json similarity index 56% rename from plugins/agentation/package.json rename to plugins/agentation-mentions/package.json index 9802d65..0c17019 100644 --- a/plugins/agentation/package.json +++ b/plugins/agentation-mentions/package.json @@ -1,26 +1,26 @@ { - "name": "@phosphorco/bb-plugin-agentation", + "name": "@phosphorco/bb-plugin-agentation-mentions", "version": "0.1.0", "type": "module", "license": "MIT AND PolyForm-Shield-1.0.0", - "description": "Point at any part of the bb UI and turn it into structured feedback that bb agents can read, answer, and resolve.", - "author": "Phosphor, derived from work by Scott Sunarto", - "homepage": "https://github.com/phosphorco/bb-community-plugins/tree/main/plugins/agentation#readme", + "description": "Based on Agentation by Scott Sunarto; adds bb mentions, queued delivery, and verified identity tags to visual feedback.", + "author": "Phosphor; based on Agentation by Scott Sunarto", + "homepage": "https://github.com/phosphorco/bb-community-plugins/tree/main/plugins/agentation-mentions#readme", "bugs": { "url": "https://github.com/phosphorco/bb-community-plugins/issues" }, "repository": { "type": "git", "url": "git+https://github.com/phosphorco/bb-community-plugins.git", - "directory": "plugins/agentation" + "directory": "plugins/agentation-mentions" }, "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" }, - "keywords": ["bb", "bb-plugin", "agentation", "annotations", "visual-feedback", "afs"], - "engines": { "bb": ">=0.37", "bbPluginSdk": "^0.4.1" }, + "keywords": ["bb", "bb-plugin", "agentation", "mentions", "queued-delivery", "identity-tags", "annotations", "visual-feedback", "afs"], + "engines": { "bb": ">=0.39.0 <1.0.0", "bbPluginSdk": ">=0.4.8" }, "bb": { - "name": "Agentation", - "description": "Point at any part of the bb UI and turn it into structured feedback that bb agents can read, answer, and resolve.", + "name": "Agentation → Mentions", + "description": "Based on Agentation by Scott Sunarto; adds bb mentions, queued delivery, and verified identity tags to visual feedback.", "branding": { - "icon": "./assets/icon.svg", - "logo": { "light": "./assets/logo.svg", "dark": "./assets/logo-dark.svg" } + "icon": "AtSign", + "logo": { "light": "./assets/icon-32.png" } }, "server": "./server.ts", "app": "./app.tsx", @@ -28,17 +28,19 @@ }, "phosphor": { "upstream": { + "name": "Agentation", + "author": "Scott Sunarto", "repository": "https://github.com/smsunarto/bb-plugins.git", - "revision": "e092db69a10ae159aed208b13303b1764683e03c" + "revision": "8bc27b91333e2228607b137d09b195d90e6aecfa", + "version": "0.2.2" } }, - "files": ["dist/", "assets/", "docs/", "skills/", "README.md", "LICENSE", "THIRD_PARTY_NOTICES.md", "server.ts", "app.tsx", "app.css", "lib/", "components/", "hooks/"], + "files": ["dist/", "assets/", "docs/", "skills/", "vendor/", "README.md", "LICENSE", "THIRD_PARTY_NOTICES.md", "CHANGELOG.md", "server.ts", "app.tsx", "app.css", "lib/", "components/", "hooks/"], "dependencies": { "@hugeicons/core-free-icons": "4.2.3", "@hugeicons/react": "^1.1.6", "@radix-ui/react-dialog": "^1.1.19", "@radix-ui/react-slot": "^1.3.0", - "agentation": "^3.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "sonner": "^2.0.7", diff --git a/plugins/agentation/server.ts b/plugins/agentation-mentions/server.ts similarity index 86% rename from plugins/agentation/server.ts rename to plugins/agentation-mentions/server.ts index 5328d1b..3cbbdf0 100644 --- a/plugins/agentation/server.ts +++ b/plugins/agentation-mentions/server.ts @@ -1,4 +1,4 @@ -// @smsunarto/bb-plugin-agentation — backend. +// Agentation → Mentions — based on Agentation by Scott Sunarto at 8bc27b91333e2228607b137d09b195d90e6aecfa. // // The browser toolbar is the only writer of annotation bodies; this backend is // the durable store, the agent-facing surface, and the change bus that pushes @@ -8,7 +8,7 @@ // rpc the toolbar and composer attachment UI talk here // GET /events server-sent events, so a resolve lands in the browser at once // agent tools the loop an agent actually runs (pending → fix → resolve) -// bb agentation the same loop for agents that prefer a shell +// bb agentation-mentions the same loop for agents that prefer a shell import { defineRpcContract, type BbPluginApi } from "@bb/plugin-sdk"; import { z } from "zod"; @@ -28,6 +28,7 @@ import { import { projectIdFromRoute, threadIdFromRoute } from "./lib/route.ts"; import { renderAnnotation, + renderAnnotationAssignment, renderAnnotationLine, renderAnnotations, } from "./lib/markdown.ts"; @@ -119,9 +120,7 @@ export const rpcContract = defineRpcContract({ }), }, pullSession: { - input: z - .object({ sessionId: z.string(), cursor: z.number().int() }) - .strict(), + input: z.object({ sessionId: z.string(), cursor: z.number().int() }).strict(), output: z.object({ cursor: z.number().int(), changed: z.boolean(), @@ -138,9 +137,7 @@ export const rpcContract = defineRpcContract({ output: z.object({ annotations: z.array(storedAnnotationSchema) }), }, discardStagedAnnotations: { - input: z - .object({ annotationIds: z.array(z.string()).min(1) }) - .strict(), + input: z.object({ annotationIds: z.array(z.string()).min(1) }).strict(), output: z.object({ outcome: z.enum(["discarded", "stale"]), discardedIds: z.array(z.string()), @@ -207,13 +204,7 @@ export const rpcContract = defineRpcContract({ input: z .object({ annotationId: z.string(), - action: z.enum([ - "acknowledge", - "resolve", - "dismiss", - "reopen", - "delete", - ]), + action: z.enum(["acknowledge", "resolve", "dismiss", "reopen", "delete"]), note: z.string().nullable(), }) .strict(), @@ -223,9 +214,7 @@ export const rpcContract = defineRpcContract({ }), }, replyToAnnotation: { - input: z - .object({ annotationId: z.string(), message: z.string().min(1) }) - .strict(), + input: z.object({ annotationId: z.string(), message: z.string().min(1) }).strict(), output: z.object({ annotation: storedAnnotationSchema.nullable() }), }, }); @@ -244,9 +233,7 @@ export default async function plugin(bb: BbPluginApi) { const recoveredDispatches = recoverInterruptedDispatches(db); if (recoveredDispatches > 0) { - bb.log.warn( - `re-staged ${recoveredDispatches} annotations interrupted during delivery`, - ); + bb.log.warn(`re-staged ${recoveredDispatches} annotations interrupted during delivery`); } // Whether the toolbar is showing is live state, not configuration: it is @@ -269,7 +256,7 @@ export default async function plugin(bb: BbPluginApi) { // // Three consumers care about a write: composer integrations (bb realtime), // every open toolbar (server-sent events), and any agent parked in - // `agentation_watch_annotations`. + // `agentation_mentions_watch_annotations`. // ------------------------------------------------------------------------- const streams = new Set>(); @@ -290,9 +277,7 @@ export default async function plugin(bb: BbPluginApi) { bb.realtime.publish("annotations", payload); - const frame = encoder.encode( - `event: change\ndata: ${JSON.stringify(payload)}\n\n`, - ); + const frame = encoder.encode(`event: change\ndata: ${JSON.stringify(payload)}\n\n`); // Both loops may delete the entry they are standing on — well defined for // a Set, and nothing here removes any other entry. for (const controller of streams) { @@ -306,9 +291,7 @@ export default async function plugin(bb: BbPluginApi) { for (const wake of watchers) wake(); } - function dropStream( - controller: ReadableStreamDefaultController, - ): void { + function dropStream(controller: ReadableStreamDefaultController): void { const heartbeat = heartbeats.get(controller); if (heartbeat) clearInterval(heartbeat); heartbeats.delete(controller); @@ -317,7 +300,7 @@ export default async function plugin(bb: BbPluginApi) { bb.ui.registerMentionProvider({ id: AGENTATION_MENTION_PROVIDER, - label: "Agentation feedback", + label: "Agentation → Mentions feedback", search: () => [], async resolve(itemId) { const attachment = decodeAgentationAttachment(itemId); @@ -356,7 +339,7 @@ export default async function plugin(bb: BbPluginApi) { } return { - context: `${sections.join("\n\n")}\n\nResolve each item with the \`agentation_resolve\` tool once it is fixed.`, + context: `${sections.join("\n\n")}\n\nResolve each item with the \`agentation_mentions_resolve\` tool once it is fixed.`, }; }, }); @@ -371,9 +354,7 @@ export default async function plugin(bb: BbPluginApi) { self = controller; streams.add(controller); controller.enqueue( - encoder.encode( - `event: hello\ndata: ${JSON.stringify({ cursor: currentSeq(db) })}\n\n`, - ), + encoder.encode(`event: hello\ndata: ${JSON.stringify({ cursor: currentSeq(db) })}\n\n`), ); // An idle stream gets dropped by proxies and by the tunnel used for // remote bb access; a comment frame is the cheapest thing that keeps @@ -474,7 +455,7 @@ export default async function plugin(bb: BbPluginApi) { } input.push({ type: "text", - text: "\n\nResolve each item with the `agentation_resolve` tool once it is fixed, or `agentation_reply` if you need a decision from me.", + text: "\n\nResolve each item with the `agentation_mentions_resolve` tool once it is fixed, or `agentation_mentions_reply` if you need a decision from me.", mentions: [], visibility: "agent-only", }); @@ -594,16 +575,13 @@ export default async function plugin(bb: BbPluginApi) { outcome: "stale" as const, discardedIds: [], remainingCount, - message: - "The staged annotations changed. Review the current batch and discard it again.", + message: "The staged annotations changed. Review the current batch and discard it again.", }; } broadcast({ type: "annotations", sessionId: null }); - const discardedIds = result.annotations.map( - (annotation) => annotation.id, - ); + const discardedIds = result.annotations.map((annotation) => annotation.id); return { outcome: "discarded" as const, discardedIds, @@ -700,9 +678,7 @@ export default async function plugin(bb: BbPluginApi) { const routing = getAnnotationRouting(db, input.annotationId); if (routing?.state !== "assigned" || !routing.assignedThreadId) { - throw new Error( - "Stage and send this annotation to a thread before you reply.", - ); + throw new Error("Stage and send this annotation to a thread before you reply."); } const context = renderAnnotation(existing); @@ -742,7 +718,7 @@ export default async function plugin(bb: BbPluginApi) { } bb.agents.registerTool({ - name: "agentation_list_sessions", + name: "agentation_mentions_list_sessions", description: "List annotation sessions — one per bb page a human has left visual feedback on. Start here to discover which pages have feedback.", experimental_statusLabels: { @@ -765,7 +741,7 @@ export default async function plugin(bb: BbPluginApi) { }); bb.agents.registerTool({ - name: "agentation_get_session", + name: "agentation_mentions_get_session", description: "Get one annotation session with every annotation on it, including resolved and dismissed ones.", experimental_statusLabels: { @@ -786,7 +762,7 @@ export default async function plugin(bb: BbPluginApi) { }); bb.agents.registerTool({ - name: "agentation_get_pending", + name: "agentation_mentions_get_pending", description: "Get the open (pending or acknowledged) annotations for one session, rendered with the bb route, owning plugin, and DOM selector for each.", experimental_statusLabels: { @@ -797,50 +773,40 @@ export default async function plugin(bb: BbPluginApi) { execute({ sessionId }) { const session = getSession(db, sessionId); return toolText( - renderAnnotations( - listAnnotations(db, { sessionId, statuses: openStatuses }), - { - title: `Open annotations in ${sessionId}`, - sessions: session ? [session] : [], - }, - ), + renderAnnotations(listAnnotations(db, { sessionId, statuses: openStatuses }), { + title: `Open annotations in ${sessionId}`, + sessions: session ? [session] : [], + }), ); }, }); bb.agents.registerTool({ - name: "agentation_get_all_pending", + name: "agentation_mentions_get_all_pending", description: - "Get every open annotation across all bb pages. Use this first when the human says to address their UI feedback.", + "Get every open annotation across all bb pages. Use this when the human refers to UI feedback but did not supply a self-contained Agentation annotation batch.", instructions: - "When the human refers to feedback they left on the bb interface, read it with agentation_get_all_pending before searching the code. Each annotation names the bb route and, for plugin surfaces, the owning plugin id.", + "When the human refers to feedback they left on the bb interface and their message does not already contain an Agentation annotation batch, read it with agentation_mentions_get_all_pending before searching the code. A supplied batch is self-contained; do not fetch other pending feedback. Each annotation names the bb route and, for plugin surfaces, the owning plugin id.", experimental_statusLabels: { pending: "Reading all pending annotations", completed: "Read all pending annotations", }, parameters: z.object({ - pluginId: z - .string() - .optional() - .describe("Only annotations on this plugin's UI surfaces."), + pluginId: z.string().optional().describe("Only annotations on this plugin's UI surfaces."), }), execute({ pluginId }) { return toolText( - renderAnnotations( - listAnnotations(db, { statuses: openStatuses, pluginId }), - { - title: "Open bb UI feedback", - sessions: listSessions(db, {}), - }, - ), + renderAnnotations(listAnnotations(db, { statuses: openStatuses, pluginId }), { + title: "Open bb UI feedback", + sessions: listSessions(db, {}), + }), ); }, }); bb.agents.registerTool({ - name: "agentation_acknowledge", - description: - "Mark an annotation as acknowledged so the human can see you have picked it up.", + name: "agentation_mentions_acknowledge", + description: "Mark an annotation as acknowledged so the human can see you have picked it up.", experimental_statusLabels: { pending: "Acknowledging annotation", completed: "Acknowledged annotation", @@ -859,7 +825,7 @@ export default async function plugin(bb: BbPluginApi) { }); bb.agents.registerTool({ - name: "agentation_resolve", + name: "agentation_mentions_resolve", description: "Mark an annotation as resolved after you have fixed it. The marker disappears from the human's toolbar. Include a short summary of what changed.", experimental_statusLabels: { @@ -884,7 +850,7 @@ export default async function plugin(bb: BbPluginApi) { }); bb.agents.registerTool({ - name: "agentation_dismiss", + name: "agentation_mentions_dismiss", description: "Dismiss an annotation you have decided not to act on. A reason is required — the human sees it.", experimental_statusLabels: { @@ -906,7 +872,7 @@ export default async function plugin(bb: BbPluginApi) { }); bb.agents.registerTool({ - name: "agentation_reply", + name: "agentation_mentions_reply", description: "Add a message to an annotation's thread — ask a clarifying question, or report progress. The human reads and answers it in the Agentation annotator UI.", experimental_statusLabels: { @@ -926,7 +892,7 @@ export default async function plugin(bb: BbPluginApi) { }); bb.agents.registerTool({ - name: "agentation_watch_annotations", + name: "agentation_mentions_watch_annotations", description: "Block until new annotations appear, then return the batch. Call it in a loop for hands-free feedback: watch, fix, resolve, watch again.", experimental_statusLabels: { @@ -934,19 +900,14 @@ export default async function plugin(bb: BbPluginApi) { completed: "Collected new annotations", }, parameters: z.object({ - sessionId: z - .string() - .optional() - .describe("Only watch one page's session."), + sessionId: z.string().optional().describe("Only watch one page's session."), batchWindowSeconds: z .number() .int() .min(0) .max(60) .optional() - .describe( - "After the first new annotation, keep collecting for this long. Default 10.", - ), + .describe("After the first new annotation, keep collecting for this long. Default 10."), timeoutSeconds: z .number() .int() @@ -967,12 +928,10 @@ export default async function plugin(bb: BbPluginApi) { sinceSeq: startCursor, }); - const appeared = await waitForChange(timeoutMs, context.signal, () => - fresh().length > 0, - ); + const appeared = await waitForChange(timeoutMs, context.signal, () => fresh().length > 0); if (!appeared) { return toolText( - "No new annotations before the timeout. Call agentation_watch_annotations again to keep waiting.", + "No new annotations before the timeout. Call agentation_mentions_watch_annotations again to keep waiting.", ); } @@ -1036,7 +995,7 @@ export default async function plugin(bb: BbPluginApi) { try { const pending = countByStatus(db).pending; if (pending === 0) return null; - return `The human has ${pending} unresolved Agentation annotation${pending === 1 ? "" : "s"} on the bb interface. Read them with agentation_get_all_pending before acting on any request about the bb UI, and resolve each one you fix.`; + return `The human has ${pending} unresolved Agentation annotation${pending === 1 ? "" : "s"} on the bb interface. Before acting on a request about the bb UI, call agentation_mentions_get_all_pending only when the request does not already contain an Agentation annotation batch. A supplied batch is self-contained; work only on its listed annotation IDs. Resolve each annotation you fix.`; } catch { return null; } @@ -1047,75 +1006,72 @@ export default async function plugin(bb: BbPluginApi) { // ------------------------------------------------------------------------- bb.cli.register({ - name: "agentation", + name: "agentation-mentions", summary: "Read and resolve visual feedback left on the bb interface", commands: [ { name: "pending", summary: "Show every open annotation", - usage: "bb agentation pending [--plugin ] [--json]", + usage: "bb agentation-mentions pending [--plugin ] [--json]", }, { name: "staged", summary: "Show annotations waiting for a thread", - usage: "bb agentation staged [--json]", + usage: "bb agentation-mentions staged [--json]", }, { name: "send", summary: "Assign staged annotations to a thread now or queue them", - usage: "bb agentation send [--queue] [annotationId…]", + usage: "bb agentation-mentions send [--queue] [annotationId…]", }, { name: "restage", summary: "Return an assigned annotation to staging", - usage: "bb agentation restage ", + usage: "bb agentation-mentions restage ", }, { name: "sessions", summary: "List annotated pages", - usage: "bb agentation sessions", + usage: "bb agentation-mentions sessions", }, { name: "show", summary: "Show one annotation in full", - usage: "bb agentation show ", + usage: "bb agentation-mentions show ", }, { name: "acknowledge", summary: "Mark an annotation as seen", - usage: "bb agentation acknowledge ", + usage: "bb agentation-mentions acknowledge ", }, { name: "resolve", summary: "Mark an annotation as fixed", - usage: "bb agentation resolve [summary…]", + usage: "bb agentation-mentions resolve [summary…]", }, { name: "dismiss", summary: "Decline an annotation, with a reason", - usage: "bb agentation dismiss ", + usage: "bb agentation-mentions dismiss ", }, { name: "reply", summary: "Ask the human a question on an annotation", - usage: "bb agentation reply ", + usage: "bb agentation-mentions reply ", }, { name: "toolbar", summary: "Show or set whether the annotation toolbar is displayed", - usage: "bb agentation toolbar [on|off]", + usage: "bb agentation-mentions toolbar [on|off]", }, ], async run(argv) { const [command, ...rest] = argv; const flagIndex = rest.indexOf("--plugin"); - const pluginId = - flagIndex >= 0 ? (rest[flagIndex + 1] ?? undefined) : undefined; + const pluginId = flagIndex >= 0 ? (rest[flagIndex + 1] ?? undefined) : undefined; const json = rest.includes("--json"); const positional = rest.filter( - (value, index) => - !value.startsWith("--") && - !(flagIndex >= 0 && index === flagIndex + 1), + (value, index) => !value.startsWith("--") && !(flagIndex >= 0 && index === flagIndex + 1), ); const ok = (stdout: string) => ({ exitCode: 0, stdout }); @@ -1126,17 +1082,17 @@ export default async function plugin(bb: BbPluginApi) { case "help": return ok( [ - "bb agentation pending [--plugin ] [--json]", - "bb agentation staged [--json]", - "bb agentation send [--queue] [annotationId…]", - "bb agentation restage ", - "bb agentation sessions", - "bb agentation show ", - "bb agentation acknowledge ", - "bb agentation resolve [summary…]", - "bb agentation dismiss ", - "bb agentation reply ", - "bb agentation toolbar [on|off]", + "bb agentation-mentions pending [--plugin ] [--json]", + "bb agentation-mentions staged [--json]", + "bb agentation-mentions send [--queue] [annotationId…]", + "bb agentation-mentions restage ", + "bb agentation-mentions sessions", + "bb agentation-mentions show ", + "bb agentation-mentions acknowledge ", + "bb agentation-mentions resolve [summary…]", + "bb agentation-mentions dismiss ", + "bb agentation-mentions reply ", + "bb agentation-mentions toolbar [on|off]", ].join("\n"), ); @@ -1146,7 +1102,7 @@ export default async function plugin(bb: BbPluginApi) { return ok((await isToolbarEnabled()) ? "on" : "off"); } if (desired !== "on" && desired !== "off") { - return fail("usage: bb agentation toolbar [on|off]"); + return fail("usage: bb agentation-mentions toolbar [on|off]"); } await bb.storage.kv.set(TOOLBAR_KEY, desired === "on"); broadcast({ type: "config", sessionId: null }); @@ -1174,7 +1130,7 @@ export default async function plugin(bb: BbPluginApi) { const threadId = positional[0]; if (!threadId) { return fail( - "usage: bb agentation send [--queue] [annotationId…]", + "usage: bb agentation-mentions send [--queue] [annotationId…]", ); } const requestedIds = positional.slice(1); @@ -1196,7 +1152,7 @@ export default async function plugin(bb: BbPluginApi) { case "restage": { const id = positional[0]; - if (!id) return fail("usage: bb agentation restage "); + if (!id) return fail("usage: bb agentation-mentions restage "); const routing = restageStoredAnnotation(db, id); if (!routing) { return fail(`Annotation ${id} is not an assigned open annotation.`); @@ -1220,7 +1176,7 @@ export default async function plugin(bb: BbPluginApi) { case "show": { const id = positional[0]; - if (!id) return fail("usage: bb agentation show "); + if (!id) return fail("usage: bb agentation-mentions show "); const annotation = getAnnotation(db, id); if (!annotation) return fail(`No annotation ${id}.`); return ok(renderAnnotation(annotation)); @@ -1230,10 +1186,10 @@ export default async function plugin(bb: BbPluginApi) { case "resolve": case "dismiss": { const id = positional[0]; - if (!id) return fail(`usage: bb agentation ${command} `); + if (!id) return fail(`usage: bb agentation-mentions ${command} `); const note = positional.slice(1).join(" ") || null; if (command === "dismiss" && !note) { - return fail("usage: bb agentation dismiss "); + return fail("usage: bb agentation-mentions dismiss "); } const status: AnnotationStatus = command === "acknowledge" @@ -1256,7 +1212,7 @@ export default async function plugin(bb: BbPluginApi) { const id = positional[0]; const message = positional.slice(1).join(" "); if (!id || !message) { - return fail("usage: bb agentation reply "); + return fail("usage: bb agentation-mentions reply "); } const annotation = appendThreadMessage(db, id, { role: "agent", @@ -1268,9 +1224,7 @@ export default async function plugin(bb: BbPluginApi) { } default: - return fail( - `Unknown command "${command}". Run \`bb agentation help\`.`, - ); + return fail(`Unknown command "${command}". Run \`bb agentation-mentions help\`.`); } }, }); diff --git a/plugins/agentation/skills/agentation/SKILL.md b/plugins/agentation-mentions/skills/agentation-mentions/SKILL.md similarity index 53% rename from plugins/agentation/skills/agentation/SKILL.md rename to plugins/agentation-mentions/skills/agentation-mentions/SKILL.md index e01e562..3c06e1a 100644 --- a/plugins/agentation/skills/agentation/SKILL.md +++ b/plugins/agentation-mentions/skills/agentation-mentions/SKILL.md @@ -1,11 +1,12 @@ --- -name: agentation -description: Read and act on visual feedback the human left on the bb interface with the Agentation toolbar — annotations that name a bb route, the owning plugin, and a DOM selector. Use when the user says they annotated, marked up, or left feedback on the UI, when they ask you to address annotation N, or when they ask for watch mode, hands-free mode, or a UI critique loop. +name: agentation-mentions +description: Read and act on visual feedback captured by Agentation → Mentions, the Scott Sunarto Agentation derivative that delivers annotations through bb mentions, queueing, and verified identity tags. Use when the user refers to its annotation batch, asks to address visual feedback, or requests watch mode. --- -# Agentation +# Agentation → Mentions -The human points at part of the bb interface and writes what should change. Each +Based on Agentation by Scott Sunarto. The human points at part of the bb +interface and writes what should change. Each annotation carries the bb route, the owning plugin id when the element was drawn by a plugin, the DOM selector, and — for React trees — the component path. Your job is to turn that into a code change and close the loop. @@ -18,26 +19,30 @@ source context, not a delivery target. ## The loop -1. `agentation_get_all_pending` — read every open annotation. Do this before - searching the code; the annotation already tells you where to look. -2. `agentation_acknowledge` — for each item you are taking on, so the human sees +1. Read the assigned feedback: + - If the human's message contains an Agentation annotation batch, treat it + as the complete assignment. Work only on its listed annotation IDs and do + not call `agentation_mentions_get_all_pending`. + - Otherwise, call `agentation_mentions_get_all_pending` before searching the code; + the annotation already tells you where to look. +2. `agentation_mentions_acknowledge` — for each item you are taking on, so the human sees you picked it up. 3. Find the code, make the change. -4. `agentation_resolve` with a one-line summary of what you changed. The marker +4. `agentation_mentions_resolve` with a one-line summary of what you changed. The marker disappears from every open bb window. -Use `agentation_dismiss` with a reason when you decide against a change, and -`agentation_reply` when you need a decision before you can act. Never resolve an +Use `agentation_mentions_dismiss` with a reason when you decide against a change, and +`agentation_mentions_reply` when you need a decision before you can act. Never resolve an annotation you did not actually fix — dismiss it or ask. ## Locating the code The `Where` line is the fastest route to the source. -| `Where` says | The code lives in | -|---|---| +| `Where` says | The code lives in | +| ----------------- | ----------------------------------------- | | `plugin \`\`` | that plugin's `app.tsx` and `components/` | -| `bb app shell` | the bb app itself, not this workspace | +| `bb app shell` | the bb app itself, not this workspace | `Selector` is a live DOM path — grep it for class names and element structure. `React` is the component path; the last segment is usually the component to @@ -50,7 +55,7 @@ workspace is not one, say so and reply on the annotation rather than guessing. - `intent` — `fix` is a defect, `change` is a preference, `question` wants an answer not a diff, `approve` is praise. Answer a `question` with - `agentation_reply`. + `agentation_mentions_reply`. - `severity` — `blocking` first, then `important`, then `suggestion`. - `Layout request` — a `placement` annotation asks for a new component in that spot; a `rearrange` annotation asks for a different section order. @@ -59,7 +64,7 @@ workspace is not one, say so and reply on the annotation rather than guessing. When the human asks for watch mode or hands-free mode: -1. Call `agentation_watch_annotations`. It blocks until new annotations appear, +1. Call `agentation_mentions_watch_annotations`. It blocks until new annotations appear, then returns the batch. 2. Acknowledge, fix, and resolve each one. 3. Call it again. Keep looping until the human stops you. @@ -72,15 +77,15 @@ batches so the human can follow along without reading the diff. Every tool has a CLI form for environments where the shell is easier: ```sh -bb agentation pending [--plugin ] [--json] -bb agentation staged [--json] -bb agentation send [--queue] [annotationId…] -bb agentation restage -bb agentation show -bb agentation resolve fixed the wrapping -bb agentation dismiss intentional, matches the design system -bb agentation reply should this be 24px or 16px? -bb agentation toolbar off +bb agentation-mentions pending [--plugin ] [--json] +bb agentation-mentions staged [--json] +bb agentation-mentions send [--queue] [annotationId…] +bb agentation-mentions restage +bb agentation-mentions show +bb agentation-mentions resolve fixed the wrapping +bb agentation-mentions dismiss intentional, matches the design system +bb agentation-mentions reply should this be 24px or 16px? +bb agentation-mentions toolbar off ``` ## Boundaries diff --git a/plugins/agentation/test/attachment.test.ts b/plugins/agentation-mentions/test/attachment.test.ts similarity index 100% rename from plugins/agentation/test/attachment.test.ts rename to plugins/agentation-mentions/test/attachment.test.ts diff --git a/plugins/agentation/test/delivery.test.ts b/plugins/agentation-mentions/test/delivery.test.ts similarity index 100% rename from plugins/agentation/test/delivery.test.ts rename to plugins/agentation-mentions/test/delivery.test.ts diff --git a/plugins/agentation/test/identity.test.ts b/plugins/agentation-mentions/test/identity.test.ts similarity index 100% rename from plugins/agentation/test/identity.test.ts rename to plugins/agentation-mentions/test/identity.test.ts diff --git a/plugins/agentation/test/orphans.test.ts b/plugins/agentation-mentions/test/orphans.test.ts similarity index 95% rename from plugins/agentation/test/orphans.test.ts rename to plugins/agentation-mentions/test/orphans.test.ts index 4b03172..4f2f5eb 100644 --- a/plugins/agentation/test/orphans.test.ts +++ b/plugins/agentation-mentions/test/orphans.test.ts @@ -46,8 +46,5 @@ test("a lost ledger keeps feedback rather than dropping it", () => { }); test("nothing local means nothing to recover", () => { - assert.deepEqual( - ids(selectOrphans([], new Set(["a"]), new Set(["a"]))), - [], - ); + assert.deepEqual(ids(selectOrphans([], new Set(["a"]), new Set(["a"]))), []); }); diff --git a/plugins/agentation-mentions/test/reconciliation-contract.test.ts b/plugins/agentation-mentions/test/reconciliation-contract.test.ts new file mode 100644 index 0000000..083d40c --- /dev/null +++ b/plugins/agentation-mentions/test/reconciliation-contract.test.ts @@ -0,0 +1,80 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { existsSync, readFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +const pluginRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const repositoryRoot = resolve(pluginRoot, "../.."); + +function readJson(path: string): any { + return JSON.parse(readFileSync(path, "utf8")); +} + +function sha256(path: string): string { + return createHash("sha256").update(readFileSync(path)).digest("hex"); +} + +test("Agentation to Mentions has one distinct package, collection, runtime, and release identity", () => { + const manifest = readJson(join(pluginRoot, "package.json")); + const collection = readJson(join(repositoryRoot, ".bb/plugins.json")); + const workflow = readFileSync(join(repositoryRoot, ".github/workflows/publish.yml"), "utf8"); + const server = readFileSync(join(pluginRoot, "server.ts"), "utf8"); + + assert.equal(manifest.name, "@phosphorco/bb-plugin-agentation-mentions"); + assert.equal(manifest.version, "0.1.0"); + assert.equal(manifest.bb.name, "Agentation → Mentions"); + assert.equal(manifest.repository.directory, "plugins/agentation-mentions"); + assert.equal( + collection.plugins.some( + (entry: { name: string; source: string }) => + entry.name === "agentation-mentions" && + entry.source === "./plugins/agentation-mentions", + ), + true, + ); + assert.match(workflow, /agentation-mentions\/v\*/); + assert.match( + workflow, + /agentation-mentions\) workspace="@phosphorco\/bb-plugin-agentation-mentions"/, + ); + assert.match(server, /name: "agentation-mentions"/); + assert.match(server, /name: "agentation_mentions_get_all_pending"/); +}); + +test("the bundled Agentation dependency preserves React 19 component paths in production", () => { + const vendorBundle = join(pluginRoot, "vendor/agentation/dist/index.mjs"); + assert.equal( + existsSync(vendorBundle), + true, + "expected the reviewed Agentation 3.0.2 vendored build", + ); + + const source = readFileSync(vendorBundle, "utf8"); + assert.match(source, /key\.startsWith\("__reactContainer\$"\)/); + assert.match( + source, + /process\.env\.NODE_ENV === "development" \|\| isReactPage\(\)/, + ); +}); + +test("the manifest and package preserve the complete approved AM02-A receipt", () => { + const manifest = readJson(join(pluginRoot, "package.json")); + assert.equal(manifest.bb.branding.icon, "AtSign"); + assert.equal(manifest.bb.branding.logo.light, "./assets/icon-32.png"); + assert.deepEqual( + Object.fromEntries( + ["icon-source.png", "icon-16.png", "icon-24.png", "icon-32.png"].map((name) => [ + name, + sha256(join(pluginRoot, "assets", name)), + ]), + ), + { + "icon-source.png": "3a263167ba31f16abeb6b494dbc4ebd6805901d1fc9dc7cd7c6e4b67628c6a3b", + "icon-16.png": "3581d070371a8e6fb1455a49237d58ee53205c2a9687bb5096c3d3d8cd2e3ac0", + "icon-24.png": "ce3b9b9ac2cdbc949b829590a60c079150719ba7185a0e157513254907f0e84a", + "icon-32.png": "6a640b4cb701af1c0571e2659058dabccd054a41a98141924d75debb098ca4b1", + }, + ); +}); diff --git a/plugins/agentation/test/rendering.test.ts b/plugins/agentation-mentions/test/rendering.test.ts similarity index 88% rename from plugins/agentation/test/rendering.test.ts rename to plugins/agentation-mentions/test/rendering.test.ts index f4a5af6..6118b63 100644 --- a/plugins/agentation/test/rendering.test.ts +++ b/plugins/agentation-mentions/test/rendering.test.ts @@ -4,6 +4,7 @@ import test from "node:test"; import { sanitizeJson, type StoredAnnotation } from "../lib/afs.ts"; import { renderAnnotation, + renderAnnotationAssignment, renderAnnotationLine, renderAnnotations, } from "../lib/markdown.ts"; @@ -117,6 +118,18 @@ test("an empty batch says so rather than rendering an empty heading", () => { assert.equal(renderAnnotations([]), "No annotations."); }); +test("a thread assignment is self-contained and preserves React context", () => { + const output = renderAnnotationAssignment( + [stored({ reactComponents: "