Valeriy Selitskiy
Use 4096-token release defaults
36a170e
Raw
History Blame Contribute Delete
51 kB
import { useEffect, useMemo, useRef, useState } from 'react';
import {
BrowserEngineClient,
EngineClientError,
evaluateModelGate,
isShardDownloadFailureDetails,
loadModelManifestV2,
type EngineCapabilities,
type BackendReport,
type EngineChatMessage,
type EngineChatToolCall,
type EngineEvent,
type ManifestModelV2,
type ModelManifestV2,
type ModelTierId,
type RequestedBackend,
} from '../engine';
import { executeAgentTool, type ArtifactDocument } from '../lib/agent-tools';
import type {
ChatMessage,
ChatSession,
ComposerSubmission,
ModelLoadProgress,
ModelId,
ModelOption,
RuntimeSettings,
ShardRetryNotice,
StorageStatus,
StreamState,
TelemetrySnapshot,
ToolEvent,
ToolRun,
} from '../lib/contracts';
import { formatBytes } from '../lib/format';
import { DEFAULT_CONTEXT_SIZE, DEFAULT_MAX_TOKENS } from '../lib/runtime-defaults';
import {
conversationTitle,
normalizeConversationModelId,
planMessageRegeneration,
upsertConversation,
} from '../lib/conversations';
import { deleteConversation, listConversations, saveConversation } from '../lib/idb';
import { parseThoughtStream } from '../lib/think-parser';
import { parseToolCalls } from '../lib/tool-protocol';
import { planClientTools } from '../lib/tool-routing';
import { BonsaiShell } from './components/BonsaiShell';
const MANIFEST_PATH = 'manifest/models.json';
const MAX_TOOL_ROUNDS = 5;
const MAX_TOOL_CALLS_PER_ROUND = 4;
const MAX_TOOL_CALLS_TOTAL = 12;
export const DEFAULT_MODEL_ID: ModelId = 'bonsai-27b';
export { DEFAULT_CONTEXT_SIZE, DEFAULT_MAX_TOKENS } from '../lib/runtime-defaults';
const MODEL_DECOR: Record<ModelTierId, {
id: ModelId;
parameters: string;
}> = {
'1_7b': {
id: 'bonsai-1.7b',
parameters: '1.7B',
},
'4b': {
id: 'bonsai-4b',
parameters: '4B',
},
'8b': {
id: 'bonsai-8b',
parameters: '8B',
},
'27b': {
id: 'bonsai-27b',
parameters: '27B',
},
};
export function modelArchitectureLabel(architecture: string): string {
if (architecture === 'qwen3') return 'Qwen3 dense';
if (architecture === 'qwen35') return 'Qwen3.5 hybrid';
return architecture;
}
const UI_TO_TIER: Record<ModelId, ModelTierId> = Object.fromEntries(
Object.entries(MODEL_DECOR).map(([tier, decor]) => [decor.id, tier]),
) as Record<ModelId, ModelTierId>;
const INITIAL_SETTINGS: RuntimeSettings = {
backend: 'auto',
contextSize: DEFAULT_CONTEXT_SIZE,
temperature: 0.7,
maxTokens: DEFAULT_MAX_TOKENS,
systemPrompt: 'You are Bonsai, a private browser-local assistant. Be concise and evidence-led. Use a client tool only when it materially improves the answer, and never claim a tool succeeded before its response arrives.',
};
const INITIAL_TELEMETRY: TelemetrySnapshot = {
backend: 'Unavailable',
device: 'Inspecting browser capabilities…',
modelMemoryBytes: 0,
kvMemoryBytes: 0,
storageUsedBytes: 0,
contextUsed: 0,
contextLimit: DEFAULT_CONTEXT_SIZE,
tokensPerSecond: 0,
prefillTokensPerSecond: 0,
decodeTokensPerSecond: 0,
inferencePhase: 'idle',
promptProcessed: 0,
promptTotal: 0,
completionTokens: 0,
timeToFirstTokenMs: 0,
gpuLayers: '—',
graphSplits: null,
};
const INITIAL_STORAGE: StorageStatus = {
usedBytes: 0,
quotaBytes: 0,
persistent: false,
downloads: [],
};
const INITIAL_MODEL_LOAD_PROGRESS: ModelLoadProgress = {
modelId: null,
modelLabel: '',
stage: 'idle',
stageProgress: 0,
downloadedBytes: 0,
totalBytes: 0,
residentBytes: 0,
nativeStage: null,
shards: [],
};
let sharedClient: BrowserEngineClient | null = null;
function getEngineClient(): BrowserEngineClient {
sharedClient ??= new BrowserEngineClient();
return sharedClient;
}
function manifestUrl(): string {
return new URL(`${import.meta.env.BASE_URL}${MANIFEST_PATH}`, document.baseURI).href;
}
function clock(): string {
return new Date().toLocaleTimeString('en-GB', { hour12: false });
}
function eventClock(): string {
return new Date().toISOString().slice(11, 23);
}
function createConversationId(): string {
return crypto.randomUUID();
}
function errorMessage(error: unknown): string {
if (error instanceof EngineClientError) return `${error.code}: ${error.message}`;
return error instanceof Error ? error.message : String(error);
}
function isAborted(error: unknown, signal: AbortSignal): boolean {
return signal.aborted
|| (error instanceof DOMException && error.name === 'AbortError')
|| (error instanceof EngineClientError && error.code === 'ABORTED');
}
export function requiresModelReload(error: unknown): error is EngineClientError {
return error instanceof EngineClientError && [
'WEBGPU_DEVICE_LOST',
'ENGINE_WORKER_FAILED',
'MODEL_NOT_LOADED',
].includes(error.code);
}
export function reportsModelWeightProgress(
phase: Extract<EngineEvent, { event: 'progress' }>['phase'],
): boolean {
return phase !== 'manifest';
}
export function reconcileCompletionTokens(
engineCompletionTokens: number | undefined,
streamedTokenEvents: number,
): number {
const engineCount = typeof engineCompletionTokens === 'number'
&& Number.isSafeInteger(engineCompletionTokens)
&& engineCompletionTokens >= 0
? engineCompletionTokens
: 0;
const streamCount = Number.isSafeInteger(streamedTokenEvents) && streamedTokenEvents >= 0
? streamedTokenEvents
: 0;
return Math.max(engineCount, streamCount);
}
export function reconcileTotalTokens(
engineTotalTokens: number | undefined,
enginePromptTokens: number | undefined,
completionTokens: number,
): number {
const totalCount = typeof engineTotalTokens === 'number'
&& Number.isSafeInteger(engineTotalTokens)
&& engineTotalTokens >= 0
? engineTotalTokens
: 0;
const promptCount = typeof enginePromptTokens === 'number'
&& Number.isSafeInteger(enginePromptTokens)
&& enginePromptTokens >= 0
? enginePromptTokens
: 0;
return Math.max(totalCount, promptCount + completionTokens);
}
export function publishBackendReport(report: BackendReport): void {
globalThis.__bonsaiBackendReport = report;
}
function appendSegment(current: string, next: string): string {
const trimmed = next.trim();
if (!trimmed) return current;
return current ? `${current}\n\n${trimmed}` : trimmed;
}
function replaceMessage(
setter: React.Dispatch<React.SetStateAction<ChatMessage[]>>,
id: string,
update: (message: ChatMessage) => ChatMessage,
): void {
setter((current) => current.map((message) => message.id === id ? update(message) : message));
}
function buildEngineHistory(messages: ChatMessage[], systemPrompt: string): EngineChatMessage[] {
const history: EngineChatMessage[] = systemPrompt.trim()
? [{ role: 'system', content: systemPrompt.trim() }]
: [];
for (const message of messages) {
if (message.role === 'user' && message.content.trim() && !message.error) {
history.push({ role: 'user', content: message.content });
} else if (message.role === 'assistant' && !message.error) {
const completedTools = (message.tools ?? []).filter((tool) => tool.output !== undefined);
if (message.content.trim() || completedTools.length > 0) {
history.push({
role: 'assistant',
content: message.content || null,
...(completedTools.length > 0 ? {
tool_calls: completedTools.map((tool) => ({
id: tool.id,
type: 'function' as const,
function: { name: tool.name, arguments: tool.input },
})),
} : {}),
});
for (const tool of completedTools) {
history.push({ role: 'tool', content: tool.output ?? '', tool_call_id: tool.id });
}
}
}
}
return history;
}
function fallbackToolCalls(text: string): EngineChatToolCall[] {
return parseToolCalls(text).calls.map((call) => ({
id: crypto.randomUUID(),
type: 'function',
function: {
name: call.name,
arguments: JSON.stringify(call.arguments),
},
}));
}
export function App() {
const [client] = useState(getEngineClient);
const [manifest, setManifest] = useState<ModelManifestV2 | null>(null);
const [capabilities, setCapabilities] = useState<EngineCapabilities | null>(null);
const [activeModelId, setActiveModelId] = useState<ModelId>(DEFAULT_MODEL_ID);
const [loadedModelId, setLoadedModelId] = useState<ModelId | null>(null);
const [loadingModelId, setLoadingModelId] = useState<ModelId | null>(null);
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [conversations, setConversations] = useState<ChatSession[]>([]);
const [activeConversationId, setActiveConversationId] = useState(createConversationId);
const [conversationListOpen, setConversationListOpen] = useState(false);
const [streamState, setStreamState] = useState<StreamState>('idle');
const [modelLoadProgress, setModelLoadProgress] = useState<ModelLoadProgress>(INITIAL_MODEL_LOAD_PROGRESS);
const [toolsEnabled, setToolsEnabled] = useState(false);
const [settings, setSettings] = useState<RuntimeSettings>(INITIAL_SETTINGS);
const [settingsOpen, setSettingsOpen] = useState(false);
const [storage, setStorage] = useState<StorageStatus>(INITIAL_STORAGE);
const [telemetry, setTelemetry] = useState<TelemetrySnapshot>(INITIAL_TELEMETRY);
const [toolEvents, setToolEvents] = useState<ToolEvent[]>([]);
const [artifact, setArtifact] = useState<ArtifactDocument | null>(null);
const [error, setError] = useState<string | null>(null);
const [shardRetry, setShardRetry] = useState<ShardRetryNotice | null>(null);
const [bootStatus, setBootStatus] = useState('Inspecting local runtime…');
const operationRef = useRef<AbortController | null>(null);
const loadedConfigurationRef = useRef<{ id: ModelId; backend: RequestedBackend; contextSize: number } | null>(null);
const historyReadyRef = useRef(false);
const loadProgressFrameRef = useRef<number | null>(null);
const pendingLoadProgressRef = useRef<ModelLoadProgress | null>(null);
const modelLoadProgressRef = useRef<ModelLoadProgress>(INITIAL_MODEL_LOAD_PROGRESS);
const inferenceFrameRef = useRef<number | null>(null);
const pendingInferenceRef = useRef<Extract<EngineEvent, { event: 'generation' }> | null>(null);
const publishModelLoadProgress = (next: ModelLoadProgress): void => {
modelLoadProgressRef.current = next;
pendingLoadProgressRef.current = next;
if (loadProgressFrameRef.current !== null) return;
loadProgressFrameRef.current = window.requestAnimationFrame(() => {
loadProgressFrameRef.current = null;
const pending = pendingLoadProgressRef.current;
pendingLoadProgressRef.current = null;
if (pending) setModelLoadProgress(pending);
});
};
const publishInferenceProgress = (
next: Extract<EngineEvent, { event: 'generation' }>,
): void => {
pendingInferenceRef.current = next;
if (inferenceFrameRef.current !== null) return;
inferenceFrameRef.current = window.requestAnimationFrame(() => {
inferenceFrameRef.current = null;
const pending = pendingInferenceRef.current;
pendingInferenceRef.current = null;
if (!pending) return;
const liveRate = pending.phase === 'prefill'
? pending.promptTokensPerSecond
: pending.decodeTokensPerSecond;
setTelemetry((current) => ({
...current,
inferencePhase: pending.phase,
tokensPerSecond: liveRate,
prefillTokensPerSecond: pending.promptTokensPerSecond || current.prefillTokensPerSecond,
decodeTokensPerSecond: pending.decodeTokensPerSecond || current.decodeTokensPerSecond,
promptProcessed: pending.promptProcessed,
promptTotal: pending.promptTotal,
completionTokens: pending.completionTokens,
contextUsed: Math.min(
current.contextLimit,
Math.max(current.contextUsed, pending.promptProcessed + pending.completionTokens),
),
}));
});
};
const clearPendingInferenceProgress = (): void => {
if (inferenceFrameRef.current !== null) {
window.cancelAnimationFrame(inferenceFrameRef.current);
inferenceFrameRef.current = null;
}
pendingInferenceRef.current = null;
};
const handleModelInvalidation = (runtimeError: unknown): boolean => {
if (!requiresModelReload(runtimeError)) return false;
setLoadedModelId(null);
loadedConfigurationRef.current = null;
setTelemetry((current) => ({
...current,
backend: 'Unavailable',
modelMemoryBytes: 0,
kvMemoryBytes: 0,
contextUsed: 0,
tokensPerSecond: 0,
prefillTokensPerSecond: 0,
decodeTokensPerSecond: 0,
inferencePhase: 'idle',
promptProcessed: 0,
promptTotal: 0,
completionTokens: 0,
gpuLayers: '—',
graphSplits: null,
}));
setError(errorMessage(runtimeError));
setBootStatus(runtimeError.code === 'WEBGPU_DEVICE_LOST'
? 'WebGPU device lost · model unloaded · run again to reload'
: 'Browser engine restarted · model unloaded · run again to reload');
return true;
};
const persistenceAttemptedRef = useRef(false);
useEffect(() => () => {
if (loadProgressFrameRef.current !== null) {
window.cancelAnimationFrame(loadProgressFrameRef.current);
}
if (inferenceFrameRef.current !== null) {
window.cancelAnimationFrame(inferenceFrameRef.current);
}
}, []);
useEffect(() => {
let cancelled = false;
const bootstrap = async () => {
let loadedManifest: ModelManifestV2 | null = null;
try {
loadedManifest = await loadModelManifestV2(manifestUrl());
} catch (bootstrapError) {
if (!cancelled) setError(`Manifest unavailable: ${errorMessage(bootstrapError)}`);
}
try {
const nextCapabilities = await client.capabilities();
if (!cancelled) {
setCapabilities(nextCapabilities);
setStorage({
usedBytes: nextCapabilities.storage.usageBytes ?? 0,
quotaBytes: nextCapabilities.storage.quotaBytes ?? 0,
persistent: nextCapabilities.storage.persisted,
downloads: [],
});
setTelemetry((current) => ({
...current,
device: nextCapabilities.webgpu.name || (nextCapabilities.webgpu.available ? 'WebGPU adapter' : 'CPU · WebAssembly'),
storageUsedBytes: nextCapabilities.storage.usageBytes ?? 0,
}));
setBootStatus(nextCapabilities.webgpu.available
? `${nextCapabilities.webgpu.name || 'WebGPU'} ready · model runs after local load`
: 'WebGPU unavailable · dense tiers can use CPU-WASM');
}
} catch (capabilityError) {
if (!cancelled) setError(`Capability inspection failed: ${errorMessage(capabilityError)}`);
}
try {
const storedConversations = (await listConversations<ChatMessage>()).map((conversation) => ({
...conversation,
modelId: normalizeConversationModelId(conversation.modelId),
systemPrompt: typeof conversation.systemPrompt === 'string'
? conversation.systemPrompt
: INITIAL_SETTINGS.systemPrompt,
}));
const latest = storedConversations[0];
if (!cancelled) {
setConversations(storedConversations);
if (latest) {
setActiveConversationId(latest.id);
setActiveModelId(latest.modelId);
setMessages(latest.messages);
setSettings((current) => ({ ...current, systemPrompt: latest.systemPrompt }));
}
}
} catch {
// Private browsing may disable IndexedDB. Chat remains usable for the tab.
if (!cancelled) setBootStatus((current) => `${current} · history is session-only`);
} finally {
historyReadyRef.current = true;
if (!cancelled && loadedManifest) setManifest(loadedManifest);
}
};
void bootstrap();
return () => { cancelled = true; };
}, [client]);
useEffect(() => {
if (!historyReadyRef.current) return;
const alreadySaved = conversations.some((conversation) => conversation.id === activeConversationId);
if (messages.length === 0 && settings.systemPrompt === INITIAL_SETTINGS.systemPrompt && !alreadySaved) return;
const nextConversation: ChatSession = {
id: activeConversationId,
title: conversationTitle(messages),
messages,
modelId: activeModelId,
systemPrompt: settings.systemPrompt,
updatedAt: Date.now(),
};
const timer = window.setTimeout(() => {
setConversations((current) => upsertConversation(current, nextConversation));
void saveConversation(nextConversation).catch(() => setBootStatus((current) => current.includes('history is session-only')
? current
: `${current} · history is session-only`));
}, 250);
return () => window.clearTimeout(timer);
}, [activeConversationId, activeModelId, messages, settings.systemPrompt]);
const models = useMemo<ModelOption[]>(() => {
if (!manifest) return [];
return manifest.models.map((model) => {
const decor = MODEL_DECOR[model.id];
const configured = loadedConfigurationRef.current;
const cachedDownload = storage.downloads.find((download) => (
download.modelId === decor.id && download.state === 'complete'
));
const configuredContext = model.id === '27b'
? Math.min(settings.contextSize, model.defaultContext)
: settings.contextSize;
const isLoaded = loadedModelId === decor.id
&& configured?.id === decor.id
&& configured.backend === settings.backend
&& configured.contextSize === configuredContext;
let availability: ModelOption['availability'] = isLoaded
? 'loaded'
: loadingModelId === decor.id ? 'loading' : cachedDownload ? 'cached' : 'remote';
let limitReason: string | undefined;
if (!isLoaded && capabilities) {
const gate = evaluateModelGate(
model,
settings.backend,
capabilities.webgpu,
capabilities.storage,
cachedDownload?.totalBytes ?? 0,
);
if (!gate.allowed) {
availability = 'limited';
limitReason = gate.reasons.join(' ');
}
}
return {
id: decor.id,
manifestId: model.id,
label: model.displayName,
architectureLabel: modelArchitectureLabel(model.architecture),
parameters: decor.parameters,
runtimeLabel: model.cpuFallback ? 'WebGPU · WASM fallback' : 'WebGPU required',
footprint: formatBytes(model.downloadBytes),
availability,
contextLimit: model.contextLength,
defaultContext: model.defaultContext,
...(limitReason ? { limitReason } : {}),
};
});
}, [capabilities, loadedModelId, loadingModelId, manifest, settings.backend, settings.contextSize, storage.downloads]);
const activeTier = UI_TO_TIER[activeModelId];
const refreshStorage = async (): Promise<void> => {
const estimate = await client.storageEstimate();
setStorage((current) => ({
...current,
usedBytes: estimate.usageBytes ?? 0,
quotaBytes: estimate.quotaBytes ?? 0,
persistent: estimate.persisted,
}));
setCapabilities((current) => current ? { ...current, storage: estimate } : current);
setTelemetry((current) => ({ ...current, storageUsedBytes: estimate.usageBytes ?? 0 }));
};
const requestPersistentStorage = async (force = false): Promise<void> => {
if (storage.persistent || (!force && persistenceAttemptedRef.current)) return;
persistenceAttemptedRef.current = true;
setBootStatus('Requesting persistent browser storage before download…');
try {
const estimate = await client.storagePersist();
setStorage((current) => ({
...current,
usedBytes: estimate.usageBytes ?? 0,
quotaBytes: estimate.quotaBytes ?? 0,
persistent: estimate.persisted,
}));
setCapabilities((current) => current ? { ...current, storage: estimate } : current);
setTelemetry((current) => ({ ...current, storageUsedBytes: estimate.usageBytes ?? 0 }));
setBootStatus(estimate.granted
? 'Persistent browser storage granted'
: 'Persistent storage was not granted · continuing with browser-managed storage');
} catch (persistError) {
if (handleModelInvalidation(persistError)) return;
setBootStatus('Persistent storage request unavailable · continuing with browser-managed storage');
}
};
const loadModel = async (
modelId: ModelId,
controller: AbortController,
contextSize: number,
): Promise<void> => {
if (!manifest) throw new Error('The pinned model manifest is still loading.');
const tier = UI_TO_TIER[modelId];
const model = manifest.models.find((candidate) => candidate.id === tier);
if (!model) throw new Error(`Model ${tier} is absent from the pinned manifest.`);
setLoadedModelId(null);
loadedConfigurationRef.current = null;
setLoadingModelId(modelId);
setStreamState('loading');
setError(null);
setShardRetry(null);
setBootStatus(`Preparing ${model.displayName}…`);
const initialProgress: ModelLoadProgress = {
...INITIAL_MODEL_LOAD_PROGRESS,
modelId,
modelLabel: model.displayName,
stage: 'manifest',
totalBytes: model.downloadBytes,
};
modelLoadProgressRef.current = initialProgress;
setModelLoadProgress(initialProgress);
try {
const result = await client.loadModel({
manifestUrl: manifestUrl(),
modelId: tier,
backend: settings.backend,
contextSize,
}, {
signal: controller.signal,
onProgress: (progress) => {
const received = Math.min(progress.loadedBytes, model.downloadBytes);
const shard = progress.shardIndex === null ? '' : ` · shard ${progress.shardIndex + 1}/${progress.shardCount}`;
const stageProgress = progress.stageProgress
?? (progress.totalBytes > 0 ? progress.loadedBytes / progress.totalBytes : 0);
publishModelLoadProgress({
modelId,
modelLabel: model.displayName,
stage: progress.phase,
stageProgress: Math.min(1, Math.max(0, stageProgress)),
downloadedBytes: received,
totalBytes: model.downloadBytes,
residentBytes: progress.residentBytes ?? 0,
nativeStage: progress.nativeStage,
shards: progress.shards.map((item) => ({ ...item })),
});
if (progress.residentBytes !== null) {
setTelemetry((current) => ({ ...current, modelMemoryBytes: progress.residentBytes ?? 0 }));
}
if (!reportsModelWeightProgress(progress.phase)) {
setBootStatus('Reading pinned model manifest…');
return;
}
const stageLabel = progress.phase === 'cache'
? 'Inspecting cache'
: progress.phase === 'verify'
? 'Verifying shards'
: progress.phase === 'load'
? 'Allocating model'
: 'Downloading model';
setBootStatus(`${stageLabel}${shard} · ${Math.round(stageProgress * 100)}%`);
if (progress.phase !== 'load') {
setStorage((current) => ({
...current,
downloads: [
...current.downloads.filter((download) => download.modelId !== modelId),
{
modelId,
label: model.displayName,
receivedBytes: received,
totalBytes: model.downloadBytes,
state: received >= model.downloadBytes ? 'complete' : 'downloading',
},
],
}));
}
},
});
setLoadedModelId(modelId);
setShardRetry(null);
loadedConfigurationRef.current = { id: modelId, backend: settings.backend, contextSize };
setTelemetry((current) => ({
...current,
backend: result.backend === 'webgpu' ? 'WebGPU' : 'WASM',
device: result.backend === 'webgpu'
? capabilities?.webgpu.name || 'WebGPU adapter'
: `CPU · ${capabilities?.hardwareConcurrency ?? navigator.hardwareConcurrency ?? 1} logical cores`,
modelMemoryBytes: result.backendReport.modelBufferBytes ?? model.downloadBytes,
kvMemoryBytes: result.backendReport.webgpuKvBufferBytes ?? 0,
contextUsed: 0,
contextLimit: result.context.size,
gpuLayers: result.backendReport.layersGpu
? `${result.backendReport.layersGpu.offloaded} / ${result.backendReport.layersGpu.total}`
: result.backend === 'wasm' ? 'CPU' : 'unreported',
graphSplits: result.backendReport.nGraphSplits,
}));
publishModelLoadProgress({
...modelLoadProgressRef.current,
modelId,
modelLabel: model.displayName,
stage: 'ready',
stageProgress: 1,
downloadedBytes: model.downloadBytes,
totalBytes: model.downloadBytes,
residentBytes: result.backendReport.allocatedBufferBytes ?? result.backendReport.modelBufferBytes ?? model.downloadBytes,
nativeStage: null,
});
setBootStatus(`${model.displayName} loaded · ${result.backend.toUpperCase()} · ${result.context.size.toLocaleString()} ctx`);
await refreshStorage();
} catch (loadError) {
const operatorAborted = controller.signal.aborted;
const retryDetails = !operatorAborted && loadError instanceof EngineClientError
&& isShardDownloadFailureDetails(loadError.details)
? loadError.details
: null;
if (operatorAborted) setShardRetry(null);
if (retryDetails) {
setShardRetry({
modelId,
shardIndex: retryDetails.shardIndex,
shardCount: retryDetails.shardCount,
shardPath: retryDetails.shardPath,
error: errorMessage(loadError),
});
}
publishModelLoadProgress({
...modelLoadProgressRef.current,
stage: operatorAborted ? 'idle' : 'error',
nativeStage: null,
});
setStorage((current) => ({
...current,
downloads: operatorAborted
? current.downloads.filter((download) => download.modelId !== modelId)
: current.downloads.map((download) => download.modelId === modelId
? {
...download,
label: retryDetails
? `${model.displayName} · shard ${retryDetails.shardIndex + 1}/${retryDetails.shardCount} · ${retryDetails.shardPath}`
: download.label,
state: 'error',
}
: download),
}));
throw loadError;
} finally {
setLoadingModelId(null);
}
};
const targetContext = (model: ManifestModelV2): number => {
if (model.id === activeTier) {
const provenLimit = model.id === '27b' ? model.defaultContext : model.contextLength;
return Math.min(settings.contextSize, provenLimit);
}
return Math.min(DEFAULT_CONTEXT_SIZE, model.defaultContext);
};
const handleModelSelect = (modelId: ModelId) => {
if (operationRef.current) return;
setShardRetry(null);
setActiveModelId(modelId);
const model = manifest?.models.find((candidate) => candidate.id === UI_TO_TIER[modelId]);
if (model) {
setSettings((current) => ({
...current,
backend: model.cpuFallback ? current.backend : current.backend === 'wasm' ? 'auto' : current.backend,
contextSize: Math.min(DEFAULT_CONTEXT_SIZE, model.defaultContext),
}));
}
};
const handleLoadModel = (modelId: ModelId) => {
if (operationRef.current || !manifest) return;
setActiveModelId(modelId);
const model = manifest.models.find((candidate) => candidate.id === UI_TO_TIER[modelId]);
if (!model) return;
const contextSize = targetContext(model);
if (model.id !== activeTier) setSettings((current) => ({ ...current, contextSize }));
const controller = new AbortController();
operationRef.current = controller;
void requestPersistentStorage()
.then(() => {
if (controller.signal.aborted) throw new DOMException('Model loading was aborted.', 'AbortError');
return loadModel(modelId, controller, contextSize);
})
.then(() => setStreamState('complete'))
.catch((loadError) => {
if (handleModelInvalidation(loadError)) {
setStreamState('error');
return;
}
if (!isAborted(loadError, controller.signal)) {
const steer = model.id === '27b' ? ' Choose Bonsai 8B for the supported fallback path.' : '';
setError(`${errorMessage(loadError)}${steer}`);
setStreamState('error');
setBootStatus('Model load failed safely');
} else {
setStreamState('idle');
setBootStatus('Load stopped by operator');
}
})
.finally(() => {
if (operationRef.current === controller) operationRef.current = null;
});
};
const startSubmission = (
{ prompt, toolsEnabled: submissionToolsEnabled }: ComposerSubmission,
baseMessages: ChatMessage[],
existingUserMessage?: ChatMessage,
): boolean => {
if (operationRef.current || !manifest) return false;
const tier = UI_TO_TIER[activeModelId];
const selectedModel = manifest.models.find((candidate) => candidate.id === tier);
if (!selectedModel) {
setError(`Model ${tier} is absent from the pinned manifest.`);
return false;
}
const requestedContext = targetContext(selectedModel);
const configured = loadedConfigurationRef.current;
const needsLoad = loadedModelId !== activeModelId
|| configured?.backend !== settings.backend
|| configured.contextSize !== requestedContext;
if (needsLoad) {
setError(`Load ${selectedModel.displayName} before running this prompt.`);
setBootStatus('Explicit model load required');
return false;
}
const controller = new AbortController();
operationRef.current = controller;
const nonce = crypto.randomUUID();
const userMessage: ChatMessage = existingUserMessage ?? {
id: `user-${nonce}`,
role: 'user',
content: prompt,
timestamp: clock(),
};
const assistantId = `assistant-${nonce}`;
const assistantMessage: ChatMessage = {
id: assistantId,
role: 'assistant',
content: '',
reasoning: '',
timestamp: clock(),
state: 'streaming',
tools: [],
};
const historySnapshot = [...baseMessages, userMessage];
setMessages([...historySnapshot, assistantMessage]);
setError(null);
const run = async () => {
setStreamState('streaming');
setBootStatus(`Generating with ${selectedModel.displayName}…`);
const initialToolPlan = planClientTools(prompt, submissionToolsEnabled, 0);
const routedSystemPrompt = [settings.systemPrompt, initialToolPlan.systemInstruction]
.filter((value): value is string => Boolean(value?.trim()))
.join('\n\n');
const engineMessages = buildEngineHistory(historySnapshot, routedSystemPrompt);
let visibleText = '';
let reasoningText = '';
let totalCompletionTokens = 0;
let totalPromptTokens = 0;
let lastTotalTokens = 0;
let tokensPerSecond = 0;
let prefillTokensPerSecond = 0;
let totalPromptSeconds = 0;
let totalDecodeSeconds = 0;
let firstTokenAt = 0;
let outputError: string | null = null;
const generationStartedAt = performance.now();
let toolRounds = 0;
let totalToolCalls = 0;
clearPendingInferenceProgress();
setTelemetry((current) => ({
...current,
inferencePhase: 'prefill',
tokensPerSecond: 0,
prefillTokensPerSecond: 0,
decodeTokensPerSecond: 0,
promptProcessed: 0,
promptTotal: 0,
completionTokens: 0,
timeToFirstTokenMs: 0,
}));
while (true) {
const toolPlan = planClientTools(prompt, submissionToolsEnabled, toolRounds);
let streamedText = '';
let streamedReasoning = '';
let streamedTokenEvents = 0;
const completionOffset = totalCompletionTokens;
const result = await client.generate({
messages: engineMessages,
maxTokens: Math.max(settings.maxTokens, toolPlan.minimumMaxTokens),
temperature: settings.temperature,
topK: settings.temperature === 0 ? 1 : 40,
topP: settings.temperature === 0 ? 1 : 0.9,
...(toolPlan.tools ? { tools: toolPlan.tools } : {}),
toolChoice: toolPlan.toolChoice,
cachePrompt: true,
}, {
signal: controller.signal,
onGenerationProgress: (progress) => {
publishInferenceProgress({
...progress,
completionTokens: completionOffset + progress.completionTokens,
});
},
onToken: (delta, reasoningDelta) => {
if (!firstTokenAt && (delta || reasoningDelta)) firstTokenAt = performance.now();
if (delta || reasoningDelta) streamedTokenEvents += 1;
streamedText += delta;
streamedReasoning += reasoningDelta ?? '';
const thought = parseThoughtStream(streamedText);
const toolText = parseToolCalls(thought.content);
const currentReasoning = streamedReasoning || thought.reasoning;
replaceMessage(setMessages, assistantId, (message) => ({
...message,
content: appendSegment(visibleText, toolText.content),
reasoning: appendSegment(reasoningText, currentReasoning),
isThinking: thought.isThinking || Boolean(reasoningDelta),
state: 'streaming',
}));
},
onToolCallProgress: (activity) => {
if (!firstTokenAt) firstTokenAt = performance.now();
replaceMessage(setMessages, assistantId, (message) => ({
...message,
toolActivity: {
name: activity.name || message.toolActivity?.name || 'tool call',
argumentCharacters: activity.argumentCharacters,
},
state: 'streaming',
}));
},
});
const roundCompletionTokens = reconcileCompletionTokens(
result.usage?.completionTokens,
streamedTokenEvents,
);
const roundPromptTokens = result.usage?.promptTokens ?? 0;
totalPromptTokens += roundPromptTokens;
totalCompletionTokens += roundCompletionTokens;
lastTotalTokens = Math.max(
lastTotalTokens,
reconcileTotalTokens(
result.usage?.totalTokens,
result.usage?.promptTokens,
roundCompletionTokens,
),
);
const roundPrefillRate = result.timings?.promptTokensPerSecond ?? 0;
const roundDecodeRate = result.timings?.predictedTokensPerSecond ?? 0;
if (roundPromptTokens > 0 && roundPrefillRate > 0) {
totalPromptSeconds += roundPromptTokens / roundPrefillRate;
}
if (roundCompletionTokens > 0 && roundDecodeRate > 0) {
totalDecodeSeconds += roundCompletionTokens / roundDecodeRate;
}
const thought = parseThoughtStream(result.text);
const textual = parseToolCalls(thought.content);
if (textual.incomplete || textual.errors.length > 0) {
throw new Error(`Malformed textual tool call: ${textual.errors.join('; ') || 'closing </tool_call> is missing'}`);
}
visibleText = appendSegment(visibleText, textual.content);
reasoningText = appendSegment(reasoningText, streamedReasoning || thought.reasoning);
const calls = result.toolCalls.length > 0 ? result.toolCalls : fallbackToolCalls(thought.content);
if (toolPlan.requiredToolName && calls.length === 0) {
outputError = (result.toolCallError || result.finishReason === 'length')
? `The ${toolPlan.requiredToolName} call ended before its arguments were complete. Regenerate, shorten the request, or clear earlier turns so the ${DEFAULT_MAX_TOKENS.toLocaleString()}-token completion ceiling fits the context.`
: `The model did not call the required ${toolPlan.requiredToolName} tool for this artifact request.`;
}
if (toolPlan.requiredToolName && calls.some((call) => call.function.name !== toolPlan.requiredToolName)) {
throw new Error(`The model returned a different tool when ${toolPlan.requiredToolName} was required.`);
}
replaceMessage(setMessages, assistantId, (message) => ({
...message,
content: visibleText,
reasoning: reasoningText,
isThinking: false,
toolActivity: undefined,
}));
if (outputError) break;
engineMessages.push({
role: 'assistant',
content: textual.content || null,
...(calls.length > 0 ? { tool_calls: calls } : {}),
});
if (calls.length === 0) break;
if (!submissionToolsEnabled) throw new Error('The model requested a tool while tools were disabled; nothing was executed.');
if (toolRounds >= MAX_TOOL_ROUNDS) throw new Error(`Tool loop stopped at the ${MAX_TOOL_ROUNDS}-round safety limit.`);
if (calls.length > MAX_TOOL_CALLS_PER_ROUND) {
throw new Error(`Tool round requested ${calls.length} calls; the limit is ${MAX_TOOL_CALLS_PER_ROUND}.`);
}
totalToolCalls += calls.length;
if (totalToolCalls > MAX_TOOL_CALLS_TOTAL) {
throw new Error(`Tool loop requested more than the ${MAX_TOOL_CALLS_TOTAL}-call session limit.`);
}
toolRounds += 1;
for (const call of calls) {
const toolRun: ToolRun = {
id: call.id,
name: call.function.name,
input: call.function.arguments,
state: 'running',
};
const event: ToolEvent = {
id: call.id,
timestamp: eventClock(),
label: call.function.name,
detail: `round ${toolRounds} · executing locally`,
state: 'running',
};
replaceMessage(setMessages, assistantId, (message) => ({
...message,
tools: [...(message.tools ?? []), toolRun],
}));
setToolEvents((current) => [...current.filter((item) => item.id !== call.id), event].slice(-20));
const execution = await executeAgentTool(call, controller.signal);
if (controller.signal.aborted) throw new DOMException('The tool operation was aborted.', 'AbortError');
if (execution.artifact) setArtifact(execution.artifact);
replaceMessage(setMessages, assistantId, (message) => ({
...message,
tools: (message.tools ?? []).map((item) => item.id === call.id ? {
...item,
output: execution.output,
state: execution.failed ? 'error' : 'complete',
} : item),
}));
setToolEvents((current) => current.map((item) => item.id === call.id ? {
...item,
timestamp: eventClock(),
detail: execution.failed ? 'tool returned a bounded error' : 'local result returned to model',
state: execution.failed ? 'error' : 'complete',
} : item));
engineMessages.push({ role: 'tool', content: execution.output, tool_call_id: call.id });
}
}
const generationElapsedSeconds = Math.max(0.001, (performance.now() - generationStartedAt) / 1000);
tokensPerSecond = totalDecodeSeconds > 0
? totalCompletionTokens / totalDecodeSeconds
: totalCompletionTokens / generationElapsedSeconds;
prefillTokensPerSecond = totalPromptSeconds > 0 ? totalPromptTokens / totalPromptSeconds : 0;
const ttft = firstTokenAt ? Math.round(firstTokenAt - generationStartedAt) : 0;
const report = await client.backendReport();
publishBackendReport(report);
clearPendingInferenceProgress();
replaceMessage(setMessages, assistantId, (message) => ({
...message,
state: outputError ? 'error' : 'complete',
isThinking: false,
toolActivity: undefined,
...(outputError ? { error: outputError } : {}),
metrics: {
tokens: totalCompletionTokens,
tokensPerSecond,
promptTokensPerSecond: prefillTokensPerSecond,
timeToFirstTokenMs: ttft,
},
}));
setTelemetry((current) => ({
...current,
contextUsed: Math.min(current.contextLimit, Math.max(lastTotalTokens, totalCompletionTokens)),
tokensPerSecond,
prefillTokensPerSecond,
decodeTokensPerSecond: tokensPerSecond,
inferencePhase: 'complete',
promptProcessed: totalPromptTokens,
promptTotal: totalPromptTokens,
completionTokens: totalCompletionTokens,
timeToFirstTokenMs: ttft,
graphSplits: report.nGraphSplits,
gpuLayers: report.layersGpu
? `${report.layersGpu.offloaded} / ${report.layersGpu.total}`
: current.gpuLayers,
}));
setStreamState(outputError ? 'error' : 'complete');
setBootStatus(outputError
? `${selectedModel.displayName} stopped at an incomplete tool call`
: `${selectedModel.displayName} complete · ${tokensPerSecond.toFixed(1)} tok/s`);
await refreshStorage();
};
void run()
.catch((runError) => {
clearPendingInferenceProgress();
if (handleModelInvalidation(runError)) {
const message = errorMessage(runError);
replaceMessage(setMessages, assistantId, (current) => ({
...current,
state: 'error',
isThinking: false,
toolActivity: undefined,
error: message,
}));
setStreamState('error');
return;
}
if (isAborted(runError, controller.signal)) {
replaceMessage(setMessages, assistantId, (message) => ({
...message,
state: 'complete',
isThinking: false,
toolActivity: undefined,
}));
setStreamState('complete');
setTelemetry((current) => ({ ...current, inferencePhase: 'idle' }));
setBootStatus('Generation stopped by operator');
return;
}
const message = errorMessage(runError);
replaceMessage(setMessages, assistantId, (current) => ({
...current,
state: 'error',
isThinking: false,
toolActivity: undefined,
error: message,
}));
setError(message);
setStreamState('error');
setTelemetry((current) => ({ ...current, inferencePhase: 'idle' }));
setBootStatus('Runtime stopped safely');
})
.finally(() => {
if (operationRef.current === controller) operationRef.current = null;
});
return true;
};
const handleSend = (submission: ComposerSubmission) => {
startSubmission(submission, messages);
};
const handleRegenerate = (messageId: string) => {
if (operationRef.current) return;
const plan = planMessageRegeneration(messages, messageId);
if (!plan) return;
const started = startSubmission(
{ prompt: plan.userMessage.content, toolsEnabled },
plan.baseMessages,
plan.userMessage,
);
if (!started) return;
const retainedToolIds = new Set(plan.baseMessages.flatMap((message) => (
message.tools?.map((tool) => tool.id) ?? []
)));
setToolEvents((current) => current.filter((event) => retainedToolIds.has(event.id)));
setArtifact(null);
};
const handleCancel = () => {
operationRef.current?.abort();
};
const handlePersistStorage = () => {
void requestPersistentStorage(true);
};
const handleClearStorage = () => {
if (operationRef.current) return;
if (!window.confirm('Delete every cached Bonsai model from this browser profile? Conversation and memory notes are kept.')) return;
void client.storageClear()
.then((estimate) => {
setLoadedModelId(null);
loadedConfigurationRef.current = null;
setStorage({
usedBytes: estimate.usageBytes ?? 0,
quotaBytes: estimate.quotaBytes ?? 0,
persistent: estimate.persisted,
downloads: [],
});
setTelemetry((current) => ({ ...INITIAL_TELEMETRY, device: current.device }));
setBootStatus('Cached model files deleted');
})
.catch((clearError) => {
if (!handleModelInvalidation(clearError)) setError(errorMessage(clearError));
});
};
const handleClearConversation = () => {
if (operationRef.current) return;
const clearedId = activeConversationId;
setConversations((current) => current.filter((conversation) => conversation.id !== clearedId));
setActiveConversationId(createConversationId());
setMessages([]);
setToolEvents([]);
setArtifact(null);
setBootStatus('Conversation cleared · model remains local');
void deleteConversation(clearedId).catch(() => setBootStatus('Conversation cleared for this tab · local history is unavailable'));
};
const handleOpenConversationList = () => {
if (operationRef.current) return;
const alreadySaved = conversations.some((conversation) => conversation.id === activeConversationId);
if (messages.length > 0 || settings.systemPrompt !== INITIAL_SETTINGS.systemPrompt || alreadySaved) {
const nextConversation: ChatSession = {
id: activeConversationId,
title: conversationTitle(messages),
messages,
modelId: activeModelId,
systemPrompt: settings.systemPrompt,
updatedAt: Date.now(),
};
setConversations((current) => upsertConversation(current, nextConversation));
void saveConversation(nextConversation).catch(() => undefined);
}
setConversationListOpen(true);
};
const handleNewConversation = () => {
if (operationRef.current) return;
setActiveConversationId(createConversationId());
setMessages([]);
setToolEvents([]);
setArtifact(null);
setError(null);
setStreamState('idle');
setSettings((current) => ({ ...current, systemPrompt: INITIAL_SETTINGS.systemPrompt }));
setConversationListOpen(false);
setBootStatus('New local chat · model remains available');
};
const handleOpenConversation = (conversationId: string) => {
if (operationRef.current) return;
const conversation = conversations.find((candidate) => candidate.id === conversationId);
if (!conversation) return;
setActiveConversationId(conversation.id);
setMessages(conversation.messages);
setToolEvents([]);
setArtifact(null);
setError(null);
setStreamState('idle');
setSettings((current) => ({ ...current, systemPrompt: conversation.systemPrompt }));
handleModelSelect(conversation.modelId);
setConversationListOpen(false);
setBootStatus(`Opened local chat · ${conversation.title}`);
};
const handleOpenConversationSettings = (conversationId: string) => {
if (operationRef.current) return;
handleOpenConversation(conversationId);
setSettingsOpen(true);
};
const handleDeleteConversation = (conversationId: string) => {
if (operationRef.current) return;
const conversation = conversations.find((candidate) => candidate.id === conversationId);
if (!conversation || !window.confirm(`Delete “${conversation.title}” from this browser?`)) return;
setConversations((current) => current.filter((candidate) => candidate.id !== conversationId));
if (conversationId === activeConversationId) {
setActiveConversationId(createConversationId());
setMessages([]);
setToolEvents([]);
setArtifact(null);
setSettings((current) => ({ ...current, systemPrompt: INITIAL_SETTINGS.systemPrompt }));
}
void deleteConversation(conversationId)
.then(() => setBootStatus('Local chat deleted'))
.catch(() => setBootStatus('Chat removed for this tab · local history is unavailable'));
};
const effectiveLoadedModelId = (() => {
const configured = loadedConfigurationRef.current;
const loadedModel = manifest?.models.find((model) => model.id === UI_TO_TIER[loadedModelId ?? activeModelId]);
const configuredContext = loadedModel?.id === '27b'
? Math.min(settings.contextSize, loadedModel.defaultContext)
: settings.contextSize;
return configured
&& configured.id === loadedModelId
&& configured.backend === settings.backend
&& configured.contextSize === configuredContext
? loadedModelId
: null;
})();
if (models.length === 0) {
return (
<div className="boot-screen" role="status">
<span className="brand-mark" aria-hidden="true"><span /></span>
<div><p className="eyebrow">Local WebGPU chat</p><h1>Reading pinned manifest</h1><p>{error ?? bootStatus}</p></div>
</div>
);
}
return (
<BonsaiShell
models={models}
activeModelId={activeModelId}
loadedModelId={effectiveLoadedModelId}
messages={messages}
conversations={conversations}
activeConversationId={activeConversationId}
conversationListOpen={conversationListOpen}
streamState={streamState}
modelLoadProgress={modelLoadProgress}
telemetry={telemetry}
toolEvents={toolEvents}
storage={storage}
artifact={artifact}
toolsEnabled={toolsEnabled}
settings={settings}
settingsOpen={settingsOpen}
runtimeStatus={bootStatus}
error={error}
shardRetry={shardRetry}
onModelSelect={handleModelSelect}
onLoadModel={handleLoadModel}
onSend={handleSend}
onRegenerate={handleRegenerate}
onCancel={handleCancel}
onToolsEnabledChange={setToolsEnabled}
onOpenSettings={() => { if (!operationRef.current) setSettingsOpen(true); }}
onCloseSettings={() => setSettingsOpen(false)}
onSettingsChange={(nextSettings) => { if (!operationRef.current) setSettings(nextSettings); }}
onPersistStorage={handlePersistStorage}
onClearStorage={handleClearStorage}
onClearConversation={handleClearConversation}
onOpenConversationList={handleOpenConversationList}
onNewConversation={handleNewConversation}
onOpenConversation={handleOpenConversation}
onOpenConversationSettings={handleOpenConversationSettings}
onDeleteConversation={handleDeleteConversation}
onDismissError={() => setError(null)}
onRetryShard={handleLoadModel}
onCloseArtifact={() => setArtifact(null)}
/>
);
}