default model

This commit is contained in:
2026-02-22 16:49:30 +00:00
parent 285852f04d
commit e35b7340f3
10 changed files with 3404 additions and 10 deletions
+44 -4
View File
@@ -4,9 +4,26 @@ import type { ClientMessage, ServerMessage, Message, PiEvent } from './types';
import { sessionManager } from './session-manager';
import * as storage from './storage';
import * as piBridge from './pi-bridge';
import { getHomeDir } from '../../../servers/data-path';
import { getHomeDir, getUserSettingsFile } from '../../../servers/data-path';
import { logger } from './logger';
// Default model when no user preference is set
const DEFAULT_MODEL = 'opencode/big-pickle';
async function getUserDefaultModel(email: string): Promise<string | null> {
try {
const settingsPath = getUserSettingsFile(email);
const file = Bun.file(settingsPath);
if (await file.exists()) {
const settings = await file.json();
return settings?.chat?.defaultModel || null;
}
} catch (err) {
logger.error('Failed to read user settings for default model', { email, error: String(err) });
}
return null;
}
type WSData = {
userId: number;
email: string;
@@ -25,7 +42,7 @@ function sendToClient(ws: ServerWebSocket<WSData> | null, msg: ServerMessage): v
}
export async function open(ws: ServerWebSocket<WSData>): Promise<void> {
logger.info('WebSocket connection opened', { email: ws.data.email });
// logger.info('WebSocket connection opened', { email: ws.data.email });
}
export function message(ws: ServerWebSocket<WSData>, raw: string | Buffer): void {
@@ -50,7 +67,7 @@ export function message(ws: ServerWebSocket<WSData>, raw: string | Buffer): void
}
export function close(ws: ServerWebSocket<WSData>): void {
logger.info('WebSocket connection closed', { email: ws.data.email });
// logger.info('WebSocket connection closed', { email: ws.data.email });
const sessionId = wsToSessionMap.get(ws);
if (sessionId) {
@@ -207,7 +224,30 @@ async function handleChat(
): Promise<void> {
const { email } = ws.data;
const sessionId = msg.sessionId || randomUUID();
const model = msg.model || 'opencode/big-pickle';
// Use provided model, or fall back to user default, or use system default
let model = msg.model;
let modelSource = 'client-provided';
let userDefault = null;
if (!model) {
userDefault = await getUserDefaultModel(email);
if (userDefault) {
model = userDefault;
modelSource = 'user-settings';
} else {
model = DEFAULT_MODEL;
modelSource = 'system-default';
}
}
logger.info('Model selected for chat', {
sessionId,
model,
modelSource,
clientModel: msg.model || null,
userDefault,
});
const cwd = msg.cwd || getHomeDir(email);
const groupSlug = msg.groupSlug || null;
@@ -112,6 +112,7 @@ export function ChatLauncher({
model={selectedModel}
isConnected={true}
isGenerating={false}
hasStarted={false}
/>
<WebpageDialog open={urlDialogOpen} onOpenChange={setUrlDialogOpen} onSubmit={attachWebpage} />
@@ -35,6 +35,7 @@ export function useEmbeddableChat(params: UseEmbeddableChatParams) {
sessionId,
model,
selectedModel,
hasStarted,
setSelectedModel,
sendPrompt,
stopGeneration,
@@ -170,6 +171,7 @@ export function useEmbeddableChat(params: UseEmbeddableChatParams) {
sessionId,
model,
selectedModel,
hasStarted,
setSelectedModel,
sendPrompt,
stopGeneration,
@@ -28,6 +28,7 @@ export const InputArea = ({ manager }: InputAreaProps) => {
selectedModel,
setSelectedModel,
model,
hasStarted,
attachments,
attachWebpage,
attachImage,
@@ -94,6 +95,7 @@ export const InputArea = ({ manager }: InputAreaProps) => {
model={model}
isConnected={isConnected}
isGenerating={isGenerating}
hasStarted={hasStarted}
/>
<WebpageDialog open={urlDialogOpen} onOpenChange={setUrlDialogOpen} onSubmit={attachWebpage} />
@@ -27,6 +27,7 @@ type ModelSelectorProps = {
model: string | null;
isConnected: boolean;
isGenerating: boolean;
hasStarted: boolean;
};
export function ModelSelector({
@@ -37,23 +38,40 @@ export function ModelSelector({
model,
isConnected,
isGenerating,
hasStarted,
}: ModelSelectorProps) {
const providers = [...new Set(availableModels.map((m) => m.provider).filter(Boolean))] as string[];
const activeProvider = availableModels.find((m) => m.id === selectedModel)?.provider ?? providers[0];
// Determine which model to display: selectedModel takes precedence, then model (from server), then fallback
const displayModel = selectedModel || model;
const activeProvider = availableModels.find((m) => m.id === displayModel)?.provider ?? providers[0];
const providerModels = availableModels.filter((m) => m.provider === activeProvider);
const fallbackModelId = providerModels[0]?.id ?? null;
// Lock after session has started
const isLocked = hasStarted || isGenerating || !isConnected;
const handleProviderClick = (provider: string) => {
if (isLocked) return;
const firstModel = availableModels.find((m) => m.provider === provider);
if (firstModel) onModelChange(firstModel.id);
};
const displayName = (provider: string) => PROVIDER_DISPLAY[provider] ?? provider;
// Get display text for the model
const getModelDisplayText = () => {
if (displayModel) {
const found = availableModels.find((m) => m.id === displayModel);
return found?.name || displayModel;
}
return 'Select model';
};
return (
<div className="flex items-center justify-between mt-2">
{messages.length > 0 ? (
{isLocked ? (
<span className="rounded-md bg-duck-dark/80 px-3 py-1 text-xs font-medium text-white">
{activeProvider ? displayName(activeProvider) : 'Pi'}
</span>
@@ -77,9 +95,9 @@ export function ModelSelector({
<div className="text-xs text-duck-dark/50">
{providerModels.length > 0 ? (
<Select
value={selectedModel ?? fallbackModelId ?? undefined}
onValueChange={(v) => onModelChange(v)}
disabled={isGenerating || !isConnected}
value={displayModel ?? fallbackModelId ?? undefined}
onValueChange={(v) => !isLocked && onModelChange(v)}
disabled={isLocked}
>
<SelectTrigger className="h-auto border-0 bg-transparent p-0 text-xs text-duck-dark/50 shadow-none focus:ring-0 gap-1 cursor-pointer">
<SelectValue />
@@ -93,7 +111,7 @@ export function ModelSelector({
</SelectContent>
</Select>
) : (
<span>{model ?? 'Pi'}</span>
<span>{getModelDisplayText()}</span>
)}
</div>
</div>
@@ -1,6 +1,7 @@
import { useState, useEffect, useRef } from 'react';
import { useChatWebSocket } from 'hooks/useChatWebSocket';
import { useChatSessions } from 'state/useChatSessions';
import { useSettings } from 'state/useSettings';
import type { ChatMessage, ServerMessage, TaskInfo, Message } from '../apps/Chat/types';
const SAVE_DEBOUNCE_MS = 1000;
@@ -27,6 +28,19 @@ export function usePiChat(initialSessionId?: string, initialModel?: string | nul
const [selectedModel, setSelectedModel] = useState<string | null>(initialModel ?? null);
const [cwd, setCwd] = useState<string | null>(null);
// Track if session has started (first message sent)
const [hasStarted, setHasStarted] = useState(false);
// Get user settings for default model
const { settings } = useSettings();
// Set default model from settings when starting a new chat (no initialSessionId, no initialModel)
useEffect(() => {
if (!initialSessionId && !initialModel && settings?.chat?.defaultModel) {
setSelectedModel(settings.chat.defaultModel);
}
}, [initialSessionId, initialModel, settings]);
const streamingRef = useRef('');
const rafRef = useRef<number | null>(null);
const sessionIdRef = useRef<string | null>(initialSessionId ?? null);
@@ -238,6 +252,11 @@ export function usePiChat(initialSessionId?: string, initialModel?: string | nul
cwdParam?: { root?: string; path: string },
groupSlug?: string | null,
) {
// Mark session as started on first message
if (!hasStarted) {
setHasStarted(true);
}
setMessages((prev) => [...prev, { role: 'user', text, ...(images?.length ? { images } : {}) }]);
setIsGenerating(true);
streamingRef.current = '';
@@ -277,6 +296,7 @@ export function usePiChat(initialSessionId?: string, initialModel?: string | nul
sessionId,
model,
selectedModel,
hasStarted,
cwd,
setSelectedModel,
sendPrompt,