mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 11:20:42 +01:00
147 lines
7.0 KiB
TypeScript
147 lines
7.0 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import { Search, Zap } from "lucide-react";
|
|
import type { Agent, AgentEnvConfig } from "@paperclipai/shared";
|
|
import type { AdapterModel } from "../api/agents";
|
|
import { adapterLabels } from "./agent-config-primitives";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { cn } from "../lib/utils";
|
|
|
|
export type AgentConfigurationSectionId =
|
|
| "runtime"
|
|
| "environment"
|
|
| "schedule"
|
|
| "access"
|
|
| "keys"
|
|
| "danger"
|
|
| "history";
|
|
|
|
export const agentConfigurationSections: ReadonlyArray<{
|
|
id: AgentConfigurationSectionId;
|
|
label: string;
|
|
instant?: boolean;
|
|
fields: string[];
|
|
}> = [
|
|
{ id: "runtime", label: "Runtime", fields: ["adapter", "model", "effort", "turns", "command", "arguments", "engine", "timeout", "chrome"] },
|
|
{ id: "environment", label: "Environment", fields: ["execution environment", "environment override", "variables", "secrets"] },
|
|
{ id: "schedule", label: "Schedule & Runs", fields: ["heartbeat", "wake on demand", "cooldown", "concurrent runs", "continuation"] },
|
|
{ id: "access", label: "Access & Governance", instant: true, fields: ["trust preset", "boundary", "create agents", "create skills", "assign tasks"] },
|
|
{ id: "keys", label: "API Keys", instant: true, fields: ["credentials", "create key", "revoke key"] },
|
|
{ id: "danger", label: "Danger & Legacy", fields: ["skip permissions", "bypass sandbox", "working directory", "deprecated"] },
|
|
{ id: "history", label: "History", instant: true, fields: ["configuration revisions", "restore"] },
|
|
];
|
|
|
|
export function filterAgentConfigurationSections(query: string): Set<AgentConfigurationSectionId> {
|
|
const normalized = query.trim().toLowerCase();
|
|
if (!normalized) return new Set(agentConfigurationSections.map((section) => section.id));
|
|
return new Set(
|
|
agentConfigurationSections
|
|
.filter((section) => section.label.toLowerCase().includes(normalized) || section.fields.some((field) => field.includes(normalized)))
|
|
.map((section) => section.id),
|
|
);
|
|
}
|
|
|
|
function readString(value: unknown): string {
|
|
return typeof value === "string" ? value : "";
|
|
}
|
|
|
|
export function resolveEffectiveConfiguration(agent: Agent, adapterModels: AdapterModel[] = [], apiKeyCount = 0) {
|
|
const adapterConfig = (agent.adapterConfig ?? {}) as Record<string, unknown>;
|
|
const runtimeConfig = (agent.runtimeConfig ?? {}) as Record<string, unknown>;
|
|
const heartbeat = (runtimeConfig.heartbeat ?? {}) as Record<string, unknown>;
|
|
const profiles = (runtimeConfig.modelProfiles ?? {}) as Record<string, unknown>;
|
|
const cheap = (profiles.cheap ?? {}) as Record<string, unknown>;
|
|
const cheapConfig = (cheap.adapterConfig ?? {}) as Record<string, unknown>;
|
|
const explicitModel = readString(adapterConfig.model);
|
|
const resolvedModel = explicitModel || adapterModels[0]?.label || adapterModels[0]?.id || "Adapter default";
|
|
const effort = readString(adapterConfig.modelReasoningEffort || adapterConfig.reasoningEffort || adapterConfig.effort || adapterConfig.variant || adapterConfig.mode);
|
|
const env = (adapterConfig.env ?? {}) as AgentEnvConfig;
|
|
return {
|
|
adapter: adapterLabels[agent.adapterType] ?? agent.adapterType,
|
|
model: `${resolvedModel}${effort ? ` · ${effort}` : ""}`,
|
|
modelInherited: !explicitModel,
|
|
cheapModel: readString(cheapConfig.model) || "Primary model",
|
|
cheapInherited: !readString(cheapConfig.model),
|
|
environment: agent.defaultEnvironmentId ? "Override" : "Company default",
|
|
environmentInherited: !agent.defaultEnvironmentId,
|
|
cadence: heartbeat.enabled === true ? `Every ${Number(heartbeat.intervalSec ?? 300)} sec` : "On demand",
|
|
trust: agent.permissions?.trustPreset === "low_trust_review" ? "Low-trust review" : "Standard",
|
|
apiKeyCount,
|
|
environmentVariableCount: Object.keys(env).length,
|
|
};
|
|
}
|
|
|
|
export function AgentConfigurationRail({
|
|
query,
|
|
onQueryChange,
|
|
visibleSections,
|
|
dirtySections,
|
|
}: {
|
|
query: string;
|
|
onQueryChange: (query: string) => void;
|
|
visibleSections: Set<AgentConfigurationSectionId>;
|
|
dirtySections: ReadonlySet<string>;
|
|
}) {
|
|
const shown = agentConfigurationSections.filter((section) => visibleSections.has(section.id));
|
|
return (
|
|
<>
|
|
<div className="sticky top-0 z-20 bg-background py-2 md:hidden">
|
|
<select
|
|
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm"
|
|
aria-label="Configuration section"
|
|
onChange={(event) => document.getElementById(`config-${event.target.value}`)?.scrollIntoView({ behavior: "smooth" })}
|
|
>
|
|
{shown.map((section) => <option key={section.id} value={section.id}>{section.label}</option>)}
|
|
</select>
|
|
</div>
|
|
<aside className="sticky top-4 hidden self-start md:block">
|
|
<label className="flex items-center gap-2 rounded-md border border-border px-2.5 py-2">
|
|
<Search className="h-3.5 w-3.5 text-muted-foreground" />
|
|
<input
|
|
value={query}
|
|
onChange={(event) => onQueryChange(event.target.value)}
|
|
placeholder="Find a setting"
|
|
className="min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
|
|
/>
|
|
</label>
|
|
<nav className="mt-3 space-y-1" aria-label="Configuration sections">
|
|
{shown.map((section) => (
|
|
<a key={section.id} href={`#config-${section.id}`} className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-muted-foreground hover:bg-accent hover:text-foreground">
|
|
<span className="flex-1">{section.label}</span>
|
|
{dirtySections.has(section.id) ? <span className="text-primary" aria-label="Unsaved changes">●</span> : null}
|
|
{section.instant ? <Zap className="h-3 w-3" aria-label="Applies immediately" /> : null}
|
|
</a>
|
|
))}
|
|
</nav>
|
|
</aside>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export function EffectiveConfigurationStrip({ chips }: { chips: Array<{ label: string; value: string; section: AgentConfigurationSectionId; inherited?: boolean }> }) {
|
|
return (
|
|
<div className="flex gap-2 overflow-x-auto border-b border-border pb-3" aria-label="Effective configuration">
|
|
{chips.map((chip) => (
|
|
<a key={chip.label} href={`#config-${chip.section}`} className="shrink-0">
|
|
<Badge variant="outline" className={cn("gap-1.5 py-1", chip.inherited && "text-muted-foreground")}>
|
|
<span>{chip.label}</span>
|
|
<span className="font-mono">{chip.value}</span>
|
|
{chip.inherited ? <span>inherited</span> : null}
|
|
</Badge>
|
|
</a>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function ConfigurationSection({ id, title, instant, children }: { id: AgentConfigurationSectionId; title: string; instant?: boolean; children: ReactNode }) {
|
|
return (
|
|
<section id={`config-${id}`} className="scroll-mt-24 space-y-3">
|
|
<div className="flex items-center gap-2">
|
|
<h2 className="text-sm font-semibold">{title}</h2>
|
|
{instant ? <Badge variant="outline" className="gap-1"><Zap className="h-3 w-3" /> applies immediately</Badge> : null}
|
|
</div>
|
|
{children}
|
|
</section>
|
|
);
|
|
}
|