Skip to content

Commit 36f6f99

Browse files
authored
Add project quick-new thread button (#140)
- Add a desktop quick-new thread icon in project headers - Reuse shared project thread creation logic for sidebar actions - Update sidebar project thread styling and add coverage
1 parent e589cd0 commit 36f6f99

2 files changed

Lines changed: 65 additions & 9 deletions

File tree

apps/web/src/components/ChatView.browser.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1481,6 +1481,31 @@ describe("ChatView timeline estimator parity (full app)", () => {
14811481
}
14821482
});
14831483

1484+
it("shows a project quick-new button on desktop and creates a thread from it", async () => {
1485+
const mounted = await mountChatView({
1486+
viewport: DEFAULT_VIEWPORT,
1487+
snapshot: createSnapshotForTargetUser({
1488+
targetMessageId: "msg-user-project-quick-new-thread-test" as MessageId,
1489+
targetText: "project quick new thread test",
1490+
}),
1491+
});
1492+
1493+
try {
1494+
const quickNewThreadButton = page.getByTestId("project-quick-new-thread-button");
1495+
await expect.element(quickNewThreadButton).toBeInTheDocument();
1496+
1497+
await quickNewThreadButton.click();
1498+
1499+
await waitForURL(
1500+
mounted.router,
1501+
(path) => UUID_ROUTE_RE.test(path),
1502+
"Route should have changed to a new draft thread UUID from the project quick-new button.",
1503+
);
1504+
} finally {
1505+
await mounted.cleanup();
1506+
}
1507+
});
1508+
14841509
it("snapshots sticky codex settings into a new draft thread", async () => {
14851510
useComposerDraftStore.setState({
14861511
stickyModel: "gpt-5.3-codex",

apps/web/src/components/Sidebar.tsx

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,17 @@ export default function Sidebar() {
618618

619619
const canAddProject = newCwd.trim().length > 0 && !isAddingProject;
620620

621+
const createNewThreadForProject = useCallback(
622+
(projectId: ProjectId) => {
623+
return handleNewThread(projectId, {
624+
envMode: resolveSidebarNewThreadEnvMode({
625+
defaultEnvMode: appSettings.defaultThreadEnvMode,
626+
}),
627+
});
628+
},
629+
[appSettings.defaultThreadEnvMode, handleNewThread],
630+
);
631+
621632
const handlePickFolder = async () => {
622633
const api = readNativeApi();
623634
if (!api || isPickingFolder) return;
@@ -1370,11 +1381,11 @@ export default function Sidebar() {
13701381

13711382
return (
13721383
<Collapsible className="group/collapsible" open={shouldShowThreadPanel}>
1373-
<div className="group/project-header relative">
1384+
<div className="group/project-header relative flex items-center gap-1.5">
13741385
<SidebarMenuButton
13751386
ref={isManualProjectSorting ? dragHandleProps?.setActivatorNodeRef : undefined}
13761387
size="sm"
1377-
className={`gap-2 rounded-lg border border-transparent px-2.5 py-2.5 text-left bg-accent/40 hover:bg-accent/70 group-hover/project-header:bg-accent/70 group-hover/project-header:text-sidebar-accent-foreground dark:bg-accent/30 dark:hover:bg-accent/50 dark:group-hover/project-header:bg-accent/50 ${
1388+
className={`min-w-0 flex-1 gap-2 rounded-lg border border-transparent px-2.5 py-2.5 text-left bg-accent/40 hover:bg-accent/70 group-hover/project-header:bg-accent/70 group-hover/project-header:text-sidebar-accent-foreground dark:bg-accent/30 dark:hover:bg-accent/50 dark:group-hover/project-header:bg-accent/50 ${
13781389
isManualProjectSorting ? "cursor-grab active:cursor-grabbing" : "cursor-pointer"
13791390
}`}
13801391
{...(isManualProjectSorting && dragHandleProps ? dragHandleProps.attributes : {})}
@@ -1424,6 +1435,30 @@ export default function Sidebar() {
14241435
{project.name}
14251436
</span>
14261437
</SidebarMenuButton>
1438+
<Tooltip>
1439+
<TooltipTrigger
1440+
render={
1441+
<Button
1442+
type="button"
1443+
variant="outline"
1444+
size="icon-xs"
1445+
aria-label={`Create new thread in ${project.name}`}
1446+
data-testid="project-quick-new-thread-button"
1447+
className="hidden shrink-0 border-primary/25 bg-background/85 text-primary shadow-[0_10px_24px_-18px_color-mix(in_srgb,var(--primary)_70%,transparent)] transition-all duration-150 hover:border-primary/45 hover:bg-primary/10 hover:text-primary lg:inline-flex"
1448+
onClick={(event) => {
1449+
event.preventDefault();
1450+
event.stopPropagation();
1451+
void createNewThreadForProject(project.id);
1452+
}}
1453+
>
1454+
<PlusIcon className="size-3.5" />
1455+
</Button>
1456+
}
1457+
/>
1458+
<TooltipPopup side="right">
1459+
{newThreadShortcutLabel ? `New thread (${newThreadShortcutLabel})` : "New thread"}
1460+
</TooltipPopup>
1461+
</Tooltip>
14271462
</div>
14281463

14291464
<CollapsibleContent>
@@ -1481,18 +1516,14 @@ export default function Sidebar() {
14811516
}
14821517
data-thread-selection-safe
14831518
size="sm"
1484-
className="h-6 w-full translate-x-0 justify-start gap-1.5 px-2 text-left text-[11px] text-muted-foreground/35 transition-colors duration-150 hover:bg-accent/50 hover:text-muted-foreground/65"
1519+
className="h-8 w-full translate-x-0 justify-start gap-2 rounded-md border border-primary/20 bg-linear-to-r from-primary/14 via-primary/10 to-transparent px-2.5 text-left text-[11px] font-medium text-primary shadow-[inset_0_1px_0_hsl(0_0%_100%/0.08)] transition-all duration-150 hover:border-primary/35 hover:from-primary/18 hover:via-primary/12 hover:text-primary"
14851520
onClick={(event) => {
14861521
event.preventDefault();
14871522
event.stopPropagation();
1488-
void handleNewThread(project.id, {
1489-
envMode: resolveSidebarNewThreadEnvMode({
1490-
defaultEnvMode: appSettings.defaultThreadEnvMode,
1491-
}),
1492-
});
1523+
void createNewThreadForProject(project.id);
14931524
}}
14941525
>
1495-
<PlusIcon className="size-3 shrink-0" />
1526+
<PlusIcon className="size-3.5 shrink-0" />
14961527
<span>New thread</span>
14971528
</SidebarMenuSubButton>
14981529
</SidebarMenuSubItem>

0 commit comments

Comments
 (0)