Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/web/src/app/api/sessions/[id]/prompt/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand All @@ -30,6 +30,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
authorId: userId,
source: "web",
model,
reasoningEffort,
}),
});

Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/api/sessions/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
44 changes: 26 additions & 18 deletions packages/web/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -16,30 +18,16 @@ 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();
const [repos, setRepos] = useState<Repo[]>([]);
const [loadingRepos, setLoadingRepos] = useState(false);
const [selectedRepo, setSelectedRepo] = useState<string>("");
const [selectedModel, setSelectedModel] = useState("claude-haiku-4-5");
const [reasoningEffort, setReasoningEffort] = useState<string | undefined>(
getDefaultReasoningEffort("claude-haiku-4-5")
);
const [prompt, setPrompt] = useState("");
const [creating, setCreating] = useState(false);
const [error, setError] = useState("");
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -179,6 +172,7 @@ export default function Home() {
body: JSON.stringify({
content: prompt,
model: selectedModel,
reasoningEffort,
}),
});

Expand Down Expand Up @@ -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}
Expand All @@ -232,6 +228,8 @@ function HomeContent({
setSelectedRepo,
selectedModel,
setSelectedModel,
reasoningEffort,
setReasoningEffort,
prompt,
handlePromptChange,
creating,
Expand All @@ -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;
Expand Down Expand Up @@ -468,6 +468,14 @@ function HomeContent({
</div>
)}
</div>

{/* Reasoning effort pills */}
<ReasoningEffortPills
selectedModel={selectedModel}
reasoningEffort={reasoningEffort}
onSelect={setReasoningEffort}
disabled={creating}
/>
</div>

{/* Right side - Agent label */}
Expand Down
142 changes: 78 additions & 64 deletions packages/web/src/app/session/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
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
Expand Down Expand Up @@ -61,25 +67,6 @@
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 (
<svg className="w-4 h-4 text-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
Expand All @@ -93,7 +80,7 @@
isSelected,
onSelect,
}: {
model: ModelOption;
model: ModelDisplayInfo;
isSelected: boolean;
onSelect: () => void;
}) {
Expand Down Expand Up @@ -167,18 +154,29 @@

const [prompt, setPrompt] = useState("");
const [selectedModel, setSelectedModel] = useState("claude-haiku-4-5");
const [reasoningEffort, setReasoningEffort] = useState<string | undefined>(
getDefaultReasoningEffort("claude-haiku-4-5")
);
const [modelDropdownOpen, setModelDropdownOpen] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
const typingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const modelDropdownRef = useRef<HTMLDivElement>(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(() => {
Expand All @@ -202,7 +200,7 @@
e.preventDefault();
if (!prompt.trim() || isProcessing) return;

sendPrompt(prompt, selectedModel);
sendPrompt(prompt, selectedModel, reasoningEffort);
setPrompt("");
};

Expand Down Expand Up @@ -250,14 +248,16 @@
prompt={prompt}
isProcessing={isProcessing}
selectedModel={selectedModel}
reasoningEffort={reasoningEffort}
modelDropdownOpen={modelDropdownOpen}
modelDropdownRef={modelDropdownRef}
inputRef={inputRef}
handleSubmit={handleSubmit}
handleInputChange={handleInputChange}
handleKeyDown={handleKeyDown}
setModelDropdownOpen={setModelDropdownOpen}
setSelectedModel={setSelectedModel}
setSelectedModel={handleModelChange}
setReasoningEffort={setReasoningEffort}
stopExecution={stopExecution}
handleArchive={handleArchive}
handleUnarchive={handleUnarchive}
Expand All @@ -283,6 +283,7 @@
prompt,
isProcessing,
selectedModel,
reasoningEffort,
modelDropdownOpen,
modelDropdownRef,
inputRef,
Expand All @@ -291,6 +292,7 @@
handleKeyDown,
setModelDropdownOpen,
setSelectedModel,
setReasoningEffort,
stopExecution,
handleArchive,
handleUnarchive,
Expand All @@ -311,6 +313,7 @@
prompt: string;
isProcessing: boolean;
selectedModel: string;
reasoningEffort: string | undefined;
modelDropdownOpen: boolean;
modelDropdownRef: React.RefObject<HTMLDivElement | null>;
inputRef: React.RefObject<HTMLTextAreaElement | null>;
Expand All @@ -319,6 +322,7 @@
handleKeyDown: (e: React.KeyboardEvent) => void;
setModelDropdownOpen: (open: boolean) => void;
setSelectedModel: (model: string) => void;
setReasoningEffort: (value: string | undefined) => void;
stopExecution: () => void;
handleArchive: () => void;
handleUnarchive: () => void;
Expand Down Expand Up @@ -382,7 +386,7 @@
if (isNearBottomRef.current && !isPrependingRef.current) {
messagesEndRef.current?.scrollIntoView({ behavior: "auto" });
}
}, [events]);

Check warning on line 389 in packages/web/src/app/session/[id]/page.tsx

View workflow job for this annotation

GitHub Actions / Lint & Format (TypeScript)

React Hook useEffect has a missing dependency: 'messagesEndRef'. Either include it or remove the dependency array

// Deduplicate and group events for rendering
const groupedEvents = useMemo(() => {
Expand Down Expand Up @@ -571,49 +575,59 @@
</div>
</div>

{/* Footer row with model selector and agent label */}
{/* Footer row with model selector, reasoning pills, and agent label */}
<div className="flex items-center justify-between px-4 py-2 border-t border-border-muted">
{/* Left side - Model selector */}
<div className="relative" ref={modelDropdownRef}>
<button
type="button"
onClick={() => !isProcessing && setModelDropdownOpen(!modelDropdownOpen)}
disabled={isProcessing}
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground disabled:opacity-50 disabled:cursor-not-allowed transition"
>
<svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
</svg>
<span>{formatModelNameLower(selectedModel)}</span>
</button>
{/* Left side - Model selector + Reasoning pills */}
<div className="flex items-center gap-4">
<div className="relative" ref={modelDropdownRef}>
<button
type="button"
onClick={() => !isProcessing && setModelDropdownOpen(!modelDropdownOpen)}
disabled={isProcessing}
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground disabled:opacity-50 disabled:cursor-not-allowed transition"
>
<svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
</svg>
<span>{formatModelNameLower(selectedModel)}</span>
</button>

{/* Dropdown menu */}
{modelDropdownOpen && (
<div className="absolute bottom-full left-0 mb-2 w-56 bg-background shadow-lg border border-border py-1 z-50">
{MODEL_OPTIONS.map((group, groupIdx) => (
<div key={group.category}>
<div
className={`px-3 py-1.5 text-xs font-medium text-secondary-foreground uppercase tracking-wider ${
groupIdx > 0 ? "border-t border-border-muted mt-1" : ""
}`}
>
{group.category}
{/* Dropdown menu */}
{modelDropdownOpen && (
<div className="absolute bottom-full left-0 mb-2 w-56 bg-background shadow-lg border border-border py-1 z-50">
{MODEL_OPTIONS.map((group, groupIdx) => (
<div key={group.category}>
<div
className={`px-3 py-1.5 text-xs font-medium text-secondary-foreground uppercase tracking-wider ${
groupIdx > 0 ? "border-t border-border-muted mt-1" : ""
}`}
>
{group.category}
</div>
{group.models.map((model) => (
<ModelOptionButton
key={model.id}
model={model}
isSelected={selectedModel === model.id}
onSelect={() => {
setSelectedModel(model.id);
setModelDropdownOpen(false);
}}
/>
))}
</div>
{group.models.map((model) => (
<ModelOptionButton
key={model.id}
model={model}
isSelected={selectedModel === model.id}
onSelect={() => {
setSelectedModel(model.id);
setModelDropdownOpen(false);
}}
/>
))}
</div>
))}
</div>
)}
))}
</div>
)}
</div>

{/* Reasoning effort pills */}
<ReasoningEffortPills
selectedModel={selectedModel}
reasoningEffort={reasoningEffort}
onSelect={setReasoningEffort}
disabled={isProcessing}
/>
</div>

{/* Right side - Agent label */}
Expand Down
Loading
Loading