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
80 changes: 51 additions & 29 deletions apps/app/src/components/sidebar/AppSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { cn } from "@bb/shared-ui/lib/utils";
import { THREAD_JUMP_APP_COMMAND_IDS } from "@bb/domain";
import { Link, useNavigate } from "react-router-dom";
import { useAtomValue } from "jotai";
import { Icon } from "@bb/shared-ui/icon";
import { COARSE_POINTER_CHILD_ICON_BUTTON_CLASS } from "@bb/shared-ui/coarse-pointer-sizing";
import { OverflowFade } from "@/components/ui/overflow-fade.js";
Expand Down Expand Up @@ -66,6 +67,11 @@ import {
} from "@/components/commands/AppCommandProvider";
import { useRouteState } from "@/hooks/useRouteState";
import { usePluginNavPanelChrome } from "@/lib/plugin-nav-panel-chrome";
import { SidebarTopRegionCustomizeMenu } from "./SidebarTopRegionCustomizeMenu";
import {
sidebarTopRegionItemPreferencesAtom,
type SidebarTopRegionItemId,
} from "./sidebarTopRegionItemPreferences";

const BUG_REPORT_NEW_ISSUE_URL = "https://github.com/get-bb/bb/issues/new";
const SIDEBAR_FOOTER_ACTION_CLASS = cn(
Expand Down Expand Up @@ -179,6 +185,9 @@ export function AppSidebar({
);
const isAppCommandModifierHeld = useIsAppCommandModifierHeld();
const settingsShortcut = useAppCommandShortcut("settings.open");
const topRegionItemPreferences = useAtomValue(
sidebarTopRegionItemPreferencesAtom,
);
const pluginNavPanels = usePluginNavPanelChrome();
const automationsNavPanel = pluginNavPanels.find(
({ chrome }) => chrome.pluginId === AUTOMATIONS_PLUGIN_ID,
Expand Down Expand Up @@ -298,6 +307,33 @@ export function AppSidebar({
isCreatingProject={quickCreateProject.isCreating}
/>
);
const topRegionItemNodes: Record<SidebarTopRegionItemId, ReactNode | null> = {
"new-thread": (
<ProjectListActionButtons
splitEnabled
newThreadSplit={newThreadSplit}
onNewChat={handleNewChat}
onSplit={onSplit}
/>
),
extensions: toolsRoutePath ? (
<ExtensionsNavSidebarItem
routePath={toolsRoutePath}
onNavigate={closeOnMobile}
/>
) : null,
automations: automationsNavPanel ? (
<AutomationsNavSidebarItem
chrome={automationsNavPanel.chrome}
onNavigate={closeOnMobile}
/>
) : null,
};
const visibleTopRegionItems = topRegionItemPreferences.order.flatMap((id) => {
if (topRegionItemPreferences.hiddenIds.includes(id)) return [];
const node = topRegionItemNodes[id];
return node === null ? [] : [<Fragment key={id}>{node}</Fragment>];
});

const body = (
<>
Expand All @@ -323,42 +359,28 @@ export function AppSidebar({
usesDesktopChrome && MACOS_WINDOW_DRAG_CLASS,
)}
>
<SidebarHistoryNavigationControls
onNavigate={closeOnMobile}
<div
className={cn(
"group-data-[collapsible=icon]:hidden",
"flex items-center gap-1 group-data-[collapsible=icon]:hidden",
usesDesktopChrome && MACOS_CHROME_CONTROL_NO_DRAG_CLASS,
)}
/>
>
<SidebarHistoryNavigationControls onNavigate={closeOnMobile} />
<SidebarTopRegionCustomizeMenu />
</div>
</div>
) : null}
<SidebarTopLevelSections
sections={{
"new-thread-extensions": (
<div
data-testid="app-sidebar-primary-actions"
className="space-y-1 px-2 py-2 group-data-[collapsible=icon]:hidden"
>
<ProjectListActionButtons
splitEnabled
newThreadSplit={newThreadSplit}
onNewChat={handleNewChat}
onSplit={onSplit}
/>
{toolsRoutePath ? (
<ExtensionsNavSidebarItem
routePath={toolsRoutePath}
onNavigate={closeOnMobile}
/>
) : null}
{automationsNavPanel ? (
<AutomationsNavSidebarItem
chrome={automationsNavPanel.chrome}
onNavigate={closeOnMobile}
/>
) : null}
</div>
),
"new-thread-extensions":
visibleTopRegionItems.length > 0 ? (
<div
data-testid="app-sidebar-primary-actions"
className="space-y-1 px-2 py-2 group-data-[collapsible=icon]:hidden"
>
{visibleTopRegionItems}
</div>
) : null,
"plugin-pages": hasTraditionalPluginPanels ? (
<PluginNavSidebarItems onNavigate={closeOnMobile} splitEnabled />
) : null,
Expand Down
43 changes: 21 additions & 22 deletions apps/app/src/components/sidebar/ProjectList.modes.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ function getModeOrderProbeConfig(mode: SidebarOrganizationMode): {
switch (mode) {
case "project":
return { entitySectionIds: ["project:a"] };
case "chronological":
case "manual":
return { entitySectionIds: ["section:a"] };
case "machine":
return { entitySectionIds: [], hasThreadsSection: true };
Expand All @@ -82,23 +82,21 @@ function ModeOrderProbe({ mode }: { mode: SidebarOrganizationMode }) {

interface ActiveModeOrderProbeProps {
mode: SidebarOrganizationMode;
renderChronological?: () => ReactNode;
renderManual?: () => ReactNode;
renderMachine?: () => ReactNode;
renderProject?: () => ReactNode;
}

function ActiveModeOrderProbe({
mode,
renderChronological = () => (
<ModeOrderProbe key="chronological" mode="chronological" />
),
renderManual = () => <ModeOrderProbe key="manual" mode="manual" />,
renderMachine = () => <ModeOrderProbe key="machine" mode="machine" />,
renderProject = () => <ModeOrderProbe key="project" mode="project" />,
}: ActiveModeOrderProbeProps) {
return (
<ActiveSidebarModeSections
mode={mode}
renderChronological={renderChronological}
renderManual={renderManual}
renderMachine={renderMachine}
renderProject={renderProject}
/>
Expand Down Expand Up @@ -183,8 +181,8 @@ function MachineModeProbe({ threads = [] }: { threads?: ThreadListEntry[] }) {
collapsedThreadIds={new Set()}
collapsedEnvironmentIds={new Set()}
compareThreads={() => 0}
renderSectionDisplayOptions={() => null}
isSectionDisplayOptionsOpen={() => false}
displayOptions={<button aria-label="Display marker" />}
displayOptionsOpen={false}
onToggleCollapsed={handleToggleCollapsed}
onToggleThreadCollapsed={vi.fn()}
onToggleEnvironmentCollapsed={vi.fn()}
Expand All @@ -199,7 +197,7 @@ afterEach(() => {
});

describe("sidebar organization mode sections", () => {
it.each<SidebarOrganizationMode>(["project", "chronological", "machine"])(
it.each<SidebarOrganizationMode>(["project", "manual", "machine"])(
"keeps drafts above %s sections and archived rows trailing",
(mode) => {
const { container } = render(
Expand All @@ -208,7 +206,7 @@ describe("sidebar organization mode sections", () => {
activeModeSections={
<ActiveSidebarModeSections
mode={mode}
renderChronological={() => <div>Chronological</div>}
renderManual={() => <div>Manual</div>}
renderMachine={() => <div>Machine</div>}
renderProject={() => <div>Project</div>}
/>
Expand All @@ -219,8 +217,8 @@ describe("sidebar organization mode sections", () => {
);

const activeLabel =
mode === "chronological"
? "Chronological"
mode === "manual"
? "Manual"
: mode === "machine"
? "Machine"
: "Project";
Expand All @@ -233,17 +231,15 @@ describe("sidebar organization mode sections", () => {
store.set(sidebarSectionOrderAtom, ["threads", "project:a", "pinned"]);
store.set(sidebarManualSectionOrderAtom, ["section:stale"]);
store.set(sidebarMachineSectionOrderAtom, ["machine:stale"]);
const renderChronological = vi.fn(() => (
<ModeOrderProbe mode="chronological" />
));
const renderManual = vi.fn(() => <ModeOrderProbe mode="manual" />);
const renderMachine = vi.fn(() => <MachineModeProbe />);
const renderProject = vi.fn(() => <ModeOrderProbe mode="project" />);

render(
<JotaiProvider store={store}>
<ActiveModeOrderProbe
mode="project"
renderChronological={renderChronological}
renderManual={renderManual}
renderMachine={renderMachine}
renderProject={renderProject}
/>
Expand All @@ -252,7 +248,7 @@ describe("sidebar organization mode sections", () => {

await screen.findByTestId("project-order");
expect(renderProject).toHaveBeenCalledOnce();
expect(renderChronological).not.toHaveBeenCalled();
expect(renderManual).not.toHaveBeenCalled();
expect(renderMachine).not.toHaveBeenCalled();
expect(mockUseHosts).not.toHaveBeenCalled();
expect(mockBuildMachineThreadGroups).not.toHaveBeenCalled();
Expand All @@ -278,8 +274,8 @@ describe("sidebar organization mode sections", () => {
);

expect(await screen.findByTestId("project-order")).not.toBeNull();
act(() => store.set(sidebarOrganizationModeAtom, "chronological"));
expect(await screen.findByTestId("chronological-order")).not.toBeNull();
act(() => store.set(sidebarOrganizationModeAtom, "manual"));
expect(await screen.findByTestId("manual-order")).not.toBeNull();
act(() => store.set(sidebarOrganizationModeAtom, "machine"));
expect(await screen.findByTestId("machine-order")).not.toBeNull();
act(() => store.set(sidebarOrganizationModeAtom, "project"));
Expand All @@ -292,7 +288,7 @@ describe("sidebar organization mode sections", () => {
});
});

it("collapses and expands empty-machine Threads", () => {
it("uses a Machines fallback and one global display control when empty", () => {
const store = createStore();
store.set(sidebarMachineSectionOrderAtom, ["threads"]);
store.set(collapsedSidebarSectionIdsAtom, []);
Expand All @@ -304,13 +300,16 @@ describe("sidebar organization mode sections", () => {
);

expect(screen.getByText("No threads")).not.toBeNull();
expect(
screen.getAllByRole("button", { name: "Display marker" }),
).toHaveLength(1);
fireEvent.click(
screen.getByRole("button", { name: "Collapse Threads section" }),
screen.getByRole("button", { name: "Collapse Machines section" }),
);
expect(screen.queryByText("No threads")).toBeNull();

fireEvent.click(
screen.getByRole("button", { name: "Expand Threads section" }),
screen.getByRole("button", { name: "Expand Machines section" }),
);
expect(screen.getByText("No threads")).not.toBeNull();
expect(mockBuildMachineThreadGroups).toHaveBeenCalledWith([], []);
Expand Down
Loading