mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-04 19:32:12 +00:00
fix: stabilize agent selector defaults (#1414)
* fix: stabilize agent defaults and repo selector Co-authored-by: open-swe[bot] <215916821+open-swe[bot]@users.noreply.github.com> * fix: align default repo selector styling, restore text fallback Match the repo selector to sibling settings controls (h-7, bg-input/20, text-xs). Fall back to a text input when the repo list is empty so a default repo can still be entered. * fix: make repo selector dropdown more compact --------- Co-authored-by: open-swe[bot] <215916821+open-swe[bot]@users.noreply.github.com>
This commit is contained in:
parent
8b18e7e95d
commit
a75e9b027b
9 changed files with 376 additions and 284 deletions
|
|
@ -67,6 +67,8 @@ from .slack_oauth import (
|
||||||
)
|
)
|
||||||
from .team_settings import (
|
from .team_settings import (
|
||||||
TeamSettingsUpdate,
|
TeamSettingsUpdate,
|
||||||
|
get_team_default_model,
|
||||||
|
get_team_default_subagent_model,
|
||||||
get_team_settings,
|
get_team_settings,
|
||||||
upsert_team_settings,
|
upsert_team_settings,
|
||||||
)
|
)
|
||||||
|
|
@ -279,7 +281,15 @@ async def me(session: dict[str, Any] = _SESSION_DEP) -> dict[str, Any]:
|
||||||
|
|
||||||
@router.get("/options")
|
@router.get("/options")
|
||||||
async def options() -> dict[str, Any]:
|
async def options() -> dict[str, Any]:
|
||||||
return {"models": SUPPORTED_MODELS}
|
agent_model, agent_effort = await get_team_default_model("agent")
|
||||||
|
subagent_model, subagent_effort = await get_team_default_subagent_model("agent")
|
||||||
|
return {
|
||||||
|
"models": SUPPORTED_MODELS,
|
||||||
|
"default_agent_model": agent_model,
|
||||||
|
"default_agent_reasoning_effort": agent_effort,
|
||||||
|
"default_agent_subagent_model": subagent_model,
|
||||||
|
"default_agent_subagent_reasoning_effort": subagent_effort,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/profile")
|
@router.get("/profile")
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,17 @@
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { FolderIcon } from "@phosphor-icons/react";
|
import { FolderIcon } from "@phosphor-icons/react";
|
||||||
|
|
||||||
|
import type { SessionUser } from "@/lib/api";
|
||||||
|
import type { PendingPrompt } from "@/lib/agents/pendingPrompts";
|
||||||
|
import type { AgentThread, Message } from "@/lib/agents/types";
|
||||||
|
import type { ModelSelection } from "@/lib/agents/useModelOptions";
|
||||||
import { AgentPromptBar } from "@/components/agents/AgentPromptBar";
|
import { AgentPromptBar } from "@/components/agents/AgentPromptBar";
|
||||||
import { AgentsShell } from "@/components/agents/AgentsSidebar";
|
import { AgentsShell } from "@/components/agents/AgentsSidebar";
|
||||||
import { MessageView } from "@/components/agents/ported";
|
import { MessageView } from "@/components/agents/ported";
|
||||||
import type { SessionUser } from "@/lib/api";
|
|
||||||
import type { AgentThread, Message } from "@/lib/agents/types";
|
|
||||||
import { useSendAgentMessage } from "@/lib/agents/queries";
|
import { useSendAgentMessage } from "@/lib/agents/queries";
|
||||||
import {
|
import { dropPendingPrompts, getPendingPrompts } from "@/lib/agents/pendingPrompts";
|
||||||
dropPendingPrompts,
|
|
||||||
getPendingPrompts,
|
|
||||||
type PendingPrompt,
|
|
||||||
} from "@/lib/agents/pendingPrompts";
|
|
||||||
import { useAgentThreadStream } from "@/lib/agents/useThreadStream";
|
import { useAgentThreadStream } from "@/lib/agents/useThreadStream";
|
||||||
import { useModelOptions, type ModelSelection } from "@/lib/agents/useModelOptions";
|
import { useModelOptions } from "@/lib/agents/useModelOptions";
|
||||||
|
|
||||||
interface AgentThreadViewProps {
|
interface AgentThreadViewProps {
|
||||||
user: SessionUser;
|
user: SessionUser;
|
||||||
|
|
@ -23,7 +21,7 @@ interface AgentThreadViewProps {
|
||||||
export function AgentThreadView({ user, thread }: AgentThreadViewProps) {
|
export function AgentThreadView({ user, thread }: AgentThreadViewProps) {
|
||||||
const sendMessage = useSendAgentMessage(thread.id);
|
const sendMessage = useSendAgentMessage(thread.id);
|
||||||
useAgentThreadStream(thread.id, thread.status === "running");
|
useAgentThreadStream(thread.id, thread.status === "running");
|
||||||
const [pendingPrompts, setPendingPrompts] = useState<PendingPrompt[]>(() =>
|
const [pendingPrompts, setPendingPrompts] = useState<Array<PendingPrompt>>(() =>
|
||||||
getPendingPrompts(thread.id),
|
getPendingPrompts(thread.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -37,12 +35,7 @@ export function AgentThreadView({ user, thread }: AgentThreadViewProps) {
|
||||||
return { modelId: thread.model, effort: thread.effort };
|
return { modelId: thread.model, effort: thread.effort };
|
||||||
}, [models, thread.model, thread.effort]);
|
}, [models, thread.model, thread.effort]);
|
||||||
const [selection, setSelection] = useState<ModelSelection | null>(null);
|
const [selection, setSelection] = useState<ModelSelection | null>(null);
|
||||||
|
const activeSelection = selection ?? threadSelection ?? defaultSelection;
|
||||||
useEffect(() => {
|
|
||||||
if (selection !== null) return;
|
|
||||||
if (threadSelection) setSelection(threadSelection);
|
|
||||||
else if (defaultSelection) setSelection(defaultSelection);
|
|
||||||
}, [defaultSelection, selection, threadSelection]);
|
|
||||||
|
|
||||||
const userMessageTexts = useMemo(() => {
|
const userMessageTexts = useMemo(() => {
|
||||||
return new Set(
|
return new Set(
|
||||||
|
|
@ -51,7 +44,7 @@ export function AgentThreadView({ user, thread }: AgentThreadViewProps) {
|
||||||
.map((m) =>
|
.map((m) =>
|
||||||
m.chunks
|
m.chunks
|
||||||
.filter((c) => c.kind === "text")
|
.filter((c) => c.kind === "text")
|
||||||
.map((c) => (c as { kind: "text"; text: string }).text)
|
.map((c) => c.text)
|
||||||
.join(""),
|
.join(""),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -67,7 +60,7 @@ export function AgentThreadView({ user, thread }: AgentThreadViewProps) {
|
||||||
});
|
});
|
||||||
}, [thread.id, userMessageTexts]);
|
}, [thread.id, userMessageTexts]);
|
||||||
|
|
||||||
const displayMessages = useMemo<Message[]>(() => {
|
const displayMessages = useMemo<Array<Message>>(() => {
|
||||||
if (pendingPrompts.length === 0) return thread.messages;
|
if (pendingPrompts.length === 0) return thread.messages;
|
||||||
const baseTimestamp = new Date().toISOString();
|
const baseTimestamp = new Date().toISOString();
|
||||||
const result = thread.messages.slice();
|
const result = thread.messages.slice();
|
||||||
|
|
@ -119,12 +112,12 @@ export function AgentThreadView({ user, thread }: AgentThreadViewProps) {
|
||||||
onSubmit={(content) =>
|
onSubmit={(content) =>
|
||||||
sendMessage.mutate({
|
sendMessage.mutate({
|
||||||
content,
|
content,
|
||||||
model_id: selection?.modelId ?? null,
|
model_id: activeSelection?.modelId ?? null,
|
||||||
effort: selection?.effort ?? null,
|
effort: activeSelection?.effort ?? null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
models={models}
|
models={models}
|
||||||
selection={selection ?? threadSelection ?? defaultSelection}
|
selection={activeSelection}
|
||||||
onSelectionChange={setSelection}
|
onSelectionChange={setSelection}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -142,12 +135,12 @@ export function AgentThreadView({ user, thread }: AgentThreadViewProps) {
|
||||||
onSubmit={(content) =>
|
onSubmit={(content) =>
|
||||||
sendMessage.mutate({
|
sendMessage.mutate({
|
||||||
content,
|
content,
|
||||||
model_id: selection?.modelId ?? null,
|
model_id: activeSelection?.modelId ?? null,
|
||||||
effort: selection?.effort ?? null,
|
effort: activeSelection?.effort ?? null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
models={models}
|
models={models}
|
||||||
selection={selection ?? threadSelection ?? defaultSelection}
|
selection={activeSelection}
|
||||||
onSelectionChange={setSelection}
|
onSelectionChange={setSelection}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Link } from "@tanstack/react-router"
|
import { Link } from "@tanstack/react-router"
|
||||||
import { useEffect, useState } from "react"
|
import { useState } from "react"
|
||||||
|
|
||||||
import type { ModelSelection } from "@/lib/agents/useModelOptions"
|
import type { ModelSelection } from "@/lib/agents/useModelOptions"
|
||||||
import { AgentPromptBar } from "@/components/agents/AgentPromptBar"
|
import { AgentPromptBar } from "@/components/agents/AgentPromptBar"
|
||||||
|
|
@ -16,17 +16,18 @@ export function AgentsHome() {
|
||||||
const recentRuns = (threadsQuery.data ?? []).slice(0, 5)
|
const recentRuns = (threadsQuery.data ?? []).slice(0, 5)
|
||||||
const { models, defaultSelection } = useModelOptions()
|
const { models, defaultSelection } = useModelOptions()
|
||||||
const [selection, setSelection] = useState<ModelSelection | null>(null)
|
const [selection, setSelection] = useState<ModelSelection | null>(null)
|
||||||
|
const activeSelection = selection ?? defaultSelection
|
||||||
|
|
||||||
const reposQuery = useRepos()
|
const reposQuery = useRepos()
|
||||||
const profileQuery = useProfile()
|
const profileQuery = useProfile()
|
||||||
// undefined = untouched (fall back to the profile default); null = explicitly "no repo".
|
// undefined = untouched (fall back to the profile default); null = explicitly "no repo".
|
||||||
const [repoOverride, setRepoOverride] = useState<string | null | undefined>(undefined)
|
const [repoOverride, setRepoOverride] = useState<string | null | undefined>(
|
||||||
|
undefined
|
||||||
|
)
|
||||||
const repo =
|
const repo =
|
||||||
repoOverride === undefined ? (profileQuery.data?.default_repo ?? null) : repoOverride
|
repoOverride === undefined
|
||||||
|
? (profileQuery.data?.default_repo ?? null)
|
||||||
useEffect(() => {
|
: repoOverride
|
||||||
if (selection === null && defaultSelection) setSelection(defaultSelection)
|
|
||||||
}, [defaultSelection, selection])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-w-0 flex-1 flex-col overflow-y-auto px-6 py-8">
|
<div className="flex min-w-0 flex-1 flex-col overflow-y-auto px-6 py-8">
|
||||||
|
|
@ -39,13 +40,13 @@ export function AgentsHome() {
|
||||||
createThread.mutate({
|
createThread.mutate({
|
||||||
prompt,
|
prompt,
|
||||||
repo,
|
repo,
|
||||||
model_id: selection?.modelId ?? null,
|
model_id: activeSelection?.modelId ?? null,
|
||||||
effort: selection?.effort ?? null,
|
effort: activeSelection?.effort ?? null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
disabled={createThread.isPending}
|
disabled={createThread.isPending}
|
||||||
models={models}
|
models={models}
|
||||||
selection={selection ?? defaultSelection}
|
selection={activeSelection}
|
||||||
onSelectionChange={setSelection}
|
onSelectionChange={setSelection}
|
||||||
repos={reposQuery.data?.repositories}
|
repos={reposQuery.data?.repositories}
|
||||||
selectedRepo={repo}
|
selectedRepo={repo}
|
||||||
|
|
|
||||||
142
ui/src/components/agents/RepoSelector.tsx
Normal file
142
ui/src/components/agents/RepoSelector.tsx
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
import { useEffect, useMemo, useRef, useState } from "react"
|
||||||
|
import { CaretDownIcon, FolderIcon } from "@phosphor-icons/react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
type RepoOption = { full_name: string }
|
||||||
|
|
||||||
|
interface RepoSelectorProps {
|
||||||
|
repos?: Array<RepoOption>
|
||||||
|
selectedRepo?: string | null
|
||||||
|
onRepoChange: (repo: string | null) => void
|
||||||
|
placeholder?: string
|
||||||
|
emptySelectionLabel?: string
|
||||||
|
searchPlaceholder?: string
|
||||||
|
noMatchesLabel?: string
|
||||||
|
className?: string
|
||||||
|
triggerClassName?: string
|
||||||
|
dropdownClassName?: string
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RepoSelector({
|
||||||
|
repos,
|
||||||
|
selectedRepo = null,
|
||||||
|
onRepoChange,
|
||||||
|
placeholder = "Select repository",
|
||||||
|
emptySelectionLabel = "No repository",
|
||||||
|
searchPlaceholder = "Search repositories…",
|
||||||
|
noMatchesLabel = "No matches",
|
||||||
|
className,
|
||||||
|
triggerClassName,
|
||||||
|
dropdownClassName,
|
||||||
|
disabled = false,
|
||||||
|
}: RepoSelectorProps) {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [query, setQuery] = useState("")
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const filteredRepos = useMemo(() => {
|
||||||
|
const all = repos ?? []
|
||||||
|
const q = query.trim().toLowerCase()
|
||||||
|
if (!q) return all
|
||||||
|
return all.filter((repo) => repo.full_name.toLowerCase().includes(q))
|
||||||
|
}, [repos, query])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleClickOutside(e: MouseEvent) {
|
||||||
|
const target = e.target as Node
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(target)) {
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener("mousedown", handleClickOutside)
|
||||||
|
return () => document.removeEventListener("mousedown", handleClickOutside)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={dropdownRef} className={cn("relative min-w-0 shrink", className)}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() => setOpen((value) => !value)}
|
||||||
|
className={cn(
|
||||||
|
"flex max-w-[260px] cursor-pointer items-center gap-1 text-muted-foreground transition-opacity hover:opacity-80 disabled:cursor-default disabled:opacity-60",
|
||||||
|
triggerClassName
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FolderIcon className="size-3.5 shrink-0" />
|
||||||
|
<span className="flex-1 truncate text-left">
|
||||||
|
{selectedRepo || placeholder}
|
||||||
|
</span>
|
||||||
|
<CaretDownIcon className="size-3 shrink-0 opacity-70" />
|
||||||
|
</button>
|
||||||
|
{open && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute top-full left-0 z-50 mt-1 flex max-h-72 w-72 flex-col overflow-hidden rounded border border-border bg-popover text-xs text-popover-foreground shadow-lg",
|
||||||
|
dropdownClassName
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
autoFocus
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
placeholder={searchPlaceholder}
|
||||||
|
className="w-full border-b border-border bg-transparent px-2 py-1.5 text-foreground outline-none placeholder:text-muted-foreground"
|
||||||
|
/>
|
||||||
|
<div className="overflow-y-auto">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
onRepoChange(null)
|
||||||
|
setOpen(false)
|
||||||
|
setQuery("")
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"flex w-full items-center px-2 py-1.5 text-left transition-colors hover:bg-muted",
|
||||||
|
selectedRepo ? "text-muted-foreground" : "text-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{emptySelectionLabel}
|
||||||
|
{!selectedRepo && (
|
||||||
|
<span className="ml-auto pl-3 text-muted-foreground">✓</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{filteredRepos.length === 0 ? (
|
||||||
|
<div className="px-2 py-1.5 text-muted-foreground">
|
||||||
|
{noMatchesLabel}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredRepos.map((repo) => {
|
||||||
|
const selected = repo.full_name === selectedRepo
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={repo.full_name}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
onRepoChange(repo.full_name)
|
||||||
|
setOpen(false)
|
||||||
|
setQuery("")
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"flex w-full items-center px-2 py-1.5 text-left transition-colors hover:bg-muted",
|
||||||
|
selected ? "text-foreground" : "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="truncate">{repo.full_name}</span>
|
||||||
|
{selected && (
|
||||||
|
<span className="ml-auto pl-3 text-muted-foreground">
|
||||||
|
✓
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,32 +1,38 @@
|
||||||
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
||||||
import { CaretDownIcon, FolderIcon } from "@phosphor-icons/react";
|
|
||||||
|
|
||||||
import type { ModelOption } from "@/lib/api";
|
|
||||||
import {
|
import {
|
||||||
formatModelSelection,
|
memo,
|
||||||
type ModelSelection,
|
useCallback,
|
||||||
} from "@/lib/agents/useModelOptions";
|
useEffect,
|
||||||
import { cn } from "@/lib/utils";
|
useLayoutEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react"
|
||||||
|
|
||||||
const PROMPT_TEXTAREA_MAX_HEIGHT = 200;
|
import type { ModelOption } from "@/lib/api"
|
||||||
|
import type { ModelSelection } from "@/lib/agents/useModelOptions"
|
||||||
|
import { RepoSelector } from "@/components/agents/RepoSelector"
|
||||||
|
import { formatModelSelection } from "@/lib/agents/useModelOptions"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const PROMPT_TEXTAREA_MAX_HEIGHT = 200
|
||||||
|
|
||||||
export interface CloudPromptBarProps {
|
export interface CloudPromptBarProps {
|
||||||
placeholder?: string;
|
placeholder?: string
|
||||||
compact?: boolean;
|
compact?: boolean
|
||||||
disabled?: boolean;
|
disabled?: boolean
|
||||||
busy?: boolean;
|
busy?: boolean
|
||||||
onSubmit?: (value: string) => void;
|
onSubmit?: (value: string) => void
|
||||||
models?: ModelOption[];
|
models?: Array<ModelOption>
|
||||||
selection?: ModelSelection | null;
|
selection?: ModelSelection | null
|
||||||
onSelectionChange?: (next: ModelSelection) => void;
|
onSelectionChange?: (next: ModelSelection) => void
|
||||||
/** Repos the user can target. When provided with onRepoChange, a repo picker is shown. */
|
/** Repos the user can target. When provided with onRepoChange, a repo picker is shown. */
|
||||||
repos?: Array<{ full_name: string }>;
|
repos?: Array<{ full_name: string }>
|
||||||
selectedRepo?: string | null;
|
selectedRepo?: string | null
|
||||||
onRepoChange?: (repo: string | null) => void;
|
onRepoChange?: (repo: string | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Web-adapted PromptBar from open-swe-app — local state, no Electron/Zustand deps. */
|
/** Web-adapted PromptBar from open-swe-app — local state, no Electron/Zustand deps. */
|
||||||
export const CloudPromptBar = memo(function CloudPromptBar({
|
export const CloudPromptBar = memo(function CloudPromptBarComponent({
|
||||||
placeholder = "Ask Open SWE to build, fix bugs, explore",
|
placeholder = "Ask Open SWE to build, fix bugs, explore",
|
||||||
compact = false,
|
compact = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
|
@ -39,156 +45,84 @@ export const CloudPromptBar = memo(function CloudPromptBar({
|
||||||
selectedRepo = null,
|
selectedRepo = null,
|
||||||
onRepoChange,
|
onRepoChange,
|
||||||
}: CloudPromptBarProps) {
|
}: CloudPromptBarProps) {
|
||||||
const [value, setValue] = useState("");
|
const [value, setValue] = useState("")
|
||||||
const [modelDropdownOpen, setModelDropdownOpen] = useState(false);
|
const [modelDropdownOpen, setModelDropdownOpen] = useState(false)
|
||||||
const [repoDropdownOpen, setRepoDropdownOpen] = useState(false);
|
const inputRef = useRef<HTMLTextAreaElement>(null)
|
||||||
const [repoQuery, setRepoQuery] = useState("");
|
const modelDropdownRef = useRef<HTMLDivElement>(null)
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
||||||
const modelDropdownRef = useRef<HTMLDivElement>(null);
|
|
||||||
const repoDropdownRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const repoPickerEnabled = !!onRepoChange;
|
const combos = useMemo<Array<ModelSelection>>(() => {
|
||||||
const filteredRepos = useMemo(() => {
|
const list: Array<ModelSelection> = []
|
||||||
const all = repos ?? [];
|
|
||||||
const q = repoQuery.trim().toLowerCase();
|
|
||||||
if (!q) return all;
|
|
||||||
return all.filter((r) => r.full_name.toLowerCase().includes(q));
|
|
||||||
}, [repos, repoQuery]);
|
|
||||||
|
|
||||||
const combos = useMemo<ModelSelection[]>(() => {
|
|
||||||
const list: ModelSelection[] = [];
|
|
||||||
for (const model of models) {
|
for (const model of models) {
|
||||||
for (const effort of model.efforts) {
|
for (const effort of model.efforts) {
|
||||||
list.push({ modelId: model.id, effort });
|
list.push({ modelId: model.id, effort })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return list;
|
return list
|
||||||
}, [models]);
|
}, [models])
|
||||||
|
|
||||||
const selectionLabel = formatModelSelection(models, selection);
|
const selectionLabel = formatModelSelection(models, selection)
|
||||||
|
|
||||||
const handleSubmit = useCallback(() => {
|
const handleSubmit = useCallback(() => {
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim()
|
||||||
if (!trimmed || disabled) return;
|
if (!trimmed || disabled) return
|
||||||
onSubmit?.(trimmed);
|
onSubmit?.(trimmed)
|
||||||
setValue("");
|
setValue("")
|
||||||
}, [disabled, onSubmit, value]);
|
}, [disabled, onSubmit, value])
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const el = inputRef.current;
|
const el = inputRef.current
|
||||||
if (!el) return;
|
if (!el) return
|
||||||
|
|
||||||
el.style.height = "auto";
|
el.style.height = "auto"
|
||||||
const clampedHeight = Math.min(el.scrollHeight, PROMPT_TEXTAREA_MAX_HEIGHT);
|
const clampedHeight = Math.min(el.scrollHeight, PROMPT_TEXTAREA_MAX_HEIGHT)
|
||||||
el.style.height = `${clampedHeight}px`;
|
el.style.height = `${clampedHeight}px`
|
||||||
el.style.overflowY = el.scrollHeight > PROMPT_TEXTAREA_MAX_HEIGHT ? "auto" : "hidden";
|
el.style.overflowY =
|
||||||
}, [value]);
|
el.scrollHeight > PROMPT_TEXTAREA_MAX_HEIGHT ? "auto" : "hidden"
|
||||||
|
}, [value])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleClickOutside(e: MouseEvent) {
|
function handleClickOutside(e: MouseEvent) {
|
||||||
const target = e.target as Node;
|
const target = e.target as Node
|
||||||
if (modelDropdownRef.current && !modelDropdownRef.current.contains(target)) {
|
if (
|
||||||
setModelDropdownOpen(false);
|
modelDropdownRef.current &&
|
||||||
}
|
!modelDropdownRef.current.contains(target)
|
||||||
if (repoDropdownRef.current && !repoDropdownRef.current.contains(target)) {
|
) {
|
||||||
setRepoDropdownOpen(false);
|
setModelDropdownOpen(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
document.addEventListener("mousedown", handleClickOutside)
|
||||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
return () => document.removeEventListener("mousedown", handleClickOutside)
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (e.key === "Enter" && !e.shiftKey && value.trim()) {
|
if (e.key === "Enter" && !e.shiftKey && value.trim()) {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
handleSubmit();
|
handleSubmit()
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const pickerDisabled = combos.length === 0 || !onSelectionChange;
|
const pickerDisabled = combos.length === 0 || !onSelectionChange
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("relative w-full font-sans text-[13px]", compact ? "max-w-none" : "max-w-2xl")}>
|
<div
|
||||||
{repoPickerEnabled && (
|
className={cn(
|
||||||
|
"relative w-full font-sans text-[13px]",
|
||||||
|
compact ? "max-w-none" : "max-w-2xl"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{onRepoChange && (
|
||||||
<div className="mb-2 flex items-center gap-2 px-1 text-xs">
|
<div className="mb-2 flex items-center gap-2 px-1 text-xs">
|
||||||
<div ref={repoDropdownRef} className="relative min-w-0 shrink">
|
<RepoSelector
|
||||||
<button
|
repos={repos}
|
||||||
type="button"
|
selectedRepo={selectedRepo}
|
||||||
onClick={() => setRepoDropdownOpen((open) => !open)}
|
onRepoChange={onRepoChange}
|
||||||
className="flex max-w-[260px] cursor-pointer items-center gap-1 text-[color:var(--ui-text-muted)] transition-opacity hover:opacity-80"
|
/>
|
||||||
>
|
|
||||||
<FolderIcon className="size-3.5 shrink-0" />
|
|
||||||
<span className="truncate">{selectedRepo || "Select repository"}</span>
|
|
||||||
<CaretDownIcon className="size-3 shrink-0 opacity-70" />
|
|
||||||
</button>
|
|
||||||
{repoDropdownOpen && (
|
|
||||||
<div className="absolute left-0 top-full z-50 mt-1 flex max-h-72 w-72 flex-col overflow-hidden rounded border border-[var(--ui-border)] bg-[var(--ui-surface)] shadow-lg">
|
|
||||||
<input
|
|
||||||
autoFocus
|
|
||||||
value={repoQuery}
|
|
||||||
onChange={(e) => setRepoQuery(e.target.value)}
|
|
||||||
placeholder="Search repositories…"
|
|
||||||
className="w-full border-b border-[var(--ui-border)] bg-transparent px-3 py-2 text-[color:var(--ui-text)] outline-none placeholder:text-[color:var(--ui-text-dim)]"
|
|
||||||
/>
|
|
||||||
<div className="overflow-y-auto">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
onRepoChange(null);
|
|
||||||
setRepoDropdownOpen(false);
|
|
||||||
setRepoQuery("");
|
|
||||||
}}
|
|
||||||
className={cn(
|
|
||||||
"flex w-full items-center px-3 py-1.5 text-left transition-colors hover:bg-[var(--ui-panel-2)]",
|
|
||||||
selectedRepo
|
|
||||||
? "text-[color:var(--ui-text-muted)]"
|
|
||||||
: "text-[color:var(--ui-text)]",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
No repository
|
|
||||||
{!selectedRepo && (
|
|
||||||
<span className="ml-auto pl-3 text-[color:var(--ui-text-dim)]">✓</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
{filteredRepos.length === 0 ? (
|
|
||||||
<div className="px-3 py-1.5 text-[color:var(--ui-text-dim)]">No matches</div>
|
|
||||||
) : (
|
|
||||||
filteredRepos.map((repo) => {
|
|
||||||
const selected = repo.full_name === selectedRepo;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={repo.full_name}
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
onRepoChange(repo.full_name);
|
|
||||||
setRepoDropdownOpen(false);
|
|
||||||
setRepoQuery("");
|
|
||||||
}}
|
|
||||||
className={cn(
|
|
||||||
"flex w-full items-center px-3 py-1.5 text-left transition-colors hover:bg-[var(--ui-panel-2)]",
|
|
||||||
selected
|
|
||||||
? "text-[color:var(--ui-text)]"
|
|
||||||
: "text-[color:var(--ui-text-muted)]",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className="truncate">{repo.full_name}</span>
|
|
||||||
{selected && (
|
|
||||||
<span className="ml-auto pl-3 text-[color:var(--ui-text-dim)]">✓</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex min-h-[106px] flex-col rounded-2xl border border-[var(--ui-border)] bg-[var(--ui-surface)] px-4 py-3.5 shadow-sm",
|
"relative flex min-h-[106px] flex-col rounded-2xl border border-[var(--ui-border)] bg-[var(--ui-surface)] px-4 py-3.5 shadow-sm",
|
||||||
compact && "min-h-[88px]",
|
compact && "min-h-[88px]"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
|
|
@ -201,7 +135,7 @@ export const CloudPromptBar = memo(function CloudPromptBar({
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full min-w-0 resize-none overflow-hidden bg-transparent leading-[1.45] text-[color:var(--ui-text)] outline-none placeholder:text-[color:var(--ui-text-dim)]",
|
"w-full min-w-0 resize-none overflow-hidden bg-transparent leading-[1.45] text-[color:var(--ui-text)] outline-none placeholder:text-[color:var(--ui-text-dim)]",
|
||||||
compact ? "min-h-[36px]" : "min-h-[52px]",
|
compact ? "min-h-[36px]" : "min-h-[52px]"
|
||||||
)}
|
)}
|
||||||
style={{ maxHeight: PROMPT_TEXTAREA_MAX_HEIGHT }}
|
style={{ maxHeight: PROMPT_TEXTAREA_MAX_HEIGHT }}
|
||||||
/>
|
/>
|
||||||
|
|
@ -222,28 +156,30 @@ export const CloudPromptBar = memo(function CloudPromptBar({
|
||||||
const selected =
|
const selected =
|
||||||
!!selection &&
|
!!selection &&
|
||||||
selection.modelId === combo.modelId &&
|
selection.modelId === combo.modelId &&
|
||||||
selection.effort === combo.effort;
|
selection.effort === combo.effort
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={`${combo.modelId}::${combo.effort}`}
|
key={`${combo.modelId}::${combo.effort}`}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onSelectionChange?.(combo);
|
onSelectionChange?.(combo)
|
||||||
setModelDropdownOpen(false);
|
setModelDropdownOpen(false)
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full items-center gap-2 whitespace-nowrap px-3 py-1.5 text-left transition-colors hover:bg-[var(--ui-panel-2)]",
|
"flex w-full items-center gap-2 px-3 py-1.5 text-left whitespace-nowrap transition-colors hover:bg-[var(--ui-panel-2)]",
|
||||||
selected
|
selected
|
||||||
? "text-[color:var(--ui-text)]"
|
? "text-[color:var(--ui-text)]"
|
||||||
: "text-[color:var(--ui-text-muted)]",
|
: "text-[color:var(--ui-text-muted)]"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{formatModelSelection(models, combo)}
|
{formatModelSelection(models, combo)}
|
||||||
{selected && (
|
{selected && (
|
||||||
<span className="ml-auto pl-3 text-[color:var(--ui-text-dim)]">✓</span>
|
<span className="ml-auto pl-3 text-[color:var(--ui-text-dim)]">
|
||||||
|
✓
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -251,5 +187,5 @@ export const CloudPromptBar = memo(function CloudPromptBar({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,59 @@
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import type { ModelOption } from "@/lib/api"
|
||||||
|
import { useOptions, useProfile } from "@/lib/profile"
|
||||||
import { api, type ModelOption } from "@/lib/api";
|
|
||||||
|
|
||||||
export interface ModelSelection {
|
export interface ModelSelection {
|
||||||
modelId: string;
|
modelId: string
|
||||||
effort: string;
|
effort: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModelOptionsResult {
|
export interface ModelOptionsResult {
|
||||||
models: ModelOption[];
|
models: Array<ModelOption>
|
||||||
defaultSelection: ModelSelection | null;
|
defaultSelection: ModelSelection | null
|
||||||
isLoading: boolean;
|
isLoading: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function toSupportedSelection(
|
||||||
|
models: Array<ModelOption>,
|
||||||
|
modelId?: string | null,
|
||||||
|
effort?: string | null
|
||||||
|
): ModelSelection | null {
|
||||||
|
if (!modelId || !effort) return null
|
||||||
|
const supported = models.some(
|
||||||
|
(model) => model.id === modelId && model.efforts.includes(effort)
|
||||||
|
)
|
||||||
|
return supported ? { modelId, effort } : null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useModelOptions(): ModelOptionsResult {
|
export function useModelOptions(): ModelOptionsResult {
|
||||||
const optionsQuery = useQuery({
|
const optionsQuery = useOptions()
|
||||||
queryKey: ["dashboard", "options"],
|
const profileQuery = useProfile()
|
||||||
queryFn: () => api.options(),
|
|
||||||
staleTime: 5 * 60_000,
|
|
||||||
});
|
|
||||||
const profileQuery = useQuery({
|
|
||||||
queryKey: ["dashboard", "profile"],
|
|
||||||
queryFn: () => api.profile(),
|
|
||||||
staleTime: 5 * 60_000,
|
|
||||||
});
|
|
||||||
|
|
||||||
const models = optionsQuery.data?.models ?? [];
|
const models = optionsQuery.data?.models ?? []
|
||||||
const profile = profileQuery.data;
|
const profile = profileQuery.data
|
||||||
|
const profileSelection = toSupportedSelection(
|
||||||
let defaultSelection: ModelSelection | null = null;
|
models,
|
||||||
const profileModelId = profile?.default_model;
|
profile?.default_model,
|
||||||
const profileEffort = profile?.reasoning_effort;
|
profile?.reasoning_effort
|
||||||
if (
|
)
|
||||||
profileModelId &&
|
const teamDefaultSelection = toSupportedSelection(
|
||||||
profileEffort &&
|
models,
|
||||||
models.some((m) => m.id === profileModelId && m.efforts.includes(profileEffort))
|
optionsQuery.data?.default_agent_model,
|
||||||
) {
|
optionsQuery.data?.default_agent_reasoning_effort
|
||||||
defaultSelection = { modelId: profileModelId, effort: profileEffort };
|
)
|
||||||
} else if (models.length > 0) {
|
const firstModel = models[0]
|
||||||
const first = models[0]!;
|
const firstSelection = firstModel
|
||||||
defaultSelection = { modelId: first.id, effort: first.default_effort };
|
? { modelId: firstModel.id, effort: firstModel.default_effort }
|
||||||
}
|
: null
|
||||||
|
const defaultSelection =
|
||||||
|
optionsQuery.data && !profileQuery.isLoading
|
||||||
|
? (profileSelection ?? teamDefaultSelection ?? firstSelection)
|
||||||
|
: null
|
||||||
|
|
||||||
return {
|
return {
|
||||||
models,
|
models,
|
||||||
defaultSelection,
|
defaultSelection,
|
||||||
isLoading: optionsQuery.isLoading || profileQuery.isLoading,
|
isLoading: optionsQuery.isLoading || profileQuery.isLoading,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const EFFORT_LABELS: Record<string, string> = {
|
const EFFORT_LABELS: Record<string, string> = {
|
||||||
|
|
@ -55,15 +62,15 @@ const EFFORT_LABELS: Record<string, string> = {
|
||||||
high: "High",
|
high: "High",
|
||||||
xhigh: "XHigh",
|
xhigh: "XHigh",
|
||||||
max: "Max",
|
max: "Max",
|
||||||
};
|
}
|
||||||
|
|
||||||
export function formatModelSelection(
|
export function formatModelSelection(
|
||||||
models: ModelOption[],
|
models: Array<ModelOption>,
|
||||||
selection: ModelSelection | null,
|
selection: ModelSelection | null
|
||||||
): string {
|
): string {
|
||||||
if (!selection) return "Default";
|
if (!selection) return "Default"
|
||||||
const model = models.find((m) => m.id === selection.modelId);
|
const model = models.find((m) => m.id === selection.modelId)
|
||||||
const modelLabel = model?.label ?? selection.modelId;
|
const modelLabel = model?.label ?? selection.modelId
|
||||||
const effortLabel = EFFORT_LABELS[selection.effort] ?? selection.effort;
|
const effortLabel = EFFORT_LABELS[selection.effort] ?? selection.effort
|
||||||
return `${modelLabel} ${effortLabel}`;
|
return `${modelLabel} ${effortLabel}`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,14 @@ export interface ModelOption {
|
||||||
default_effort: string;
|
default_effort: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OptionsPayload {
|
||||||
|
models: Array<ModelOption>;
|
||||||
|
default_agent_model: string;
|
||||||
|
default_agent_reasoning_effort: string;
|
||||||
|
default_agent_subagent_model: string;
|
||||||
|
default_agent_subagent_reasoning_effort: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Profile {
|
export interface Profile {
|
||||||
login?: string;
|
login?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
|
|
@ -167,7 +175,7 @@ export interface ReviewStyle {
|
||||||
|
|
||||||
export const api = {
|
export const api = {
|
||||||
me: () => request<SessionUser>("/me"),
|
me: () => request<SessionUser>("/me"),
|
||||||
options: () => request<{ models: Array<ModelOption> }>("/options"),
|
options: () => request<OptionsPayload>("/options"),
|
||||||
profile: () => request<Profile>("/profile"),
|
profile: () => request<Profile>("/profile"),
|
||||||
saveProfile: (body: ProfileUpdate) =>
|
saveProfile: (body: ProfileUpdate) =>
|
||||||
request<Profile>("/profile", { method: "PUT", body: JSON.stringify(body) }),
|
request<Profile>("/profile", { method: "PUT", body: JSON.stringify(body) }),
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,7 @@ import { useEffect, useRef, useState } from "react"
|
||||||
import type { ModelOption } from "@/lib/api"
|
import type { ModelOption } from "@/lib/api"
|
||||||
import { AppShell, SettingsRow, SettingsSection } from "@/components/AppShell"
|
import { AppShell, SettingsRow, SettingsSection } from "@/components/AppShell"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import { RepoSelector } from "@/components/agents/RepoSelector"
|
||||||
Combobox,
|
|
||||||
ComboboxContent,
|
|
||||||
ComboboxEmpty,
|
|
||||||
ComboboxInput,
|
|
||||||
ComboboxItem,
|
|
||||||
ComboboxList,
|
|
||||||
} from "@/components/ui/combobox"
|
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
|
|
@ -53,6 +46,16 @@ function CloudAgentsPage() {
|
||||||
const initialized = useRef(false)
|
const initialized = useRef(false)
|
||||||
|
|
||||||
const firstModel: ModelOption | undefined = options.data?.models[0]
|
const firstModel: ModelOption | undefined = options.data?.models[0]
|
||||||
|
const defaultAgentModel =
|
||||||
|
options.data?.default_agent_model ?? firstModel?.id ?? ""
|
||||||
|
const defaultAgentEffort =
|
||||||
|
options.data?.default_agent_reasoning_effort ??
|
||||||
|
firstModel?.default_effort ??
|
||||||
|
""
|
||||||
|
const defaultSubagentModel =
|
||||||
|
options.data?.default_agent_subagent_model ?? defaultAgentModel
|
||||||
|
const defaultSubagentEffort =
|
||||||
|
options.data?.default_agent_subagent_reasoning_effort ?? defaultAgentEffort
|
||||||
const currentModel: ModelOption | undefined =
|
const currentModel: ModelOption | undefined =
|
||||||
options.data?.models.find((m) => m.id === modelId) ?? firstModel
|
options.data?.models.find((m) => m.id === modelId) ?? firstModel
|
||||||
const currentSubagentModel: ModelOption | undefined =
|
const currentSubagentModel: ModelOption | undefined =
|
||||||
|
|
@ -60,29 +63,31 @@ function CloudAgentsPage() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!profile.data || initialized.current) return
|
if (!profile.data || initialized.current) return
|
||||||
// For users with no saved profile, wait until the options API has loaded
|
const hasModel = !!profile.data.default_model || !!defaultAgentModel
|
||||||
// so the model/effort selects can initialise to the first available option.
|
|
||||||
const hasModel = !!profile.data.default_model || !!firstModel
|
|
||||||
if (!hasModel) return
|
if (!hasModel) return
|
||||||
initialized.current = true
|
initialized.current = true
|
||||||
setModelId(profile.data.default_model ?? firstModel?.id ?? "")
|
setModelId(profile.data.default_model ?? defaultAgentModel)
|
||||||
setEffort(profile.data.reasoning_effort ?? firstModel?.default_effort ?? "")
|
setEffort(profile.data.reasoning_effort ?? defaultAgentEffort)
|
||||||
setSubagentModelId(
|
setSubagentModelId(
|
||||||
profile.data.default_subagent_model ??
|
profile.data.default_subagent_model ??
|
||||||
profile.data.default_model ??
|
profile.data.default_model ??
|
||||||
firstModel?.id ??
|
defaultSubagentModel
|
||||||
""
|
|
||||||
)
|
)
|
||||||
setSubagentEffort(
|
setSubagentEffort(
|
||||||
profile.data.subagent_reasoning_effort ??
|
profile.data.subagent_reasoning_effort ??
|
||||||
profile.data.reasoning_effort ??
|
profile.data.reasoning_effort ??
|
||||||
firstModel?.default_effort ??
|
defaultSubagentEffort
|
||||||
""
|
|
||||||
)
|
)
|
||||||
setDefaultRepo(profile.data.default_repo ?? "")
|
setDefaultRepo(profile.data.default_repo ?? "")
|
||||||
setBaseBranch(profile.data.base_branch ?? "")
|
setBaseBranch(profile.data.base_branch ?? "")
|
||||||
setBranchPrefix(profile.data.branch_prefix ?? "")
|
setBranchPrefix(profile.data.branch_prefix ?? "")
|
||||||
}, [profile.data, firstModel?.id, firstModel?.default_effort, firstModel])
|
}, [
|
||||||
|
profile.data,
|
||||||
|
defaultAgentModel,
|
||||||
|
defaultAgentEffort,
|
||||||
|
defaultSubagentModel,
|
||||||
|
defaultSubagentEffort,
|
||||||
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentModel && !currentModel.efforts.includes(effort)) {
|
if (currentModel && !currentModel.efforts.includes(effort)) {
|
||||||
|
|
@ -108,8 +113,8 @@ function CloudAgentsPage() {
|
||||||
}
|
}
|
||||||
if (!session.data) return <Navigate to="/login" />
|
if (!session.data) return <Navigate to="/login" />
|
||||||
|
|
||||||
const fallbackModel = firstModel?.id ?? ""
|
const fallbackModel = defaultAgentModel
|
||||||
const fallbackEffort = firstModel?.default_effort ?? ""
|
const fallbackEffort = defaultAgentEffort
|
||||||
|
|
||||||
const persist = (patch: Parameters<typeof buildProfileUpdate>[1]) => {
|
const persist = (patch: Parameters<typeof buildProfileUpdate>[1]) => {
|
||||||
setError(null)
|
setError(null)
|
||||||
|
|
@ -222,39 +227,26 @@ function CloudAgentsPage() {
|
||||||
label="Default Repository"
|
label="Default Repository"
|
||||||
description="Used when no repository is specified"
|
description="Used when no repository is specified"
|
||||||
control={
|
control={
|
||||||
<div className="w-64">
|
repos.data?.repositories?.length ? (
|
||||||
{repos.data && repos.data.repositories.length > 0 ? (
|
<div className="w-56">
|
||||||
<Combobox
|
<RepoSelector
|
||||||
items={repos.data.repositories.map((r) => r.full_name)}
|
repos={repos.data.repositories}
|
||||||
value={defaultRepo}
|
selectedRepo={defaultRepo || null}
|
||||||
onValueChange={(v) =>
|
onRepoChange={(repo) => setDefaultRepo(repo ?? "")}
|
||||||
setDefaultRepo(typeof v === "string" ? v : "")
|
placeholder="Pick a repository…"
|
||||||
}
|
emptySelectionLabel="No default repository"
|
||||||
>
|
triggerClassName="h-7 w-full max-w-none rounded-md border border-input bg-input/20 px-2 py-1.5 text-xs/relaxed text-foreground transition-colors hover:opacity-100 dark:bg-input/30"
|
||||||
<ComboboxInput
|
dropdownClassName="w-56"
|
||||||
placeholder="Pick a repository…"
|
|
||||||
showClear
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<ComboboxContent className="min-w-[var(--anchor-width)]">
|
|
||||||
<ComboboxList className="max-h-64">
|
|
||||||
<ComboboxEmpty>No matches</ComboboxEmpty>
|
|
||||||
{repos.data.repositories.map((r) => (
|
|
||||||
<ComboboxItem key={r.full_name} value={r.full_name}>
|
|
||||||
<span className="truncate">{r.full_name}</span>
|
|
||||||
</ComboboxItem>
|
|
||||||
))}
|
|
||||||
</ComboboxList>
|
|
||||||
</ComboboxContent>
|
|
||||||
</Combobox>
|
|
||||||
) : (
|
|
||||||
<Input
|
|
||||||
placeholder="owner/repo"
|
|
||||||
value={defaultRepo}
|
|
||||||
onChange={(e) => setDefaultRepo(e.target.value)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</div>
|
) : (
|
||||||
|
<Input
|
||||||
|
className="w-56"
|
||||||
|
placeholder="owner/repo"
|
||||||
|
value={defaultRepo}
|
||||||
|
onChange={(e) => setDefaultRepo(e.target.value)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<SettingsRow
|
<SettingsRow
|
||||||
|
|
|
||||||
|
|
@ -148,8 +148,11 @@ function MySettingsPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const firstModel = options.data?.models[0]
|
const firstModel = options.data?.models[0]
|
||||||
const fallbackModel = firstModel?.id ?? ""
|
const fallbackModel = options.data?.default_agent_model ?? firstModel?.id ?? ""
|
||||||
const fallbackEffort = firstModel?.default_effort ?? ""
|
const fallbackEffort =
|
||||||
|
options.data?.default_agent_reasoning_effort ??
|
||||||
|
firstModel?.default_effort ??
|
||||||
|
""
|
||||||
|
|
||||||
const draftChoice = toChoice(profile.data?.review_draft_prs)
|
const draftChoice = toChoice(profile.data?.review_draft_prs)
|
||||||
const teamDefaultOn = teamSettings.data?.review_draft_prs ?? false
|
const teamDefaultOn = teamSettings.data?.review_draft_prs ?? false
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue