mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
Polish Share Compute settings (#3735)
## Summary - Refresh Share Compute with the shared agent-style model controls. - Reveal sharing details and advanced options only while sharing. - Remove the preview-only mesh API path. ## Validation - `pnpm check` - `pnpm test` - `pnpm exec playwright test tests/e2e/mesh-compute.spec.ts` Snapshots are attached in a follow-up comment. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
@@ -1,9 +1,19 @@
|
||||
import * as React from "react";
|
||||
import { ChevronDown, Cpu } from "lucide-react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
||||
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Switch } from "@/shared/ui/switch";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import {
|
||||
AgentConfigTextInput,
|
||||
AgentDropdownSelect,
|
||||
type AgentDropdownOption,
|
||||
} from "@/features/agents/ui/agentConfigControls";
|
||||
import {
|
||||
CUSTOM_MODEL_DROPDOWN_VALUE,
|
||||
PERSONA_FIELD_CONTROL_CLASS,
|
||||
PERSONA_FIELD_SHELL_CLASS,
|
||||
} from "@/features/agents/ui/agentConfigOptions";
|
||||
|
||||
import {
|
||||
meshStartNode,
|
||||
@@ -17,10 +27,6 @@ import type {
|
||||
MeshModelOption,
|
||||
MeshNodeStatus,
|
||||
} from "@/shared/api/tauriMesh";
|
||||
import {
|
||||
SettingsOptionGroup,
|
||||
SettingsOptionRow,
|
||||
} from "@/features/settings/ui/SettingsOptionGroup";
|
||||
import { SettingsSectionHeader } from "@/features/settings/ui/SettingsSectionHeader";
|
||||
import { classifyModelRef } from "../classifyModelRef";
|
||||
import {
|
||||
@@ -36,6 +42,20 @@ import { deriveServingIndicator } from "../servingUsage";
|
||||
const MODEL_DRAFT_STORAGE_KEY = "buzz.mesh-compute.share.model.v1";
|
||||
const MAX_VRAM_DRAFT_STORAGE_KEY = "buzz.mesh-compute.share.max-vram-gb.v1";
|
||||
|
||||
// Keep the Share compute controls visually and behaviorally aligned with the
|
||||
// agent configuration fields. This is intentionally the same shell used by
|
||||
// AgentDefaultsEditor rather than a local approximation of a select.
|
||||
const MESH_SELECT_TRIGGER_CLASS = cn(
|
||||
PERSONA_FIELD_CONTROL_CLASS,
|
||||
PERSONA_FIELD_SHELL_CLASS,
|
||||
"h-11 px-3 py-2 leading-6 hover:bg-muted/40 focus:bg-muted/40 [&>svg]:text-muted-foreground/60",
|
||||
);
|
||||
|
||||
const SHARE_COMPUTE_REVEAL_TRANSITION = {
|
||||
duration: 0.22,
|
||||
ease: [0.23, 1, 0.32, 1],
|
||||
} as const;
|
||||
|
||||
function readDraft(key: string): string {
|
||||
try {
|
||||
return window.localStorage.getItem(key) ?? "";
|
||||
@@ -64,6 +84,7 @@ function writeDraft(key: string, value: string): void {
|
||||
* exposing implementation protocols or raw mesh controls.
|
||||
*/
|
||||
export function MeshComputeSettingsCard() {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
const { status, error, refresh } = useMeshNodeStatus();
|
||||
const [installedModels, setInstalledModels] = React.useState<
|
||||
MeshModelOption[]
|
||||
@@ -75,6 +96,7 @@ export function MeshComputeSettingsCard() {
|
||||
const [maxVramGb, setMaxVramGb] = React.useState<string>(() =>
|
||||
readDraft(MAX_VRAM_DRAFT_STORAGE_KEY),
|
||||
);
|
||||
const [isCustomModelEditing, setIsCustomModelEditing] = React.useState(false);
|
||||
const [advancedOpen, setAdvancedOpen] = React.useState(false);
|
||||
const [actionInFlight, setActionInFlight] = React.useState(false);
|
||||
const [pendingAction, setPendingAction] = React.useState<
|
||||
@@ -163,6 +185,7 @@ export function MeshComputeSettingsCard() {
|
||||
const controlsDisabled = actionInFlight || (slotOccupied && !isConsuming);
|
||||
const refClass = classifyModelRef(modelInput);
|
||||
const canStart = refClass.kind !== "unknown" && !actionInFlight;
|
||||
const showSharingControls = isSharing || pendingAction === "start";
|
||||
|
||||
async function handleToggle(next: boolean) {
|
||||
// Never let the Share switch tear down a consume session. The switch is
|
||||
@@ -204,12 +227,7 @@ export function MeshComputeSettingsCard() {
|
||||
<section className="min-w-0" data-testid="settings-mesh-share-compute">
|
||||
<SettingsSectionHeader
|
||||
title="Share compute"
|
||||
description={
|
||||
<>
|
||||
Share this machine with your relay. When on, other members can run
|
||||
their agents here.
|
||||
</>
|
||||
}
|
||||
description="Share this machine with members of this relay so they can run agents here."
|
||||
/>
|
||||
|
||||
{error ? (
|
||||
@@ -226,8 +244,8 @@ export function MeshComputeSettingsCard() {
|
||||
<DownloadProgressBar progress={downloadProgress} />
|
||||
) : null}
|
||||
|
||||
<SettingsOptionGroup>
|
||||
<SettingsOptionRow>
|
||||
<div className="space-y-5">
|
||||
<div className="flex min-w-0 items-start justify-between gap-6">
|
||||
<div className="min-w-0">
|
||||
<label
|
||||
className="text-sm font-medium"
|
||||
@@ -235,29 +253,12 @@ export function MeshComputeSettingsCard() {
|
||||
>
|
||||
Share this machine
|
||||
</label>
|
||||
<StatusLine
|
||||
isConsuming={isConsuming}
|
||||
pendingAction={pendingAction}
|
||||
status={status}
|
||||
/>
|
||||
{servingIndicator.show ? (
|
||||
<p
|
||||
className={
|
||||
servingIndicator.hasRemoteConsumers
|
||||
? "mt-0.5 text-2xs text-emerald-600 dark:text-emerald-400"
|
||||
: "mt-0.5 text-2xs text-muted-foreground"
|
||||
}
|
||||
data-testid="mesh-serving-usage"
|
||||
title={servingIndicator.detail ?? undefined}
|
||||
>
|
||||
{servingIndicator.label}
|
||||
{servingIndicator.detail ? (
|
||||
<span className="text-muted-foreground">
|
||||
{" "}
|
||||
· {servingIndicator.detail}
|
||||
</span>
|
||||
) : null}
|
||||
</p>
|
||||
{!isSharing ? (
|
||||
<StatusLine
|
||||
isConsuming={isConsuming}
|
||||
pendingAction={pendingAction}
|
||||
status={status}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<Switch
|
||||
@@ -277,126 +278,125 @@ export function MeshComputeSettingsCard() {
|
||||
id="mesh-share-compute-toggle"
|
||||
onCheckedChange={handleToggle}
|
||||
/>
|
||||
</SettingsOptionRow>
|
||||
|
||||
<div className="px-4 pb-4 pt-5">
|
||||
<label
|
||||
className="mb-3 flex items-center gap-2 text-sm font-medium"
|
||||
htmlFor="mesh-share-compute-model"
|
||||
>
|
||||
<Cpu className="h-4 w-4 text-muted-foreground" />
|
||||
Model
|
||||
</label>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Input
|
||||
data-testid="mesh-share-compute-model"
|
||||
disabled={controlsDisabled}
|
||||
id="mesh-share-compute-model"
|
||||
onChange={(e) => {
|
||||
const next = e.target.value;
|
||||
setModelInput(next);
|
||||
writeDraft(MODEL_DRAFT_STORAGE_KEY, next);
|
||||
}}
|
||||
placeholder="Qwen3-8B-Q4_K_M or hf://meshllm/qwen3-8b@main"
|
||||
value={modelInput}
|
||||
/>
|
||||
<p className="text-sm font-normal text-muted-foreground">
|
||||
Choose a suggested model below, or enter a model reference or
|
||||
local file. Buzz downloads remote models when sharing starts.
|
||||
</p>
|
||||
{catalog && catalog.entries.length > 0 ? (
|
||||
<CatalogPicker
|
||||
catalog={catalog}
|
||||
disabled={controlsDisabled}
|
||||
onPick={(name) => {
|
||||
setModelInput(name);
|
||||
writeDraft(MODEL_DRAFT_STORAGE_KEY, name);
|
||||
}}
|
||||
selected={modelInput.trim()}
|
||||
/>
|
||||
) : null}
|
||||
{installedModels.length > 0 ? (
|
||||
<div className="mt-1">
|
||||
<p className="text-sm font-normal text-muted-foreground">
|
||||
Already installed on this machine:
|
||||
</p>
|
||||
<ul
|
||||
className="mt-1 flex flex-wrap gap-1.5"
|
||||
data-testid="mesh-share-compute-installed-list"
|
||||
>
|
||||
{installedModels.map((m) => (
|
||||
<li key={m.id}>
|
||||
<button
|
||||
className="rounded border border-border/60 bg-muted/20 px-2 py-0.5 text-sm hover:bg-muted/40 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
disabled={controlsDisabled}
|
||||
onClick={() => {
|
||||
setModelInput(m.id);
|
||||
writeDraft(MODEL_DRAFT_STORAGE_KEY, m.id);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{m.name ?? m.id}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details
|
||||
className="px-4 py-3"
|
||||
onToggle={(e) =>
|
||||
setAdvancedOpen((e.target as HTMLDetailsElement).open)
|
||||
}
|
||||
open={advancedOpen}
|
||||
>
|
||||
<summary className="flex cursor-pointer items-center gap-1.5 text-sm font-medium text-foreground">
|
||||
<MeshModelPicker
|
||||
catalog={catalog}
|
||||
disabled={controlsDisabled}
|
||||
installedModels={installedModels}
|
||||
isCustomModelEditing={isCustomModelEditing}
|
||||
model={modelInput}
|
||||
onCustomModelEditingChange={setIsCustomModelEditing}
|
||||
onModelChange={(next) => {
|
||||
setModelInput(next);
|
||||
writeDraft(MODEL_DRAFT_STORAGE_KEY, next);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="pt-3">
|
||||
<button
|
||||
aria-expanded={advancedOpen}
|
||||
className="inline-flex h-9 items-center gap-1.5 text-sm font-medium text-foreground transition-colors hover:text-foreground/80 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
|
||||
data-testid="mesh-share-compute-advanced-toggle"
|
||||
onClick={() => setAdvancedOpen((current) => !current)}
|
||||
type="button"
|
||||
>
|
||||
<span>Advanced</span>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"h-4 w-4 text-muted-foreground transition-transform",
|
||||
advancedOpen ? "rotate-0" : "-rotate-90",
|
||||
"h-4 w-4 text-muted-foreground transition-transform duration-150 ease-out",
|
||||
advancedOpen && "rotate-180",
|
||||
)}
|
||||
/>
|
||||
Advanced
|
||||
</summary>
|
||||
<div className="mt-3 flex flex-col gap-2">
|
||||
<label className="text-sm font-medium" htmlFor="mesh-vram">
|
||||
Max VRAM (GB)
|
||||
</label>
|
||||
<Input
|
||||
data-testid="mesh-share-compute-vram"
|
||||
id="mesh-vram"
|
||||
inputMode="decimal"
|
||||
onChange={(e) => {
|
||||
const next = e.target.value;
|
||||
setMaxVramGb(next);
|
||||
writeDraft(MAX_VRAM_DRAFT_STORAGE_KEY, next);
|
||||
}}
|
||||
placeholder="No limit"
|
||||
value={maxVramGb}
|
||||
/>
|
||||
{status?.consoleUrl ? (
|
||||
<p className="text-sm font-normal text-muted-foreground">
|
||||
Debug console:{" "}
|
||||
<a
|
||||
className="underline"
|
||||
href={status.consoleUrl}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{status.consoleUrl}
|
||||
</a>
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</details>
|
||||
</SettingsOptionGroup>
|
||||
</button>
|
||||
{advancedOpen ? (
|
||||
<div className="mt-3 space-y-1.5">
|
||||
<label className="text-sm font-medium" htmlFor="mesh-vram">
|
||||
Max VRAM (GB)
|
||||
</label>
|
||||
<AgentConfigTextInput
|
||||
data-testid="mesh-share-compute-vram"
|
||||
disabled={controlsDisabled}
|
||||
id="mesh-vram"
|
||||
inputMode="decimal"
|
||||
onChange={(e) => {
|
||||
const next = e.target.value;
|
||||
setMaxVramGb(next);
|
||||
writeDraft(MAX_VRAM_DRAFT_STORAGE_KEY, next);
|
||||
}}
|
||||
placeholder="No limit"
|
||||
usePersonaInputStyle
|
||||
value={maxVramGb}
|
||||
/>
|
||||
{status?.consoleUrl ? (
|
||||
<p className="text-sm font-normal text-muted-foreground">
|
||||
Debug console:{" "}
|
||||
<a
|
||||
className="underline"
|
||||
href={status.consoleUrl}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{status.consoleUrl}
|
||||
</a>
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<p className="mt-3 rounded-lg bg-muted/30 px-3 py-2 text-sm font-normal text-muted-foreground">
|
||||
Only members of this relay can use this machine's shared compute.
|
||||
</p>
|
||||
<AnimatePresence initial={false}>
|
||||
{showSharingControls ? (
|
||||
<motion.div
|
||||
animate={{ height: "auto", opacity: 1 }}
|
||||
className="overflow-hidden"
|
||||
data-testid="mesh-share-compute-options-motion"
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
key="mesh-share-compute-options"
|
||||
transition={
|
||||
shouldReduceMotion
|
||||
? { duration: 0 }
|
||||
: SHARE_COMPUTE_REVEAL_TRANSITION
|
||||
}
|
||||
>
|
||||
<section
|
||||
className="space-y-1"
|
||||
data-testid="mesh-share-compute-sharing-status"
|
||||
>
|
||||
<h3 className="text-sm font-medium">Sharing</h3>
|
||||
<div className="space-y-1 rounded-lg bg-muted/30 px-3 py-2">
|
||||
<StatusLine
|
||||
isConsuming={isConsuming}
|
||||
omitSharingVerb
|
||||
pendingAction={pendingAction}
|
||||
status={status}
|
||||
/>
|
||||
{servingIndicator.show ? (
|
||||
<p
|
||||
className={
|
||||
servingIndicator.hasRemoteConsumers
|
||||
? "text-2xs text-emerald-600 dark:text-emerald-400"
|
||||
: "text-2xs text-muted-foreground"
|
||||
}
|
||||
data-testid="mesh-serving-usage"
|
||||
title={servingIndicator.detail ?? undefined}
|
||||
>
|
||||
{servingIndicator.label}
|
||||
{servingIndicator.detail ? (
|
||||
<span className="text-muted-foreground">
|
||||
{" "}
|
||||
· {servingIndicator.detail}
|
||||
</span>
|
||||
) : null}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
</motion.div>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -465,100 +465,159 @@ const FIT_CLASS: Record<MeshCatalogEntry["fit"], string> = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Hardware-ranked curated model list (mesh-console's diagnose pattern).
|
||||
* Click a row to fill the model field. Models too large for this machine are
|
||||
* listed but disabled — honest about why, instead of hiding them.
|
||||
* Share Compute's models use the agent configuration picker instead of a
|
||||
* second, hand-rolled option system. The catalog remains hardware-aware, but
|
||||
* its recommendations, installed models, and advanced choices now appear in
|
||||
* the same searchable dropdown used when customizing an agent.
|
||||
*/
|
||||
function CatalogPicker({
|
||||
function MeshModelPicker({
|
||||
catalog,
|
||||
disabled,
|
||||
onPick,
|
||||
selected,
|
||||
installedModels,
|
||||
isCustomModelEditing,
|
||||
model,
|
||||
onCustomModelEditingChange,
|
||||
onModelChange,
|
||||
}: {
|
||||
catalog: MeshModelCatalog;
|
||||
catalog: MeshModelCatalog | null;
|
||||
disabled: boolean;
|
||||
onPick: (name: string) => void;
|
||||
selected: string;
|
||||
installedModels: readonly MeshModelOption[];
|
||||
isCustomModelEditing: boolean;
|
||||
model: string;
|
||||
onCustomModelEditingChange: (editing: boolean) => void;
|
||||
onModelChange: (model: string) => void;
|
||||
}) {
|
||||
const [expanded, setExpanded] = React.useState(false);
|
||||
// Above the fold: the Buzz-curated picks (models known to work well with
|
||||
// agents on shared compute). Below: everything else, as advanced options.
|
||||
const curated = catalog.entries.filter((e) => e.curated);
|
||||
const advanced = catalog.entries.filter((e) => !e.curated);
|
||||
const visible = expanded ? catalog.entries : curated;
|
||||
const options = React.useMemo<AgentDropdownOption[]>(() => {
|
||||
const seen = new Set<string>();
|
||||
const catalogOptions = (catalog?.entries ?? []).map((entry) => {
|
||||
seen.add(entry.name);
|
||||
return {
|
||||
disabled: entry.fit === "too_large",
|
||||
label: <MeshModelOptionLabel entry={entry} />,
|
||||
value: entry.name,
|
||||
};
|
||||
});
|
||||
const localOptions = installedModels.flatMap((installed) => {
|
||||
if (seen.has(installed.id)) return [];
|
||||
return [
|
||||
{
|
||||
label: (
|
||||
<div className="flex min-w-0 items-center justify-between gap-3">
|
||||
<span className="min-w-0 truncate">
|
||||
{installed.name ?? installed.id}
|
||||
</span>
|
||||
<span className="shrink-0 text-2xs text-muted-foreground">
|
||||
Installed
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
value: installed.id,
|
||||
},
|
||||
];
|
||||
});
|
||||
return [
|
||||
...catalogOptions,
|
||||
...localOptions,
|
||||
{ label: "Custom model…", value: CUSTOM_MODEL_DROPDOWN_VALUE },
|
||||
];
|
||||
}, [catalog?.entries, installedModels]);
|
||||
const knownModel = options.some((option) => option.value === model.trim());
|
||||
const showCustomModelInput =
|
||||
isCustomModelEditing || (model.trim().length > 0 && !knownModel);
|
||||
const selectedValue = showCustomModelInput
|
||||
? CUSTOM_MODEL_DROPDOWN_VALUE
|
||||
: model.trim();
|
||||
|
||||
function handleModelChange(next: string) {
|
||||
if (next === CUSTOM_MODEL_DROPDOWN_VALUE) {
|
||||
onCustomModelEditingChange(true);
|
||||
return;
|
||||
}
|
||||
onCustomModelEditingChange(false);
|
||||
onModelChange(next);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-1" data-testid="mesh-share-compute-catalog">
|
||||
<div className="space-y-1.5" data-testid="mesh-share-compute-catalog">
|
||||
<label className="text-sm font-medium" htmlFor="mesh-share-compute-model">
|
||||
Model
|
||||
</label>
|
||||
<AgentDropdownSelect
|
||||
className={MESH_SELECT_TRIGGER_CLASS}
|
||||
disabled={disabled}
|
||||
id="mesh-share-compute-model"
|
||||
onValueChange={handleModelChange}
|
||||
options={options}
|
||||
placeholder="Select a model"
|
||||
placeholderClassName="text-muted-foreground/55"
|
||||
searchable
|
||||
testId="mesh-share-compute-model"
|
||||
value={selectedValue}
|
||||
/>
|
||||
{showCustomModelInput ? (
|
||||
<AgentConfigTextInput
|
||||
aria-label="Custom model reference"
|
||||
autoCorrect="off"
|
||||
disabled={disabled}
|
||||
onChange={(event) => {
|
||||
// A stored custom ref starts out inferred rather than explicitly
|
||||
// selected. Mark it as an active custom edit before applying a
|
||||
// cleared value so the field stays mounted while it is replaced.
|
||||
onCustomModelEditingChange(true);
|
||||
onModelChange(event.target.value);
|
||||
}}
|
||||
placeholder="Qwen3-8B-Q4_K_M or hf://meshllm/qwen3-8b@main"
|
||||
usePersonaInputStyle
|
||||
value={model}
|
||||
/>
|
||||
) : null}
|
||||
<p className="text-sm font-normal text-muted-foreground">
|
||||
Recommended for this machine
|
||||
{catalog.gpuName ? ` (${catalog.gpuName}, ` : " ("}
|
||||
{catalog.vramDisplay} AI memory):
|
||||
{catalog
|
||||
? `Recommended for this machine${catalog.gpuName ? ` (${catalog.gpuName}, ${catalog.vramDisplay} AI memory)` : ""}.`
|
||||
: "Choose a model or enter a model reference or local file."}{" "}
|
||||
Buzz downloads remote models when sharing starts.
|
||||
</p>
|
||||
<ul className="mt-1.5 flex max-h-56 flex-col gap-1 overflow-y-auto">
|
||||
{visible.map((entry) => {
|
||||
const isSelected = entry.name === selected;
|
||||
const tooLarge = entry.fit === "too_large";
|
||||
return (
|
||||
<li key={entry.name}>
|
||||
<button
|
||||
className={cn(
|
||||
"flex w-full items-baseline gap-2 rounded border px-2 py-1 text-left text-sm",
|
||||
isSelected
|
||||
? "border-primary/60 bg-primary/10"
|
||||
: "border-border/60 bg-muted/20 hover:bg-muted/40",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
)}
|
||||
data-testid={`mesh-catalog-${entry.name}`}
|
||||
disabled={disabled || tooLarge}
|
||||
onClick={() => onPick(entry.name)}
|
||||
title={entry.description}
|
||||
type="button"
|
||||
>
|
||||
<span className="min-w-0 truncate font-medium">
|
||||
{entry.name}
|
||||
</span>
|
||||
<span className="shrink-0 text-muted-foreground">
|
||||
{entry.size}
|
||||
</span>
|
||||
<span className={cn("shrink-0", FIT_CLASS[entry.fit])}>
|
||||
{FIT_LABEL[entry.fit]}
|
||||
</span>
|
||||
{entry.recommended ? (
|
||||
<span className="shrink-0 rounded bg-primary/15 px-1.5 text-2xs font-medium text-primary">
|
||||
Recommended
|
||||
</span>
|
||||
) : null}
|
||||
{entry.installed ? (
|
||||
<span className="shrink-0 text-2xs text-muted-foreground">
|
||||
Installed
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
{advanced.length > 0 ? (
|
||||
<button
|
||||
className="mt-1 text-sm text-muted-foreground underline hover:text-foreground"
|
||||
data-testid="mesh-catalog-advanced-toggle"
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
type="button"
|
||||
>
|
||||
{expanded
|
||||
? "Hide advanced models"
|
||||
: `Advanced: ${advanced.length} more models`}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MeshModelOptionLabel({ entry }: { entry: MeshCatalogEntry }) {
|
||||
return (
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className="min-w-0 flex-1 truncate font-medium">{entry.name}</span>
|
||||
<span className="shrink-0 text-muted-foreground">{entry.size}</span>
|
||||
<span className={cn("shrink-0", FIT_CLASS[entry.fit])}>
|
||||
{FIT_LABEL[entry.fit]}
|
||||
</span>
|
||||
{entry.recommended ? (
|
||||
<span className="shrink-0 rounded bg-primary/15 px-1.5 text-2xs font-medium text-primary">
|
||||
Recommended
|
||||
</span>
|
||||
) : null}
|
||||
{entry.installed ? (
|
||||
<span className="shrink-0 text-2xs text-muted-foreground">
|
||||
Installed
|
||||
</span>
|
||||
) : null}
|
||||
{!entry.curated ? (
|
||||
<span className="shrink-0 text-2xs text-muted-foreground">
|
||||
Advanced
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusLine({
|
||||
displayModel,
|
||||
isConsuming,
|
||||
omitSharingVerb = false,
|
||||
pendingAction,
|
||||
status,
|
||||
}: {
|
||||
displayModel?: string;
|
||||
isConsuming: boolean;
|
||||
omitSharingVerb?: boolean;
|
||||
pendingAction: "start" | "stop" | null;
|
||||
status: MeshNodeStatus | null;
|
||||
}) {
|
||||
@@ -583,12 +642,10 @@ function StatusLine({
|
||||
return <p className="text-sm text-muted-foreground">Checking status…</p>;
|
||||
}
|
||||
const { state, health, modelId, modelName } = status;
|
||||
const modelLabel = modelName ?? modelId ?? "";
|
||||
const modelLabel = displayModel ?? modelName ?? modelId ?? "";
|
||||
|
||||
if (state === "off") {
|
||||
return (
|
||||
<p className="text-sm text-muted-foreground">Not sharing right now.</p>
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if (state === "starting") {
|
||||
const reason =
|
||||
@@ -614,7 +671,9 @@ function StatusLine({
|
||||
}
|
||||
return (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Sharing{modelLabel ? ` ${modelLabel}` : ""} with relay members.
|
||||
{omitSharingVerb ? "" : "Sharing"}
|
||||
{modelLabel ? `${omitSharingVerb ? "" : " "}${modelLabel}` : ""} with
|
||||
relay members.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2950,6 +2950,7 @@ const ZERO_SERVING_USAGE: MockServingUsage = {
|
||||
const mockMeshState: {
|
||||
admitted: boolean;
|
||||
models: Array<{ id: string; name: string | null }>;
|
||||
activeModel: { id: string; name: string | null } | null;
|
||||
denyReason: string;
|
||||
nodeState: "off" | "running";
|
||||
nodeMode: "serve" | "client" | null;
|
||||
@@ -2957,6 +2958,7 @@ const mockMeshState: {
|
||||
} = {
|
||||
admitted: true,
|
||||
models: [{ id: "Gemma-4-E4B-it-Q4_K_M", name: "Gemma 4 E4B" }],
|
||||
activeModel: null,
|
||||
denyReason: "not a relay member",
|
||||
nodeState: "off",
|
||||
nodeMode: null,
|
||||
@@ -2966,6 +2968,7 @@ const mockMeshState: {
|
||||
function resetMockMesh() {
|
||||
mockMeshState.admitted = true;
|
||||
mockMeshState.models = [{ id: "Gemma-4-E4B-it-Q4_K_M", name: "Gemma 4 E4B" }];
|
||||
mockMeshState.activeModel = null;
|
||||
mockMeshState.denyReason = "not a relay member";
|
||||
mockMeshState.nodeState = "off";
|
||||
mockMeshState.nodeMode = null;
|
||||
@@ -9908,22 +9911,32 @@ export function maybeInstallE2eTauriMocks() {
|
||||
window.__BUZZ_E2E_SEED_OBSERVER_EVENTS__ = ({ agentPubkey, events }) => {
|
||||
injectObserverEventsForE2E(agentPubkey, events);
|
||||
};
|
||||
const meshModelName = (modelId: string) => {
|
||||
const basename = modelId.split("/").at(-1) ?? modelId;
|
||||
return basename
|
||||
.replace(/-(?:Instruct|GGUF)(?:-|:|$).*/, "")
|
||||
.replace(/:.*/, "")
|
||||
.replaceAll("-", " ");
|
||||
};
|
||||
const meshNodeStatus = (
|
||||
state: "off" | "running",
|
||||
mode: "serve" | "client" | null,
|
||||
) => ({
|
||||
state,
|
||||
mode,
|
||||
health: { status: "ok" as const, reason: null },
|
||||
apiBaseUrl: state === "running" ? "http://127.0.0.1:9337/v1" : null,
|
||||
consoleUrl: null,
|
||||
modelId: mockMeshState.models[0]?.id ?? null,
|
||||
modelName: mockMeshState.models[0]?.name ?? null,
|
||||
inviteToken: state === "running" ? "mock-endpoint-addr" : null,
|
||||
endpointId: state === "running" ? "mock-endpoint-id" : null,
|
||||
deviceId: state === "running" ? "mock-endpoint-id" : null,
|
||||
deviceName: state === "running" ? "Mock desktop" : null,
|
||||
});
|
||||
) => {
|
||||
const model = mockMeshState.activeModel ?? mockMeshState.models[0] ?? null;
|
||||
return {
|
||||
state,
|
||||
mode,
|
||||
health: { status: "ok" as const, reason: null },
|
||||
apiBaseUrl: state === "running" ? "http://127.0.0.1:9337/v1" : null,
|
||||
consoleUrl: null,
|
||||
modelId: model?.id ?? null,
|
||||
modelName: model?.name ?? null,
|
||||
inviteToken: state === "running" ? "mock-endpoint-addr" : null,
|
||||
endpointId: state === "running" ? "mock-endpoint-id" : null,
|
||||
deviceId: state === "running" ? "mock-endpoint-id" : null,
|
||||
deviceName: state === "running" ? "Mock desktop" : null,
|
||||
};
|
||||
};
|
||||
let mockImportedVoices: Array<{
|
||||
key: string;
|
||||
displayName: string;
|
||||
@@ -10300,10 +10313,15 @@ export function maybeInstallE2eTauriMocks() {
|
||||
return mockMeshState.servingUsage;
|
||||
case "mesh_start_node": {
|
||||
const req = (
|
||||
payload as { request?: { mode?: "serve" | "client" } } | null
|
||||
payload as {
|
||||
request?: { mode?: "serve" | "client"; modelId?: string };
|
||||
} | null
|
||||
)?.request;
|
||||
mockMeshState.nodeState = "running";
|
||||
mockMeshState.nodeMode = req?.mode ?? "serve";
|
||||
mockMeshState.activeModel = req?.modelId
|
||||
? { id: req.modelId, name: meshModelName(req.modelId) }
|
||||
: (mockMeshState.models[0] ?? null);
|
||||
return meshNodeStatus(mockMeshState.nodeState, mockMeshState.nodeMode);
|
||||
}
|
||||
case "mesh_stop_node":
|
||||
@@ -10317,6 +10335,7 @@ export function maybeInstallE2eTauriMocks() {
|
||||
}
|
||||
mockMeshState.nodeState = "off";
|
||||
mockMeshState.nodeMode = null;
|
||||
mockMeshState.activeModel = null;
|
||||
return meshNodeStatus("off", null);
|
||||
case "get_identity": {
|
||||
const isLost =
|
||||
|
||||
@@ -226,6 +226,7 @@ test.describe("global agent config screenshots", () => {
|
||||
await page
|
||||
.getByTestId("global-agent-default-harness-option-claude")
|
||||
.click();
|
||||
await waitForAnimations(page);
|
||||
await expect(page.getByTestId("global-agent-model")).toHaveText(
|
||||
/Default model/,
|
||||
);
|
||||
|
||||
@@ -15,9 +15,8 @@ type E2eWindow = Window & {
|
||||
}) => void;
|
||||
};
|
||||
|
||||
test("Share compute selects the curated default and starts and stops sharing", async ({
|
||||
page,
|
||||
}) => {
|
||||
test("Share compute chooses a model before sharing", async ({ page }) => {
|
||||
const modelRef = "hf://demo/SmolLM2-135M-Instruct-GGUF:Q4_K_M";
|
||||
await installMockBridge(page);
|
||||
await page.goto("/");
|
||||
await openSettings(page, "compute");
|
||||
@@ -26,19 +25,34 @@ test("Share compute selects the curated default and starts and stops sharing", a
|
||||
const toggle = page.getByTestId("mesh-share-compute-toggle");
|
||||
const model = page.getByTestId("mesh-share-compute-model");
|
||||
|
||||
await expect(card).toContainText("Not sharing right now");
|
||||
await expect(card).toContainText(
|
||||
"Choose a suggested model below, or enter a model reference or local file",
|
||||
);
|
||||
await expect(model).toHaveValue("Gemma-4-E4B-it-Q4_K_M");
|
||||
await expect(card).not.toContainText("Not sharing right now");
|
||||
await expect(
|
||||
page.getByTestId("mesh-share-compute-options-motion"),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByTestId("mesh-share-compute-sharing-status"),
|
||||
).toHaveCount(0);
|
||||
await expect(model).toBeVisible();
|
||||
await expect(toggle).toBeEnabled();
|
||||
await model.click();
|
||||
await page.getByRole("option", { name: "Custom model…" }).click();
|
||||
await page.getByLabel("Custom model reference").fill(modelRef);
|
||||
|
||||
await toggle.click();
|
||||
await expect(
|
||||
page.getByTestId("mesh-share-compute-options-motion"),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByTestId("mesh-share-compute-sharing-status"),
|
||||
).toBeVisible();
|
||||
await expect(model).toBeVisible();
|
||||
await expect(card).toContainText(
|
||||
"Buzz downloads remote models when sharing starts",
|
||||
);
|
||||
|
||||
await toggle.click();
|
||||
await expect(toggle).toBeChecked();
|
||||
await expect(card).toContainText("Sharing Gemma 4 E4B with relay members");
|
||||
await expect(
|
||||
page.getByTestId("mesh-share-compute-sharing-status"),
|
||||
).toContainText("SmolLM2 135M with relay members");
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(() => (window as E2eWindow).__BUZZ_E2E_COMMANDS__ ?? []),
|
||||
@@ -53,13 +67,20 @@ test("Share compute selects the curated default and starts and stops sharing", a
|
||||
.toContainEqual({
|
||||
command: "mesh_start_node",
|
||||
payload: {
|
||||
request: { mode: "serve", modelId: "Gemma-4-E4B-it-Q4_K_M" },
|
||||
request: { mode: "serve", modelId: modelRef },
|
||||
},
|
||||
});
|
||||
|
||||
await toggle.click();
|
||||
await expect(toggle).not.toBeChecked();
|
||||
await expect(card).toContainText("Not sharing right now");
|
||||
await expect(card).not.toContainText("Not sharing right now");
|
||||
await expect(
|
||||
page.getByTestId("mesh-share-compute-options-motion"),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByTestId("mesh-share-compute-sharing-status"),
|
||||
).toHaveCount(0);
|
||||
await expect(model).toBeVisible();
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(() => (window as E2eWindow).__BUZZ_E2E_COMMANDS__ ?? []),
|
||||
@@ -97,16 +118,20 @@ test("a consuming client can switch to sharing its saved local model", async ({
|
||||
|
||||
const card = page.getByTestId("settings-mesh-share-compute");
|
||||
const toggle = page.getByTestId("mesh-share-compute-toggle");
|
||||
const model = page.getByTestId("mesh-share-compute-model");
|
||||
|
||||
await expect(card).toContainText(
|
||||
"This machine is currently using another member's shared compute",
|
||||
);
|
||||
await expect(card).toContainText("Buzz may briefly restart");
|
||||
await expect(toggle).not.toBeChecked();
|
||||
await expect(model).toBeEnabled();
|
||||
await expect(model).toHaveValue(localModel);
|
||||
await expect(
|
||||
page.getByTestId("mesh-share-compute-options-motion"),
|
||||
).toHaveCount(0);
|
||||
await expect(toggle).toBeEnabled();
|
||||
const customModel = page.getByLabel("Custom model reference");
|
||||
await expect(customModel).toHaveValue(localModel);
|
||||
await customModel.fill("");
|
||||
await expect(customModel).toBeVisible();
|
||||
await customModel.fill("hf://demo/replacement-model:Q4_K_M");
|
||||
await toggle.click();
|
||||
await expect(toggle).toBeChecked();
|
||||
|
||||
@@ -117,6 +142,8 @@ test("a consuming client can switch to sharing its saved local model", async ({
|
||||
expect(commands.names).not.toContain("mesh_stop_node");
|
||||
expect(commands.payloads).toContainEqual({
|
||||
command: "mesh_start_node",
|
||||
payload: { request: { mode: "serve", modelId: localModel } },
|
||||
payload: {
|
||||
request: { mode: "serve", modelId: "hf://demo/replacement-model:Q4_K_M" },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user