Merge PR #62: Full OpenCode adapter integration
Merges paperclipai/paperclip#62 onto latest master (494448d). Adds complete OpenCode provider with strict model selection, dynamic model discovery, CLI/server/UI adapter registration. Resolved conflicts with master's cursor adapter additions, node v24 typing, and containerized opencode support (201d91b). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,7 @@ import {
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { FolderOpen, Heart, ChevronDown, X } from "lucide-react";
|
||||
import { cn } from "../lib/utils";
|
||||
import { extractModelName, extractProviderId } from "../lib/model-utils";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import {
|
||||
@@ -42,6 +43,7 @@ import { getUIAdapter } from "../adapters";
|
||||
import { ClaudeLocalAdvancedFields } from "../adapters/claude-local/config-fields";
|
||||
import { MarkdownEditor } from "./MarkdownEditor";
|
||||
import { ChoosePathButton } from "./PathInstructionsModal";
|
||||
import { OpenCodeLogoIcon } from "./OpenCodeLogoIcon";
|
||||
|
||||
/* ---- Create mode values ---- */
|
||||
|
||||
@@ -132,7 +134,7 @@ const codexThinkingEffortOptions = [
|
||||
{ id: "high", label: "High" },
|
||||
] as const;
|
||||
|
||||
const opencodeVariantOptions = [
|
||||
const openCodeThinkingEffortOptions = [
|
||||
{ id: "", label: "Auto" },
|
||||
{ id: "minimal", label: "Minimal" },
|
||||
{ id: "low", label: "Low" },
|
||||
@@ -279,9 +281,15 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
||||
const uiAdapter = useMemo(() => getUIAdapter(adapterType), [adapterType]);
|
||||
|
||||
// Fetch adapter models for the effective adapter type
|
||||
const { data: fetchedModels } = useQuery({
|
||||
queryKey: ["adapter-models", adapterType],
|
||||
queryFn: () => agentsApi.adapterModels(adapterType),
|
||||
const {
|
||||
data: fetchedModels,
|
||||
error: fetchedModelsError,
|
||||
} = useQuery({
|
||||
queryKey: selectedCompanyId
|
||||
? queryKeys.agents.adapterModels(selectedCompanyId, adapterType)
|
||||
: ["agents", "none", "adapter-models", adapterType],
|
||||
queryFn: () => agentsApi.adapterModels(selectedCompanyId!, adapterType),
|
||||
enabled: Boolean(selectedCompanyId),
|
||||
});
|
||||
const models = fetchedModels ?? externalModels ?? [];
|
||||
|
||||
@@ -339,17 +347,17 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
||||
? "modelReasoningEffort"
|
||||
: adapterType === "cursor"
|
||||
? "mode"
|
||||
: adapterType === "opencode_local"
|
||||
? "variant"
|
||||
: "effort";
|
||||
: adapterType === "opencode_local"
|
||||
? "variant"
|
||||
: "effort";
|
||||
const thinkingEffortOptions =
|
||||
adapterType === "codex_local"
|
||||
? codexThinkingEffortOptions
|
||||
: adapterType === "cursor"
|
||||
? cursorModeOptions
|
||||
: adapterType === "opencode_local"
|
||||
? opencodeVariantOptions
|
||||
: claudeThinkingEffortOptions;
|
||||
: adapterType === "opencode_local"
|
||||
? openCodeThinkingEffortOptions
|
||||
: claudeThinkingEffortOptions;
|
||||
const currentThinkingEffort = isCreate
|
||||
? val!.thinkingEffort
|
||||
: adapterType === "codex_local"
|
||||
@@ -360,8 +368,8 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
||||
)
|
||||
: adapterType === "cursor"
|
||||
? eff("adapterConfig", "mode", String(config.mode ?? ""))
|
||||
: adapterType === "opencode_local"
|
||||
? eff("adapterConfig", "variant", String(config.variant ?? ""))
|
||||
: adapterType === "opencode_local"
|
||||
? eff("adapterConfig", "variant", String(config.variant ?? ""))
|
||||
: eff("adapterConfig", "effort", String(config.effort ?? ""));
|
||||
const codexSearchEnabled = adapterType === "codex_local"
|
||||
? (isCreate ? Boolean(val!.search) : eff("adapterConfig", "search", Boolean(config.search)))
|
||||
@@ -605,9 +613,9 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
||||
? "codex"
|
||||
: adapterType === "cursor"
|
||||
? "agent"
|
||||
: adapterType === "opencode_local"
|
||||
? "opencode"
|
||||
: "claude"
|
||||
: adapterType === "opencode_local"
|
||||
? "opencode"
|
||||
: "claude"
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
@@ -622,7 +630,17 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
||||
}
|
||||
open={modelOpen}
|
||||
onOpenChange={setModelOpen}
|
||||
allowDefault={adapterType !== "opencode_local"}
|
||||
required={adapterType === "opencode_local"}
|
||||
groupByProvider={adapterType === "opencode_local"}
|
||||
/>
|
||||
{fetchedModelsError && (
|
||||
<p className="text-xs text-destructive">
|
||||
{fetchedModelsError instanceof Error
|
||||
? fetchedModelsError.message
|
||||
: "Failed to load adapter models."}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<ThinkingEffortDropdown
|
||||
value={currentThinkingEffort}
|
||||
@@ -898,7 +916,10 @@ function AdapterTypeDropdown({
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button className="inline-flex items-center gap-1.5 rounded-md border border-border px-2.5 py-1.5 text-sm hover:bg-accent/50 transition-colors w-full justify-between">
|
||||
<span>{adapterLabels[value] ?? value}</span>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
{value === "opencode_local" ? <OpenCodeLogoIcon className="h-3.5 w-3.5" /> : null}
|
||||
<span>{adapterLabels[value] ?? value}</span>
|
||||
</span>
|
||||
<ChevronDown className="h-3 w-3 text-muted-foreground" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
@@ -918,7 +939,10 @@ function AdapterTypeDropdown({
|
||||
if (!item.comingSoon) onChange(item.value);
|
||||
}}
|
||||
>
|
||||
<span>{item.label}</span>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
{item.value === "opencode_local" ? <OpenCodeLogoIcon className="h-3.5 w-3.5" /> : null}
|
||||
<span>{item.label}</span>
|
||||
</span>
|
||||
{item.comingSoon && (
|
||||
<span className="text-[10px] text-muted-foreground">Coming soon</span>
|
||||
)}
|
||||
@@ -1184,20 +1208,56 @@ function ModelDropdown({
|
||||
onChange,
|
||||
open,
|
||||
onOpenChange,
|
||||
allowDefault,
|
||||
required,
|
||||
groupByProvider,
|
||||
}: {
|
||||
models: AdapterModel[];
|
||||
value: string;
|
||||
onChange: (id: string) => void;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
allowDefault: boolean;
|
||||
required: boolean;
|
||||
groupByProvider: boolean;
|
||||
}) {
|
||||
const [modelSearch, setModelSearch] = useState("");
|
||||
const selected = models.find((m) => m.id === value);
|
||||
const filteredModels = models.filter((m) => {
|
||||
if (!modelSearch.trim()) return true;
|
||||
const q = modelSearch.toLowerCase();
|
||||
return m.id.toLowerCase().includes(q) || m.label.toLowerCase().includes(q);
|
||||
});
|
||||
const filteredModels = useMemo(() => {
|
||||
return models.filter((m) => {
|
||||
if (!modelSearch.trim()) return true;
|
||||
const q = modelSearch.toLowerCase();
|
||||
const provider = extractProviderId(m.id) ?? "";
|
||||
return (
|
||||
m.id.toLowerCase().includes(q) ||
|
||||
m.label.toLowerCase().includes(q) ||
|
||||
provider.toLowerCase().includes(q)
|
||||
);
|
||||
});
|
||||
}, [models, modelSearch]);
|
||||
const groupedModels = useMemo(() => {
|
||||
if (!groupByProvider) {
|
||||
return [
|
||||
{
|
||||
provider: "models",
|
||||
entries: [...filteredModels].sort((a, b) => a.id.localeCompare(b.id)),
|
||||
},
|
||||
];
|
||||
}
|
||||
const map = new Map<string, AdapterModel[]>();
|
||||
for (const model of filteredModels) {
|
||||
const provider = extractProviderId(model.id) ?? "other";
|
||||
const group = map.get(provider) ?? [];
|
||||
group.push(model);
|
||||
map.set(provider, group);
|
||||
}
|
||||
return Array.from(map.entries())
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([provider, entries]) => ({
|
||||
provider,
|
||||
entries: [...entries].sort((a, b) => a.id.localeCompare(b.id)),
|
||||
}));
|
||||
}, [filteredModels, groupByProvider]);
|
||||
|
||||
return (
|
||||
<Field label="Model" hint={help.model}>
|
||||
@@ -1211,7 +1271,9 @@ function ModelDropdown({
|
||||
<PopoverTrigger asChild>
|
||||
<button className="inline-flex items-center gap-1.5 rounded-md border border-border px-2.5 py-1.5 text-sm hover:bg-accent/50 transition-colors w-full justify-between">
|
||||
<span className={cn(!value && "text-muted-foreground")}>
|
||||
{selected ? selected.label : value || "Default"}
|
||||
{selected
|
||||
? selected.label
|
||||
: value || (allowDefault ? "Default" : required ? "Select model (required)" : "Select model")}
|
||||
</span>
|
||||
<ChevronDown className="h-3 w-3 text-muted-foreground" />
|
||||
</button>
|
||||
@@ -1225,33 +1287,45 @@ function ModelDropdown({
|
||||
autoFocus
|
||||
/>
|
||||
<div className="max-h-[240px] overflow-y-auto">
|
||||
<button
|
||||
className={cn(
|
||||
"flex items-center gap-2 w-full px-2 py-1.5 text-sm rounded hover:bg-accent/50",
|
||||
!value && "bg-accent",
|
||||
)}
|
||||
onClick={() => {
|
||||
onChange("");
|
||||
onOpenChange(false);
|
||||
}}
|
||||
>
|
||||
Default
|
||||
</button>
|
||||
{filteredModels.map((m) => (
|
||||
{allowDefault && (
|
||||
<button
|
||||
key={m.id}
|
||||
className={cn(
|
||||
"flex items-center justify-between w-full px-2 py-1.5 text-sm rounded hover:bg-accent/50",
|
||||
m.id === value && "bg-accent",
|
||||
"flex items-center gap-2 w-full px-2 py-1.5 text-sm rounded hover:bg-accent/50",
|
||||
!value && "bg-accent",
|
||||
)}
|
||||
onClick={() => {
|
||||
onChange(m.id);
|
||||
onChange("");
|
||||
onOpenChange(false);
|
||||
}}
|
||||
>
|
||||
<span>{m.label}</span>
|
||||
<span className="text-xs text-muted-foreground font-mono">{m.id}</span>
|
||||
Default
|
||||
</button>
|
||||
)}
|
||||
{groupedModels.map((group) => (
|
||||
<div key={group.provider} className="mb-1 last:mb-0">
|
||||
{groupByProvider && (
|
||||
<div className="px-2 py-1 text-[10px] uppercase tracking-wide text-muted-foreground">
|
||||
{group.provider} ({group.entries.length})
|
||||
</div>
|
||||
)}
|
||||
{group.entries.map((m) => (
|
||||
<button
|
||||
key={m.id}
|
||||
className={cn(
|
||||
"flex items-center w-full px-2 py-1.5 text-sm rounded hover:bg-accent/50",
|
||||
m.id === value && "bg-accent",
|
||||
)}
|
||||
onClick={() => {
|
||||
onChange(m.id);
|
||||
onOpenChange(false);
|
||||
}}
|
||||
>
|
||||
<span className="block w-full text-left truncate" title={m.id}>
|
||||
{groupByProvider ? extractModelName(m.id) : m.label}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
{filteredModels.length === 0 && (
|
||||
<p className="px-2 py-1.5 text-xs text-muted-foreground">No models found.</p>
|
||||
|
||||
Reference in New Issue
Block a user