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:
@@ -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"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user