|
5 | 5 | * and approvals panel. Extracted from LeftSidebar to reduce file size. |
6 | 6 | */ |
7 | 7 |
|
8 | | -import { useState, useEffect } from 'react' |
| 8 | +import { useState, useEffect, useRef, useLayoutEffect } from 'react' |
9 | 9 | import { |
10 | 10 | IconPlus, IconShieldCheck, IconArchive, IconFileText, |
11 | 11 | IconChevronDown, IconChevronRight, IconRobot, IconSparkles, IconPencil, IconGitBranch, IconRobotFace, |
12 | 12 | } from '@tabler/icons-react' |
13 | | -import type { Session } from '../types' |
| 13 | +import type { Session, CodingProvider } from '../types' |
| 14 | +import { PROVIDERS } from '../types' |
14 | 15 | import { listArchivedSessions, type ArchivedSessionInfo } from '../lib/ccApi' |
15 | 16 | import { ApprovalsPanel } from './ApprovalsPanel' |
16 | 17 | import { DocsFilePicker } from './DocsFilePicker' |
@@ -71,6 +72,84 @@ function archivedCompactAge(dateStr: string): string { |
71 | 72 | return `${Math.floor(hours / 24)}d` |
72 | 73 | } |
73 | 74 |
|
| 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 | + |
74 | 153 | // -------------------------------------------------------------------------- |
75 | 154 | // Types |
76 | 155 | // -------------------------------------------------------------------------- |
@@ -325,31 +404,7 @@ export function RepoSection({ |
325 | 404 | })} |
326 | 405 |
|
327 | 406 | {/* 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} />} |
353 | 408 |
|
354 | 409 | {/* Inline docs picker */} |
355 | 410 | {docsPickerOpen && docsPickerRepoDir === node.workingDir && onDocsPickerSelect && onDocsPickerClose && ( |
|
0 commit comments