feat: instant draft-first worktree creation and multi-run launcher redesign (#741)
## Summary - **Instant worktree creation from chat draft**: selecting "+ New worktree" in the draft branch selector immediately creates a session draft and bootstraps the worktree in the background — no modal interruption - **Redesigned multi-run launcher**: compact 2-column grid layout in a right-sized dialog with scroll shadow, sticky footer, tooltips replacing verbose descriptions, and project icons in the selector - **Branch selector aligned across surfaces**: multi-run and agent manager branch pickers now use the shared git store and match NewWorktreeDialog behavior (same default resolution cascade, no synthetic HEAD option, all branches shown) - **Opaque model multi-select dropdown**: fixes text bleed-through on translucent backgrounds by compositing `--surface-elevated` over `--surface-background` - **"+ New" inline button in sidebar worktree headers** for faster worktree creation ## Why Worktree creation was behind modal flow that interrupted the user's train of thought. The draft-first approach lets users start typing immediately while the worktree bootstraps. The multi-run launcher had an oversized form layout with redundant explanations, and its branch picker behaved differently from the main worktree dialog - causing confusion about which branches were available and what the default was.
This commit is contained in:
committed by
GitHub
parent
c66d480782
commit
53c2a0d919
@@ -97,6 +97,8 @@ export interface ModelMultiSelectProps {
|
||||
showChips?: boolean;
|
||||
/** Maximum models allowed */
|
||||
maxModels?: number;
|
||||
/** Optional className for add model trigger button */
|
||||
addButtonClassName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,6 +113,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
|
||||
addButtonLabel = 'Add model',
|
||||
showChips = true,
|
||||
maxModels,
|
||||
addButtonClassName,
|
||||
}) => {
|
||||
const { providers, modelsMetadata } = useConfigStore();
|
||||
const { favoriteModelsList, recentModelsList } = useModelLists();
|
||||
@@ -201,15 +204,29 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
|
||||
|
||||
const hasResults = filteredFavorites.length > 0 || filteredRecents.length > 0 || filteredProviders.length > 0;
|
||||
|
||||
// Calculate available height when dropdown opens
|
||||
// Calculate available height: space above trigger within visible area
|
||||
React.useEffect(() => {
|
||||
if (isOpen && triggerRef.current) {
|
||||
const rect = triggerRef.current.getBoundingClientRect();
|
||||
// Space above trigger minus padding from top edge
|
||||
const spaceAbove = rect.top - 100;
|
||||
// Cap at 400px max, minimum 150px
|
||||
setAvailableHeight(Math.max(150, Math.min(400, spaceAbove)));
|
||||
if (!isOpen || !triggerRef.current) return;
|
||||
|
||||
const triggerRect = triggerRef.current.getBoundingClientRect();
|
||||
|
||||
// Find the nearest dialog or overflow ancestor to constrain within
|
||||
let container: HTMLElement | null = triggerRef.current.parentElement;
|
||||
while (container) {
|
||||
if (container.getAttribute('role') === 'dialog' || container.hasAttribute('data-scroll-shadow')) {
|
||||
break;
|
||||
}
|
||||
const style = getComputedStyle(container);
|
||||
if (style.overflow === 'auto' || style.overflow === 'hidden' || style.overflowY === 'auto' || style.overflowY === 'hidden') {
|
||||
break;
|
||||
}
|
||||
container = container.parentElement;
|
||||
}
|
||||
|
||||
const topBound = container ? container.getBoundingClientRect().top : 0;
|
||||
const spaceAbove = triggerRect.top - topBound - 16;
|
||||
// Cap: min 150, max 300
|
||||
setAvailableHeight(Math.max(150, Math.min(300, spaceAbove)));
|
||||
}, [isOpen]);
|
||||
|
||||
// Focus search input when opened
|
||||
@@ -308,7 +325,15 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className={CHIP_HEIGHT_CLASS}
|
||||
className={cn(
|
||||
CHIP_HEIGHT_CLASS,
|
||||
'!border-border/80 !bg-[var(--surface-subtle)]/95 !backdrop-blur-sm hover:!bg-[var(--interactive-hover)]/70',
|
||||
addButtonClassName,
|
||||
)}
|
||||
style={{
|
||||
backdropFilter: 'blur(10px)',
|
||||
WebkitBackdropFilter: 'blur(10px)',
|
||||
}}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<RiAddLine className="h-3.5 w-3.5 mr-1" />
|
||||
@@ -379,7 +404,12 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
|
||||
let currentFlatIndex = 0;
|
||||
|
||||
return (
|
||||
<div style={{ backgroundColor: 'var(--surface-elevated)' }} className="absolute bottom-full left-0 mb-1 z-50 border border-border/30 rounded-xl overflow-hidden shadow-none w-[min(380px,calc(100vw-2rem))] flex flex-col">
|
||||
<div
|
||||
className="absolute bottom-full left-0 mb-1 z-50 w-[min(380px,calc(100vw-2rem))] max-w-[calc(100vw-2rem)] flex flex-col overflow-hidden rounded-xl border border-border/50 shadow-lg"
|
||||
style={{
|
||||
background: 'linear-gradient(var(--surface-elevated),var(--surface-elevated)),linear-gradient(var(--surface-background),var(--surface-background))',
|
||||
}}
|
||||
>
|
||||
{/* Search input */}
|
||||
<div className="p-2 border-b border-border/40">
|
||||
<div className="relative">
|
||||
@@ -411,10 +441,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
|
||||
{/* Favorites Section */}
|
||||
{filteredFavorites.length > 0 && (
|
||||
<>
|
||||
<div
|
||||
style={{ backgroundColor: 'var(--surface-elevated)' }}
|
||||
className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 border-b border-border/30"
|
||||
>
|
||||
<div className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider sticky top-0 z-10 -mx-1 flex items-center gap-2 border-b border-border/30 px-3 py-1.5 [background:linear-gradient(var(--surface-elevated),var(--surface-elevated)),linear-gradient(var(--surface-background),var(--surface-background))]">
|
||||
<RiStarFill className="h-4 w-4 text-primary" />
|
||||
Favorites
|
||||
</div>
|
||||
@@ -429,10 +456,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
|
||||
{filteredRecents.length > 0 && (
|
||||
<>
|
||||
{filteredFavorites.length > 0 && <div className="h-px bg-border/40 my-1" />}
|
||||
<div
|
||||
style={{ backgroundColor: 'var(--surface-elevated)' }}
|
||||
className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 border-b border-border/30"
|
||||
>
|
||||
<div className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider sticky top-0 z-10 -mx-1 flex items-center gap-2 border-b border-border/30 px-3 py-1.5 [background:linear-gradient(var(--surface-elevated),var(--surface-elevated)),linear-gradient(var(--surface-background),var(--surface-background))]">
|
||||
<RiTimeLine className="h-4 w-4" />
|
||||
Recent
|
||||
</div>
|
||||
@@ -452,7 +476,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
|
||||
{filteredProviders.map((provider, index) => (
|
||||
<React.Fragment key={provider.id}>
|
||||
{index > 0 && <div className="h-px bg-border/40 my-1" />}
|
||||
<div style={{ backgroundColor: 'var(--surface-elevated)' }} className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 -mx-1 px-3 py-1.5 sticky top-0 z-10 border-b border-border/30">
|
||||
<div className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider sticky top-0 z-10 -mx-1 flex items-center gap-2 border-b border-border/30 px-3 py-1.5 [background:linear-gradient(var(--surface-elevated),var(--surface-elevated)),linear-gradient(var(--surface-background),var(--surface-background))]">
|
||||
<ProviderLogo
|
||||
providerId={provider.id}
|
||||
className="h-4 w-4 flex-shrink-0"
|
||||
@@ -513,7 +537,14 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
|
||||
onUpdate(index, { ...model, variant: nextVariant });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger size="chip" className="px-2 gap-1.5 rounded-md bg-interactive-selection/20 border-border/30 hover:bg-interactive-hover/30 typography-meta font-medium text-foreground">
|
||||
<SelectTrigger
|
||||
size="chip"
|
||||
className="px-2 gap-1.5 rounded-md !border-border/80 !bg-[var(--surface-subtle)]/95 !backdrop-blur-sm hover:!bg-[var(--interactive-hover)]/70 typography-meta font-medium text-foreground"
|
||||
style={{
|
||||
backdropFilter: 'blur(10px)',
|
||||
WebkitBackdropFilter: 'blur(10px)',
|
||||
}}
|
||||
>
|
||||
<RiBrainAi3Line
|
||||
className={cn(
|
||||
'h-3.5 w-3.5 flex-shrink-0',
|
||||
|
||||
Reference in New Issue
Block a user