Skip to content
Open
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
7 changes: 7 additions & 0 deletions apps/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
APP_ROOT_ROUTE_PATH,
AUTH_CALLBACK_ROUTE_PATH,
AUTOMATIONS_ROUTE_PATH,
SKILLS_ROUTE_PATH,
AUTOMATION_DETAIL_ROUTE_PATH,
LEGACY_PROJECT_COMPOSE_ROUTE_PATH,
POPOUT_ROUTE_PATH,
Expand Down Expand Up @@ -52,6 +53,11 @@ const AutomationDetailView = lazy(() =>
default: m.AutomationDetailView,
})),
);
const SkillsView = lazy(() =>
import("./views/SkillsView").then((m) => ({
default: m.SkillsView,
})),
);
const ProjectSettingsView = lazy(() =>
import("./views/ProjectSettingsView").then((m) => ({
default: m.ProjectSettingsView,
Expand Down Expand Up @@ -109,6 +115,7 @@ function AppRoutes() {
path={AUTOMATION_DETAIL_ROUTE_PATH}
element={<AutomationDetailView />}
/>
<Route path={SKILLS_ROUTE_PATH} element={<SkillsView />} />
<Route
path={LEGACY_PROJECT_COMPOSE_ROUTE_PATH}
element={<RootComposeRoute />}
Expand Down
214 changes: 214 additions & 0 deletions apps/app/src/components/create-via-prompt-examples.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import { Button } from "@/components/ui/button.js";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu.js";
import { Icon } from "@/components/ui/icon.js";
import {
CREATE_LOOP_PROMPT,
CREATE_SKILL_PROMPT,
} from "@/components/promptbox/PromptBoxActionsMenu";

export type CreateViaPromptKind = "skill" | "loop";

interface Example {
label: string;
/** Completes the "Create a new bb {kind} …" prompt; also shown on the card. */
description: string;
}

interface KindConfig {
prefix: string;
explainer: string;
examples: readonly Example[];
}

// The description completes the prompt prefix, so each card both teaches and
// seeds the composer. Skills are standard Agent Skills whose bb edge is being
// cross-provider; loops are cheap scripts that escalate to threads.
const CONFIG: Record<CreateViaPromptKind, KindConfig> = {
skill: {
prefix: CREATE_SKILL_PROMPT,
explainer:
"Write a skill once, and every agent in bb can run it, whatever the provider.",
examples: [
{
label: "Repro & fix",
description:
"turns a bug report into a failing test, then makes it pass",
},
{
label: "Scaffold to our patterns",
description:
"scaffolds a new component with its test and story to match our conventions",
},
{
label: "Onboard to a subsystem",
description:
"traces how a feature works across the codebase and writes an explainer",
},
],
},
loop: {
prefix: CREATE_LOOP_PROMPT,
explainer:
"Pay for agents only when there's real work, and fan a problem out across many threads in parallel.",
examples: [
{
label: "Flaky-test sweep",
description:
"run nightly, find flaky tests with a script, and spawn a fixer thread for each one",
},
{
label: "Silent health watch",
description:
"check the app every 15 minutes with a cheap script and spawn a thread only when something breaks",
},
{
label: "Error sentinel",
description:
"poll the error dashboard hourly and spawn a triage thread only on a new spike",
},
],
},
};

export interface CreateExample {
label: string;
description: string;
/** Full composer prompt seeded when this example is picked. */
prompt: string;
}

/**
* The shared create-via-prompt content for a kind: the marketing one-liner and
* the examples with their full seeded prompts. Surfaces render it how they like
* (cards, chips) without duplicating the copy.
*/
export function getCreateExamples(kind: CreateViaPromptKind): {
explainer: string;
examples: CreateExample[];
} {
const config = CONFIG[kind];
return {
explainer: config.explainer,
examples: config.examples.map((example) => ({
label: example.label,
description: example.description,
prompt: `${config.prefix}${example.description}.`,
})),
};
}

export interface CreateViaPromptExamplesProps {
kind: CreateViaPromptKind;
/** Opens the composer seeded with the given full prompt. */
onCreate: (prompt: string) => void;
}

/**
* Teaching panel for the Loops empty state: a one-line explainer plus clickable
* example cards that seed the create-via-prompt composer.
*/
export function CreateViaPromptExamples({
kind,
onCreate,
}: CreateViaPromptExamplesProps) {
const { explainer, examples } = getCreateExamples(kind);
return (
<div>
<p className="max-w-prose text-sm text-muted-foreground">{explainer}</p>
<p className="mt-3 text-xs font-medium text-subtle-foreground">
Start from an example
</p>
<div className="mt-1.5 grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
{examples.map((example) => (
<button
key={example.label}
type="button"
onClick={() => onCreate(example.prompt)}
className="rounded-lg border border-border bg-background p-3 text-left transition-colors hover:border-file-accent/50 hover:bg-state-hover focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
<span className="block text-sm font-medium text-foreground">
{example.label}
</span>
<span className="mt-1 block text-xs leading-snug text-subtle-foreground">
{example.description}
</span>
</button>
))}
</div>
</div>
);
}

export interface CreateWithTemplatesButtonProps {
kind: CreateViaPromptKind;
/** Main-button text, e.g. "New loop" or "New bb skill". */
label: string;
/** Blank when called with no argument; seeded when given an example prompt. */
onCreate: (prompt?: string) => void;
}

/**
* Split (combo) button: the left half creates a blank one immediately; the right
* half opens a menu of example templates that seed the composer. Shared by the
* Skills and Loops library toolbars.
*/
export function CreateWithTemplatesButton({
kind,
label,
onCreate,
}: CreateWithTemplatesButtonProps) {
const { examples } = getCreateExamples(kind);
return (
<div className="flex shrink-0 items-stretch">
<Button
type="button"
size="sm"
className="rounded-r-none"
onClick={() => onCreate()}
>
<Icon name="Plus" className="size-4" />
{label}
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
size="sm"
aria-label={`${label} from a template`}
className="rounded-l-none border-l border-background/25 px-1.5"
>
<Icon name="ChevronDown" className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-72"
mobileTitle="Start from an example"
>
<DropdownMenuLabel className="text-xs font-normal text-subtle-foreground">
Start from an example
</DropdownMenuLabel>
{examples.map((example) => (
<DropdownMenuItem
key={example.label}
onSelect={() => onCreate(example.prompt)}
>
<div className="flex min-w-0 flex-col">
<span className="text-sm text-foreground">{example.label}</span>
<span className="text-xs text-muted-foreground">
{example.description}
</span>
</div>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}
5 changes: 3 additions & 2 deletions apps/app/src/components/layout/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,8 @@ function SidebarTriggerOverlay({
const routeTitles: Record<string, { title: string; subtitle?: string }> = {
"/": { title: "bb" },
"/settings": { title: "Settings" },
"/automations": { title: "Automations" },
"/automations": { title: "Loops" },
"/skills": { title: "Skills" },
};

interface AppHeaderProps {
Expand Down Expand Up @@ -461,7 +462,7 @@ export function AppLayout({ children }: AppLayoutProps) {
title: "",
subtitle: undefined,
breadcrumbs: [
{ label: "Automations", to: getAutomationsRoutePath() },
{ label: "Loops", to: getAutomationsRoutePath() },
{ label: automationName },
],
}
Expand Down
2 changes: 2 additions & 0 deletions apps/app/src/components/promptbox/PromptBoxActionsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ interface PromptBoxActionsMenuProps {
}

export const CREATE_LOOP_PROMPT = "Create a new bb loop to ";
// Skill creation always targets a bb skill (the only manageable scope).
export const CREATE_SKILL_PROMPT = "Create a new bb skill that ";
export const LOOP_PROMPT_ACTION: PromptBoxAction = {
kind: "loop",
text: CREATE_LOOP_PROMPT,
Expand Down
10 changes: 9 additions & 1 deletion apps/app/src/components/sidebar/AppSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
import {
getAutomationsRoutePath,
getRootComposeRoutePath,
getSkillsRoutePath,
getThreadRoutePath,
} from "@/lib/route-paths";
import { useRouteState } from "@/hooks/useRouteState";
Expand Down Expand Up @@ -73,7 +74,7 @@ export function AppSidebar({
const quickCreateProject = useQuickCreateProjectController();
const navigate = useNavigate();
const closeOnMobile = useCloseMobileSidebar();
const { isAutomationsView } = useRouteState();
const { isAutomationsView, isSkillsView } = useRouteState();
const { isCompactViewport, setOpen, setOpenMobile } = useSidebar();
const [desktopInfo] = useState(getBbDesktopInfo);
const [isThreadSearchActive, setIsThreadSearchActive] = useState(false);
Expand Down Expand Up @@ -162,6 +163,11 @@ export function AppSidebar({
void navigate(getAutomationsRoutePath());
}, [closeOnMobile, navigate]);

const handleOpenSkills = useCallback(() => {
closeOnMobile();
void navigate(getSkillsRoutePath());
}, [closeOnMobile, navigate]);

const handleThreadSearchKeyDown = useCallback<
KeyboardEventHandler<HTMLDivElement>
>(
Expand Down Expand Up @@ -291,6 +297,8 @@ export function AppSidebar({
>
<ProjectListActionButtons
onNewChat={handleNewChat}
onOpenSkills={handleOpenSkills}
isSkillsActive={isSkillsView}
onOpenAutomations={handleOpenAutomations}
isAutomationsActive={isAutomationsView}
threadSearch={{
Expand Down
26 changes: 24 additions & 2 deletions apps/app/src/components/sidebar/ProjectList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ interface ProjectListProps {

export interface ProjectListActionButtonsProps {
onNewChat?: () => void;
onOpenSkills?: () => void;
isSkillsActive?: boolean;
onOpenAutomations?: () => void;
isAutomationsActive?: boolean;
threadSearch?: SidebarThreadSearchInputController;
Expand Down Expand Up @@ -1169,6 +1171,8 @@ const SortableSidebarSection = memo(function SortableSidebarSection({

export function ProjectListActionButtons({
onNewChat,
onOpenSkills,
isSkillsActive = false,
onOpenAutomations,
isAutomationsActive = false,
threadSearch,
Expand Down Expand Up @@ -1252,6 +1256,23 @@ export function ProjectListActionButtons({
) : null}
</div>
)}
{onOpenSkills ? (
<Button
type="button"
size="sm"
variant="ghost"
className={cn(
PROJECT_LIST_ACTION_BUTTON_CLASS,
isSkillsActive && "bg-sidebar-accent text-sidebar-foreground",
)}
aria-current={isSkillsActive ? "page" : undefined}
onClick={onOpenSkills}
title="Skills"
>
<Icon name="Zap" />
<span className="min-w-0 flex-1 truncate text-left">Skills</span>
</Button>
) : null}
{onOpenAutomations ? (
<Button
type="button"
Expand All @@ -1263,9 +1284,10 @@ export function ProjectListActionButtons({
)}
aria-current={isAutomationsActive ? "page" : undefined}
onClick={onOpenAutomations}
title="Loops"
>
<Icon name="Clock" />
<span className="min-w-0 flex-1 truncate text-left">Automations</span>
<Icon name="Repeat" />
<span className="min-w-0 flex-1 truncate text-left">Loops</span>
</Button>
) : null}
</div>
Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/components/ui/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ const DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps>(
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-state-active data-[state=open]:text-foreground">
<DialogPrimitive.Close className="absolute right-4 top-4 cursor-pointer rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-state-active data-[state=open]:text-foreground">
<Icon name="X" className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
Expand Down
4 changes: 2 additions & 2 deletions apps/app/src/components/ui/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@
--card-foreground: var(--ink);
--popover: var(--canvas);
--popover-foreground: var(--ink);
--primary: oklch(0.4891 0 0);
--primary: oklch(0.27 0 0);
--primary-foreground: oklch(1 0 0);
/* Subtle fill. secondary and accent share one low-emphasis step a touch more
* prominent than the card; they stay separate tokens for their distinct
Expand Down Expand Up @@ -519,7 +519,7 @@
--card-foreground: var(--ink);
--popover: var(--canvas);
--popover-foreground: var(--ink);
--primary: oklch(0.7058 0 0);
--primary: oklch(0.82 0 0);
--primary-foreground: oklch(0.2178 0 0);
/* Subtle fill: secondary and accent share one step; muted is a distinct step
* more prominent (in light too), so the ramp reads the same in both themes. */
Expand Down
Loading
Loading