mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-25 00:01:55 +00:00
feat(ai-chat): align footer bar + DropdownV2 mode/autonomy selectors (#9308)
* feat(ai-chat): align footer bar, use DropdownV2 for mode/autonomy selectors Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(dropdown): add `selected` item prop rendering a trailing check Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style(ai-chat): add small spacing between chat input and footer bar Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(ai-chat): always offer the 3 autonomy options in the auto-accept picker Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ai-chat): default autonomy mode to auto-accept on Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(ai-chat): use Button component for footer dropdown triggers Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style(ai-chat): use a hand icon for the auto-accept-off autonomy state Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style(ai-chat): use subtle Button variant for mode and model selectors Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style(ai-chat): tighten spacing between input and footer bar Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(ai-chat): reword autonomy levels as ask/auto-accept/bypass permissions Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(button): add 2xs unified size with tighter padding Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(ai-chat): compact footer bar — 2xs buttons, AtSign context icon, short Yolo label, discreet model Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style(ai-chat): widen the permission selector dropdown Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(dropdown): group shortcut + selected check to avoid ml-auto collision Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(ai-chat): cover getPersistedAutonomyMode default; clarify default comment Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import MenuItem from '$lib/components/meltComponents/MenuItem.svelte'
|
||||
import DropdownSubmenuItem from '$lib/components/DropdownSubmenuItem.svelte'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import { Check, Loader2 } from 'lucide-svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import type { MenubarMenuElements, createDropdownMenu } from '@melt-ui/svelte'
|
||||
import type { Item } from '$lib/utils'
|
||||
@@ -62,8 +62,17 @@
|
||||
{item.displayName}
|
||||
</p>
|
||||
{@render item.extra?.()}
|
||||
{#if item.shortcut}
|
||||
<span class="ml-auto pl-4 text-2xs text-secondary shrink-0">{item.shortcut}</span>
|
||||
{#if item.shortcut || item.selected}
|
||||
<!-- Single trailing group so `shortcut` and `selected` can coexist:
|
||||
two `ml-auto` siblings would collapse to one right-aligned item. -->
|
||||
<div class="ml-auto flex shrink-0 items-center gap-2">
|
||||
{#if item.shortcut}
|
||||
<span class="pl-4 text-2xs text-secondary">{item.shortcut}</span>
|
||||
{/if}
|
||||
{#if item.selected}
|
||||
<Check size={14} class="text-primary" />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if item.tooltip}
|
||||
<Tooltip>
|
||||
|
||||
@@ -283,12 +283,14 @@
|
||||
})
|
||||
|
||||
const overlayPaddingX: Record<InputSize, string> = {
|
||||
'2xs': 'px-1',
|
||||
xs: 'px-1',
|
||||
sm: 'px-2',
|
||||
md: 'px-2',
|
||||
lg: 'px-2'
|
||||
}
|
||||
const overlayHeight: Record<InputSize, string> = {
|
||||
'2xs': 'h-5',
|
||||
xs: 'h-5',
|
||||
sm: 'h-7',
|
||||
md: 'h-8',
|
||||
|
||||
@@ -17,7 +17,7 @@ export namespace ButtonType {
|
||||
* @deprecated Use `UnifiedSize` instead
|
||||
*/
|
||||
export type Size = 'xs3' | 'xs2' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||||
export type UnifiedSize = 'xs' | 'sm' | 'md' | 'lg'
|
||||
export type UnifiedSize = '2xs' | 'xs' | 'sm' | 'md' | 'lg'
|
||||
export type ExtendedSize = 'xs2' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||||
/**
|
||||
* @deprecated Use `Variant` instead
|
||||
@@ -221,6 +221,7 @@ export namespace ButtonType {
|
||||
|
||||
// New unified sizing system
|
||||
export const UnifiedSizingClasses: Record<ButtonType.UnifiedSize, string> = {
|
||||
'2xs': 'px-1', // Compact horizontal padding
|
||||
xs: 'px-2',
|
||||
sm: 'px-2', // Regular horizontal padding
|
||||
md: 'px-4',
|
||||
@@ -228,6 +229,7 @@ export namespace ButtonType {
|
||||
}
|
||||
|
||||
export const UnifiedIconOnlySizingClasses: Record<ButtonType.UnifiedSize, string> = {
|
||||
'2xs': 'px-1',
|
||||
xs: 'px-1',
|
||||
sm: 'px-2', // Square padding for icon-only (same as width padding)
|
||||
md: 'px-2',
|
||||
@@ -235,6 +237,7 @@ export namespace ButtonType {
|
||||
}
|
||||
|
||||
export const UnifiedMinHeightClasses: Record<ButtonType.UnifiedSize, string> = {
|
||||
'2xs': 'min-h-5',
|
||||
xs: 'min-h-5',
|
||||
sm: 'min-h-7',
|
||||
md: 'min-h-8',
|
||||
@@ -242,6 +245,7 @@ export namespace ButtonType {
|
||||
}
|
||||
|
||||
export const UnifiedHeightClasses: Record<ButtonType.UnifiedSize, string> = {
|
||||
'2xs': 'h-5',
|
||||
xs: 'h-5',
|
||||
sm: 'h-7',
|
||||
md: 'h-8',
|
||||
@@ -249,6 +253,7 @@ export namespace ButtonType {
|
||||
}
|
||||
|
||||
export const UnifiedIconSizes: Record<ButtonType.UnifiedSize, number> = {
|
||||
'2xs': 12,
|
||||
xs: 12,
|
||||
sm: 13,
|
||||
md: 14,
|
||||
@@ -256,6 +261,7 @@ export namespace ButtonType {
|
||||
}
|
||||
|
||||
export const UnifiedFontSizes: Record<ButtonType.UnifiedSize, string> = {
|
||||
'2xs': 'font-normal',
|
||||
xs: 'font-normal',
|
||||
sm: 'font-normal',
|
||||
md: 'font-medium',
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
import {
|
||||
AlertTriangle,
|
||||
ArrowDown,
|
||||
AtSign,
|
||||
ChevronDown,
|
||||
ChevronsRight,
|
||||
CheckIcon,
|
||||
Hand,
|
||||
HistoryIcon,
|
||||
Hourglass,
|
||||
MousePointer2,
|
||||
@@ -20,6 +22,7 @@
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
||||
import DropdownV2 from '$lib/components/DropdownV2.svelte'
|
||||
import { type DisplayMessage } from './shared'
|
||||
import type { ContextElement } from './context'
|
||||
import ChatQuickActions from './ChatQuickActions.svelte'
|
||||
@@ -38,16 +41,21 @@
|
||||
|
||||
const MAX_YOLO_TOOLTIP_TOOLS = 8
|
||||
const aiChatManager = getAiChatManager()
|
||||
type AutonomyModeOption = { label: string; mode: AIAutonomyMode }
|
||||
// `label` is shown in the dropdown; `shortLabel` (when set) is shown in the
|
||||
// compact trigger pill to save horizontal space.
|
||||
type AutonomyModeOption = { label: string; shortLabel?: string; mode: AIAutonomyMode }
|
||||
const autonomyModeOptions: AutonomyModeOption[] = [
|
||||
{ label: 'auto accept off', mode: AIAutonomyMode.DEFAULT },
|
||||
{ label: 'auto accept on', mode: AIAutonomyMode.ACCEPT_EDIT },
|
||||
{ label: 'yolo on', mode: AIAutonomyMode.YOLO }
|
||||
{ label: 'Ask permission', mode: AIAutonomyMode.DEFAULT },
|
||||
{ label: 'Auto-accept edits', mode: AIAutonomyMode.ACCEPT_EDIT },
|
||||
{ label: 'Yolo (bypass permissions)', shortLabel: 'Yolo', mode: AIAutonomyMode.YOLO }
|
||||
]
|
||||
const autonomyModeLabel = (
|
||||
mode: AIAutonomyMode,
|
||||
options: AutonomyModeOption[] = autonomyModeOptions
|
||||
) => options.find((option) => option.mode === mode)?.label ?? autonomyModeOptions[0].label
|
||||
const autonomyModeLabel = (mode: AIAutonomyMode) => {
|
||||
const option = autonomyModeOptions.find((o) => o.mode === mode) ?? autonomyModeOptions[0]
|
||||
return option.shortLabel ?? option.label
|
||||
}
|
||||
// "Auto-accept edits" only applies where script/flow edits can be accepted,
|
||||
// "Bypass permissions" only where tool confirmations exist; filter the picker
|
||||
// to the levels that actually do something in the current mode.
|
||||
const isAutonomyModeAvailable = (
|
||||
mode: AIAutonomyMode,
|
||||
autoAcceptEditsAvailable: boolean,
|
||||
@@ -63,6 +71,16 @@
|
||||
}
|
||||
return false
|
||||
}
|
||||
// Ask-permission holds (raised hand); auto-accept/bypass fast-forward. Color
|
||||
// ramps from muted (ask) to accent (auto-accept) to red (bypass).
|
||||
const autonomyModeIcon = (mode: AIAutonomyMode) =>
|
||||
mode === AIAutonomyMode.DEFAULT ? Hand : ChevronsRight
|
||||
const autonomyModeIconColor = (mode: AIAutonomyMode) =>
|
||||
mode === AIAutonomyMode.YOLO
|
||||
? 'text-red-500'
|
||||
: mode === AIAutonomyMode.DEFAULT
|
||||
? 'text-secondary'
|
||||
: 'text-accent'
|
||||
|
||||
let {
|
||||
messages,
|
||||
@@ -218,6 +236,8 @@
|
||||
)
|
||||
)
|
||||
)
|
||||
// Fall back to ask-permission when the persisted mode isn't applicable in the
|
||||
// current AI mode (e.g. auto-accept edits while in a mode without edits).
|
||||
const effectiveAutonomyMode = $derived(
|
||||
availableAutonomyModeOptions.some((option) => option.mode === aiChatManager.autonomyMode)
|
||||
? aiChatManager.autonomyMode
|
||||
@@ -405,7 +425,7 @@
|
||||
{#if showTypingIndicator}
|
||||
<div
|
||||
class={twMerge(
|
||||
'sticky z-10 mt-2 ml-2 self-start pointer-events-none',
|
||||
'sticky z-10 mt-0.5 ml-2 self-start pointer-events-none',
|
||||
showFlowPendingActionControls ? 'bottom-14' : 'bottom-2'
|
||||
)}
|
||||
>
|
||||
@@ -494,7 +514,7 @@
|
||||
isFirstMessage={messages.length === 0}
|
||||
/>
|
||||
<div
|
||||
class="flex flex-row items-center gap-x-1.5"
|
||||
class="mt-1 flex flex-row flex-wrap items-center gap-x-1.5 gap-y-1"
|
||||
class:justify-between={showFooterLeftControls}
|
||||
class:justify-end={!showFooterLeftControls}
|
||||
>
|
||||
@@ -503,12 +523,14 @@
|
||||
{#if showContextPicker && !disabled}
|
||||
<Popover>
|
||||
{#snippet trigger()}
|
||||
<div
|
||||
class="text-primary text-xs flex flex-row items-center font-normal border px-1 rounded-lg hover:bg-surface-hover bg-surface"
|
||||
<Button
|
||||
nonCaptureEvent
|
||||
unifiedSize="2xs"
|
||||
variant="default"
|
||||
title="Add context"
|
||||
>
|
||||
@
|
||||
</div>
|
||||
iconOnly
|
||||
startIcon={{ icon: AtSign }}
|
||||
/>
|
||||
{/snippet}
|
||||
{#snippet content({ close })}
|
||||
{#if aiChatManager.mode === AIMode.APP}
|
||||
@@ -538,53 +560,33 @@
|
||||
</Popover>
|
||||
{/if}
|
||||
{#if showAutonomyModeSelector}
|
||||
<div class="min-w-0">
|
||||
<Popover class="max-w-full">
|
||||
{#snippet trigger()}
|
||||
<div
|
||||
class="text-primary text-xs flex flex-row items-center font-normal gap-0.5 border px-1 rounded-lg"
|
||||
title={autonomyModeTooltip}
|
||||
>
|
||||
<ChevronsRight
|
||||
size={13}
|
||||
class={twMerge(
|
||||
'shrink-0',
|
||||
effectiveAutonomyMode === AIAutonomyMode.YOLO
|
||||
? 'text-red-500'
|
||||
: 'text-accent'
|
||||
)}
|
||||
/>
|
||||
<span class="truncate"
|
||||
>{autonomyModeLabel(
|
||||
effectiveAutonomyMode,
|
||||
availableAutonomyModeOptions
|
||||
)}</span
|
||||
>
|
||||
<div class="shrink-0">
|
||||
<ChevronDown size={16} />
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet content({ close })}
|
||||
<div class="flex flex-col gap-1 p-1 min-w-32">
|
||||
{#each availableAutonomyModeOptions as option (option.mode)}
|
||||
<button
|
||||
class={twMerge(
|
||||
'text-left text-xs hover:bg-surface-hover rounded-md p-1 font-normal',
|
||||
effectiveAutonomyMode === option.mode && 'bg-surface-hover'
|
||||
)}
|
||||
onclick={() => {
|
||||
aiChatManager.setAutonomyMode(option.mode)
|
||||
close()
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/snippet}
|
||||
</Popover>
|
||||
</div>
|
||||
<DropdownV2
|
||||
items={() =>
|
||||
availableAutonomyModeOptions.map((option) => ({
|
||||
displayName: option.label,
|
||||
selected: effectiveAutonomyMode === option.mode,
|
||||
action: () => aiChatManager.setAutonomyMode(option.mode)
|
||||
}))}
|
||||
placement="bottom-start"
|
||||
fixedHeight={false}
|
||||
customWidth={240}
|
||||
>
|
||||
{#snippet buttonReplacement()}
|
||||
<Button
|
||||
nonCaptureEvent
|
||||
unifiedSize="2xs"
|
||||
variant="default"
|
||||
title={autonomyModeTooltip}
|
||||
startIcon={{
|
||||
icon: autonomyModeIcon(effectiveAutonomyMode),
|
||||
classes: autonomyModeIconColor(effectiveAutonomyMode)
|
||||
}}
|
||||
endIcon={{ icon: ChevronDown }}
|
||||
>
|
||||
{autonomyModeLabel(effectiveAutonomyMode)}
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DropdownV2>
|
||||
{/if}
|
||||
{#if effectiveAutonomyMode === AIAutonomyMode.YOLO && aiChatManager.autoAcceptToolConfirmationsAvailable}
|
||||
<Tooltip small placement="top">
|
||||
@@ -593,8 +595,8 @@
|
||||
<div class="max-w-64 text-xs">
|
||||
<p class="font-semibold">
|
||||
{aiChatManager.autoAcceptEditsAvailable
|
||||
? 'Yolo auto-accepts edits and tool usage.'
|
||||
: 'Yolo auto-accepts tool usage.'}
|
||||
? 'Bypass permissions auto-accepts edits and tool usage.'
|
||||
: 'Bypass permissions auto-accepts tool usage.'}
|
||||
</p>
|
||||
<p class="mt-1">
|
||||
{aiChatManager.autoAcceptEditsAvailable
|
||||
|
||||
@@ -126,15 +126,18 @@ function isWorkspacePath(path: string | undefined): path is string {
|
||||
|
||||
function getPersistedAutonomyMode(): AIAutonomyMode {
|
||||
if (!BROWSER || typeof localStorage === 'undefined') {
|
||||
return AIAutonomyMode.DEFAULT
|
||||
return AIAutonomyMode.ACCEPT_EDIT
|
||||
}
|
||||
const persistedMode = localStorage.getItem(AI_AUTONOMY_MODE_STORAGE_KEY)
|
||||
if (isAIAutonomyMode(persistedMode)) {
|
||||
return persistedMode
|
||||
}
|
||||
// No stored preference: default to auto-accepting edits (tool calls still
|
||||
// require confirmation; only YOLO bypasses those). Note this means users who
|
||||
// never opened the autonomy picker now start with edit auto-accept on.
|
||||
return localStorage.getItem(LEGACY_AUTO_ACCEPT_TOOL_CONFIRMATIONS_STORAGE_KEY) === 'true'
|
||||
? AIAutonomyMode.YOLO
|
||||
: AIAutonomyMode.DEFAULT
|
||||
: AIAutonomyMode.ACCEPT_EDIT
|
||||
}
|
||||
|
||||
function persistAutonomyMode(mode: AIAutonomyMode) {
|
||||
|
||||
@@ -46,6 +46,13 @@ vi.mock('./global/gate', () => ({
|
||||
isGlobalAiEnabled: () => true
|
||||
}))
|
||||
|
||||
// Force BROWSER=true so localStorage-backed autonomy persistence is exercised
|
||||
// (the vitest "server" env reports BROWSER=false, which would short-circuit it).
|
||||
vi.mock('esm-env', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('esm-env')>()),
|
||||
BROWSER: true
|
||||
}))
|
||||
|
||||
function createFlowHelpers({
|
||||
hasPendingChanges,
|
||||
acceptAllModuleActions
|
||||
@@ -75,6 +82,9 @@ function createFlowHelpers({
|
||||
describe('AIChatManager autonomy mode', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
// These tests exercise the transition into auto-accept, so start from the
|
||||
// ask-permission baseline rather than the new auto-accept-edits default.
|
||||
localStorage.setItem('ai-chat-autonomy-mode', AIAutonomyMode.DEFAULT)
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
@@ -153,3 +163,28 @@ describe('AIChatManager autonomy mode', () => {
|
||||
expect(applied).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('AIChatManager persisted autonomy default', () => {
|
||||
// Mirrors the private storage keys in AIChatManager.svelte.ts.
|
||||
const AUTONOMY_KEY = 'ai-chat-autonomy-mode'
|
||||
const LEGACY_YOLO_KEY = 'ai-chat-yolo-mode'
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('defaults to auto-accept edits when no preference is stored', () => {
|
||||
expect(new AIChatManager().autonomyMode).toBe(AIAutonomyMode.ACCEPT_EDIT)
|
||||
})
|
||||
|
||||
it('maps the legacy auto-accept-tool-confirmations flag to YOLO', () => {
|
||||
localStorage.setItem(LEGACY_YOLO_KEY, 'true')
|
||||
expect(new AIChatManager().autonomyMode).toBe(AIAutonomyMode.YOLO)
|
||||
})
|
||||
|
||||
it('restores an explicitly persisted autonomy mode', () => {
|
||||
localStorage.setItem(AUTONOMY_KEY, AIAutonomyMode.DEFAULT)
|
||||
expect(new AIChatManager().autonomyMode).toBe(AIAutonomyMode.DEFAULT)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,54 +1,37 @@
|
||||
<script lang="ts">
|
||||
import { ChevronDown } from 'lucide-svelte'
|
||||
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import DropdownV2 from '$lib/components/DropdownV2.svelte'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { aiChatManager, AIMode } from './AIChatManager.svelte'
|
||||
|
||||
const modeLabel = (mode: AIMode) => mode.charAt(0).toUpperCase() + mode.slice(1) + ' mode'
|
||||
|
||||
let allowedModeList = $derived(
|
||||
Object.values(AIMode).filter((mode) => aiChatManager.allowedModes[mode])
|
||||
)
|
||||
let hasMultiple = $derived(allowedModeList.length > 1)
|
||||
</script>
|
||||
|
||||
<div class="min-w-0">
|
||||
<Popover
|
||||
disablePopup={Object.keys(aiChatManager.allowedModes).filter(
|
||||
(k) => aiChatManager.allowedModes[k]
|
||||
).length < 2}
|
||||
class="max-w-full"
|
||||
{#if hasMultiple}
|
||||
<DropdownV2
|
||||
items={() =>
|
||||
allowedModeList.map((mode) => ({
|
||||
displayName: modeLabel(mode),
|
||||
selected: aiChatManager.mode === mode,
|
||||
action: () => aiChatManager.changeMode(mode)
|
||||
}))}
|
||||
placement="bottom-start"
|
||||
fixedHeight={false}
|
||||
customWidth={170}
|
||||
>
|
||||
{#snippet trigger()}
|
||||
|
||||
<div
|
||||
class="text-primary text-xs flex flex-row items-center font-normal gap-0.5 border px-1 rounded-lg"
|
||||
>
|
||||
<span class={`truncate`}>
|
||||
{aiChatManager.mode.charAt(0).toUpperCase() + aiChatManager.mode.slice(1)} mode
|
||||
</span>
|
||||
{#if Object.keys(aiChatManager.allowedModes).filter((k) => aiChatManager.allowedModes[k]).length > 1}
|
||||
<div class="shrink-0">
|
||||
<ChevronDown size={16} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{/snippet}
|
||||
{#snippet content({ close })}
|
||||
|
||||
<div class="flex flex-col gap-1 p-1 min-w-24">
|
||||
{#each Object.values(AIMode) as possibleMode}
|
||||
{#if aiChatManager.allowedModes[possibleMode]}
|
||||
<button
|
||||
class={twMerge(
|
||||
'text-left text-xs hover:bg-surface-hover rounded-md p-1 font-normal',
|
||||
aiChatManager.mode === possibleMode && 'bg-surface-hover'
|
||||
)}
|
||||
onclick={() => {
|
||||
aiChatManager.changeMode(possibleMode)
|
||||
close()
|
||||
}}
|
||||
>
|
||||
{possibleMode.charAt(0).toUpperCase() + possibleMode.slice(1)} mode
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{/snippet}
|
||||
</Popover>
|
||||
</div>
|
||||
{#snippet buttonReplacement()}
|
||||
<Button nonCaptureEvent unifiedSize="2xs" variant="subtle" endIcon={{ icon: ChevronDown }}>
|
||||
{modeLabel(aiChatManager.mode)}
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DropdownV2>
|
||||
{:else}
|
||||
<Button unifiedSize="2xs" variant="subtle">
|
||||
{modeLabel(aiChatManager.mode)}
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { ChevronDown } from 'lucide-svelte'
|
||||
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
||||
import DropdownV2 from '$lib/components/DropdownV2.svelte'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import {
|
||||
COPILOT_SESSION_MODEL_SETTING_NAME,
|
||||
COPILOT_SESSION_PROVIDER_SETTING_NAME
|
||||
} from '$lib/stores'
|
||||
import { storeLocalSetting } from '$lib/utils'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { copilotInfo, copilotSessionModel } from '$lib/aiStore'
|
||||
|
||||
let providerModel = $derived(
|
||||
@@ -21,37 +21,35 @@
|
||||
let multipleModels = $derived($copilotInfo.aiModels.length > 1)
|
||||
</script>
|
||||
|
||||
<div class="min-w-0">
|
||||
<Popover disablePopup={!multipleModels} class="max-w-full">
|
||||
{#snippet trigger()}
|
||||
<div class="text-secondary text-2xs flex flex-row items-center font-normal gap-0.5">
|
||||
<span class={`truncate ${multipleModels ? '' : 'pr-2'}`}>{providerModel.model}</span>
|
||||
{#if multipleModels}
|
||||
<div class="shrink-0">
|
||||
<ChevronDown size={16} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if multipleModels}
|
||||
<DropdownV2
|
||||
items={() =>
|
||||
$copilotInfo.aiModels.map((m) => ({
|
||||
displayName: m.model,
|
||||
selected: m.model === providerModel.model,
|
||||
action: () => {
|
||||
$copilotSessionModel = m
|
||||
storeLocalSetting(COPILOT_SESSION_MODEL_SETTING_NAME, m.model)
|
||||
storeLocalSetting(COPILOT_SESSION_PROVIDER_SETTING_NAME, m.provider)
|
||||
}
|
||||
}))}
|
||||
placement="bottom-end"
|
||||
fixedHeight={false}
|
||||
>
|
||||
{#snippet buttonReplacement()}
|
||||
<Button
|
||||
nonCaptureEvent
|
||||
unifiedSize="2xs"
|
||||
variant="subtle"
|
||||
endIcon={{ icon: ChevronDown }}
|
||||
btnClasses="max-w-[160px] text-secondary font-normal"
|
||||
>
|
||||
<span class="truncate">{providerModel.model}</span>
|
||||
</Button>
|
||||
{/snippet}
|
||||
{#snippet content({ close })}
|
||||
<div class="flex flex-col gap-1 p-1 min-w-24">
|
||||
{#each $copilotInfo.aiModels as providerModel}
|
||||
<button
|
||||
class={twMerge(
|
||||
'text-left text-xs hover:bg-surface-hover rounded-md p-1 font-normal',
|
||||
providerModel.model === $copilotSessionModel?.model && 'bg-surface-hover'
|
||||
)}
|
||||
onclick={() => {
|
||||
$copilotSessionModel = providerModel
|
||||
storeLocalSetting(COPILOT_SESSION_MODEL_SETTING_NAME, providerModel.model)
|
||||
storeLocalSetting(COPILOT_SESSION_PROVIDER_SETTING_NAME, providerModel.provider)
|
||||
close()
|
||||
}}
|
||||
>
|
||||
{providerModel.model}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/snippet}
|
||||
</Popover>
|
||||
</div>
|
||||
</DropdownV2>
|
||||
{:else}
|
||||
<Button unifiedSize="2xs" variant="subtle" btnClasses="max-w-[160px] text-secondary font-normal">
|
||||
<span class="truncate">{providerModel.model}</span>
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
@@ -24,6 +24,11 @@
|
||||
import { ButtonType } from '$lib/components/common/button/model'
|
||||
|
||||
export const inputSizeClasses = {
|
||||
'2xs': twMerge(
|
||||
ButtonType.UnifiedSizingClasses['2xs'],
|
||||
ButtonType.UnifiedMinHeightClasses['2xs'],
|
||||
'px-1 !py-0.5'
|
||||
),
|
||||
xs: twMerge(
|
||||
ButtonType.UnifiedSizingClasses.xs,
|
||||
ButtonType.UnifiedMinHeightClasses.xs,
|
||||
@@ -47,6 +52,7 @@
|
||||
// so the exact centered value there is (content-box height − 2px). Scoped to
|
||||
// the same 1760px breakpoint as the font-size bump; small mode is unchanged.
|
||||
export const inputLeadingClasses: Record<ButtonType.UnifiedSize, string> = {
|
||||
'2xs': 'leading-4 min-[1760px]:leading-[calc(1rem_-_2px)]', // h-5 − py-0.5 → 1rem
|
||||
xs: 'leading-4 min-[1760px]:leading-[calc(1rem_-_2px)]', // h-5 − py-0.5 → 1rem
|
||||
sm: 'leading-6 min-[1760px]:leading-[calc(1.5rem_-_2px)]', // h-7 − py-0.5 → 1.5rem
|
||||
md: 'leading-8 min-[1760px]:leading-[calc(2rem_-_2px)]', // h-8, no py → 2rem
|
||||
|
||||
@@ -1536,6 +1536,9 @@ export type Item = {
|
||||
separatorTop?: boolean
|
||||
submenuItems?: Item[]
|
||||
shortcut?: string
|
||||
// Renders a trailing check on the right of the label to mark the
|
||||
// currently-selected item (for dropdowns used as a single-choice picker).
|
||||
selected?: boolean
|
||||
}
|
||||
|
||||
export function isObjectTooBig(obj: any): boolean {
|
||||
|
||||
Reference in New Issue
Block a user