thinking
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
+2
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user