wip: context-meter + model-label UI and provider/inference tweaks

Checkpoint of in-flight work so the orchestrator branch can rebase onto a
clean main: ContextBar → ContextMeter, model-label helper, model/agent picker
+ provider-snapshot/registry changes, inference payload + message-columns.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 14:55:38 +00:00
parent 5f4c7a9050
commit 163b5b86f7
21 changed files with 471 additions and 233 deletions

View File

@@ -13,6 +13,7 @@ import {
} from '@/components/ui/dropdown-menu';
import { BottomSheet } from '@/components/BottomSheet';
import { useViewport } from '@/hooks/useViewport';
import { formatModelLabel } from '@/lib/model-label';
import { cn } from '@/lib/utils';
const PREFS_KEY = 'boocode.coder.agent-prefs';
@@ -95,9 +96,12 @@ interface PickerProps {
icon?: React.ReactNode;
/** Mobile: render icon + chevron only (no value label) to save row width. */
iconOnly?: boolean;
/** Grow to fill the row's free space and render the value brighter — used for
* the Model picker so the active model is the most visible control. */
flexible?: boolean;
}
function CompactPicker({ label, value, disabled, options, onPick, icon, iconOnly }: PickerProps) {
function CompactPicker({ label, value, disabled, options, onPick, icon, iconOnly, flexible }: PickerProps) {
const { isMobile } = useViewport();
const [open, setOpen] = useState(false);
const currentLabel = options.find((o) => o.id === value)?.label ?? (value || label);
@@ -129,10 +133,16 @@ function CompactPicker({ label, value, disabled, options, onPick, icon, iconOnly
disabled={disabled}
onClick={() => setOpen(true)}
aria-label={`${label}: ${currentLabel}`}
className="inline-flex items-center gap-1 min-h-[44px] px-1.5 rounded text-xs text-muted-foreground hover:text-foreground disabled:opacity-40"
className={cn(
'inline-flex items-center gap-1 min-h-[36px] rounded-lg border border-border bg-muted/50 hover:bg-muted text-xs hover:text-foreground disabled:opacity-40',
iconOnly ? 'px-1.5' : 'px-2.5',
flexible ? 'flex-1 min-w-0 text-foreground' : 'text-muted-foreground',
)}
>
{icon}
{!iconOnly && <span className="truncate max-w-[120px]">{currentLabel}</span>}
{!iconOnly && (
<span className={cn('truncate', flexible ? 'flex-1 text-left' : 'max-w-[120px]')}>{currentLabel}</span>
)}
<ChevronDown className="size-3 opacity-70 shrink-0" />
</button>
<BottomSheet open={open} onClose={() => setOpen(false)} title={label}>
@@ -148,10 +158,14 @@ function CompactPicker({ label, value, disabled, options, onPick, icon, iconOnly
<button
type="button"
disabled={disabled}
className="text-xs text-muted-foreground hover:text-foreground flex items-center gap-1 px-1.5 py-0.5 rounded hover:bg-muted/60 disabled:opacity-40"
className={cn(
'text-xs hover:text-foreground flex items-center gap-1 py-1 rounded-lg border border-border bg-muted/50 hover:bg-muted disabled:opacity-40',
iconOnly ? 'px-1.5' : 'px-2.5',
flexible ? 'flex-1 min-w-0 text-foreground' : 'text-muted-foreground',
)}
>
{icon}
<span className="truncate max-w-[180px]">{currentLabel}</span>
<span className={cn('truncate', flexible ? 'flex-1 text-left' : 'max-w-[180px]')}>{currentLabel}</span>
<ChevronDown className="size-3 opacity-70 shrink-0" />
</button>
</DropdownMenuTrigger>
@@ -337,31 +351,47 @@ export function AgentComposerBar({ projectPath, value, onChange, onProviderComma
const providerOptions = entries.map((e) => ({ id: e.name, label: e.label }));
const modeOptions = (currentEntry?.modes ?? []).map((m) => ({ id: m.id, label: m.label }));
const modelOptions = (currentEntry?.models ?? []).map((m) => ({ id: m.id, label: m.label }));
const modelOptions = (currentEntry?.models ?? []).map((m) => ({ id: m.id, label: formatModelLabel(m.label) }));
const thinkingOpts = thinkingOptions.map((t) => ({ id: t.id, label: t.label }));
return (
<div className="flex items-center gap-1 px-2 py-1 border-b border-border bg-muted/20 shrink-0">
<div className="flex items-center gap-1.5 px-2 py-1 border-b border-border bg-muted/20 shrink-0">
<CompactPicker
label="Provider"
value={value.provider}
options={providerOptions}
onPick={pickProvider}
icon={
currentEntry?.status === 'loading'
? <Loader2 size={13} className="shrink-0 animate-spin" />
: providerIcon(value.provider)
<>
{/* Status indicator lives inside the agent button, to its left:
normalized agent status (external agents only) + WS liveness. */}
{agentStatus && value.provider !== 'boocode' && (
<AgentStatusDot entry={agentStatus} agent={value.provider} />
)}
{connected !== undefined && (
<span
className={cn('inline-block w-1.5 h-1.5 rounded-full shrink-0', connected ? 'bg-green-500' : 'bg-red-500')}
title={connected ? 'Connected' : 'Disconnected'}
/>
)}
{currentEntry?.status === 'loading'
? <Loader2 size={13} className="shrink-0 animate-spin" />
: providerIcon(value.provider)}
</>
}
/>
<CompactPicker
label="Mode"
value={value.modeId ?? ''}
disabled={modeOptions.length === 0}
options={modeOptions}
onPick={(modeId) => persist({ ...value, modeId })}
icon={<Shield className="size-3 shrink-0" />}
iconOnly
/>
{/* Mode (shield) only when the provider actually exposes modes. Native
BooCoder has none, so it's hidden rather than shown disabled. */}
{modeOptions.length > 0 && (
<CompactPicker
label="Mode"
value={value.modeId ?? ''}
options={modeOptions}
onPick={(modeId) => persist({ ...value, modeId })}
icon={<Shield className="size-3 shrink-0" />}
iconOnly
/>
)}
<CompactPicker
label="Model"
value={value.model}
@@ -369,6 +399,7 @@ export function AgentComposerBar({ projectPath, value, onChange, onProviderComma
options={modelOptions}
onPick={pickModel}
icon={<Bot size={13} className="shrink-0" />}
flexible
/>
{thinkingOpts.length > 0 && (
<CompactPicker
@@ -377,26 +408,16 @@ export function AgentComposerBar({ projectPath, value, onChange, onProviderComma
options={thinkingOpts}
onPick={(thinkingOptionId) => persist({ ...value, thinkingOptionId })}
icon={<Brain className="size-3 shrink-0" />}
iconOnly
/>
)}
{/* Status dot + refresh — pinned right (ml-auto), never on its own line. */}
<div className="ml-auto flex items-center gap-1 shrink-0">
{/* #10: normalized agent status — only for an external agent with a
live status frame. Distinct from the WS-liveness dot that follows. */}
{agentStatus && value.provider !== 'boocode' && (
<AgentStatusDot entry={agentStatus} agent={value.provider} />
)}
{connected !== undefined && (
<span
className={cn('inline-block w-1.5 h-1.5 rounded-full shrink-0', connected ? 'bg-green-500' : 'bg-red-500')}
title={connected ? 'Connected' : 'Disconnected'}
/>
)}
{/* Refresh — pinned right. Status now lives in the agent button (left). */}
<div className="ml-auto flex items-center shrink-0">
<button
type="button"
onClick={() => void handleRefresh()}
disabled={refreshing}
className="inline-flex items-center justify-center size-7 max-md:min-h-[44px] max-md:min-w-[44px] rounded text-muted-foreground hover:text-foreground disabled:opacity-40"
className="inline-flex items-center justify-center size-9 max-md:min-h-[36px] max-md:min-w-[44px] rounded-lg border border-border bg-muted/50 hover:bg-muted text-muted-foreground hover:text-foreground disabled:opacity-40"
aria-label="Refresh provider list"
title="Refresh providers"
>