Skip to content

Commit 4e2a387

Browse files
alari76claude
andauthored
feat: replace per-provider new-session buttons with compact dropdown (#509)
The sidebar repo section showed three inline buttons (+ Claude, + OpenCode, + Codex) that grew with each provider. Replace them with a single "+ New session" button opening a compact provider dropdown, driven by the PROVIDERS list so future providers appear automatically. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent cadb6e4 commit 4e2a387

1 file changed

Lines changed: 82 additions & 27 deletions

File tree

src/components/RepoSection.tsx

Lines changed: 82 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55
* and approvals panel. Extracted from LeftSidebar to reduce file size.
66
*/
77

8-
import { useState, useEffect } from 'react'
8+
import { useState, useEffect, useRef, useLayoutEffect } from 'react'
99
import {
1010
IconPlus, IconShieldCheck, IconArchive, IconFileText,
1111
IconChevronDown, IconChevronRight, IconRobot, IconSparkles, IconPencil, IconGitBranch, IconRobotFace,
1212
} from '@tabler/icons-react'
13-
import type { Session } from '../types'
13+
import type { Session, CodingProvider } from '../types'
14+
import { PROVIDERS } from '../types'
1415
import { listArchivedSessions, type ArchivedSessionInfo } from '../lib/ccApi'
1516
import { ApprovalsPanel } from './ApprovalsPanel'
1617
import { DocsFilePicker } from './DocsFilePicker'
@@ -71,6 +72,84 @@ function archivedCompactAge(dateStr: string): string {
7172
return `${Math.floor(hours / 24)}d`
7273
}
7374

75+
/**
76+
* Compact "+ New" button with a provider dropdown (Claude / OpenCode / Codex).
77+
* Uses fixed positioning so the menu is not clipped by the sidebar scroll
78+
* container; dismisses on click-outside or Escape.
79+
*/
80+
function NewSessionMenu({ onNewSession, isMobile }: {
81+
onNewSession: (provider: CodingProvider) => void
82+
isMobile?: boolean
83+
}) {
84+
const [open, setOpen] = useState(false)
85+
const buttonRef = useRef<HTMLButtonElement>(null)
86+
const menuRef = useRef<HTMLDivElement>(null)
87+
88+
useLayoutEffect(() => {
89+
if (!open || !buttonRef.current || !menuRef.current) return
90+
const btn = buttonRef.current.getBoundingClientRect()
91+
const menu = menuRef.current
92+
const menuHeight = menu.offsetHeight
93+
// Open below the button; flip above if it would overflow the viewport
94+
const top = btn.bottom + menuHeight + 4 > window.innerHeight
95+
? btn.top - menuHeight - 4
96+
: btn.bottom + 4
97+
menu.style.top = `${top}px`
98+
menu.style.left = `${btn.left}px`
99+
}, [open])
100+
101+
useEffect(() => {
102+
if (!open) return
103+
function handleKeyDown(e: KeyboardEvent) {
104+
if (e.key === 'Escape') setOpen(false)
105+
}
106+
function handleClickOutside(e: MouseEvent) {
107+
const target = e.target as Node
108+
if (buttonRef.current && !buttonRef.current.contains(target) &&
109+
menuRef.current && !menuRef.current.contains(target)) {
110+
setOpen(false)
111+
}
112+
}
113+
document.addEventListener('keydown', handleKeyDown)
114+
document.addEventListener('mousedown', handleClickOutside)
115+
return () => {
116+
document.removeEventListener('keydown', handleKeyDown)
117+
document.removeEventListener('mousedown', handleClickOutside)
118+
}
119+
}, [open])
120+
121+
return (
122+
<div className={`pl-10 ${isMobile || open ? 'opacity-100' : 'opacity-0 group-hover/repo:opacity-100 focus-within:opacity-100'}`}>
123+
<button
124+
ref={buttonRef}
125+
onClick={() => setOpen(!open)}
126+
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-md ${isMobile ? 'text-[15px]' : 'text-[13px]'} transition-colors ${
127+
open ? 'bg-neutral-6/50 text-neutral-2' : 'text-neutral-5 hover:text-neutral-2 hover:bg-neutral-6/50'
128+
}`}
129+
title="New session"
130+
>
131+
<IconPlus size={12} stroke={2} className="flex-shrink-0" />
132+
<span>New session</span>
133+
<IconChevronDown size={11} stroke={2} className={`flex-shrink-0 transition-transform ${open ? 'rotate-180' : ''}`} />
134+
</button>
135+
{open && (
136+
<div ref={menuRef} className="fixed z-50 min-w-36 rounded-md border border-neutral-8 bg-neutral-11 py-1 shadow-lg shadow-black/30">
137+
{PROVIDERS.map(p => (
138+
<button
139+
key={p.id}
140+
onClick={() => { setOpen(false); onNewSession(p.id) }}
141+
className="w-full flex items-center gap-2 px-3 py-1 text-left text-[13px] text-neutral-3 hover:bg-neutral-6/50 hover:text-neutral-1 transition-colors"
142+
title={p.description}
143+
>
144+
{p.label}
145+
</button>
146+
))}
147+
</div>
148+
)}
149+
</div>
150+
)
151+
}
152+
74153
// --------------------------------------------------------------------------
75154
// Types
76155
// --------------------------------------------------------------------------
@@ -325,31 +404,7 @@ export function RepoSection({
325404
})}
326405

327406
{/* New session for this repo (visible on hover) */}
328-
{onNewSession && (
329-
<div className={`pl-10 flex gap-1 ${isMobile ? 'opacity-100' : 'opacity-0 group-hover/repo:opacity-100'}`}>
330-
<button
331-
onClick={() => onNewSession('claude')}
332-
className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-md text-[13px] text-neutral-5 hover:text-neutral-2 hover:bg-neutral-6/50 transition-colors"
333-
>
334-
<IconPlus size={12} stroke={2} className="flex-shrink-0" />
335-
<span>Claude</span>
336-
</button>
337-
<button
338-
onClick={() => onNewSession('opencode')}
339-
className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-md text-[13px] text-neutral-5 hover:text-neutral-2 hover:bg-neutral-6/50 transition-colors"
340-
>
341-
<IconPlus size={12} stroke={2} className="flex-shrink-0" />
342-
<span>OpenCode</span>
343-
</button>
344-
<button
345-
onClick={() => onNewSession('codex')}
346-
className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-md text-[13px] text-neutral-5 hover:text-neutral-2 hover:bg-neutral-6/50 transition-colors"
347-
>
348-
<IconPlus size={12} stroke={2} className="flex-shrink-0" />
349-
<span>Codex</span>
350-
</button>
351-
</div>
352-
)}
407+
{onNewSession && <NewSessionMenu onNewSession={onNewSession} isMobile={isMobile} />}
353408

354409
{/* Inline docs picker */}
355410
{docsPickerOpen && docsPickerRepoDir === node.workingDir && onDocsPickerSelect && onDocsPickerClose && (

0 commit comments

Comments
 (0)