chore: snapshot working tree - pty_exited notifications + in-flight inference WIP
feat(booterm): structured pty_exited WS notifications. Plan-validated, impl-validated, code-reviewed green (contracts build clean, contracts test 29/29, booterm + web typecheck clean). wip: in-progress inference/provider refactor (agents.ts, provider.ts, new llama-providers.ts, removed llama-args-validator), plus arena, dispatcher, compaction, schema changes. openspec: pty-exit-notifications complete; x-agent-flags planned (not yet implemented).
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Check, ChevronDown, RefreshCw, Loader2, Shield, ShieldAlert, Eye, Brain, Bot } from 'lucide-react';
|
||||
import { Check, ChevronDown, RefreshCw, Loader2, Shield, ShieldAlert, Eye, Brain, Bot, Star } from 'lucide-react';
|
||||
import { api } from '@/api/client';
|
||||
import type { AgentSessionConfig, ProviderSnapshotEntry, AgentCommand } from '@/api/types';
|
||||
import { useProviderSnapshot, refreshProviderSnapshot } from '@/hooks/useProviderSnapshot';
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { BottomSheet } from '@/components/BottomSheet';
|
||||
@@ -113,14 +115,22 @@ interface PickerProps {
|
||||
/** 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;
|
||||
/** Grouped rendering: renders sections with labels (Favorites-first, then
|
||||
* per-provider). When provided, `options` is ignored. */
|
||||
groups?: ModelGroup[];
|
||||
}
|
||||
|
||||
function CompactPicker({ label, value, disabled, options, onPick, icon, iconOnly, flexible }: PickerProps) {
|
||||
interface ModelGroup {
|
||||
label: string;
|
||||
options: Array<{ id: string; label: string }>;
|
||||
}
|
||||
|
||||
function CompactPicker({ label, value, disabled, options, onPick, icon, iconOnly, flexible, groups }: PickerProps) {
|
||||
const { isMobile } = useViewport();
|
||||
const [open, setOpen] = useState(false);
|
||||
const currentLabel = options.find((o) => o.id === value)?.label ?? (value || label);
|
||||
|
||||
const list = (
|
||||
const flatList = (
|
||||
<div className="py-1">
|
||||
{options.map((o) => (
|
||||
<button
|
||||
@@ -139,6 +149,36 @@ function CompactPicker({ label, value, disabled, options, onPick, icon, iconOnly
|
||||
</div>
|
||||
);
|
||||
|
||||
const groupedList = (
|
||||
<div className="py-1">
|
||||
{groups!.map((g, gi) => {
|
||||
if (g.options.length === 0) return null;
|
||||
return (
|
||||
<div key={g.label}>
|
||||
{gi > 0 && <div className="h-px bg-border mx-2 my-1" />}
|
||||
<div className="text-[10px] font-medium text-muted-foreground px-2 py-0.5 uppercase tracking-wider">{g.label}</div>
|
||||
{g.options.map((o) => (
|
||||
<button
|
||||
key={o.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onPick(o.id);
|
||||
setOpen(false);
|
||||
}}
|
||||
className="w-full text-left flex items-center gap-2 font-mono text-xs px-2 py-1.5 hover:bg-accent rounded"
|
||||
>
|
||||
<Check className={cn('size-3 shrink-0', o.id === value ? 'opacity-100' : 'opacity-0')} />
|
||||
<span className="truncate">{o.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
const list = groups ? groupedList : flatList;
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<>
|
||||
@@ -243,6 +283,8 @@ function AgentStatusDot({ entry, agent }: { entry: AgentStatusEntry; agent: stri
|
||||
);
|
||||
}
|
||||
|
||||
const FAVORITE_MODELS_KEY = 'favorite_models';
|
||||
|
||||
export function AgentComposerBar({ projectPath, value, onChange, onProviderCommandsChange, connected, agentStatus }: Props) {
|
||||
const allEntries = useProviderSnapshot(projectPath);
|
||||
// 5.5 — the composer picker only offers ENABLED providers that are ready (or
|
||||
@@ -254,9 +296,20 @@ export function AgentComposerBar({ projectPath, value, onChange, onProviderComma
|
||||
[allEntries],
|
||||
);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [favoriteModels, setFavoriteModels] = useState<string[]>([]);
|
||||
|
||||
const hydratedRef = useRef(false);
|
||||
|
||||
// Fetch favorites from settings for the grouped model picker (W5).
|
||||
useEffect(() => {
|
||||
api.settings.get().then((settings) => {
|
||||
const raw = settings[FAVORITE_MODELS_KEY];
|
||||
if (Array.isArray(raw)) {
|
||||
setFavoriteModels(raw.filter((m): m is string => typeof m === 'string'));
|
||||
}
|
||||
}).catch(() => { /* settings fetch is best-effort */ });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
hydratedRef.current = false;
|
||||
}, [projectPath]);
|
||||
@@ -318,6 +371,54 @@ export function AgentComposerBar({ projectPath, value, onChange, onProviderComma
|
||||
onProviderCommandsChange?.(currentEntry?.commands ?? []);
|
||||
}, [currentEntry, onProviderCommandsChange]);
|
||||
|
||||
// Build grouped model options for the native boocode provider (W5).
|
||||
// For other providers, use a flat list. Groups: Favorites first, then
|
||||
// one section per local provider prefix (matching BooChat's ModelPicker).
|
||||
const modelGroups = useMemo<ModelGroup[] | null>(() => {
|
||||
if (!currentEntry || currentEntry.name !== 'boocode') return null;
|
||||
const models = currentEntry.models;
|
||||
if (models.length === 0) return [];
|
||||
|
||||
const favSet = new Set(favoriteModels);
|
||||
|
||||
// Build a model map for quick lookup
|
||||
const modelMap = new Map(models.map((m) => [m.id, m]));
|
||||
|
||||
// Group models by provider prefix (the part before the first slash)
|
||||
const byProvider = new Map<string, Array<{ id: string; label: string }>>();
|
||||
for (const m of models) {
|
||||
const slash = m.id.indexOf('/');
|
||||
const providerPrefix = slash > 0 ? m.id.slice(0, slash) : 'other';
|
||||
const formatted = { id: m.id, label: formatModelLabel(m.label) };
|
||||
const arr = byProvider.get(providerPrefix) ?? [];
|
||||
arr.push(formatted);
|
||||
byProvider.set(providerPrefix, arr);
|
||||
}
|
||||
|
||||
const groups: ModelGroup[] = [];
|
||||
|
||||
// Favorites section: only models that exist in the live inventory
|
||||
const favModels = [...favSet]
|
||||
.filter((id) => modelMap.has(id))
|
||||
.map((id) => ({ id, label: formatModelLabel(modelMap.get(id)!.label) }));
|
||||
if (favModels.length > 0) {
|
||||
groups.push({ label: 'Favorites', options: favModels });
|
||||
}
|
||||
|
||||
// One section per provider group
|
||||
for (const [provider, opts] of byProvider) {
|
||||
groups.push({ label: provider, options: opts });
|
||||
}
|
||||
|
||||
return groups;
|
||||
}, [currentEntry, favoriteModels]);
|
||||
|
||||
// Flat model options for non-boocode providers
|
||||
const modelOptions = useMemo(
|
||||
() => (currentEntry?.models ?? []).map((m) => ({ id: m.id, label: formatModelLabel(m.label) })),
|
||||
[currentEntry],
|
||||
);
|
||||
|
||||
function persist(next: AgentSessionConfig): void {
|
||||
const prefs = loadPrefs();
|
||||
prefs[next.provider] = {
|
||||
@@ -369,7 +470,6 @@ export function AgentComposerBar({ projectPath, value, onChange, onProviderComma
|
||||
// derived from it.
|
||||
const permissionModes = availablePermissionModes(currentEntry?.modes ?? []);
|
||||
const currentPermission = permissionForModeId(value.modeId, currentEntry?.modes ?? []);
|
||||
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 (
|
||||
@@ -423,8 +523,9 @@ export function AgentComposerBar({ projectPath, value, onChange, onProviderComma
|
||||
<CompactPicker
|
||||
label="Model"
|
||||
value={value.model}
|
||||
disabled={modelOptions.length === 0}
|
||||
disabled={modelGroups ? modelGroups.every((g) => g.options.length === 0) : modelOptions.length === 0}
|
||||
options={modelOptions}
|
||||
groups={modelGroups ?? undefined}
|
||||
onPick={pickModel}
|
||||
icon={<Bot size={13} className="shrink-0" />}
|
||||
flexible
|
||||
|
||||
Reference in New Issue
Block a user