diff --git a/packages/web/src/app/api/sessions/[id]/prompt/route.ts b/packages/web/src/app/api/sessions/[id]/prompt/route.ts index eb45532e5..9e337c5f1 100644 --- a/packages/web/src/app/api/sessions/[id]/prompt/route.ts +++ b/packages/web/src/app/api/sessions/[id]/prompt/route.ts @@ -14,7 +14,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ try { const body = await request.json(); - const { content, model } = body; + const { content, model, reasoningEffort } = body; if (!content) { return NextResponse.json({ error: "content is required" }, { status: 400 }); @@ -30,6 +30,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ authorId: userId, source: "web", model, + reasoningEffort, }), }); diff --git a/packages/web/src/app/api/sessions/route.ts b/packages/web/src/app/api/sessions/route.ts index b5761222e..2c617414b 100644 --- a/packages/web/src/app/api/sessions/route.ts +++ b/packages/web/src/app/api/sessions/route.ts @@ -58,6 +58,7 @@ export async function POST(request: NextRequest) { repoOwner: body.repoOwner, repoName: body.repoName, model: body.model, + reasoningEffort: body.reasoningEffort, title: body.title, githubToken, userId, diff --git a/packages/web/src/app/page.tsx b/packages/web/src/app/page.tsx index f19aa5afe..d408ebbf3 100644 --- a/packages/web/src/app/page.tsx +++ b/packages/web/src/app/page.tsx @@ -6,6 +6,8 @@ import { useState, useEffect, useRef, useCallback } from "react"; import Link from "next/link"; import { SidebarLayout, useSidebarContext } from "@/components/sidebar-layout"; import { formatModelNameLower } from "@/lib/format"; +import { MODEL_OPTIONS, getDefaultReasoningEffort } from "@open-inspect/shared"; +import { ReasoningEffortPills } from "@/components/reasoning-effort-pills"; interface Repo { id: number; @@ -16,23 +18,6 @@ interface Repo { private: boolean; } -interface ModelOption { - id: string; - name: string; - description: string; -} - -const MODEL_OPTIONS: { category: string; models: ModelOption[] }[] = [ - { - category: "Model", - models: [ - { id: "claude-haiku-4-5", name: "claude haiku 4.5", description: "Fast and efficient" }, - { id: "claude-sonnet-4-5", name: "claude sonnet 4.5", description: "Balanced performance" }, - { id: "claude-opus-4-5", name: "claude opus 4.5", description: "Most capable" }, - ], - }, -]; - export default function Home() { const { data: session, status } = useSession(); const router = useRouter(); @@ -40,6 +25,9 @@ export default function Home() { const [loadingRepos, setLoadingRepos] = useState(false); const [selectedRepo, setSelectedRepo] = useState(""); const [selectedModel, setSelectedModel] = useState("claude-haiku-4-5"); + const [reasoningEffort, setReasoningEffort] = useState( + getDefaultReasoningEffort("claude-haiku-4-5") + ); const [prompt, setPrompt] = useState(""); const [creating, setCreating] = useState(false); const [error, setError] = useState(""); @@ -142,6 +130,11 @@ export default function Home() { return promise; }, [selectedRepo, selectedModel, pendingSessionId]); + const handleModelChange = useCallback((model: string) => { + setSelectedModel(model); + setReasoningEffort(getDefaultReasoningEffort(model)); + }, []); + const handlePromptChange = (value: string) => { const wasEmpty = prompt.length === 0; setPrompt(value); @@ -179,6 +172,7 @@ export default function Home() { body: JSON.stringify({ content: prompt, model: selectedModel, + reasoningEffort, }), }); @@ -212,7 +206,9 @@ export default function Home() { selectedRepo={selectedRepo} setSelectedRepo={setSelectedRepo} selectedModel={selectedModel} - setSelectedModel={setSelectedModel} + setSelectedModel={handleModelChange} + reasoningEffort={reasoningEffort} + setReasoningEffort={setReasoningEffort} prompt={prompt} handlePromptChange={handlePromptChange} creating={creating} @@ -232,6 +228,8 @@ function HomeContent({ setSelectedRepo, selectedModel, setSelectedModel, + reasoningEffort, + setReasoningEffort, prompt, handlePromptChange, creating, @@ -246,6 +244,8 @@ function HomeContent({ setSelectedRepo: (value: string) => void; selectedModel: string; setSelectedModel: (value: string) => void; + reasoningEffort: string | undefined; + setReasoningEffort: (value: string | undefined) => void; prompt: string; handlePromptChange: (value: string) => void; creating: boolean; @@ -468,6 +468,14 @@ function HomeContent({ )} + + {/* Reasoning effort pills */} + {/* Right side - Agent label */} diff --git a/packages/web/src/app/session/[id]/page.tsx b/packages/web/src/app/session/[id]/page.tsx index d4a17c9f7..5c5623747 100644 --- a/packages/web/src/app/session/[id]/page.tsx +++ b/packages/web/src/app/session/[id]/page.tsx @@ -10,6 +10,12 @@ import { SidebarLayout, useSidebarContext } from "@/components/sidebar-layout"; import { SessionRightSidebar } from "@/components/session-right-sidebar"; import { ActionBar } from "@/components/action-bar"; import { formatModelNameLower } from "@/lib/format"; +import { + MODEL_OPTIONS, + getDefaultReasoningEffort, + type ModelDisplayInfo, +} from "@open-inspect/shared"; +import { ReasoningEffortPills } from "@/components/reasoning-effort-pills"; import type { SandboxEvent } from "@/lib/tool-formatters"; // Event grouping types @@ -61,25 +67,6 @@ function groupEvents(events: SandboxEvent[]): EventGroup[] { return groups; } -// Model options configuration -interface ModelOption { - id: string; - name: string; - description: string; - category?: string; -} - -const MODEL_OPTIONS: { category: string; models: ModelOption[] }[] = [ - { - category: "Model", - models: [ - { id: "claude-haiku-4-5", name: "claude haiku 4.5", description: "Fast and efficient" }, - { id: "claude-sonnet-4-5", name: "claude sonnet 4.5", description: "Balanced performance" }, - { id: "claude-opus-4-5", name: "claude opus 4.5", description: "Most capable" }, - ], - }, -]; - function CheckIcon() { return ( @@ -93,7 +80,7 @@ function ModelOptionButton({ isSelected, onSelect, }: { - model: ModelOption; + model: ModelDisplayInfo; isSelected: boolean; onSelect: () => void; }) { @@ -167,18 +154,29 @@ export default function SessionPage() { const [prompt, setPrompt] = useState(""); const [selectedModel, setSelectedModel] = useState("claude-haiku-4-5"); + const [reasoningEffort, setReasoningEffort] = useState( + getDefaultReasoningEffort("claude-haiku-4-5") + ); const [modelDropdownOpen, setModelDropdownOpen] = useState(false); const messagesEndRef = useRef(null); const inputRef = useRef(null); const typingTimeoutRef = useRef(null); const modelDropdownRef = useRef(null); - // Sync selectedModel with session's model when session state loads + const handleModelChange = useCallback((model: string) => { + setSelectedModel(model); + setReasoningEffort(getDefaultReasoningEffort(model)); + }, []); + + // Sync selectedModel and reasoningEffort with session state when it loads useEffect(() => { if (sessionState?.model) { setSelectedModel(sessionState.model); + setReasoningEffort( + sessionState.reasoningEffort ?? getDefaultReasoningEffort(sessionState.model) + ); } - }, [sessionState?.model]); + }, [sessionState?.model, sessionState?.reasoningEffort]); // Redirect if not authenticated useEffect(() => { @@ -202,7 +200,7 @@ export default function SessionPage() { e.preventDefault(); if (!prompt.trim() || isProcessing) return; - sendPrompt(prompt, selectedModel); + sendPrompt(prompt, selectedModel, reasoningEffort); setPrompt(""); }; @@ -250,6 +248,7 @@ export default function SessionPage() { prompt={prompt} isProcessing={isProcessing} selectedModel={selectedModel} + reasoningEffort={reasoningEffort} modelDropdownOpen={modelDropdownOpen} modelDropdownRef={modelDropdownRef} inputRef={inputRef} @@ -257,7 +256,8 @@ export default function SessionPage() { handleInputChange={handleInputChange} handleKeyDown={handleKeyDown} setModelDropdownOpen={setModelDropdownOpen} - setSelectedModel={setSelectedModel} + setSelectedModel={handleModelChange} + setReasoningEffort={setReasoningEffort} stopExecution={stopExecution} handleArchive={handleArchive} handleUnarchive={handleUnarchive} @@ -283,6 +283,7 @@ function SessionContent({ prompt, isProcessing, selectedModel, + reasoningEffort, modelDropdownOpen, modelDropdownRef, inputRef, @@ -291,6 +292,7 @@ function SessionContent({ handleKeyDown, setModelDropdownOpen, setSelectedModel, + setReasoningEffort, stopExecution, handleArchive, handleUnarchive, @@ -311,6 +313,7 @@ function SessionContent({ prompt: string; isProcessing: boolean; selectedModel: string; + reasoningEffort: string | undefined; modelDropdownOpen: boolean; modelDropdownRef: React.RefObject; inputRef: React.RefObject; @@ -319,6 +322,7 @@ function SessionContent({ handleKeyDown: (e: React.KeyboardEvent) => void; setModelDropdownOpen: (open: boolean) => void; setSelectedModel: (model: string) => void; + setReasoningEffort: (value: string | undefined) => void; stopExecution: () => void; handleArchive: () => void; handleUnarchive: () => void; @@ -571,49 +575,59 @@ function SessionContent({ - {/* Footer row with model selector and agent label */} + {/* Footer row with model selector, reasoning pills, and agent label */}
- {/* Left side - Model selector */} -
- + {/* Left side - Model selector + Reasoning pills */} +
+
+ - {/* Dropdown menu */} - {modelDropdownOpen && ( -
- {MODEL_OPTIONS.map((group, groupIdx) => ( -
-
0 ? "border-t border-border-muted mt-1" : "" - }`} - > - {group.category} + {/* Dropdown menu */} + {modelDropdownOpen && ( +
+ {MODEL_OPTIONS.map((group, groupIdx) => ( +
+
0 ? "border-t border-border-muted mt-1" : "" + }`} + > + {group.category} +
+ {group.models.map((model) => ( + { + setSelectedModel(model.id); + setModelDropdownOpen(false); + }} + /> + ))}
- {group.models.map((model) => ( - { - setSelectedModel(model.id); - setModelDropdownOpen(false); - }} - /> - ))} -
- ))} -
- )} + ))} +
+ )} +
+ + {/* Reasoning effort pills */} +
{/* Right side - Agent label */} diff --git a/packages/web/src/components/reasoning-effort-pills.tsx b/packages/web/src/components/reasoning-effort-pills.tsx new file mode 100644 index 000000000..5fe193248 --- /dev/null +++ b/packages/web/src/components/reasoning-effort-pills.tsx @@ -0,0 +1,43 @@ +import { + MODEL_REASONING_CONFIG, + type ValidModel, + type ReasoningEffort, +} from "@open-inspect/shared"; + +interface ReasoningEffortPillsProps { + selectedModel: string; + reasoningEffort: string | undefined; + onSelect: (effort: string) => void; + disabled: boolean; +} + +export function ReasoningEffortPills({ + selectedModel, + reasoningEffort, + onSelect, + disabled, +}: ReasoningEffortPillsProps) { + const config = MODEL_REASONING_CONFIG[selectedModel as ValidModel]; + if (!config) return null; + + // If effort is not in the list (e.g. model just changed), -1 wraps to index 0 on cycle + const currentIndex = reasoningEffort + ? config.efforts.indexOf(reasoningEffort as ReasoningEffort) + : -1; + const handleCycle = () => { + const nextIndex = (currentIndex + 1) % config.efforts.length; + onSelect(config.efforts[nextIndex]); + }; + + return ( + + ); +} diff --git a/packages/web/src/components/session-right-sidebar.tsx b/packages/web/src/components/session-right-sidebar.tsx index 45a5d0d5f..df910f5bd 100644 --- a/packages/web/src/components/session-right-sidebar.tsx +++ b/packages/web/src/components/session-right-sidebar.tsx @@ -22,6 +22,7 @@ interface SessionState { messageCount: number; createdAt: number; model?: string; + reasoningEffort?: string; } interface Participant { @@ -83,6 +84,7 @@ export function SessionRightSidebar({ - {formatModelName(model)} + + {formatModelName(model)} + {reasoningEffort && ยท {reasoningEffort}} +
)} diff --git a/packages/web/src/hooks/use-session-socket.ts b/packages/web/src/hooks/use-session-socket.ts index 99547db01..3d28ea725 100644 --- a/packages/web/src/hooks/use-session-socket.ts +++ b/packages/web/src/hooks/use-session-socket.ts @@ -50,6 +50,7 @@ interface SessionState { messageCount: number; createdAt: number; model?: string; + reasoningEffort?: string; isProcessing: boolean; } @@ -76,7 +77,7 @@ interface UseSessionSocketReturn { isProcessing: boolean; hasMoreHistory: boolean; loadingHistory: boolean; - sendPrompt: (content: string, model?: string) => void; + sendPrompt: (content: string, model?: string, reasoningEffort?: string) => void; stopExecution: () => void; sendTyping: () => void; reconnect: () => void; @@ -517,7 +518,7 @@ export function useSessionSocket(sessionId: string): UseSessionSocketReturn { }; }, [sessionId, handleMessage, fetchWsToken]); - const sendPrompt = useCallback((content: string, model?: string) => { + const sendPrompt = useCallback((content: string, model?: string, reasoningEffort?: string) => { if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) { console.error("WebSocket not connected"); return; @@ -526,11 +527,11 @@ export function useSessionSocket(sessionId: string): UseSessionSocketReturn { if (!subscribedRef.current) { console.error("Not subscribed yet, waiting..."); // Retry after a short delay - setTimeout(() => sendPrompt(content, model), 500); + setTimeout(() => sendPrompt(content, model, reasoningEffort), 500); return; } - console.log("Sending prompt:", content, "with model:", model); + console.log("Sending prompt:", content, "with model:", model, "reasoning:", reasoningEffort); // Optimistically set isProcessing for immediate feedback // Server will confirm with processing_status message @@ -545,6 +546,7 @@ export function useSessionSocket(sessionId: string): UseSessionSocketReturn { type: "prompt", content, model, // Include model for per-message model switching + reasoningEffort, }) ); }, []);