ui : move reasoning effort beside the model selector

The reasoning level becomes its own control next to the selector, and
the add menu drops its reasoning submenu.

Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash
This commit is contained in:
Aleksander Grygier
2026-09-29 06:26:47 +02:00
parent c580708110
commit f080a7eacd
6 changed files with 97 additions and 121 deletions
@@ -1,10 +1,6 @@
<script lang="ts">
import { File, Image, MessageSquare, Mic, Plus, Video } from '@lucide/svelte';
import {
ChatFormActionAddReasoningSubmenu,
ChatFormActionAddToolsSubmenu,
McpLogo
} from '$lib/components/app';
import { ChatFormActionAddToolsSubmenu } from '$lib/components/app';
import { buttonVariants } from '$lib/components/ui/button';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import * as Tooltip from '$lib/components/ui/tooltip';
@@ -17,7 +13,6 @@
import { getChatFormActionsContext } from '$lib/contexts';
import { AttachmentAction, AttachmentItemEnabledWhen } from '$lib/enums';
import { useAttachmentMenu } from '$lib/hooks/use-attachment-menu.svelte';
import { serverStore } from '$lib/stores';
interface Props {
class?: string;
@@ -70,8 +65,10 @@
<DropdownMenu.Trigger
{...props}
class={cn(
buttonVariants({ variant: 'secondary' }),
'file-upload-button h-8 w-8 cursor-pointer rounded-full p-0'
// the composer sits on a muted surface, so this keeps a lighter one:
// ghost brings no fill of its own for the variant to fight over
buttonVariants({ variant: 'ghost' }),
'file-upload-button h-8 w-8 cursor-pointer rounded-full bg-background p-0 shadow-sm dark:bg-muted-foreground/15'
)}
disabled={chatFormActions.disabled}
>
@@ -97,13 +94,6 @@
}
}}
>
<!-- in router mode the models selector owns the reasoning submenu -->
{#if !serverStore.isRouterMode}
<ChatFormActionAddReasoningSubmenu />
<DropdownMenu.Separator />
{/if}
<DropdownMenu.Item
class="flex cursor-pointer items-center gap-2"
onclick={() => attachmentMenu.callbacks[AttachmentAction.FILE_UPLOAD]()}
@@ -144,15 +134,6 @@
</DropdownMenu.Item>
<ChatFormActionAddToolsSubmenu />
<DropdownMenu.Item
class="flex cursor-pointer items-center gap-2"
onclick={chatFormActions.onMcpSettingsClick}
>
<McpLogo class={ICON_CLASS_DEFAULT} />
<span>MCP Servers</span>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
@@ -1,75 +0,0 @@
<script lang="ts">
import { Check, Info, Lightbulb, LightbulbOff } from '@lucide/svelte';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import * as Tooltip from '$lib/components/ui/tooltip';
import { ICON_CLASS_DEFAULT } from '$lib/constants';
import { useReasoningMenu } from '$lib/hooks/use-reasoning-menu.svelte';
const reasoning = useReasoningMenu();
</script>
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger class="flex cursor-pointer items-center gap-2">
{#if reasoning.isReasoningActive}
<Lightbulb class="{ICON_CLASS_DEFAULT} shrink-0 text-amber-400" />
{:else if reasoning.isOff}
<LightbulbOff class="{ICON_CLASS_DEFAULT} shrink-0 text-muted-foreground" />
{:else}
<Lightbulb class="{ICON_CLASS_DEFAULT} shrink-0 text-muted-foreground" />
{/if}
<span
class="text-sm inline-flex gap-2 {!reasoning.isReasoningActive
? 'text-muted-foreground'
: ''}"
>
Reasoning
<span class="capitalize text-muted-foreground">
{reasoning.currentEffort}
</span>
</span>
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent
class="w-60 bg-popover p-1.5 text-popover-foreground shadow-md outline-none"
>
{#each reasoning.levels as level (level.value)}
{@const tokenLabel = reasoning.tokenLabel(level)}
<DropdownMenu.Item
class="flex w-full cursor-pointer items-center gap-3 rounded-md px-2 py-1.75 text-left text-sm transition-colors hover:bg-accent {reasoning.isSelected(
level
)
? 'bg-accent'
: ''}"
onclick={() => reasoning.select(level)}
>
{#if reasoning.isSelected(level)}
<Check class="{ICON_CLASS_DEFAULT} shrink-0 text-foreground" />
{:else}
<div class="{ICON_CLASS_DEFAULT} shrink-0"></div>
{/if}
<span class="flex-1">{level.label}</span>
{#if tokenLabel}
<span class="text-[11px] text-muted-foreground opacity-60">
{tokenLabel}
</span>
{/if}
{#if level.hasInfo}
<Tooltip.Root>
<Tooltip.Trigger>
<Info class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content side="left">
<p>Maximum reasoning effort with extended context usage</p>
</Tooltip.Content>
</Tooltip.Root>
{/if}
</DropdownMenu.Item>
{/each}
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
@@ -226,20 +226,20 @@
</div>
</Collapsible.Content>
</Collapsible.Root>
<button
class={sheetItemClass}
onclick={() => {
sheetOpen = false;
chatFormActions.onMcpSettingsClick?.();
}}
type="button"
>
<McpLogo class="inline {ICON_CLASS_DEFAULT} shrink-0" />
<span>MCP Servers</span>
</button>
{/if}
<button
class={sheetItemClass}
onclick={() => {
sheetOpen = false;
chatFormActions.onMcpSettingsClick?.();
}}
type="button"
>
<McpLogo class="inline {ICON_CLASS_DEFAULT} shrink-0" />
<span>MCP Servers</span>
</button>
</div>
</Sheet.Content>
</Sheet.Root>
@@ -0,0 +1,73 @@
<script lang="ts">
import { Check, Info, Lightbulb, LightbulbOff } from '@lucide/svelte';
import { Button } from '$lib/components/ui/button';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import * as Tooltip from '$lib/components/ui/tooltip';
import { ICON_CLASS_DEFAULT } from '$lib/constants';
import { ReasoningEffort } from '$lib/enums';
import { useReasoningMenu } from '$lib/hooks/use-reasoning-menu.svelte';
const reasoning = useReasoningMenu();
// "Default" is the resting state, where the bulb alone carries the meaning
let isDefault = $derived(reasoning.currentEffort === ReasoningEffort.DEFAULT);
</script>
<!-- Reasoning effort sits beside the model selector: the level belongs to the chat, not
to the model list, and the label says which one is in force without opening it. -->
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button
{...props}
aria-label="Reasoning effort"
class="h-auto gap-1 rounded-sm {isDefault ? 'px-1' : 'px-1.75!'} py-1 text-xs"
variant="ghost"
>
<span class="flex items-center gap-0.75 {reasoning.isOff ? 'text-muted-foreground' : ''}">
{#if reasoning.isOff}
<LightbulbOff class="size-3 shrink-0" />
{:else}
<Lightbulb class="size-3 shrink-0" />
{/if}
{#if !isDefault}
<span class="capitalize">{reasoning.currentEffort}</span>
{/if}
</span>
</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end" class="min-w-56">
{#each reasoning.levels as level (level.value)}
{@const tokenLabel = reasoning.tokenLabel(level)}
<DropdownMenu.Item class="gap-3" onSelect={() => reasoning.select(level)}>
{#if reasoning.isSelected(level)}
<Check class="{ICON_CLASS_DEFAULT} shrink-0 text-foreground" />
{:else}
<div class="{ICON_CLASS_DEFAULT} shrink-0"></div>
{/if}
<span class="min-w-0 flex-1 truncate">{level.label}</span>
{#if tokenLabel}
<span class="shrink-0 text-[11px] text-muted-foreground opacity-60">{tokenLabel}</span>
{/if}
{#if level.hasInfo}
<Tooltip.Root>
<Tooltip.Trigger>
<Info class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
</Tooltip.Trigger>
<Tooltip.Content side="right">
<p>Maximum reasoning effort with extended context usage</p>
</Tooltip.Content>
</Tooltip.Root>
{/if}
</DropdownMenu.Item>
{/each}
</DropdownMenu.Content>
</DropdownMenu.Root>
@@ -3,6 +3,7 @@
import { page } from '$app/state';
import {
ChatFormActionModels,
ChatFormActionReasoning,
ChatFormActionRecord,
ChatFormActionsAdd,
ChatFormActionSubmit,
@@ -29,9 +30,9 @@
uploadedFiles?: ChatUploadedFile[];
onFileUpload?: () => void;
onMicClick?: () => void;
onMcpSettingsClick?: () => void;
onStop?: () => void;
onSystemPromptClick?: () => void;
onMcpSettingsClick?: () => void;
}
let {
@@ -135,6 +136,7 @@
get onFileUpload() {
return onFileUpload;
},
get onMcpSettingsClick() {
return onMcpSettingsClick;
},
@@ -160,6 +162,8 @@
{/if}
{#if showModelSelector}
<ChatFormActionReasoning />
<ChatFormActionModels
bind:hasAudioModality
bind:hasModelSelected
@@ -197,6 +197,7 @@ export { default as ChatFormActionSubmit } from './ChatForm/ChatFormActions/Chat
* Exposes an `open` method for programmatically opening the selector.
*/
export { default as ChatFormActionModels } from './ChatForm/ChatFormActions/ChatFormActionModels.svelte';
export { default as ChatFormActionReasoning } from './ChatForm/ChatFormActions/ChatFormActionReasoning.svelte';
/**
* Dropdown submenu for managing tool permissions in the chat form.
@@ -220,14 +221,6 @@ export { default as ChatFormActionModels } from './ChatForm/ChatFormActions/Chat
*/
export { default as ChatFormActionAddToolsSubmenu } from './ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte';
/** Dropdown submenu for selecting reasoning effort level.
*
* Shows a "Reasoning" sub-menu item with a lightbulb icon indicating
* thinking status, and a nested list of effort levels.
* Only visible when the current model supports thinking.
*/
export { default as ChatFormActionAddReasoningSubmenu } from './ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddReasoningSubmenu.svelte';
/**
* Compact context-usage gauge with per-turn and cumulative breakdown in the tooltip.
*/