Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/surface-aware-agentation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@smsunarto/bb-plugin-agentation": patch
---

Enrich annotations with the exact public bb plugin UI surface and registration id that own the selected element, including component slots, composer contributions, and host-rendered plugin actions. Render source-oriented prompt guidance that points agents to the matching SDK registration in the plugin frontend. Keep the global React toolbar compatible with bb's foreign-DOM mutation guard.
9 changes: 5 additions & 4 deletions plugins/agentation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
Agentation puts a visual feedback toolbar over the whole bb interface — the app
shell and any surface another plugin drew. Click an element, write what should
change, and the annotation records the DOM selector, the React component path,
the bb route, and the plugin that owns the element. An agent reads that, fixes
the code, and resolves the annotation. The marker disappears from every open bb
window.
the bb route, the plugin that owns the element, and its public SDK UI
registration. An agent reads that, fixes the code, and resolves the annotation.
The marker disappears from every open bb window.

It is built on [Agentation](https://www.agentation.com) and its
[AFS 1.1](https://www.agentation.com/schema) annotation format. Agent tools are
Expand Down Expand Up @@ -96,7 +96,8 @@ where to look before it starts grepping.
| ------------------------------ | ---------------------------------------------- |
| `bb.route` | The bb route the annotation was taken on. |
| `bb.pluginId` | Owning plugin, or `null` for the bb app shell. |
| `bb.surface` | `navPanel`, `inline`, or `overlay`. |
| `bb.surface` | Public SDK surface such as `navPanel`, `composer.banners`, `experimental_threadList`, or `threadPanelAction.component`; `inline` / `overlay` for trusted custom content. |
| `bb.surfaceId` | Registration/item id exposed by the surface, such as `inbox`; omitted on older annotations or when bb does not expose one. |
| `bb.threadId` / `bb.projectId` | Source context resolved from the route. |

### Commands
Expand Down
4 changes: 3 additions & 1 deletion plugins/agentation/lib/afs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,10 @@ export const bbContextSchema = z.object({
route: z.string(),
/** Owning plugin id when the element sat inside a plugin surface. */
pluginId: z.string().nullable(),
/** Plugin surface kind (`navPanel`, `threadPanel`, …) when detectable. */
/** Public plugin UI API (`navPanel`, `composer.banners`, …) when detectable. */
surface: z.string().nullable(),
/** Registration/item id exposed by the plugin UI boundary, e.g. `inbox`. */
surfaceId: z.string().nullable().optional(),
threadId: z.string().nullable(),
projectId: z.string().nullable(),
/** Human label for the route, used in listings. */
Expand Down
21 changes: 16 additions & 5 deletions plugins/agentation/lib/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,26 @@
// the same wherever an agent meets it.

import type { Session, StoredAnnotation } from "./afs.ts";
import { pluginUiSurfacePromptContext } from "./plugin-ui-surface-map.ts";

function line(label: string, value: string | null | undefined): string {
return value ? `**${label}:** ${value}\n` : "";
}

function locationOf(annotation: StoredAnnotation): string {
const { pluginId, surface, route } = annotation.bb;
if (!pluginId) return `bb app shell · ${route}`;
const surfaceLabel = surface ? ` (${surface})` : "";
return `plugin \`${pluginId}\`${surfaceLabel} · ${route}`;
const { pluginId, route, routeLabel } = annotation.bb;
const routeContext = routeLabel ? ` \`${route}\` (${routeLabel})` : ` \`${route}\``;
if (!pluginId) return `bb app shell · route${routeContext}`;
return `plugin \`${pluginId}\` · route${routeContext}`;
}

function pluginUiOf(annotation: StoredAnnotation): string | null {
const { pluginId, surface, surfaceId } = annotation.bb;
if (!pluginId || !surface) return null;

const context = pluginUiSurfacePromptContext(surface);
const registrationId = surfaceId ? ` · registration \`${surfaceId}\`` : "";
return `\`${context.registration}\`${registrationId} — ${context.role}. Start at this registration in plugin \`${pluginId}\`'s \`app.tsx\`, then follow its component or run handler.`;
}

function describeKind(annotation: StoredAnnotation): string | null {
Expand All @@ -42,6 +52,7 @@ export function renderAnnotation(annotation: StoredAnnotation, index?: number):

let out = `${heading}\n`;
out += line("Where", locationOf(annotation));
out += line("Plugin UI", pluginUiOf(annotation));
out += line("Selector", `\`${annotation.elementPath}\``);
out += line("React", annotation.reactComponents);
out += line("Source", annotation.sourceFile);
Expand Down Expand Up @@ -88,7 +99,7 @@ export function renderAnnotations(
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`;
out += `${annotations.length} annotation${annotations.length === 1 ? "" : "s"} across ${bySession.size} page${bySession.size === 1 ? "" : "s"}. For plugin UI, start from the named SDK registration in the owning plugin's \`app.tsx\`, then use the selector and React path to narrow the rendered component.\n`;

for (const [sessionId, sessionAnnotations] of bySession) {
const session = sessionsById.get(sessionId);
Expand Down
137 changes: 137 additions & 0 deletions plugins/agentation/lib/plugin-ui-surface-map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Public bb plugin UI registrations and the source-level hints an agent needs
// to turn a captured surface name into a useful code search.

/** Translate bb's internal component-boundary names to the public SDK API. */
export const PUBLIC_SURFACE_BY_SLOT_KIND = {
composerAction: "composer.actions",
composerBanner: "composer.banners",
composerPlusMenuItem: "composer.plusMenu",
fileOpener: "fileOpener",
homepageSection: "homepageSection",
messageDirective: "messageDirective",
navPanel: "navPanel",
navPanelFixedTab: "navPanel.experimental_fixedTabs",
navPanelHeaderContent: "navPanel.headerContent",
navPanelSidebarAccessory: "navPanel.experimental_sidebarAccessory",
newThreadPanelAction: "experimental_newThreadPanelAction.component",
pendingInteraction: "pendingInteraction",
providerIcon: "experimental_providerIcon",
settingsSection: "settingsSection",
threadHeaderAction: "experimental_threadHeaderAction",
threadList: "experimental_threadList",
threadPanelAction: "threadPanelAction.component",
} as const satisfies Readonly<Record<string, string>>;

export interface PluginUiSurfacePromptContext {
registration: string;
role: string;
}

const PROMPT_CONTEXT_BY_SURFACE = {
"composer.actions": {
registration: "app.composer.customize({ actions })",
role: "a plugin component rendered in the composer action row",
},
"composer.banners": {
registration: "app.composer.customize({ banners })",
role: "a plugin component rendered above the composer",
},
"composer.plusMenu": {
registration: "app.composer.customize({ plusMenu })",
role: "a host-rendered plugin item in the composer's plus menu",
},
"composer.richText": {
registration: "app.composer.customize({ richText })",
role: "plugin-owned paint or behavior applied to composer text",
},
fileOpener: {
registration: "app.slots.fileOpener",
role: "a plugin component rendering an opened file panel",
},
homepageSection: {
registration: "app.slots.homepageSection",
role: "a plugin component rendered on bb's home page",
},
messageAction: {
registration: "app.slots.messageAction",
role: "a host-rendered action contributed to a message",
},
messageDirective: {
registration: "app.slots.messageDirective",
role: "a plugin component rendering an assistant message directive",
},
navPanel: {
registration: "app.slots.navPanel",
role: "the plugin-owned route panel",
},
"navPanel.experimental_fixedTabs": {
registration: "app.slots.navPanel({ experimental_fixedTabs })",
role: "a fixed tab declared by the plugin's navigation panel",
},
"navPanel.headerContent": {
registration: "app.slots.navPanel({ headerContent })",
role: "the plugin component rendered in its panel header",
},
"navPanel.experimental_sidebarAccessory": {
registration: "app.slots.navPanel({ experimental_sidebarAccessory })",
role: "an accessory declared beside the plugin's sidebar entry",
},
"experimental_newThreadPanelAction.component": {
registration: "app.slots.experimental_newThreadPanelAction({ component })",
role: "the plugin panel opened from the new-thread action launcher",
},
"experimental_newThreadPanelAction.run": {
registration: "app.slots.experimental_newThreadPanelAction({ run })",
role: "the host-rendered new-thread action and its run handler",
},
pendingInteraction: {
registration: "app.slots.pendingInteraction",
role: "a plugin component handling a pending thread interaction",
},
experimental_providerIcon: {
registration: "app.slots.experimental_providerIcon",
role: "a plugin component drawing an agent provider icon",
},
settingsSection: {
registration: "app.slots.settingsSection",
role: "a plugin component rendered in its settings page",
},
sidebarFooterAction: {
registration: "app.slots.sidebarFooterAction",
role: "a host-rendered plugin action in the sidebar footer",
},
experimental_threadHeaderAction: {
registration: "app.slots.experimental_threadHeaderAction",
role: "a plugin component rendered in the active thread header",
},
experimental_threadList: {
registration: "app.slots.experimental_threadList",
role: "the plugin component replacing bb's sidebar thread list",
},
"threadPanelAction.component": {
registration: "app.slots.threadPanelAction({ component })",
role: "the plugin panel opened from a thread action launcher",
},
"threadPanelAction.run": {
registration: "app.slots.threadPanelAction({ run })",
role: "the host-rendered thread action and its run handler",
},
inline: {
registration: "app.contentScripts.register or custom plugin DOM",
role: "trusted plugin content rendered outside a named component slot",
},
overlay: {
registration: "app.contentScripts.register or a plugin portal",
role: "trusted plugin content rendered in an overlay",
},
} as const satisfies Readonly<Record<string, PluginUiSurfacePromptContext>>;

/** Source-oriented context for a captured public surface, including future ones. */
export function pluginUiSurfacePromptContext(surface: string): PluginUiSurfacePromptContext {
return (
PROMPT_CONTEXT_BY_SURFACE[surface as keyof typeof PROMPT_CONTEXT_BY_SURFACE] ?? {
registration: surface,
role: "a plugin UI contribution registered from the plugin frontend",
}
);
}
Loading
Loading