+ 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.
+
+ );
+}
+
+/** 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: "