diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 8bcdb321..898d2d9c 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -185,6 +185,7 @@ export type { CompanyPortabilityPreviewProjectPlan, CompanyPortabilityPreviewIssuePlan, CompanyPortabilityPreviewResult, + CompanyPortabilityAdapterOverride, CompanyPortabilityImportRequest, CompanyPortabilityImportResult, CompanyPortabilityExportRequest, diff --git a/packages/shared/src/types/company-portability.ts b/packages/shared/src/types/company-portability.ts index 9734b682..c2526cc3 100644 --- a/packages/shared/src/types/company-portability.ts +++ b/packages/shared/src/types/company-portability.ts @@ -191,7 +191,14 @@ export interface CompanyPortabilityPreviewResult { errors: string[]; } -export interface CompanyPortabilityImportRequest extends CompanyPortabilityPreviewRequest {} +export interface CompanyPortabilityAdapterOverride { + adapterType: string; + adapterConfig?: Record; +} + +export interface CompanyPortabilityImportRequest extends CompanyPortabilityPreviewRequest { + adapterOverrides?: Record; +} export interface CompanyPortabilityImportResult { company: { diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index 0581dea2..79e9876e 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -109,6 +109,7 @@ export type { CompanyPortabilityPreviewProjectPlan, CompanyPortabilityPreviewIssuePlan, CompanyPortabilityPreviewResult, + CompanyPortabilityAdapterOverride, CompanyPortabilityImportRequest, CompanyPortabilityImportResult, CompanyPortabilityExportRequest, diff --git a/packages/shared/src/validators/company-portability.ts b/packages/shared/src/validators/company-portability.ts index 9cc853e5..c9e387b6 100644 --- a/packages/shared/src/validators/company-portability.ts +++ b/packages/shared/src/validators/company-portability.ts @@ -169,6 +169,13 @@ export const companyPortabilityPreviewSchema = z.object({ export type CompanyPortabilityPreview = z.infer; -export const companyPortabilityImportSchema = companyPortabilityPreviewSchema; +export const portabilityAdapterOverrideSchema = z.object({ + adapterType: z.string().min(1), + adapterConfig: z.record(z.unknown()).optional(), +}); + +export const companyPortabilityImportSchema = companyPortabilityPreviewSchema.extend({ + adapterOverrides: z.record(z.string().min(1), portabilityAdapterOverrideSchema).optional(), +}); export type CompanyPortabilityImport = z.infer; diff --git a/server/src/services/company-portability.ts b/server/src/services/company-portability.ts index e18016ca..752fa8fc 100644 --- a/server/src/services/company-portability.ts +++ b/server/src/services/company-portability.ts @@ -2260,16 +2260,21 @@ export function companyPortabilityService(db: Db) { warnings.push(`Missing AGENTS markdown for ${manifestAgent.slug}; imported without prompt template.`); } const markdown = markdownRaw ? parseFrontmatterMarkdown(markdownRaw) : { frontmatter: {}, body: "" }; - const adapterConfig = { - ...manifestAgent.adapterConfig, - promptTemplate: markdown.body || asString((manifestAgent.adapterConfig as Record).promptTemplate) || "", - } as Record; + const promptTemplate = markdown.body || asString((manifestAgent.adapterConfig as Record).promptTemplate) || ""; + + // Apply adapter overrides from request if present + const adapterOverride = input.adapterOverrides?.[planAgent.slug]; + const effectiveAdapterType = adapterOverride?.adapterType ?? manifestAgent.adapterType; + const baseAdapterConfig = adapterOverride?.adapterConfig + ? { ...adapterOverride.adapterConfig, promptTemplate } + : { ...manifestAgent.adapterConfig, promptTemplate } as Record; + const desiredSkills = manifestAgent.skills ?? []; const adapterConfigWithSkills = writePaperclipSkillSyncPreference( - adapterConfig, + baseAdapterConfig, desiredSkills, ); - delete adapterConfig.instructionsFilePath; + delete baseAdapterConfig.instructionsFilePath; const patch = { name: planAgent.plannedName, role: manifestAgent.role, @@ -2277,7 +2282,7 @@ export function companyPortabilityService(db: Db) { icon: manifestAgent.icon, capabilities: manifestAgent.capabilities, reportsTo: null, - adapterType: manifestAgent.adapterType, + adapterType: effectiveAdapterType, adapterConfig: adapterConfigWithSkills, runtimeConfig: manifestAgent.runtimeConfig, budgetMonthlyCents: manifestAgent.budgetMonthlyCents, diff --git a/ui/src/pages/CompanyImport.tsx b/ui/src/pages/CompanyImport.tsx index 8282eb62..c495a188 100644 --- a/ui/src/pages/CompanyImport.tsx +++ b/ui/src/pages/CompanyImport.tsx @@ -1,13 +1,15 @@ import { useEffect, useMemo, useRef, useState, type ChangeEvent } from "react"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { CompanyPortabilityPreviewResult, CompanyPortabilitySource, + CompanyPortabilityAdapterOverride, } from "@paperclipai/shared"; import { useCompany } from "../context/CompanyContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { useToast } from "../context/ToastContext"; import { companiesApi } from "../api/companies"; +import { agentsApi } from "../api/agents"; import { queryKeys } from "../lib/queryKeys"; import { MarkdownBody } from "../components/MarkdownBody"; import { Button } from "@/components/ui/button"; @@ -16,12 +18,16 @@ import { cn } from "../lib/utils"; import { ArrowRight, Check, + ChevronRight, Download, Github, Package, Upload, } from "lucide-react"; -import { Field } from "../components/agent-config-primitives"; +import { Field, adapterLabels } from "../components/agent-config-primitives"; +import { defaultCreateValues } from "../components/agent-config-defaults"; +import { getUIAdapter } from "../adapters"; +import type { CreateConfigValues } from "@paperclipai/adapter-utils"; import { type FileTreeNode, type FrontmatterData, @@ -434,6 +440,120 @@ function ConflictResolutionList({ ); } +// ── Adapter type options for import ─────────────────────────────────── + +const IMPORT_ADAPTER_OPTIONS: { value: string; label: string }[] = [ + { value: "claude_local", label: adapterLabels.claude_local ?? "Claude (local)" }, + { value: "codex_local", label: adapterLabels.codex_local ?? "Codex (local)" }, + { value: "opencode_local", label: adapterLabels.opencode_local ?? "OpenCode (local)" }, + { value: "cursor", label: adapterLabels.cursor ?? "Cursor (local)" }, +]; + +// ── Adapter picker for imported agents ─────────────────────────────── + +interface AdapterPickerItem { + slug: string; + name: string; + adapterType: string; +} + +function AdapterPickerList({ + agents, + adapterOverrides, + expandedSlugs, + configValues, + onChangeAdapter, + onToggleExpand, + onChangeConfig, +}: { + agents: AdapterPickerItem[]; + adapterOverrides: Record; + expandedSlugs: Set; + configValues: Record; + onChangeAdapter: (slug: string, adapterType: string) => void; + onToggleExpand: (slug: string) => void; + onChangeConfig: (slug: string, patch: Partial) => void; +}) { + if (agents.length === 0) return null; + + return ( +
+
+
+

Adapters

+ + {agents.length} agent{agents.length === 1 ? "" : "s"} + +
+
+ {agents.map((agent) => { + const selectedType = adapterOverrides[agent.slug] ?? agent.adapterType; + const isExpanded = expandedSlugs.has(agent.slug); + const uiAdapter = getUIAdapter(selectedType); + const vals = configValues[agent.slug] ?? { ...defaultCreateValues, adapterType: selectedType }; + + return ( +
+
+ + agent + + + {agent.name} + + + + +
+ {isExpanded && ( +
+ onChangeConfig(agent.slug, patch)} + config={{}} + eff={() => "" as any} + mark={() => {}} + models={[]} + /> +
+ )} +
+ ); + })} +
+
+
+ ); +} + // ── Helpers ─────────────────────────────────────────────────────────── async function readLocalPackageZip(file: File): Promise<{ @@ -493,6 +613,23 @@ export function CompanyImport() { const [skippedSlugs, setSkippedSlugs] = useState>(new Set()); const [confirmedSlugs, setConfirmedSlugs] = useState>(new Set()); + // Adapter override state + const [adapterOverrides, setAdapterOverrides] = useState>({}); + const [adapterExpandedSlugs, setAdapterExpandedSlugs] = useState>(new Set()); + const [adapterConfigValues, setAdapterConfigValues] = useState>({}); + + // Fetch current company agents to find CEO adapter type + const { data: companyAgents } = useQuery({ + queryKey: selectedCompanyId ? queryKeys.agents.list(selectedCompanyId) : ["agents", "none"], + queryFn: () => agentsApi.list(selectedCompanyId!), + enabled: Boolean(selectedCompanyId), + }); + const ceoAdapterType = useMemo(() => { + if (!companyAgents) return "claude_local"; + const ceo = companyAgents.find((a) => a.role === "ceo"); + return ceo?.adapterType ?? "claude_local"; + }, [companyAgents]); + useEffect(() => { setBreadcrumbs([ { label: "Org Chart", href: "/org" }, @@ -548,6 +685,15 @@ export function CompanyImport() { setSkippedSlugs(new Set()); setConfirmedSlugs(new Set()); + // Initialize adapter overrides — default all agents to the CEO's adapter type + const defaultAdapters: Record = {}; + for (const agent of result.manifest.agents) { + defaultAdapters[agent.slug] = ceoAdapterType; + } + setAdapterOverrides(defaultAdapters); + setAdapterExpandedSlugs(new Set()); + setAdapterConfigValues({}); + // Check all files by default, then uncheck COMPANY.md for existing company const allFiles = new Set(Object.keys(result.files)); if (targetMode === "existing" && result.manifest.company && result.plan.companyAction === "update") { @@ -620,6 +766,7 @@ export function CompanyImport() { collisionStrategy: "rename", nameOverrides: buildFinalNameOverrides(), selectedFiles: buildSelectedFiles(), + adapterOverrides: buildFinalAdapterOverrides(), }); }, onSuccess: async (result) => { @@ -786,6 +933,59 @@ export function CompanyImport() { }); } + function handleAdapterChange(slug: string, adapterType: string) { + setAdapterOverrides((prev) => ({ ...prev, [slug]: adapterType })); + // Reset config values when adapter type changes + setAdapterConfigValues((prev) => { + const next = { ...prev }; + delete next[slug]; + return next; + }); + } + + function handleAdapterToggleExpand(slug: string) { + setAdapterExpandedSlugs((prev) => { + const next = new Set(prev); + if (next.has(slug)) next.delete(slug); + else next.add(slug); + return next; + }); + } + + function handleAdapterConfigChange(slug: string, patch: Partial) { + setAdapterConfigValues((prev) => ({ + ...prev, + [slug]: { ...(prev[slug] ?? { ...defaultCreateValues, adapterType: adapterOverrides[slug] ?? "claude_local" }), ...patch }, + })); + } + + // Build the list of agents for adapter picking + const adapterAgents = useMemo(() => { + if (!importPreview) return []; + return importPreview.manifest.agents.map((a) => ({ + slug: a.slug, + name: a.name, + adapterType: a.adapterType, + })); + }, [importPreview]); + + // Build final adapterOverrides for import request + function buildFinalAdapterOverrides(): Record | undefined { + if (adapterAgents.length === 0) return undefined; + const overrides: Record = {}; + for (const agent of adapterAgents) { + const selectedType = adapterOverrides[agent.slug] ?? agent.adapterType; + const configVals = adapterConfigValues[agent.slug]; + const override: CompanyPortabilityAdapterOverride = { adapterType: selectedType }; + if (configVals) { + const uiAdapter = getUIAdapter(selectedType); + override.adapterConfig = uiAdapter.buildAdapterConfig(configVals); + } + overrides[agent.slug] = override; + } + return Object.keys(overrides).length > 0 ? overrides : undefined; + } + const hasSource = sourceMode === "local" ? !!localPackage : importUrl.trim().length > 0; const hasErrors = importPreview ? importPreview.errors.length > 0 : false; @@ -967,6 +1167,17 @@ export function CompanyImport() { onToggleConfirm={handleConflictToggleConfirm} /> + {/* Adapter picker list */} + + {/* Import button — below renames */}