Skip to content
2 changes: 2 additions & 0 deletions apps/staged/src/lib/components/ui/button/button.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/80',
accent:
'appearance-none bg-[var(--ui-accent)] font-semibold text-primary-foreground shadow-none hover:bg-[var(--ui-accent-hover)] disabled:bg-[var(--ui-accent)]',
outline:
'border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground',
secondary:
Expand Down
3 changes: 3 additions & 0 deletions apps/staged/src/lib/features/agents/AcpConfigPicker.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
remote?: boolean;
dropUp?: boolean;
triggerClass?: string;
layout?: 'horizontal' | 'vertical';
workingDir?: string | null;
onSelectionChange?: (selection: AcpConfigPickerSelection) => void;
}
Expand All @@ -39,6 +40,7 @@
remote = false,
dropUp = false,
triggerClass,
layout = 'horizontal',
workingDir = null,
onSelectionChange,
}: Props = $props();
Expand Down Expand Up @@ -298,6 +300,7 @@
{disabled}
{dropUp}
{triggerClass}
{layout}
{canOpen}
>
<div class="picker-column" data-picker-column="provider">
Expand Down
77 changes: 66 additions & 11 deletions apps/staged/src/lib/features/agents/AcpConfigPickerShell.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
loading?: boolean;
canOpen?: boolean;
hasColumns?: boolean;
/**
* horizontal: all parts inline in the same style, separated by "·".
* vertical: first part is the title; later parts stack below it in
* smaller text.
*/
layout?: 'horizontal' | 'vertical';
children?: Snippet;
footer?: Snippet;
}
Expand All @@ -40,6 +46,7 @@
loading = false,
canOpen = true,
hasColumns = true,
layout = 'horizontal',
children,
footer,
}: Props = $props();
Expand All @@ -60,9 +67,17 @@
labelWidth = null;
return;
}
const observer = new ResizeObserver(() => {
const observer = new ResizeObserver((entries) => {
// Measure from the observer entry, not getBoundingClientRect: the
// trigger mounts inside dialogs whose open animation scales the content
// (zoom-in-95), and a rect captured mid-animation is ~5% short. The
// sizer's local size never changes afterwards, so the observer would
// not fire again and the label would stay truncated. Entry sizes are in
// local CSS pixels, unaffected by ancestor transforms.
const entry = entries[entries.length - 1];
const width = entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width;
// Round up so sub-pixel clipping never triggers part ellipsis.
labelWidth = Math.ceil(sizer.getBoundingClientRect().width);
labelWidth = Math.ceil(width);
});
observer.observe(sizer);
return () => observer.disconnect();
Expand All @@ -80,14 +95,15 @@
{#snippet labelContent()}
<span
class="selector-label"
class:selector-label-vertical={layout === 'vertical'}
aria-label={triggerLabel}
style:width={labelWidth === null ? undefined : `${labelWidth}px`}
>
{#each renderedTriggerParts as part, index (part.id)}
{#if index > 0}
{#if layout === 'horizontal' && index > 0}
<span class="trigger-separator" aria-hidden="true">·</span>
{/if}
<span class="trigger-part">
<span class="trigger-part" class:trigger-part-subtitle={layout === 'vertical' && index > 0}>
{#key part.label}
<span
class="trigger-part-value"
Expand All @@ -101,10 +117,10 @@
{/each}
<span class="trigger-sizer" aria-hidden="true" bind:this={labelSizerEl}>
{#each renderedTriggerParts as part, index (part.id)}
{#if index > 0}
{#if layout === 'horizontal' && index > 0}
<span class="trigger-separator">·</span>
{/if}
<span>{part.label}</span>
<span class:trigger-part-subtitle={layout === 'vertical' && index > 0}>{part.label}</span>
{/each}
</span>
</span>
Expand All @@ -122,11 +138,13 @@
>
<AgentIcon id={providerId ?? ''} size={12} />
{@render labelContent()}
{#if loading}
<Spinner size={12} />
{:else}
<ChevronDown size={12} />
{/if}
<span class="trigger-caret" aria-hidden="true">
{#if loading}
<span class="trigger-caret-icon"><Spinner size={12} /></span>
{:else}
<span class="trigger-caret-icon"><ChevronDown size={12} /></span>
{/if}
</span>
</DropdownMenu.Trigger>
<DropdownMenu.Content
bind:ref={contentEl}
Expand Down Expand Up @@ -182,6 +200,20 @@
transition: width 150ms ease;
}

.selector-label-vertical {
flex-direction: column;
align-items: flex-start;
}

.selector-label-vertical .trigger-part {
max-width: 100%;
line-height: 1.25;
}

.trigger-part-subtitle {
font-size: 0.85em;
}

.trigger-sizer {
display: inline-flex;
position: absolute;
Expand All @@ -193,6 +225,29 @@
white-space: nowrap;
}

.selector-label-vertical .trigger-sizer {
flex-direction: column;
align-items: flex-start;
}

/* Fixed-size slot so swapping the spinner for the chevron never changes
how much horizontal space the trailing icon takes (the icons themselves
could otherwise shrink unevenly when the trigger is width-constrained). */
.trigger-caret {
position: relative;
flex: none;
width: 12px;
height: 12px;
}

.trigger-caret-icon {
display: inline-flex;
position: absolute;
align-items: center;
justify-content: center;
inset: 0;
}

.trigger-part {
display: inline-grid;
min-width: 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
disabled?: boolean;
dropUp?: boolean;
triggerClass?: string;
layout?: 'horizontal' | 'vertical';
onModelChange?: (value: string) => void;
onEffortChange?: (value: string) => void;
}
Expand All @@ -34,6 +35,7 @@
disabled = false,
dropUp = false,
triggerClass,
layout = 'horizontal',
onModelChange,
onEffortChange,
}: Props = $props();
Expand Down Expand Up @@ -122,6 +124,7 @@
{disabled}
{dropUp}
{triggerClass}
{layout}
hasColumns={hasPickerColumns}
>
{#if modelSelector}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<script lang="ts">
import { onMount, tick } from 'svelte';
import Send from '@lucide/svelte/icons/send';
import Spinner from '../../shared/Spinner.svelte';
import { Button } from '$lib/components/ui/button';
import { toast } from 'svelte-sonner';
Expand Down Expand Up @@ -215,7 +214,7 @@
<div class="composer-footer">
<Button
type="button"
variant="outline"
variant="accent"
class="w-full gap-1.5"
onclick={handleSubmit}
disabled={starting || !draftPrompt.trim()}
Expand All @@ -224,7 +223,6 @@
<Spinner size={14} />
{willQueue ? 'Queueing…' : 'Starting…'}
{:else}
<Send size={14} />
{willQueue ? 'Queue commit' : 'Start commit'}
{#if viewport.showShortcutHints}
<span class="shortcut-badge">⌘↵</span>
Expand Down
Loading