This commit is contained in:
2026-02-25 16:04:11 +00:00
parent 05c5b648fa
commit b6a0a57893
11 changed files with 698 additions and 22 deletions
+10
View File
@@ -451,6 +451,16 @@ function writeRpcCommand(proc: Subprocess, command: Record<string, unknown>): vo
}
}
export function setThinkingLevel(
process: Subprocess,
level: string,
): void {
writeRpcCommand(process, {
type: 'set_thinking_level',
level,
});
}
export function sendPrompt(
process: Subprocess,
prompt: string,
+3
View File
@@ -39,6 +39,8 @@ export type GroupMeta = {
sessionCount: number;
};
export type ThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
export type ClientMessage =
| {
type: "chat";
@@ -50,6 +52,7 @@ export type ClientMessage =
sandboxed?: boolean;
groupSlug?: string;
attachmentIds?: string[];
thinking?: ThinkingLevel;
}
| {
type: "resume";
+6 -1
View File
@@ -244,7 +244,7 @@ function createEventHandler(sessionId: string, model: string, cwd: string, stora
async function handleChat(
ws: ServerWebSocket<WSData>,
msg: { prompt: string; sessionId?: string; model?: string; cwd?: string; cwdRoot?: string; sandboxed?: boolean; groupSlug?: string; attachmentIds?: string[] }
msg: { prompt: string; sessionId?: string; model?: string; cwd?: string; cwdRoot?: string; sandboxed?: boolean; groupSlug?: string; attachmentIds?: string[]; thinking?: string }
): Promise<void> {
const { email, username, userId } = ws.data;
const sessionId = msg.sessionId || randomUUID();
@@ -312,6 +312,11 @@ async function handleChat(
session.meta.title = msg.prompt.slice(0, 100);
}
// Set thinking level if provided
if (msg.thinking) {
piBridge.setThinkingLevel(session.piProcess, msg.thinking);
}
// Send prompt to Pi
const requestId = randomUUID();
session.isGenerating = true;
@@ -75,7 +75,7 @@ export async function syncLocalProvidersToPiConfig(): Promise<void> {
models: models.map(m => ({
id: m.id,
name: m.name || m.id,
reasoning: false,
reasoning: isReasoningModel(m.id),
input: ['text'],
contextWindow: m.contextWindow || 128000,
maxTokens: m.maxTokens || 4096,
@@ -94,6 +94,20 @@ export async function syncLocalProvidersToPiConfig(): Promise<void> {
}
}
/** Detect reasoning-capable models by name patterns */
const REASONING_PATTERNS = [
/\bqwen3\b/i,
/\bqwq\b/i,
/\bdeepseek-r1\b/i,
/\br1\b/i,
/\breasoning\b/i,
/\bthink/i,
];
function isReasoningModel(modelId: string): boolean {
return REASONING_PATTERNS.some(p => p.test(modelId));
}
async function fetchModelsFromLocalProvider(
lp: LocalProvider,
): Promise<{ id: string; name?: string; contextWindow?: number; maxTokens?: number }[]> {
@@ -113,6 +113,8 @@ export function ChatLauncher({
isConnected={true}
isGenerating={false}
hasStarted={false}
thinkingLevel={null}
onThinkingChange={() => {}}
/>
<WebpageDialog open={urlDialogOpen} onOpenChange={setUrlDialogOpen} onSubmit={attachWebpage} />
@@ -38,7 +38,9 @@ export function useEmbeddableChat(params: UseEmbeddableChatParams) {
model,
selectedModel,
hasStarted,
thinkingLevel,
setSelectedModel,
setThinkingLevel,
sendPrompt,
stopGeneration,
} = chat;
@@ -83,6 +85,7 @@ export function useEmbeddableChat(params: UseEmbeddableChatParams) {
cwdForFirst,
undefined,
sandboxed,
thinkingLevel,
);
attachmentManager.clearAttachments();
@@ -197,7 +200,9 @@ export function useEmbeddableChat(params: UseEmbeddableChatParams) {
model,
selectedModel,
hasStarted,
thinkingLevel,
setSelectedModel,
setThinkingLevel,
sendPrompt,
stopGeneration,
input,
@@ -34,6 +34,8 @@ export const InputArea = ({ manager }: InputAreaProps) => {
attachImage,
removeAttachment,
appendToInput,
thinkingLevel,
setThinkingLevel,
} = manager;
const { recording, transcribing, toggleRecording } = useAudioRecording(appendToInput);
@@ -96,6 +98,8 @@ export const InputArea = ({ manager }: InputAreaProps) => {
isConnected={isConnected}
isGenerating={isGenerating}
hasStarted={hasStarted}
thinkingLevel={thinkingLevel}
onThinkingChange={setThinkingLevel}
/>
<WebpageDialog open={urlDialogOpen} onOpenChange={setUrlDialogOpen} onSubmit={attachWebpage} />
@@ -29,6 +29,8 @@ type ModelSelectorProps = {
isConnected: boolean;
isGenerating: boolean;
hasStarted: boolean;
thinkingLevel: string | null;
onThinkingChange: (level: string | null) => void;
};
export function ModelSelector({
@@ -40,6 +42,8 @@ export function ModelSelector({
isConnected,
isGenerating,
hasStarted,
thinkingLevel,
onThinkingChange,
}: ModelSelectorProps) {
const providers = [...new Set(availableModels.map((m) => m.provider).filter(Boolean))] as string[];
@@ -99,28 +103,57 @@ export function ModelSelector({
))}
</div>
)}
<div className="text-xs text-duck-dark/50">
{providerModels.length > 0 ? (
<Select
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 />
</SelectTrigger>
<SelectContent className="z-[800]" side="top">
{providerModels.map((m) => (
<SelectItem key={m.id} value={m.id}>
{m.name}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<span>{getModelDisplayText()}</span>
<div className="flex items-center gap-2">
{availableModels.find((m) => m.id === displayModel)?.reasoning && (
<ThinkingToggle enabled={thinkingLevel === 'high'} onToggle={() => onThinkingChange(thinkingLevel === 'high' ? 'off' : 'high')} disabled={isGenerating} />
)}
<div className="text-xs text-duck-dark/50">
{providerModels.length > 0 ? (
<Select
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 />
</SelectTrigger>
<SelectContent className="z-[800]" side="top">
{providerModels.map((m) => (
<SelectItem key={m.id} value={m.id}>
{m.name}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<span>{getModelDisplayText()}</span>
)}
</div>
</div>
</div>
);
}
// ── Thinking Toggle ──
type ThinkingToggleProps = {
enabled: boolean;
onToggle: () => void;
disabled: boolean;
};
const ThinkingToggle = ({ enabled, onToggle, disabled }: ThinkingToggleProps) => (
<button
type="button"
onClick={onToggle}
disabled={disabled}
title={enabled ? 'Thinking enabled (click to disable)' : 'Thinking disabled (click to enable)'}
className={`rounded-md px-2 py-0.5 text-xs font-medium transition-colors cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed ${
enabled
? 'bg-duck-teal/15 text-duck-teal'
: 'text-duck-dark/40 hover:text-duck-dark/60'
}`}
>
{enabled ? 'think' : 'no think'}
</button>
);
@@ -135,6 +135,8 @@ const PiMonoInner = ({ defaultInput, cwd, initialModel, taskInfo, sandboxed }: P
isConnected={chat.isConnected}
isGenerating={false}
hasStarted={false}
thinkingLevel={chat.thinkingLevel}
onThinkingChange={chat.setThinkingLevel}
/>
</div>
</>
@@ -28,6 +28,7 @@ export function usePiChat(initialSessionId?: string, initialModel?: string | nul
const [model, setModel] = useState<string | null>(null);
const [selectedModel, setSelectedModel] = useState<string | null>(initialModel ?? null);
const [cwd, setCwd] = useState<string | null>(null);
const [thinkingLevel, setThinkingLevel] = useState<string | null>(null);
// Track if session has started (first message sent)
const [hasStarted, setHasStarted] = useState(false);
@@ -265,6 +266,7 @@ export function usePiChat(initialSessionId?: string, initialModel?: string | nul
cwdParam?: { root?: string; path: string },
groupSlug?: string | null,
sandboxed?: boolean,
thinking?: string | null,
) {
// Mark session as started on first message
if (!hasStarted) {
@@ -297,6 +299,7 @@ export function usePiChat(initialSessionId?: string, initialModel?: string | nul
...(imageData?.length ? { images: imageData } : {}),
...(resourceChatDir ? { resourceChatDir } : {}),
...(taskInfo ? { taskInfo } : {}),
...(thinking ? { thinking } : {}),
});
}
@@ -314,7 +317,9 @@ export function usePiChat(initialSessionId?: string, initialModel?: string | nul
selectedModel,
hasStarted,
cwd,
thinkingLevel,
setSelectedModel,
setThinkingLevel,
sendPrompt,
stopGeneration,
};