feat: add opencode local adapter support
This commit is contained in:
47
ui/src/adapters/opencode-local/config-fields.tsx
Normal file
47
ui/src/adapters/opencode-local/config-fields.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { AdapterConfigFieldsProps } from "../types";
|
||||
import {
|
||||
Field,
|
||||
DraftInput,
|
||||
} from "../../components/agent-config-primitives";
|
||||
import { ChoosePathButton } from "../../components/PathInstructionsModal";
|
||||
|
||||
const inputClass =
|
||||
"w-full rounded-md border border-border px-2.5 py-1.5 bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40";
|
||||
const instructionsFileHint =
|
||||
"Absolute path to a markdown file (e.g. AGENTS.md) that defines this agent's behavior. Injected into the prompt at runtime.";
|
||||
|
||||
export function OpenCodeLocalConfigFields({
|
||||
isCreate,
|
||||
values,
|
||||
set,
|
||||
config,
|
||||
eff,
|
||||
mark,
|
||||
}: AdapterConfigFieldsProps) {
|
||||
return (
|
||||
<Field label="Agent instructions file" hint={instructionsFileHint}>
|
||||
<div className="flex items-center gap-2">
|
||||
<DraftInput
|
||||
value={
|
||||
isCreate
|
||||
? values!.instructionsFilePath ?? ""
|
||||
: eff(
|
||||
"adapterConfig",
|
||||
"instructionsFilePath",
|
||||
String(config.instructionsFilePath ?? ""),
|
||||
)
|
||||
}
|
||||
onCommit={(v) =>
|
||||
isCreate
|
||||
? set!({ instructionsFilePath: v })
|
||||
: mark("adapterConfig", "instructionsFilePath", v || undefined)
|
||||
}
|
||||
immediate
|
||||
className={inputClass}
|
||||
placeholder="/absolute/path/to/AGENTS.md"
|
||||
/>
|
||||
<ChoosePathButton />
|
||||
</div>
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
12
ui/src/adapters/opencode-local/index.ts
Normal file
12
ui/src/adapters/opencode-local/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { UIAdapterModule } from "../types";
|
||||
import { parseOpenCodeStdoutLine } from "@paperclipai/adapter-opencode-local/ui";
|
||||
import { OpenCodeLocalConfigFields } from "./config-fields";
|
||||
import { buildOpenCodeLocalConfig } from "@paperclipai/adapter-opencode-local/ui";
|
||||
|
||||
export const openCodeLocalUIAdapter: UIAdapterModule = {
|
||||
type: "opencode_local",
|
||||
label: "OpenCode (local)",
|
||||
parseStdoutLine: parseOpenCodeStdoutLine,
|
||||
ConfigFields: OpenCodeLocalConfigFields,
|
||||
buildAdapterConfig: buildOpenCodeLocalConfig,
|
||||
};
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { UIAdapterModule } from "./types";
|
||||
import { claudeLocalUIAdapter } from "./claude-local";
|
||||
import { codexLocalUIAdapter } from "./codex-local";
|
||||
import { openCodeLocalUIAdapter } from "./opencode-local";
|
||||
import { openClawUIAdapter } from "./openclaw";
|
||||
import { processUIAdapter } from "./process";
|
||||
import { httpUIAdapter } from "./http";
|
||||
|
||||
const adaptersByType = new Map<string, UIAdapterModule>(
|
||||
[claudeLocalUIAdapter, codexLocalUIAdapter, openClawUIAdapter, processUIAdapter, httpUIAdapter].map((a) => [a.type, a]),
|
||||
[claudeLocalUIAdapter, codexLocalUIAdapter, openCodeLocalUIAdapter, openClawUIAdapter, processUIAdapter, httpUIAdapter].map((a) => [a.type, a]),
|
||||
);
|
||||
|
||||
export function getUIAdapter(type: string): UIAdapterModule {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX,
|
||||
DEFAULT_CODEX_LOCAL_MODEL,
|
||||
} from "@paperclipai/adapter-codex-local";
|
||||
import { DEFAULT_OPENCODE_LOCAL_MODEL } from "@paperclipai/adapter-opencode-local";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
@@ -130,6 +131,15 @@ const codexThinkingEffortOptions = [
|
||||
{ id: "high", label: "High" },
|
||||
] as const;
|
||||
|
||||
const opencodeVariantOptions = [
|
||||
{ id: "", label: "Auto" },
|
||||
{ id: "minimal", label: "Minimal" },
|
||||
{ id: "low", label: "Low" },
|
||||
{ id: "medium", label: "Medium" },
|
||||
{ id: "high", label: "High" },
|
||||
{ id: "max", label: "Max" },
|
||||
] as const;
|
||||
|
||||
const claudeThinkingEffortOptions = [
|
||||
{ id: "", label: "Auto" },
|
||||
{ id: "low", label: "Low" },
|
||||
@@ -254,7 +264,10 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
||||
const adapterType = isCreate
|
||||
? props.values.adapterType
|
||||
: overlay.adapterType ?? props.agent.adapterType;
|
||||
const isLocal = adapterType === "claude_local" || adapterType === "codex_local";
|
||||
const isLocal =
|
||||
adapterType === "claude_local" ||
|
||||
adapterType === "codex_local" ||
|
||||
adapterType === "opencode_local";
|
||||
const uiAdapter = useMemo(() => getUIAdapter(adapterType), [adapterType]);
|
||||
|
||||
// Fetch adapter models for the effective adapter type
|
||||
@@ -313,9 +326,18 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
||||
? val!.model
|
||||
: eff("adapterConfig", "model", String(config.model ?? ""));
|
||||
|
||||
const thinkingEffortKey = adapterType === "codex_local" ? "modelReasoningEffort" : "effort";
|
||||
const thinkingEffortKey =
|
||||
adapterType === "codex_local"
|
||||
? "modelReasoningEffort"
|
||||
: adapterType === "opencode_local"
|
||||
? "variant"
|
||||
: "effort";
|
||||
const thinkingEffortOptions =
|
||||
adapterType === "codex_local" ? codexThinkingEffortOptions : claudeThinkingEffortOptions;
|
||||
adapterType === "codex_local"
|
||||
? codexThinkingEffortOptions
|
||||
: adapterType === "opencode_local"
|
||||
? opencodeVariantOptions
|
||||
: claudeThinkingEffortOptions;
|
||||
const currentThinkingEffort = isCreate
|
||||
? val!.thinkingEffort
|
||||
: adapterType === "codex_local"
|
||||
@@ -324,6 +346,8 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
||||
"modelReasoningEffort",
|
||||
String(config.modelReasoningEffort ?? config.reasoningEffort ?? ""),
|
||||
)
|
||||
: 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)))
|
||||
@@ -442,6 +466,8 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
||||
nextValues.model = DEFAULT_CODEX_LOCAL_MODEL;
|
||||
nextValues.dangerouslyBypassSandbox =
|
||||
DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX;
|
||||
} else if (t === "opencode_local") {
|
||||
nextValues.model = DEFAULT_OPENCODE_LOCAL_MODEL;
|
||||
}
|
||||
set!(nextValues);
|
||||
} else {
|
||||
@@ -451,9 +477,15 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
||||
...prev,
|
||||
adapterType: t,
|
||||
adapterConfig: {
|
||||
model: t === "codex_local" ? DEFAULT_CODEX_LOCAL_MODEL : "",
|
||||
model:
|
||||
t === "codex_local"
|
||||
? DEFAULT_CODEX_LOCAL_MODEL
|
||||
: t === "opencode_local"
|
||||
? DEFAULT_OPENCODE_LOCAL_MODEL
|
||||
: "",
|
||||
effort: "",
|
||||
modelReasoningEffort: "",
|
||||
variant: "",
|
||||
...(t === "codex_local"
|
||||
? {
|
||||
dangerouslyBypassApprovalsAndSandbox:
|
||||
@@ -549,7 +581,13 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
||||
}
|
||||
immediate
|
||||
className={inputClass}
|
||||
placeholder={adapterType === "codex_local" ? "codex" : "claude"}
|
||||
placeholder={
|
||||
adapterType === "codex_local"
|
||||
? "codex"
|
||||
: adapterType === "opencode_local"
|
||||
? "opencode"
|
||||
: "claude"
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
@@ -817,7 +855,7 @@ function AdapterEnvironmentResult({ result }: { result: AdapterEnvironmentTestRe
|
||||
|
||||
/* ---- Internal sub-components ---- */
|
||||
|
||||
const ENABLED_ADAPTER_TYPES = new Set(["claude_local", "codex_local"]);
|
||||
const ENABLED_ADAPTER_TYPES = new Set(["claude_local", "codex_local", "opencode_local"]);
|
||||
|
||||
/** Display list includes all real adapter types plus UI-only coming-soon entries. */
|
||||
const ADAPTER_DISPLAY_LIST: { value: string; label: string; comingSoon: boolean }[] = [
|
||||
|
||||
@@ -17,6 +17,7 @@ interface AgentPropertiesProps {
|
||||
const adapterLabels: Record<string, string> = {
|
||||
claude_local: "Claude (local)",
|
||||
codex_local: "Codex (local)",
|
||||
opencode_local: "OpenCode (local)",
|
||||
openclaw: "OpenClaw",
|
||||
cursor: "Cursor",
|
||||
process: "Process",
|
||||
|
||||
@@ -67,7 +67,7 @@ interface IssueDraft {
|
||||
assigneeUseProjectWorkspace: boolean;
|
||||
}
|
||||
|
||||
const ISSUE_OVERRIDE_ADAPTER_TYPES = new Set(["claude_local", "codex_local"]);
|
||||
const ISSUE_OVERRIDE_ADAPTER_TYPES = new Set(["claude_local", "codex_local", "opencode_local"]);
|
||||
|
||||
const ISSUE_THINKING_EFFORT_OPTIONS = {
|
||||
claude_local: [
|
||||
@@ -83,6 +83,14 @@ const ISSUE_THINKING_EFFORT_OPTIONS = {
|
||||
{ value: "medium", label: "Medium" },
|
||||
{ value: "high", label: "High" },
|
||||
],
|
||||
opencode_local: [
|
||||
{ value: "", label: "Default" },
|
||||
{ value: "minimal", label: "Minimal" },
|
||||
{ value: "low", label: "Low" },
|
||||
{ value: "medium", label: "Medium" },
|
||||
{ value: "high", label: "High" },
|
||||
{ value: "max", label: "Max" },
|
||||
],
|
||||
} as const;
|
||||
|
||||
function buildAssigneeAdapterOverrides(input: {
|
||||
@@ -102,6 +110,8 @@ function buildAssigneeAdapterOverrides(input: {
|
||||
if (input.thinkingEffortOverride) {
|
||||
if (adapterType === "codex_local") {
|
||||
adapterConfig.modelReasoningEffort = input.thinkingEffortOverride;
|
||||
} else if (adapterType === "opencode_local") {
|
||||
adapterConfig.variant = input.thinkingEffortOverride;
|
||||
} else if (adapterType === "claude_local") {
|
||||
adapterConfig.effort = input.thinkingEffortOverride;
|
||||
}
|
||||
@@ -351,6 +361,8 @@ export function NewIssueDialog() {
|
||||
const validThinkingValues =
|
||||
assigneeAdapterType === "codex_local"
|
||||
? ISSUE_THINKING_EFFORT_OPTIONS.codex_local
|
||||
: assigneeAdapterType === "opencode_local"
|
||||
? ISSUE_THINKING_EFFORT_OPTIONS.opencode_local
|
||||
: ISSUE_THINKING_EFFORT_OPTIONS.claude_local;
|
||||
if (!validThinkingValues.some((option) => option.value === assigneeThinkingEffort)) {
|
||||
setAssigneeThinkingEffort("");
|
||||
@@ -451,10 +463,14 @@ export function NewIssueDialog() {
|
||||
? "Claude options"
|
||||
: assigneeAdapterType === "codex_local"
|
||||
? "Codex options"
|
||||
: assigneeAdapterType === "opencode_local"
|
||||
? "OpenCode options"
|
||||
: "Agent options";
|
||||
const thinkingEffortOptions =
|
||||
assigneeAdapterType === "codex_local"
|
||||
? ISSUE_THINKING_EFFORT_OPTIONS.codex_local
|
||||
: assigneeAdapterType === "opencode_local"
|
||||
? ISSUE_THINKING_EFFORT_OPTIONS.opencode_local
|
||||
: ISSUE_THINKING_EFFORT_OPTIONS.claude_local;
|
||||
const assigneeOptions = useMemo<InlineEntityOption[]>(
|
||||
() =>
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX,
|
||||
DEFAULT_CODEX_LOCAL_MODEL
|
||||
} from "@paperclipai/adapter-codex-local";
|
||||
import { DEFAULT_OPENCODE_LOCAL_MODEL } from "@paperclipai/adapter-opencode-local";
|
||||
import { AsciiArtAnimation } from "./AsciiArtAnimation";
|
||||
import { ChoosePathButton } from "./PathInstructionsModal";
|
||||
import { HintIcon } from "./agent-config-primitives";
|
||||
@@ -49,6 +50,7 @@ type Step = 1 | 2 | 3 | 4;
|
||||
type AdapterType =
|
||||
| "claude_local"
|
||||
| "codex_local"
|
||||
| "opencode_local"
|
||||
| "process"
|
||||
| "http"
|
||||
| "openclaw";
|
||||
@@ -151,9 +153,14 @@ export function OnboardingWizard() {
|
||||
enabled: onboardingOpen && step === 2
|
||||
});
|
||||
const isLocalAdapter =
|
||||
adapterType === "claude_local" || adapterType === "codex_local";
|
||||
adapterType === "claude_local" || adapterType === "codex_local" || adapterType === "opencode_local";
|
||||
const effectiveAdapterCommand =
|
||||
command.trim() || (adapterType === "codex_local" ? "codex" : "claude");
|
||||
command.trim() ||
|
||||
(adapterType === "codex_local"
|
||||
? "codex"
|
||||
: adapterType === "opencode_local"
|
||||
? "opencode"
|
||||
: "claude");
|
||||
|
||||
useEffect(() => {
|
||||
if (step !== 2) return;
|
||||
@@ -212,6 +219,8 @@ export function OnboardingWizard() {
|
||||
model:
|
||||
adapterType === "codex_local"
|
||||
? model || DEFAULT_CODEX_LOCAL_MODEL
|
||||
: adapterType === "opencode_local"
|
||||
? model || DEFAULT_OPENCODE_LOCAL_MODEL
|
||||
: model,
|
||||
command,
|
||||
args,
|
||||
@@ -570,6 +579,12 @@ export function OnboardingWizard() {
|
||||
icon: Code,
|
||||
desc: "Local Codex agent"
|
||||
},
|
||||
{
|
||||
value: "opencode_local" as const,
|
||||
label: "OpenCode",
|
||||
icon: Code,
|
||||
desc: "Local OpenCode agent"
|
||||
},
|
||||
{
|
||||
value: "openclaw" as const,
|
||||
label: "OpenClaw",
|
||||
@@ -616,6 +631,8 @@ export function OnboardingWizard() {
|
||||
setAdapterType(nextType);
|
||||
if (nextType === "codex_local" && !model) {
|
||||
setModel(DEFAULT_CODEX_LOCAL_MODEL);
|
||||
} else if (nextType === "opencode_local" && !model) {
|
||||
setModel(DEFAULT_OPENCODE_LOCAL_MODEL);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -631,7 +648,8 @@ export function OnboardingWizard() {
|
||||
|
||||
{/* Conditional adapter fields */}
|
||||
{(adapterType === "claude_local" ||
|
||||
adapterType === "codex_local") && (
|
||||
adapterType === "codex_local" ||
|
||||
adapterType === "opencode_local") && (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 mb-1">
|
||||
@@ -766,18 +784,22 @@ export function OnboardingWizard() {
|
||||
<p className="text-muted-foreground font-mono break-all">
|
||||
{adapterType === "codex_local"
|
||||
? `${effectiveAdapterCommand} exec --json -`
|
||||
: adapterType === "opencode_local"
|
||||
? `${effectiveAdapterCommand} run --format json \"Respond with hello.\"`
|
||||
: `${effectiveAdapterCommand} --print - --output-format stream-json --verbose`}
|
||||
</p>
|
||||
<p className="text-muted-foreground">
|
||||
Prompt:{" "}
|
||||
<span className="font-mono">Respond with hello.</span>
|
||||
</p>
|
||||
{adapterType === "codex_local" ? (
|
||||
{adapterType === "codex_local" || adapterType === "opencode_local" ? (
|
||||
<p className="text-muted-foreground">
|
||||
If auth fails, set{" "}
|
||||
<span className="font-mono">OPENAI_API_KEY</span> in
|
||||
env or run{" "}
|
||||
<span className="font-mono">codex login</span>.
|
||||
<span className="font-mono">
|
||||
{adapterType === "codex_local" ? "codex login" : "opencode auth login"}
|
||||
</span>.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-muted-foreground">
|
||||
|
||||
@@ -52,6 +52,7 @@ export const help: Record<string, string> = {
|
||||
export const adapterLabels: Record<string, string> = {
|
||||
claude_local: "Claude (local)",
|
||||
codex_local: "Codex (local)",
|
||||
opencode_local: "OpenCode (local)",
|
||||
openclaw: "OpenClaw",
|
||||
cursor: "Cursor",
|
||||
process: "Process",
|
||||
|
||||
@@ -23,6 +23,7 @@ import type { Agent } from "@paperclipai/shared";
|
||||
const adapterLabels: Record<string, string> = {
|
||||
claude_local: "Claude",
|
||||
codex_local: "Codex",
|
||||
opencode_local: "OpenCode",
|
||||
openclaw: "OpenClaw",
|
||||
process: "Process",
|
||||
http: "HTTP",
|
||||
|
||||
@@ -18,13 +18,14 @@ const joinAdapterOptions: AgentAdapterType[] = [
|
||||
const adapterLabels: Record<string, string> = {
|
||||
claude_local: "Claude (local)",
|
||||
codex_local: "Codex (local)",
|
||||
opencode_local: "OpenCode (local)",
|
||||
openclaw: "OpenClaw",
|
||||
cursor: "Cursor",
|
||||
process: "Process",
|
||||
http: "HTTP",
|
||||
};
|
||||
|
||||
const ENABLED_INVITE_ADAPTERS = new Set(["claude_local", "codex_local"]);
|
||||
const ENABLED_INVITE_ADAPTERS = new Set(["claude_local", "codex_local", "opencode_local"]);
|
||||
|
||||
function dateTime(value: string) {
|
||||
return new Date(value).toLocaleString();
|
||||
|
||||
@@ -118,6 +118,7 @@ function collectEdges(nodes: LayoutNode[]): Array<{ parent: LayoutNode; child: L
|
||||
const adapterLabels: Record<string, string> = {
|
||||
claude_local: "Claude",
|
||||
codex_local: "Codex",
|
||||
opencode_local: "OpenCode",
|
||||
openclaw: "OpenClaw",
|
||||
process: "Process",
|
||||
http: "HTTP",
|
||||
|
||||
Reference in New Issue
Block a user