authorgravatar for 73941998+UtkarshUsername@users.noreply.github.comUtkarsh Patil <73941998+UtkarshUsername@users.noreply.github.com> 2026-08-11 18:26:53+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-11 18:26:53+05:30
log752acbf6549e6dd422ac8d8ed11c0fa02577965b
treeeaacbeffc12ec0f62f4dfd3ac480e92ba6d40bcc
parentf5fce74169a5629f701aeb8c4535cab6f7bd3c92
signature Signed by PGP key B5690EEEBB952194

feat: add ability to create a new thread in the current project with shift+click and show shortcut in tooltip (#5994)

Co-authored-by: shivam <91240327+shivamhwp@users.noreply.github.com>

3 files changed, 74 insertions(+), 20 deletions(-)

apps/web/src/components/Sidebar.logic.test.ts+16
...@@ -32,6 +32,7 @@ import {...@@ -32,6 +32,7 @@ import {
32 sortThreadsForSidebar,32 sortThreadsForSidebar,
33 sortProjectsForSidebar,33 sortProjectsForSidebar,
34 sortScopedProjectsForSidebar,34 sortScopedProjectsForSidebar,
35 shouldCreateNewThreadInCurrentProject,
35 THREAD_JUMP_HINT_SHOW_DELAY_MS,36 THREAD_JUMP_HINT_SHOW_DELAY_MS,
36} from "./Sidebar.logic";37} from "./Sidebar.logic";
37import {38import {
...@@ -410,6 +411,21 @@ describe("isTrailingDoubleClick", () => {...@@ -410,6 +411,21 @@ describe("isTrailingDoubleClick", () => {
410 });411 });
411});412});
412413
414describe("shouldCreateNewThreadInCurrentProject", () => {
415 it("creates directly on shift+click in a multi-project setup", () => {
416 expect(shouldCreateNewThreadInCurrentProject(true, 2)).toBe(true);
417 });
418
419 it("opens the picker on a plain click in a multi-project setup", () => {
420 expect(shouldCreateNewThreadInCurrentProject(false, 2)).toBe(false);
421 });
422
423 it("creates directly on any click with a single project", () => {
424 expect(shouldCreateNewThreadInCurrentProject(false, 1)).toBe(true);
425 expect(shouldCreateNewThreadInCurrentProject(true, 1)).toBe(true);
426 });
427});
428
413describe("orderItemsByPreferredIds", () => {429describe("orderItemsByPreferredIds", () => {
414 it("keeps preferred ids first, skips stale ids, and preserves the relative order of remaining items", () => {430 it("keeps preferred ids first, skips stale ids, and preserves the relative order of remaining items", () => {
415 const ordered = orderItemsByPreferredIds({431 const ordered = orderItemsByPreferredIds({
apps/web/src/components/Sidebar.logic.ts+11
...@@ -274,6 +274,17 @@ export function isTrailingDoubleClick(detail: number): boolean {...@@ -274,6 +274,17 @@ export function isTrailingDoubleClick(detail: number): boolean {
274 return detail > 1;274 return detail > 1;
275}275}
276276
277// Shift+click on the new thread button creates directly in the current
278// project, skipping the command palette's project picker. With a single
279// project there is nothing to pick, so a plain click already creates
280// immediately and the modifier changes nothing.
281export function shouldCreateNewThreadInCurrentProject(
282 shiftKey: boolean,
283 projectGroupCount: number,
284): boolean {
285 return shiftKey || projectGroupCount <= 1;
286}
287
277export function orderItemsByPreferredIds<TItem, TId>(input: {288export function orderItemsByPreferredIds<TItem, TId>(input: {
278 items: readonly TItem[];289 items: readonly TItem[];
279 preferredIds: readonly TId[];290 preferredIds: readonly TId[];
apps/web/src/components/Sidebar.tsx+47-20
...@@ -131,6 +131,7 @@ import {...@@ -131,6 +131,7 @@ import {
131 resolveSettledTimestamp,131 resolveSettledTimestamp,
132 resolveSidebarThreadStatus,132 resolveSidebarThreadStatus,
133 searchSidebarThreadsByTitle,133 searchSidebarThreadsByTitle,
134 shouldCreateNewThreadInCurrentProject,
134 resolveWorkingStartedAt,135 resolveWorkingStartedAt,
135 sortLogicalProjectsForSidebar,136 sortLogicalProjectsForSidebar,
136 sortPinnedThreadsForSidebar,137 sortPinnedThreadsForSidebar,
...@@ -3164,29 +3165,39 @@ export default function Sidebar() {...@@ -3164,29 +3165,39 @@ export default function Sidebar() {
3164 // falling back to the top project) — same resolution the command palette3165 // falling back to the top project) — same resolution the command palette
3165 // uses. The command palette already offers a "New thread in..." submenu3166 // uses. The command palette already offers a "New thread in..." submenu
3166 // for multi-project setups.3167 // for multi-project setups.
3167 const handleNewThreadClick = useCallback(() => {3168 const handleNewThreadClick = useCallback(
3168 // One project: nothing to pick, create immediately.3169 (event?: ReactMouseEvent) => {
3169 if (projectGroups.length <= 1) {3170 // One project: nothing to pick, create immediately. Shift+click creates
3171 // directly in the current project even with several projects, skipping
3172 // the palette picker.
3173 if (shouldCreateNewThreadInCurrentProject(event?.shiftKey ?? false, projectGroups.length)) {
3174 if (isMobile) setOpenMobile(false);
3175 void startNewThreadFromContext({
3176 activeDraftThread: newThreadContext.activeDraftThread,
3177 activeThread: newThreadContext.activeThread ?? undefined,
3178 defaultProjectRef: newThreadContext.defaultProjectRef,
3179 handleNewThread: newThreadContext.handleNewThread,
3180 });
3181 return;
3182 }
3170 if (isMobile) setOpenMobile(false);3183 if (isMobile) setOpenMobile(false);
3171 void startNewThreadFromContext({3184 openCommandPalette({ open: "new-thread-in" });
3172 activeDraftThread: newThreadContext.activeDraftThread,3185 },
3173 activeThread: newThreadContext.activeThread ?? undefined,3186 [isMobile, newThreadContext, projectGroups.length, setOpenMobile],
3174 defaultProjectRef: newThreadContext.defaultProjectRef,3187 );
3175 handleNewThread: newThreadContext.handleNewThread,
3176 });
3177 return;
3178 }
3179 if (isMobile) setOpenMobile(false);
3180 openCommandPalette({ open: "new-thread-in" });
3181 }, [isMobile, newThreadContext, projectGroups.length, setOpenMobile]);
31823188
3183 // The button mirrors chat.new: in multi-project setups both route through3189 // The button mirrors chat.new: in multi-project setups both route through
3184 // the command palette's "New thread in..." picker, and in single-project3190 // the command palette's "New thread in..." picker, and in single-project
3185 // setups both create immediately. chat.newLocal always creates directly, so3191 // setups both create immediately. In multi-project setups the label is only
3186 // it is only a correct label when chat.new is unbound.3192 // the picker's shortcut: falling back to chat.newLocal would advertise the
3193 // same shortcut for both the picker and direct create. In single-project
3194 // setups both commands create directly, so chat.newLocal is a valid
3195 // fallback. The second tooltip line (multi-project only) advertises
3196 // shift+click and its keyboard twin chat.newLocal for direct create.
3187 const newThreadShortcutLabel =3197 const newThreadShortcutLabel =
3188 shortcutLabelForCommand(keybindings, "chat.new") ??3198 shortcutLabelForCommand(keybindings, "chat.new") ??
3189 shortcutLabelForCommand(keybindings, "chat.newLocal");3199 (projectGroups.length <= 1 ? shortcutLabelForCommand(keybindings, "chat.newLocal") : undefined);
3200 const newThreadInProjectShortcutLabel = shortcutLabelForCommand(keybindings, "chat.newLocal");
3190 return (3201 return (
3191 <>3202 <>
3192 <SidebarChromeHeader isElectron={isElectron} />3203 <SidebarChromeHeader isElectron={isElectron} />
...@@ -3264,9 +3275,25 @@ export default function Sidebar() {...@@ -3264,9 +3275,25 @@ export default function Sidebar() {
3264 />3275 />
3265 </TooltipTrigger>3276 </TooltipTrigger>
3266 <TooltipPopup side="right">3277 <TooltipPopup side="right">
3267 {newThreadShortcutLabel3278 {projectGroups.length > 1 ? (
3268 ? `New thread (${newThreadShortcutLabel})`3279 <span className="flex flex-col gap-0.5">
3269 : "New thread"}3280 <span>
3281 {newThreadShortcutLabel
3282 ? `New thread (${newThreadShortcutLabel})`
3283 : "New thread"}
3284 </span>
3285 <span className="text-muted-foreground">
3286 New thread in current project: Shift+click
3287 {newThreadInProjectShortcutLabel
3288 ? ` (${newThreadInProjectShortcutLabel})`
3289 : ""}
3290 </span>
3291 </span>
3292 ) : newThreadShortcutLabel ? (
3293 `New thread (${newThreadShortcutLabel})`
3294 ) : (
3295 "New thread"
3296 )}
3270 </TooltipPopup>3297 </TooltipPopup>
3271 </Tooltip>3298 </Tooltip>
3272 </div>3299 </div>