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
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions plugins/follow-up/PLUGIN_OVERVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ normally live in one reply and are gone by the next turn. This keeps them.

## What you get

A banner above the composer listing what the current thread accumulated. Each
row can be pushed into the composer, handed to a new thread in the same
checkout, reordered, marked done, or dismissed. Marking a row done or
A banner above the composer listing what the current thread accumulated, laid
out like bb's Queue card. Each row leads with one action (hand off for
out-of-scope work, put in the composer for the rest), drags to reorder, and
has a ⋯ menu that edits the text in place, marks it done, or dismisses it. Marking a row done or
dismissing it takes its pill back out of your draft. A dismissed row stays
gone: an agent that notices the same thing again cannot re-add it.

Expand Down
12 changes: 8 additions & 4 deletions plugins/follow-up/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@ correct as the plugin releases and `bb plugin update` follows it.

## What it does

**The banner.** Above the composer, listing what this thread accumulated. Each
row can be pushed into the composer, handed to a new thread in the same
checkout, dragged into a different order, marked done, or dismissed.
**The banner.** Above the composer, listing what this thread accumulated, laid
out like bb's own Queue card beneath it. Each row shows one action and keeps the
rest in its ⋯ menu. An out-of-scope row leads with handing it to a new thread in
the same checkout; every other row leads with pushing it into the composer. The
menu also edits the text in place (Enter saves, Escape puts it back), describes
a row that has no detail, opens it in the panel for the detail, file and reason,
marks it done, or dismisses it. Drag the handle to reorder.

A row pushed into the composer goes in as a pill, and the row is marked as in
the composer for as long as that pill is in your draft. Mark the row done or
Expand Down Expand Up @@ -156,7 +160,7 @@ declare as a hint. With `--json`, a failure also prints
`{"ok": false, "error": {"code", "message", "hint"}}` on stdout, while stderr
keeps the readable message.

`dismiss` is the same action as the banner's x, exposed so the tombstone path is
`dismiss` is the same action as the banner's Dismiss, exposed so the tombstone path is
testable without a browser. `clear` empties the list without dismissing
anything. `forget` drops the *dismissal* record — use it when you deleted
something and want agents to be able to raise it again.
Expand Down
66 changes: 66 additions & 0 deletions plugins/follow-up/components/ui/dropdown-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// A small dropdown menu over Radix, for the banner rows' ⋯ menu.
//
// `@radix-ui/react-dropdown-menu` is one of the packages bb shims at runtime,
// so this bundles none of it: the menu is bb's own Radix copy, and it is a
// type-only devDependency here (`bb plugin types` keeps it at the host's
// version). The content portals out of the card, so it carries the plugin's
// portal-scope attributes for theming, as the detail tooltip does.
import type { ComponentProps } from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { usePortalScopeProps } from "@/lib/portal-scope";
import { cn } from "@/lib/utils";

export const DropdownMenu = DropdownMenuPrimitive.Root;
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;

export function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Content>) {
const portalProps = usePortalScopeProps();
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
{...portalProps}
sideOffset={sideOffset}
className={cn(
"z-[60] min-w-44 overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
);
}

export function DropdownMenuItem({
className,
destructive = false,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Item> & { destructive?: boolean }) {
return (
<DropdownMenuPrimitive.Item
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none",
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground",
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
destructive && "text-destructive data-[highlighted]:text-destructive",
className,
)}
{...props}
/>
);
}

export function DropdownMenuSeparator({
className,
...props
}: ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
);
}
13 changes: 13 additions & 0 deletions plugins/follow-up/lib/followups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ export const REASONS = [

export type Reason = (typeof REASONS)[number];

/** The one action a banner row shows inline; the rest are in its ⋯ menu. */
export type RowMainAction = "insert" | "handoff";

/**
* A guess at a row's likely next step, from why it was skipped. Out-of-scope
* work belongs in another thread, so it leads with Hand off. Everything else
* leads with putting it in the composer, cleanup included until there is a
* "queue it after this turn" action for it to lead with instead.
*/
export function mainActionFor(reason: Reason | null): RowMainAction {
return reason === "out-of-scope" ? "handoff" : "insert";
}

export type HandoffState = "running" | "finished" | "failed";

export interface FollowUp {
Expand Down
Loading
Loading