Extract adapter registry across CLI, server, and UI

Refactor monolithic heartbeat service, AgentConfigForm, and CLI
heartbeat-run into a proper adapter registry pattern. Each adapter
type (process, claude-local, codex-local, http) gets its own module
with server-side execution logic, CLI invocation, and UI config form.
Significantly reduces file sizes and enables adding new adapters
without touching core code.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Forgotten
2026-02-18 13:53:03 -06:00
parent 3a91ecbae3
commit 47ccd946b6
52 changed files with 1961 additions and 1361 deletions

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useRef } from "react";
import { useState, useEffect, useRef, useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
import { AGENT_ADAPTER_TYPES } from "@paperclip/shared";
import type { Agent } from "@paperclip/shared";
@@ -24,6 +24,8 @@ import {
help,
adapterLabels,
} from "./agent-config-primitives";
import { getUIAdapter } from "../adapters";
import { ClaudeLocalAdvancedFields } from "../adapters/claude-local/config-fields";
/* ---- Create mode values ---- */
@@ -251,6 +253,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
? props.values.adapterType
: overlay.adapterType ?? props.agent.adapterType;
const isLocal = adapterType === "claude_local" || adapterType === "codex_local";
const uiAdapter = useMemo(() => getUIAdapter(adapterType), [adapterType]);
// Fetch adapter models for the effective adapter type
const { data: fetchedModels } = useQuery({
@@ -259,6 +262,19 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
});
const models = fetchedModels ?? externalModels ?? [];
/** Props passed to adapter-specific config field components */
const adapterFieldProps = {
mode,
isCreate,
adapterType,
values: isCreate ? props.values : null,
set: isCreate ? (patch: Partial<CreateConfigValues>) => props.onChange(patch) : null,
config,
eff: eff as <T>(group: "adapterConfig", field: string, original: T) => T,
mark: mark as (group: "adapterConfig", field: string, value: unknown) => void,
models,
};
// Section toggle state — advanced always starts collapsed
const [adapterAdvancedOpen, setAdapterAdvancedOpen] = useState(false);
const [heartbeatOpen, setHeartbeatOpen] = useState(!isCreate);
@@ -443,130 +459,8 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
</Field>
)}
{/* Claude-specific: Skip permissions */}
{adapterType === "claude_local" && (
<ToggleField
label="Skip permissions"
hint={help.dangerouslySkipPermissions}
checked={
isCreate
? val!.dangerouslySkipPermissions
: eff(
"adapterConfig",
"dangerouslySkipPermissions",
config.dangerouslySkipPermissions !== false,
)
}
onChange={(v) =>
isCreate
? set!({ dangerouslySkipPermissions: v })
: mark("adapterConfig", "dangerouslySkipPermissions", v)
}
/>
)}
{/* Codex-specific: Bypass sandbox + Search */}
{adapterType === "codex_local" && (
<>
<ToggleField
label="Bypass sandbox"
hint={help.dangerouslyBypassSandbox}
checked={
isCreate
? val!.dangerouslyBypassSandbox
: eff(
"adapterConfig",
"dangerouslyBypassApprovalsAndSandbox",
config.dangerouslyBypassApprovalsAndSandbox !== false,
)
}
onChange={(v) =>
isCreate
? set!({ dangerouslyBypassSandbox: v })
: mark("adapterConfig", "dangerouslyBypassApprovalsAndSandbox", v)
}
/>
<ToggleField
label="Enable search"
hint={help.search}
checked={
isCreate
? val!.search
: eff("adapterConfig", "search", !!config.search)
}
onChange={(v) =>
isCreate
? set!({ search: v })
: mark("adapterConfig", "search", v)
}
/>
</>
)}
{/* Process-specific */}
{adapterType === "process" && (
<>
<Field label="Command" hint={help.command}>
<DraftInput
value={
isCreate
? val!.command
: eff("adapterConfig", "command", String(config.command ?? ""))
}
onCommit={(v) =>
isCreate
? set!({ command: v })
: mark("adapterConfig", "command", v || undefined)
}
immediate
className={inputClass}
placeholder="e.g. node, python"
/>
</Field>
<Field label="Args (comma-separated)" hint={help.args}>
<DraftInput
value={
isCreate
? val!.args
: eff("adapterConfig", "args", formatArgList(config.args))
}
onCommit={(v) =>
isCreate
? set!({ args: v })
: mark(
"adapterConfig",
"args",
v ? parseCommaArgs(v) : undefined,
)
}
immediate
className={inputClass}
placeholder="e.g. script.js, --flag"
/>
</Field>
</>
)}
{/* HTTP-specific */}
{adapterType === "http" && (
<Field label="Webhook URL" hint={help.webhookUrl}>
<DraftInput
value={
isCreate
? val!.url
: eff("adapterConfig", "url", String(config.url ?? ""))
}
onCommit={(v) =>
isCreate
? set!({ url: v })
: mark("adapterConfig", "url", v || undefined)
}
immediate
className={inputClass}
placeholder="https://..."
/>
</Field>
)}
{/* Adapter-specific fields */}
<uiAdapter.ConfigFields {...adapterFieldProps} />
{/* Advanced adapter section — collapsible in both modes */}
{isLocal && (
@@ -630,27 +524,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
)}
</Field>
{adapterType === "claude_local" && (
<Field label="Max turns per run" hint={help.maxTurnsPerRun}>
{isCreate ? (
<input
type="number"
className={inputClass}
value={val!.maxTurnsPerRun}
onChange={(e) => set!({ maxTurnsPerRun: Number(e.target.value) })}
/>
) : (
<DraftNumberInput
value={eff(
"adapterConfig",
"maxTurnsPerRun",
Number(config.maxTurnsPerRun ?? 80),
)}
onCommit={(v) => mark("adapterConfig", "maxTurnsPerRun", v || 80)}
immediate
className={inputClass}
/>
)}
</Field>
<ClaudeLocalAdvancedFields {...adapterFieldProps} />
)}
<Field label="Extra args (comma-separated)" hint={help.extraArgs}>