History

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

Utkarsh Patil committed 3 months ago

Commit 752acbf1 parent f5fce743 files · +74 −20Browse files

Unified · Split

3 files
  1. apps/web/src/components/Sidebar.logic.test.ts +16 −0
  2. apps/web/src/components/Sidebar.logic.ts +11 −0
  3. apps/web/src/components/Sidebar.tsx +47 −20
apps/web/src/components/Sidebar.logic.test.ts+16−0
@@ -32,6 +32,7 @@ import { · 31 unmodified lines
3232 sortThreadsForSidebar,
3333 sortProjectsForSidebar,
3434 sortScopedProjectsForSidebar,
35+ shouldCreateNewThreadInCurrentProject,
3536 THREAD_JUMP_HINT_SHOW_DELAY_MS,
3637 } from "./Sidebar.logic";
3738 import {
@@ -410,6 +411,21 @@ describe("isTrailingDoubleClick", () => { · 372 unmodified lines
410411 });
411412 });
412413
414+describe("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+
413429 describe("orderItemsByPreferredIds", () => {
414430 it("keeps preferred ids first, skips stale ids, and preserves the relative order of remaining items", () => {
415431 const ordered = orderItemsByPreferredIds({
apps/web/src/components/Sidebar.logic.ts+11−0
@@ -274,6 +274,17 @@ export function isTrailingDoubleClick(detail: number): boolean { · 273 unmodified lines
274274 return detail > 1;
275275 }
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.
281+export function shouldCreateNewThreadInCurrentProject(
282+ shiftKey: boolean,
283+ projectGroupCount: number,
284+): boolean {
285+ return shiftKey || projectGroupCount <= 1;
286+}
287+
277288 export function orderItemsByPreferredIds<TItem, TId>(input: {
278289 items: readonly TItem[];
279290 preferredIds: readonly TId[];
apps/web/src/components/Sidebar.tsx+47−20
@@ -131,6 +131,7 @@ import { · 130 unmodified lines
131131 resolveSettledTimestamp,
132132 resolveSidebarThreadStatus,
133133 searchSidebarThreadsByTitle,
134+ shouldCreateNewThreadInCurrentProject,
134135 resolveWorkingStartedAt,
135136 sortLogicalProjectsForSidebar,
136137 sortPinnedThreadsForSidebar,
@@ -3164,29 +3165,39 @@ export default function Sidebar() { · 3027 unmodified lines
31643165 // falling back to the top project) — same resolution the command palette
31653166 // uses. The command palette already offers a "New thread in..." submenu
31663167 // for multi-project setups.
3167− const handleNewThreadClick = useCallback(() => {
3168− // One project: nothing to pick, create immediately.
3169− if (projectGroups.length <= 1) {
3168+ const handleNewThreadClick = useCallback(
3169+ (event?: ReactMouseEvent) => {
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+ }
31703183 if (isMobile) setOpenMobile(false);
3171− void startNewThreadFromContext({
3172− activeDraftThread: newThreadContext.activeDraftThread,
3173− activeThread: newThreadContext.activeThread ?? undefined,
3174− defaultProjectRef: newThreadContext.defaultProjectRef,
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]);
3184+ openCommandPalette({ open: "new-thread-in" });
3185+ },
3186+ [isMobile, newThreadContext, projectGroups.length, setOpenMobile],
3187+ );
31823188
31833189 // The button mirrors chat.new: in multi-project setups both route through
31843190 // the command palette's "New thread in..." picker, and in single-project
3185− // setups both create immediately. chat.newLocal always creates directly, so
3186− // it is only a correct label when chat.new is unbound.
3191+ // setups both create immediately. In multi-project setups the label is only
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.
31873197 const newThreadShortcutLabel =
31883198 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");
31903201 return (
31913202 <>
31923203 <SidebarChromeHeader isElectron={isElectron} />
@@ -3264,9 +3275,25 @@ export default function Sidebar() { · 71 unmodified lines
32643275 />
32653276 </TooltipTrigger>
32663277 <TooltipPopup side="right">
3267− {newThreadShortcutLabel
3268− ? `New thread (${newThreadShortcutLabel})`
3269− : "New thread"}
3278+ {projectGroups.length > 1 ? (
3279+ <span className="flex flex-col gap-0.5">
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+ )}
32703297 </TooltipPopup>
32713298 </Tooltip>
32723299 </div>