fix: move defaultCreateValues to separate file to fix HMR
AgentConfigForm.tsx exported both React components and a plain object constant (defaultCreateValues), which broke Vite Fast Refresh. Moved the constant to agent-config-defaults.ts so the component module only exports React components. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,16 @@
|
|||||||
import { useState, useEffect, useRef, useMemo } from "react";
|
import { useState, useEffect, useRef, useMemo } from "react";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { AGENT_ADAPTER_TYPES } from "@paperclip/shared";
|
import { AGENT_ADAPTER_TYPES } from "@paperclip/shared";
|
||||||
import type { Agent, CompanySecret, EnvBinding } from "@paperclip/shared";
|
import type {
|
||||||
|
Agent,
|
||||||
|
AdapterEnvironmentTestResult,
|
||||||
|
CompanySecret,
|
||||||
|
EnvBinding,
|
||||||
|
} from "@paperclip/shared";
|
||||||
import type { AdapterModel } from "../api/agents";
|
import type { AdapterModel } from "../api/agents";
|
||||||
import { agentsApi } from "../api/agents";
|
import { agentsApi } from "../api/agents";
|
||||||
import { secretsApi } from "../api/secrets";
|
import { secretsApi } from "../api/secrets";
|
||||||
|
import { assetsApi } from "../api/assets";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
@@ -20,15 +26,14 @@ import {
|
|||||||
ToggleField,
|
ToggleField,
|
||||||
ToggleWithNumber,
|
ToggleWithNumber,
|
||||||
CollapsibleSection,
|
CollapsibleSection,
|
||||||
AutoExpandTextarea,
|
|
||||||
DraftInput,
|
DraftInput,
|
||||||
DraftTextarea,
|
|
||||||
DraftNumberInput,
|
DraftNumberInput,
|
||||||
help,
|
help,
|
||||||
adapterLabels,
|
adapterLabels,
|
||||||
} from "./agent-config-primitives";
|
} from "./agent-config-primitives";
|
||||||
import { getUIAdapter } from "../adapters";
|
import { getUIAdapter } from "../adapters";
|
||||||
import { ClaudeLocalAdvancedFields } from "../adapters/claude-local/config-fields";
|
import { ClaudeLocalAdvancedFields } from "../adapters/claude-local/config-fields";
|
||||||
|
import { MarkdownEditor } from "./MarkdownEditor";
|
||||||
|
|
||||||
/* ---- Create mode values ---- */
|
/* ---- Create mode values ---- */
|
||||||
|
|
||||||
@@ -37,27 +42,6 @@ import { ClaudeLocalAdvancedFields } from "../adapters/claude-local/config-field
|
|||||||
export type { CreateConfigValues } from "@paperclip/adapter-utils";
|
export type { CreateConfigValues } from "@paperclip/adapter-utils";
|
||||||
import type { CreateConfigValues } from "@paperclip/adapter-utils";
|
import type { CreateConfigValues } from "@paperclip/adapter-utils";
|
||||||
|
|
||||||
export const defaultCreateValues: CreateConfigValues = {
|
|
||||||
adapterType: "claude_local",
|
|
||||||
cwd: "",
|
|
||||||
promptTemplate: "",
|
|
||||||
model: "",
|
|
||||||
thinkingEffort: "",
|
|
||||||
dangerouslySkipPermissions: false,
|
|
||||||
search: false,
|
|
||||||
dangerouslyBypassSandbox: false,
|
|
||||||
command: "",
|
|
||||||
args: "",
|
|
||||||
extraArgs: "",
|
|
||||||
envVars: "",
|
|
||||||
envBindings: {},
|
|
||||||
url: "",
|
|
||||||
bootstrapPrompt: "",
|
|
||||||
maxTurnsPerRun: 80,
|
|
||||||
heartbeatEnabled: false,
|
|
||||||
intervalSec: 300,
|
|
||||||
};
|
|
||||||
|
|
||||||
/* ---- Props ---- */
|
/* ---- Props ---- */
|
||||||
|
|
||||||
type AgentConfigFormProps = {
|
type AgentConfigFormProps = {
|
||||||
@@ -174,6 +158,13 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const uploadMarkdownImage = useMutation({
|
||||||
|
mutationFn: async ({ file, namespace }: { file: File; namespace: string }) => {
|
||||||
|
if (!selectedCompanyId) throw new Error("Select a company to upload images");
|
||||||
|
return assetsApi.uploadImage(selectedCompanyId, file, namespace);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// ---- Edit mode: overlay for dirty tracking ----
|
// ---- Edit mode: overlay for dirty tracking ----
|
||||||
const [overlay, setOverlay] = useState<Overlay>(emptyOverlay);
|
const [overlay, setOverlay] = useState<Overlay>(emptyOverlay);
|
||||||
const agentRef = useRef<Agent | null>(null);
|
const agentRef = useRef<Agent | null>(null);
|
||||||
@@ -293,6 +284,25 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||||||
? (patch: Partial<CreateConfigValues>) => props.onChange(patch)
|
? (patch: Partial<CreateConfigValues>) => props.onChange(patch)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
function buildAdapterConfigForTest(): Record<string, unknown> {
|
||||||
|
if (isCreate) {
|
||||||
|
return uiAdapter.buildAdapterConfig(val!);
|
||||||
|
}
|
||||||
|
const base = config as Record<string, unknown>;
|
||||||
|
return { ...base, ...overlay.adapterConfig };
|
||||||
|
}
|
||||||
|
|
||||||
|
const testEnvironment = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
if (!selectedCompanyId) {
|
||||||
|
throw new Error("Select a company to test adapter environment");
|
||||||
|
}
|
||||||
|
return agentsApi.testEnvironment(selectedCompanyId, adapterType, {
|
||||||
|
adapterConfig: buildAdapterConfigForTest(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Current model for display
|
// Current model for display
|
||||||
const currentModelId = isCreate
|
const currentModelId = isCreate
|
||||||
? val!.model
|
? val!.model
|
||||||
@@ -356,12 +366,18 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Capabilities" hint={help.capabilities}>
|
<Field label="Capabilities" hint={help.capabilities}>
|
||||||
<DraftTextarea
|
<MarkdownEditor
|
||||||
value={eff("identity", "capabilities", props.agent.capabilities ?? "")}
|
value={eff("identity", "capabilities", props.agent.capabilities ?? "")}
|
||||||
onCommit={(v) => mark("identity", "capabilities", v || null)}
|
onChange={(v) => mark("identity", "capabilities", v || null)}
|
||||||
immediate
|
|
||||||
placeholder="Describe what this agent can do..."
|
placeholder="Describe what this agent can do..."
|
||||||
minRows={2}
|
contentClassName="min-h-[120px]"
|
||||||
|
imageUploadHandler={async (file) => {
|
||||||
|
const asset = await uploadMarkdownImage.mutateAsync({
|
||||||
|
file,
|
||||||
|
namespace: `agents/${props.agent.id}/capabilities`,
|
||||||
|
});
|
||||||
|
return asset.contentPath;
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
@@ -390,10 +406,34 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||||||
|
|
||||||
{/* ---- Adapter Configuration ---- */}
|
{/* ---- Adapter Configuration ---- */}
|
||||||
<div className={cn(isCreate ? "border-t border-border" : "border-b border-border")}>
|
<div className={cn(isCreate ? "border-t border-border" : "border-b border-border")}>
|
||||||
<div className="px-4 py-2 text-xs font-medium text-muted-foreground">
|
<div className="px-4 py-2 flex items-center justify-between gap-2">
|
||||||
Adapter Configuration
|
<span className="text-xs font-medium text-muted-foreground">
|
||||||
|
Adapter Configuration
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 px-2.5 text-xs"
|
||||||
|
onClick={() => testEnvironment.mutate()}
|
||||||
|
disabled={testEnvironment.isPending || !selectedCompanyId}
|
||||||
|
>
|
||||||
|
{testEnvironment.isPending ? "Testing..." : "Test environment"}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-4 pb-3 space-y-3">
|
<div className="px-4 pb-3 space-y-3">
|
||||||
|
{testEnvironment.error && (
|
||||||
|
<div className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
||||||
|
{testEnvironment.error instanceof Error
|
||||||
|
? testEnvironment.error.message
|
||||||
|
: "Environment test failed"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{testEnvironment.data && (
|
||||||
|
<AdapterEnvironmentResult result={testEnvironment.data} />
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Working directory */}
|
{/* Working directory */}
|
||||||
{isLocal && (
|
{isLocal && (
|
||||||
<Field label="Working directory" hint={help.cwd}>
|
<Field label="Working directory" hint={help.cwd}>
|
||||||
@@ -454,28 +494,31 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||||||
{/* Prompt template */}
|
{/* Prompt template */}
|
||||||
{isLocal && (
|
{isLocal && (
|
||||||
<Field label="Prompt template" hint={help.promptTemplate}>
|
<Field label="Prompt template" hint={help.promptTemplate}>
|
||||||
{isCreate ? (
|
<MarkdownEditor
|
||||||
<AutoExpandTextarea
|
value={
|
||||||
placeholder="You are agent {{ agent.name }}. Your role is {{ agent.role }}..."
|
isCreate
|
||||||
value={val!.promptTemplate}
|
? val!.promptTemplate
|
||||||
onChange={(v) => set!({ promptTemplate: v })}
|
: eff(
|
||||||
minRows={4}
|
"adapterConfig",
|
||||||
/>
|
"promptTemplate",
|
||||||
) : (
|
String(config.promptTemplate ?? ""),
|
||||||
<DraftTextarea
|
)
|
||||||
value={eff(
|
}
|
||||||
"adapterConfig",
|
onChange={(v) =>
|
||||||
"promptTemplate",
|
isCreate
|
||||||
String(config.promptTemplate ?? ""),
|
? set!({ promptTemplate: v })
|
||||||
)}
|
: mark("adapterConfig", "promptTemplate", v || undefined)
|
||||||
onCommit={(v) =>
|
}
|
||||||
mark("adapterConfig", "promptTemplate", v || undefined)
|
placeholder="You are agent {{ agent.name }}. Your role is {{ agent.role }}..."
|
||||||
}
|
contentClassName="min-h-[180px]"
|
||||||
immediate
|
imageUploadHandler={async (file) => {
|
||||||
placeholder="You are agent {{ agent.name }}. Your role is {{ agent.role }}..."
|
const namespace = isCreate
|
||||||
minRows={4}
|
? "agents/drafts/prompt-template"
|
||||||
/>
|
: `agents/${props.agent.id}/prompt-template`;
|
||||||
)}
|
const asset = await uploadMarkdownImage.mutateAsync({ file, namespace });
|
||||||
|
return asset.contentPath;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -539,28 +582,31 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<Field label="Bootstrap prompt (first run)" hint={help.bootstrapPrompt}>
|
<Field label="Bootstrap prompt (first run)" hint={help.bootstrapPrompt}>
|
||||||
{isCreate ? (
|
<MarkdownEditor
|
||||||
<AutoExpandTextarea
|
value={
|
||||||
placeholder="Optional initial setup prompt for the first run"
|
isCreate
|
||||||
value={val!.bootstrapPrompt}
|
? val!.bootstrapPrompt
|
||||||
onChange={(v) => set!({ bootstrapPrompt: v })}
|
: eff(
|
||||||
minRows={2}
|
"adapterConfig",
|
||||||
/>
|
"bootstrapPromptTemplate",
|
||||||
) : (
|
String(config.bootstrapPromptTemplate ?? ""),
|
||||||
<DraftTextarea
|
)
|
||||||
value={eff(
|
}
|
||||||
"adapterConfig",
|
onChange={(v) =>
|
||||||
"bootstrapPromptTemplate",
|
isCreate
|
||||||
String(config.bootstrapPromptTemplate ?? ""),
|
? set!({ bootstrapPrompt: v })
|
||||||
)}
|
: mark("adapterConfig", "bootstrapPromptTemplate", v || undefined)
|
||||||
onCommit={(v) =>
|
}
|
||||||
mark("adapterConfig", "bootstrapPromptTemplate", v || undefined)
|
placeholder="Optional initial setup prompt for the first run"
|
||||||
}
|
contentClassName="min-h-[120px]"
|
||||||
immediate
|
imageUploadHandler={async (file) => {
|
||||||
placeholder="Optional initial setup prompt for the first run"
|
const namespace = isCreate
|
||||||
minRows={2}
|
? "agents/drafts/bootstrap-prompt"
|
||||||
/>
|
: `agents/${props.agent.id}/bootstrap-prompt`;
|
||||||
)}
|
const asset = await uploadMarkdownImage.mutateAsync({ file, namespace });
|
||||||
|
return asset.contentPath;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
{adapterType === "claude_local" && (
|
{adapterType === "claude_local" && (
|
||||||
<ClaudeLocalAdvancedFields {...adapterFieldProps} />
|
<ClaudeLocalAdvancedFields {...adapterFieldProps} />
|
||||||
@@ -720,6 +766,41 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AdapterEnvironmentResult({ result }: { result: AdapterEnvironmentTestResult }) {
|
||||||
|
const statusLabel =
|
||||||
|
result.status === "pass" ? "Passed" : result.status === "warn" ? "Warnings" : "Failed";
|
||||||
|
const statusClass =
|
||||||
|
result.status === "pass"
|
||||||
|
? "text-green-300 border-green-500/40 bg-green-500/10"
|
||||||
|
: result.status === "warn"
|
||||||
|
? "text-amber-300 border-amber-500/40 bg-amber-500/10"
|
||||||
|
: "text-red-300 border-red-500/40 bg-red-500/10";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`rounded-md border px-3 py-2 text-xs ${statusClass}`}>
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<span className="font-medium">{statusLabel}</span>
|
||||||
|
<span className="text-[11px] opacity-80">
|
||||||
|
{new Date(result.testedAt).toLocaleTimeString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 space-y-1.5">
|
||||||
|
{result.checks.map((check, idx) => (
|
||||||
|
<div key={`${check.code}-${idx}`} className="text-[11px] leading-relaxed">
|
||||||
|
<span className="font-medium uppercase tracking-wide opacity-80">
|
||||||
|
{check.level}
|
||||||
|
</span>
|
||||||
|
<span className="mx-1 opacity-60">·</span>
|
||||||
|
<span>{check.message}</span>
|
||||||
|
{check.detail && <span className="opacity-75"> ({check.detail})</span>}
|
||||||
|
{check.hint && <span className="opacity-90"> Hint: {check.hint}</span>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- Internal sub-components ---- */
|
/* ---- Internal sub-components ---- */
|
||||||
|
|
||||||
function AdapterTypeDropdown({
|
function AdapterTypeDropdown({
|
||||||
|
|||||||
@@ -24,11 +24,8 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { cn } from "../lib/utils";
|
import { cn } from "../lib/utils";
|
||||||
import { roleLabels } from "./agent-config-primitives";
|
import { roleLabels } from "./agent-config-primitives";
|
||||||
import {
|
import { AgentConfigForm, type CreateConfigValues } from "./AgentConfigForm";
|
||||||
AgentConfigForm,
|
import { defaultCreateValues } from "./agent-config-defaults";
|
||||||
defaultCreateValues,
|
|
||||||
type CreateConfigValues,
|
|
||||||
} from "./AgentConfigForm";
|
|
||||||
import { getUIAdapter } from "../adapters";
|
import { getUIAdapter } from "../adapters";
|
||||||
|
|
||||||
export function NewAgentDialog() {
|
export function NewAgentDialog() {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { cn } from "../lib/utils";
|
import { cn } from "../lib/utils";
|
||||||
import { getUIAdapter } from "../adapters";
|
import { getUIAdapter } from "../adapters";
|
||||||
import { defaultCreateValues } from "./AgentConfigForm";
|
import { defaultCreateValues } from "./agent-config-defaults";
|
||||||
import {
|
import {
|
||||||
Building2,
|
Building2,
|
||||||
Bot,
|
Bot,
|
||||||
|
|||||||
22
ui/src/components/agent-config-defaults.ts
Normal file
22
ui/src/components/agent-config-defaults.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import type { CreateConfigValues } from "@paperclip/adapter-utils";
|
||||||
|
|
||||||
|
export const defaultCreateValues: CreateConfigValues = {
|
||||||
|
adapterType: "claude_local",
|
||||||
|
cwd: "",
|
||||||
|
promptTemplate: "",
|
||||||
|
model: "",
|
||||||
|
thinkingEffort: "",
|
||||||
|
dangerouslySkipPermissions: false,
|
||||||
|
search: false,
|
||||||
|
dangerouslyBypassSandbox: false,
|
||||||
|
command: "",
|
||||||
|
args: "",
|
||||||
|
extraArgs: "",
|
||||||
|
envVars: "",
|
||||||
|
envBindings: {},
|
||||||
|
url: "",
|
||||||
|
bootstrapPrompt: "",
|
||||||
|
maxTurnsPerRun: 80,
|
||||||
|
heartbeatEnabled: false,
|
||||||
|
intervalSec: 300,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user