Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
0112f24
Add folder-path helpers and sidebar grouping prefs (S1)
brsbl Jun 19, 2026
bd27c2c
Render sidebar threads grouped into folders (S2+S3)
brsbl Jun 19, 2026
2cedbb3
Add folder rename onboarding flow (S4)
brsbl Jun 19, 2026
b663050
Add manual sidebar sort state (S5)
brsbl Jun 19, 2026
2377cf0
Add manual sidebar drag reordering (S6)
brsbl Jun 19, 2026
0a75e06
Add sidebar folder Ladle stories (S7)
brsbl Jun 19, 2026
9a9f3a6
Keep rename modal unchanged for folder grouping
brsbl Jun 19, 2026
bd0a339
Restore first folder onboarding flow
brsbl Jun 19, 2026
cccd900
Auto-enable folders for existing slash titles
brsbl Jun 19, 2026
a4b14f7
Inline folder rename hint
brsbl Jun 19, 2026
e767b24
Organize sidebar by cross-project folders
brsbl Jun 19, 2026
5c08aa2
Store sidebar folder paths explicitly
brsbl Jun 19, 2026
8ef0f56
Add explicit sidebar folder creation
brsbl Jun 19, 2026
71ea5db
Add project-scoped thread folders
brsbl Jun 19, 2026
00e83c5
Limit thread folders to folder view
brsbl Jun 19, 2026
502ca91
Keep pinned threads flat with folders
brsbl Jun 19, 2026
cdce066
Reuse project icons for thread folders
brsbl Jun 19, 2026
a66b545
Add sidebar row action tooltips
brsbl Jun 19, 2026
240e38c
Remove folder shortcut from thread rename
brsbl Jun 19, 2026
cda6941
Show sidebar action tooltips below buttons
brsbl Jun 19, 2026
0ed5bd6
Add folder actions and loose threads section
brsbl Jun 19, 2026
206518c
Paint folder drop hover on rows
brsbl Jun 19, 2026
0d3a1f2
Prevent dragging folders into loose threads
brsbl Jun 19, 2026
ba5d2f0
Restrict folder drag targets to threads
brsbl Jun 19, 2026
fce12a1
Polish sidebar display tooltips
brsbl Jun 19, 2026
36609c8
Make sidebar thread dragging feel lighter
brsbl Jun 19, 2026
80c18ba
Restore row-level thread folder dragging
brsbl Jun 19, 2026
5f6839e
Use icon button for new thread action
brsbl Jun 19, 2026
49c4a66
Split sidebar organize and sort menus
brsbl Jun 19, 2026
b65d070
Refine sidebar organize and sort menus
brsbl Jun 19, 2026
9eecb5f
Merge origin/main into bb/otto-s1-sidebar-nested-folders-thr_js7dkc3iwv
brsbl Jun 19, 2026
5ae181a
Drop "folder" from project path-missing tooltip
brsbl Jun 19, 2026
6b262c4
Align folder row actions with project rows; compact sidebar menus
brsbl Jun 19, 2026
a03734c
Restore label on sidebar New thread button
brsbl Jun 19, 2026
84830fa
Rename archived-threads menu items to "View archived threads"
brsbl Jun 19, 2026
23f7ad0
Add per-folder archived threads view
brsbl Jun 19, 2026
5b1a4bd
Match sidebar tooltip timing to the agent message action bar
brsbl Jun 19, 2026
8fc2934
Scope the loose-threads archived view to unfiled threads
brsbl Jun 19, 2026
d3bf0b6
Unify archived views; show folder scope in the header breadcrumb
brsbl Jun 19, 2026
ab6710d
Fix alphabetical sort direction (leaf/folder consistency) + default A→Z
brsbl Jun 19, 2026
ef440e2
Preview + auto-expand folders when dragging a thread in
brsbl Jun 19, 2026
8da22a8
Show only the warning icon on a path-missing project row
brsbl Jun 19, 2026
7b4e57d
Share sidebar header actions across project and folders views
brsbl Jun 20, 2026
6e3ae9f
Render every sidebar thread list through one shared component
brsbl Jun 20, 2026
db083d2
Spring-load folder drag preview/expand to stop the drag from sticking
brsbl Jun 20, 2026
a057921
Merge remote-tracking branch 'origin/main' into bb/otto-s1-sidebar-ne…
brsbl Jun 20, 2026
fc6d066
Shorten folder drag-dwell to 200ms for a snappier spring-load
brsbl Jun 20, 2026
a448caa
Make folder drop placeholder an empty slot; add a story
brsbl Jun 20, 2026
c67a76a
Harden sidebar folder review issues
brsbl Jun 20, 2026
235dd81
Merge remote-tracking branch 'origin/main' into bb/otto-s1-sidebar-ne…
brsbl Jun 20, 2026
c6015d3
Add folderPath to CLI thread test fixture after main merge
brsbl Jun 20, 2026
e94094c
Drop folder schema in migrate-test replay rollbacks
brsbl Jun 20, 2026
848f110
Make thread folders a global cross-project namespace
brsbl Jun 20, 2026
e728386
Merge remote-tracking branch 'origin/main' into bb/otto-s1-sidebar-ne…
brsbl Jun 20, 2026
a2fa0ef
Polish sidebar, search, and thread-menu UI
brsbl Jun 20, 2026
ed52a2b
Add stories for the thread actions menu and search result row
brsbl Jun 20, 2026
a7f1f79
Add search deep-link, prompt-box polish, and corner-radius tweaks
brsbl Jun 20, 2026
c5de1cc
Polish prompt-box banner and warm the composer cache on thread switch
brsbl Jun 20, 2026
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
11 changes: 7 additions & 4 deletions apps/app/.ladle/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { WorkerPoolContextProvider } from "@pierre/diffs/react";
import { Provider as JotaiProvider, createStore } from "jotai";
import { MemoryRouter } from "react-router-dom";
import { AppToaster } from "../src/components/AppToaster";
import { TooltipProvider } from "../src/components/ui/tooltip";
import { setPreferredTheme } from "../src/hooks/useTheme";
import {
createDiffWorker,
Expand Down Expand Up @@ -65,10 +66,12 @@ export const Provider: GlobalProvider = ({ globalState, children }) => {
}}
highlighterOptions={{}}
>
<div className="min-h-screen text-foreground">
{children}
<AppToaster position="bottom-right" />
</div>
<TooltipProvider delayDuration={300} disableHoverableContent>
<div className="min-h-screen text-foreground">
{children}
<AppToaster position="bottom-right" />
</div>
</TooltipProvider>
</WorkerPoolContextProvider>
</QueryClientProvider>
</JotaiProvider>
Expand Down
2 changes: 2 additions & 0 deletions apps/app/.ladle/story-fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ export function makeThread(overrides: Partial<Thread> = {}): Thread {
providerId: "codex",
title: "Audit recurring permission failures",
titleFallback: "Audit recurring permission failures",
folderPath: null,
status: "idle",
parentThreadId: null,
sourceThreadId: null,
Expand All @@ -305,6 +306,7 @@ export function makeThreadListEntry(
providerId: "codex",
title: "Audit recurring permission failures",
titleFallback: "Audit recurring permission failures",
folderPath: null,
status: "idle",
parentThreadId: null,
sourceThreadId: null,
Expand Down
10 changes: 5 additions & 5 deletions apps/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ const ProjectSettingsView = lazy(() =>
default: m.ProjectSettingsView,
})),
);
const ProjectArchivedThreadsView = lazy(() =>
import("./views/ProjectArchivedThreadsView").then((m) => ({
default: m.ProjectArchivedThreadsView,
const ArchivedThreadsView = lazy(() =>
import("./views/ArchivedThreadsView").then((m) => ({
default: m.ArchivedThreadsView,
})),
);
const PopoutChatView = lazy(() =>
Expand Down Expand Up @@ -118,11 +118,11 @@ function AppRoutes() {
/>
<Route
path={PROJECT_ARCHIVED_ROUTE_PATH}
element={<ProjectArchivedThreadsView />}
element={<ArchivedThreadsView />}
/>
<Route
path={PROJECTLESS_ARCHIVED_ROUTE_PATH}
element={<ProjectArchivedThreadsView />}
element={<ArchivedThreadsView />}
/>
<Route
path={THREAD_DETAIL_ROUTE_PATH}
Expand Down
22 changes: 22 additions & 0 deletions apps/app/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -300,3 +300,25 @@
transform: scale(1) rotate(90deg);
}
}

/* Briefly highlight a conversation message that a sidebar search result
* deep-links to, then fade back to transparent. */
@keyframes bb-search-flash {
from {
background-color: color-mix(in oklch, var(--ring) 22%, transparent);
}
to {
background-color: transparent;
}
}

.bb-search-flash {
border-radius: var(--radius-lg, 0.5rem);
animation: bb-search-flash 1.6s ease-out;
}

@media (prefers-reduced-motion: reduce) {
.bb-search-flash {
animation: none;
}
}
170 changes: 170 additions & 0 deletions apps/app/src/components/dialogs/ThreadFolderCreateDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { useId, useState, type FormEvent, type RefObject } from "react";
import { Button } from "@/components/ui/button.js";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog.js";
import { Input } from "@/components/ui/input.js";
import { normalizeFolderPath } from "@/components/sidebar/folderPath";
import { useNameValidation } from "./useNameValidation.js";
import { useRenameDialogAutoFocus } from "./useRenameDialogAutoFocus.js";

interface ThreadFolderCreateDialogProps {
open: boolean;
pending?: boolean;
onOpenChange: (open: boolean) => void;
onCreate: (path: string) => void;
}

export interface ThreadFolderRenameDialogTarget {
path: string;
}

interface ThreadFolderRenameDialogProps {
target: ThreadFolderRenameDialogTarget | null;
pending?: boolean;
onOpenChange: (open: boolean) => void;
onRename: (path: string, newPath: string) => void;
}

interface ThreadFolderDialogContentProps {
description: string;
initialPath: string;
inputLabel: string;
pending: boolean;
submitLabel: string;
title: string;
onSubmit: (path: string) => void;
inputRef: RefObject<HTMLInputElement | null>;
}

export function ThreadFolderCreateDialog({
open,
pending = false,
onOpenChange,
onCreate,
}: ThreadFolderCreateDialogProps) {
const { inputRef, handleOpenAutoFocus } = useRenameDialogAutoFocus();
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent onOpenAutoFocus={handleOpenAutoFocus}>
{open ? (
<ThreadFolderDialogContent
description="Create a folder for threads."
initialPath=""
inputLabel="Folder name"
pending={pending}
submitLabel="Create folder"
title="New folder"
onSubmit={onCreate}
inputRef={inputRef}
/>
) : null}
</DialogContent>
</Dialog>
);
}

export function ThreadFolderRenameDialog({
target,
pending = false,
onOpenChange,
onRename,
}: ThreadFolderRenameDialogProps) {
const { inputRef, handleOpenAutoFocus } = useRenameDialogAutoFocus();
return (
<Dialog open={target !== null} onOpenChange={onOpenChange}>
<DialogContent onOpenAutoFocus={handleOpenAutoFocus}>
{target ? (
<ThreadFolderDialogContent
key={target.path}
description="Choose a new path for this folder."
initialPath={target.path}
inputLabel="Folder path"
pending={pending}
submitLabel="Rename folder"
title="Rename folder"
onSubmit={(newPath) => onRename(target.path, newPath)}
inputRef={inputRef}
/>
) : null}
</DialogContent>
</Dialog>
);
}

function ThreadFolderDialogContent({
description,
initialPath,
inputLabel,
pending,
submitLabel,
title,
onSubmit,
inputRef,
}: ThreadFolderDialogContentProps) {
const inputId = useId();
const [path, setPath] = useState(initialPath);
const [folderPathMessage, setFolderPathMessage] = useState<string | null>(
null,
);
const { validationMessage, validate, clearMessage } = useNameValidation({
emptyMessage: "Folder name cannot be empty.",
});

const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (pending) return;

const trimmedPath = validate(path);
if (trimmedPath === null) return;
const normalizedPath = normalizeFolderPath(trimmedPath);
if (normalizedPath === null) {
setFolderPathMessage("Folder name cannot be empty.");
return;
}

onSubmit(normalizedPath);
};
const displayedMessage = validationMessage ?? folderPathMessage;

return (
<>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<form className="space-y-4" onSubmit={handleSubmit}>
<div className="space-y-2">
<Input
ref={inputRef}
id={inputId}
aria-label={inputLabel}
value={path}
autoCapitalize="sentences"
autoCorrect="off"
spellCheck={false}
disabled={pending}
onChange={(event) => {
setPath(event.target.value);
setFolderPathMessage(null);
clearMessage();
}}
/>
{displayedMessage ? (
<p className="text-sm text-destructive">{displayedMessage}</p>
) : null}
</div>
<DialogFooter>
<Button type="submit" disabled={pending}>
{submitLabel}
</Button>
</DialogFooter>
</form>
</>
);
}
15 changes: 15 additions & 0 deletions apps/app/src/components/dialogs/ThreadRenameDialog.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ const parentTarget: ThreadRenameDialogTarget = {
currentTitle: "Frontend Parent",
};

const slashTitleTarget: ThreadRenameDialogTarget = {
id: "thr_folder",
currentTitle: "test/say hi",
};

const longTitleTarget: ThreadRenameDialogTarget = {
id: "thr_long",
currentTitle:
Expand Down Expand Up @@ -55,6 +60,16 @@ export function Overview() {
/>
</DialogStage>
</StoryRow>
<StoryRow label="slash title" hint="slashes stay part of the title">
<DialogStage>
<ThreadRenameDialogContent
target={slashTitleTarget}
pending={false}
onRename={noop}
inputRef={inputRef}
/>
</DialogStage>
</StoryRow>
<StoryRow
label="pending"
hint="submit in flight — input and submit are disabled"
Expand Down
19 changes: 15 additions & 4 deletions apps/app/src/components/dialogs/ThreadRenameDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { capitalize } from "@bb/thread-view";
import { useId, useState, type FormEvent, type RefObject } from "react";
import { Button } from "@/components/ui/button.js";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog.js";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog.js";
import { Input } from "@/components/ui/input.js";
import { useNameValidation } from "./useNameValidation.js";
import { useRenameDialogAutoFocus } from "./useRenameDialogAutoFocus.js";
Expand All @@ -11,11 +18,15 @@ export interface ThreadRenameDialogTarget {
currentTitle: string;
}

export interface ThreadRenameDialogPayload {
title: string;
}

interface ThreadRenameDialogProps {
target: ThreadRenameDialogTarget | null;
pending?: boolean;
onOpenChange: (open: boolean) => void;
onRename: (threadId: string, title: string) => void;
onRename: (threadId: string, payload: ThreadRenameDialogPayload) => void;
}

export function ThreadRenameDialog({
Expand Down Expand Up @@ -45,7 +56,7 @@ export function ThreadRenameDialog({
export interface ThreadRenameDialogContentProps {
target: ThreadRenameDialogTarget;
pending: boolean;
onRename: (threadId: string, title: string) => void;
onRename: (threadId: string, payload: ThreadRenameDialogPayload) => void;
inputRef: RefObject<HTMLInputElement | null>;
}

Expand All @@ -69,7 +80,7 @@ export function ThreadRenameDialogContent({
const trimmedTitle = validate(nextTitle);
if (trimmedTitle === null) return;

onRename(target.id, trimmedTitle);
onRename(target.id, { title: trimmedTitle });
};

return (
Expand Down
12 changes: 11 additions & 1 deletion apps/app/src/components/layout/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,11 @@ export function AppLayout({ children }: AppLayoutProps) {
{ enabled: isAutomationDetailView },
);
const automationName = automationDetail?.name ?? "Automation";
// Folder-scoped archived list (`/archived?folder=<path>`); drives the
// breadcrumb's folder segment.
const archivedFolderPath = isArchivedView
? new URLSearchParams(location.search).get("folder")
: null;
const sidebarNavigationQuery = useSidebarNavigation();
const projects = useMemo(
() => sidebarNavigationQuery.data?.projects.map(stripProjectThreads),
Expand Down Expand Up @@ -465,6 +470,9 @@ export function AppLayout({ children }: AppLayoutProps) {
subtitle: undefined,
breadcrumbs: [
{ label: "Threads", to: getRootComposeRoutePath() },
...(archivedFolderPath
? [{ label: archivedFolderPath }]
: []),
{ label: "Archived" },
],
}
Expand Down Expand Up @@ -507,7 +515,9 @@ export function AppLayout({ children }: AppLayoutProps) {
}
if (isArchivedView && projectId) {
if (isProjectlessProjectId(projectId)) {
return "Threads · Archived";
return archivedFolderPath
? `${archivedFolderPath} · Archived`
: "Threads · Archived";
}
return `${projectLabel ?? projectId} · Archived`;
}
Expand Down
Loading
Loading