Merge public-gh/master into paperclip-company-import-export

This commit is contained in:
dotta 2026-03-18 09:57:26 -05:00
commit 9e19f1d005
49 changed files with 3997 additions and 2501 deletions

View file

@ -1,11 +1,6 @@
import { useState, useEffect, useRef, useMemo, useCallback } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { AGENT_ADAPTER_TYPES } from "@paperclipai/shared";
import {
hasSessionCompactionThresholds,
resolveSessionCompactionPolicy,
type ResolvedSessionCompactionPolicy,
} from "@paperclipai/adapter-utils";
import type {
Agent,
AdapterEnvironmentTestResult,
@ -420,12 +415,6 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
heartbeat: mergedHeartbeat,
};
}, [isCreate, overlay.heartbeat, runtimeConfig, val]);
const sessionCompaction = useMemo(
() => resolveSessionCompactionPolicy(adapterType, effectiveRuntimeConfig),
[adapterType, effectiveRuntimeConfig],
);
const showSessionCompactionCard = Boolean(sessionCompaction.adapterSessionManagement);
return (
<div className={cn("relative", cards && "space-y-6")}>
{/* ---- Floating Save button (edit mode, when dirty) ---- */}
@ -735,6 +724,32 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
)}
</>
)}
{!isCreate && typeof config.bootstrapPromptTemplate === "string" && config.bootstrapPromptTemplate && (
<>
<Field label="Bootstrap prompt (legacy)" hint={help.bootstrapPrompt}>
<MarkdownEditor
value={eff(
"adapterConfig",
"bootstrapPromptTemplate",
String(config.bootstrapPromptTemplate ?? ""),
)}
onChange={(v) =>
mark("adapterConfig", "bootstrapPromptTemplate", v || undefined)
}
placeholder="Optional initial setup prompt for the first run"
contentClassName="min-h-[44px] text-sm font-mono"
imageUploadHandler={async (file) => {
const namespace = `agents/${props.agent.id}/bootstrap-prompt`;
const asset = await uploadMarkdownImage.mutateAsync({ file, namespace });
return asset.contentPath;
}}
/>
</Field>
<div className="rounded-md border border-amber-500/25 bg-amber-500/10 px-3 py-2 text-xs text-amber-200">
Bootstrap prompt is legacy and will be removed in a future release. Consider moving this content into the agent&apos;s prompt template or instructions file instead.
</div>
</>
)}
{adapterType === "claude_local" && (
<ClaudeLocalAdvancedFields {...adapterFieldProps} />
)}
@ -831,12 +846,6 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
numberHint={help.intervalSec}
showNumber={val!.heartbeatEnabled}
/>
{showSessionCompactionCard && (
<SessionCompactionPolicyCard
adapterType={adapterType}
resolution={sessionCompaction}
/>
)}
</div>
</div>
) : !isCreate ? (
@ -859,12 +868,6 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
numberHint={help.intervalSec}
showNumber={eff("heartbeat", "enabled", heartbeat.enabled !== false)}
/>
{showSessionCompactionCard && (
<SessionCompactionPolicyCard
adapterType={adapterType}
resolution={sessionCompaction}
/>
)}
</div>
<CollapsibleSection
title="Advanced Run Policy"
@ -952,69 +955,6 @@ function AdapterEnvironmentResult({ result }: { result: AdapterEnvironmentTestRe
);
}
function formatSessionThreshold(value: number, suffix: string) {
if (value <= 0) return "Off";
return `${value.toLocaleString("en-US")} ${suffix}`;
}
function SessionCompactionPolicyCard({
adapterType,
resolution,
}: {
adapterType: string;
resolution: ResolvedSessionCompactionPolicy;
}) {
const { adapterSessionManagement, policy, source } = resolution;
if (!adapterSessionManagement) return null;
const adapterLabel = adapterLabels[adapterType] ?? adapterType;
const sourceLabel = source === "agent_override" ? "Agent override" : "Adapter default";
const rotationDisabled = !policy.enabled || !hasSessionCompactionThresholds(policy);
const nativeSummary =
adapterSessionManagement.nativeContextManagement === "confirmed"
? `${adapterLabel} is treated as natively managing long context, so Paperclip fresh-session rotation defaults to off.`
: adapterSessionManagement.nativeContextManagement === "likely"
? `${adapterLabel} likely manages long context itself, but Paperclip still keeps conservative rotation defaults for now.`
: `${adapterLabel} does not have verified native compaction behavior, so Paperclip keeps conservative rotation defaults.`;
return (
<div className="rounded-md border border-sky-500/25 bg-sky-500/10 px-3 py-3 space-y-2">
<div className="flex items-center justify-between gap-3">
<div className="text-xs font-medium text-sky-50">Session compaction</div>
<span className="rounded-full border border-sky-400/30 px-2 py-0.5 text-[11px] text-sky-100">
{sourceLabel}
</span>
</div>
<p className="text-xs text-sky-100/90">
{nativeSummary}
</p>
<p className="text-xs text-sky-100/80">
{rotationDisabled
? "No Paperclip-managed fresh-session thresholds are active for this adapter."
: "Paperclip will start a fresh session when one of these thresholds is reached."}
</p>
<div className="grid grid-cols-3 gap-2 text-[11px] text-sky-100/85 tabular-nums">
<div>
<div className="text-sky-100/60">Runs</div>
<div>{formatSessionThreshold(policy.maxSessionRuns, "runs")}</div>
</div>
<div>
<div className="text-sky-100/60">Raw input</div>
<div>{formatSessionThreshold(policy.maxRawInputTokens, "tokens")}</div>
</div>
<div>
<div className="text-sky-100/60">Age</div>
<div>{formatSessionThreshold(policy.maxSessionAgeHours, "hours")}</div>
</div>
</div>
<p className="text-[11px] text-sky-100/75">
A large cumulative raw token total does not mean the full session is resent on every heartbeat.
{source === "agent_override" && " This agent has an explicit runtimeConfig session compaction override."}
</p>
</div>
);
}
/* ---- Internal sub-components ---- */
const ENABLED_ADAPTER_TYPES = new Set(["claude_local", "codex_local", "gemini_local", "opencode_local", "cursor"]);

View file

@ -32,6 +32,7 @@ import { queryKeys } from "../lib/queryKeys";
import { cn } from "../lib/utils";
import { NotFoundPage } from "../pages/NotFound";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
const INSTANCE_SETTINGS_MEMORY_KEY = "paperclip.lastInstanceSettingsPath";
@ -298,7 +299,12 @@ export function Layout() {
<span className="truncate">Documentation</span>
</a>
{health?.version && (
<span className="px-2 text-xs text-muted-foreground shrink-0">v{health.version}</span>
<Tooltip>
<TooltipTrigger asChild>
<span className="px-2 text-xs text-muted-foreground shrink-0 cursor-default">v</span>
</TooltipTrigger>
<TooltipContent>v{health.version}</TooltipContent>
</Tooltip>
)}
<Button variant="ghost" size="icon-sm" className="text-muted-foreground shrink-0" asChild>
<Link
@ -351,7 +357,12 @@ export function Layout() {
<span className="truncate">Documentation</span>
</a>
{health?.version && (
<span className="px-2 text-xs text-muted-foreground shrink-0">v{health.version}</span>
<Tooltip>
<TooltipTrigger asChild>
<span className="px-2 text-xs text-muted-foreground shrink-0 cursor-default">v</span>
</TooltipTrigger>
<TooltipContent>v{health.version}</TooltipContent>
</Tooltip>
)}
<Button variant="ghost" size="icon-sm" className="text-muted-foreground shrink-0" asChild>
<Link

View file

@ -1,7 +1,7 @@
import { useEffect, useState, useRef, useCallback, useMemo } from "react";
import { useNavigate } from "react-router-dom";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import type { AdapterEnvironmentTestResult } from "@paperclipai/shared";
import { useLocation, useNavigate, useParams } from "@/lib/router";
import { useDialog } from "../context/DialogContext";
import { useCompany } from "../context/CompanyContext";
import { companiesApi } from "../api/companies";
@ -30,6 +30,7 @@ import {
} from "@paperclipai/adapter-codex-local";
import { DEFAULT_CURSOR_LOCAL_MODEL } from "@paperclipai/adapter-cursor-local";
import { DEFAULT_GEMINI_LOCAL_MODEL } from "@paperclipai/adapter-gemini-local";
import { resolveRouteOnboardingOptions } from "../lib/onboarding-route";
import { AsciiArtAnimation } from "./AsciiArtAnimation";
import { ChoosePathButton } from "./PathInstructionsModal";
import { HintIcon } from "./agent-config-primitives";
@ -75,12 +76,29 @@ After that, hire yourself a Founding Engineer agent and then plan the roadmap an
export function OnboardingWizard() {
const { onboardingOpen, onboardingOptions, closeOnboarding } = useDialog();
const { selectedCompanyId, companies, setSelectedCompanyId } = useCompany();
const { companies, setSelectedCompanyId, loading: companiesLoading } = useCompany();
const queryClient = useQueryClient();
const navigate = useNavigate();
const location = useLocation();
const { companyPrefix } = useParams<{ companyPrefix?: string }>();
const [routeDismissed, setRouteDismissed] = useState(false);
const initialStep = onboardingOptions.initialStep ?? 1;
const existingCompanyId = onboardingOptions.companyId;
const routeOnboardingOptions =
companyPrefix && companiesLoading
? null
: resolveRouteOnboardingOptions({
pathname: location.pathname,
companyPrefix,
companies,
});
const effectiveOnboardingOpen =
onboardingOpen || (routeOnboardingOptions !== null && !routeDismissed);
const effectiveOnboardingOptions = onboardingOpen
? onboardingOptions
: routeOnboardingOptions ?? {};
const initialStep = effectiveOnboardingOptions.initialStep ?? 1;
const existingCompanyId = effectiveOnboardingOptions.companyId;
const [step, setStep] = useState<Step>(initialStep);
const [loading, setLoading] = useState(false);
@ -134,27 +152,31 @@ export function OnboardingWizard() {
const [createdAgentId, setCreatedAgentId] = useState<string | null>(null);
const [createdIssueRef, setCreatedIssueRef] = useState<string | null>(null);
useEffect(() => {
setRouteDismissed(false);
}, [location.pathname]);
// Sync step and company when onboarding opens with options.
// Keep this independent from company-list refreshes so Step 1 completion
// doesn't get reset after creating a company.
useEffect(() => {
if (!onboardingOpen) return;
const cId = onboardingOptions.companyId ?? null;
setStep(onboardingOptions.initialStep ?? 1);
if (!effectiveOnboardingOpen) return;
const cId = effectiveOnboardingOptions.companyId ?? null;
setStep(effectiveOnboardingOptions.initialStep ?? 1);
setCreatedCompanyId(cId);
setCreatedCompanyPrefix(null);
}, [
onboardingOpen,
onboardingOptions.companyId,
onboardingOptions.initialStep
effectiveOnboardingOpen,
effectiveOnboardingOptions.companyId,
effectiveOnboardingOptions.initialStep
]);
// Backfill issue prefix for an existing company once companies are loaded.
useEffect(() => {
if (!onboardingOpen || !createdCompanyId || createdCompanyPrefix) return;
if (!effectiveOnboardingOpen || !createdCompanyId || createdCompanyPrefix) return;
const company = companies.find((c) => c.id === createdCompanyId);
if (company) setCreatedCompanyPrefix(company.issuePrefix);
}, [onboardingOpen, createdCompanyId, createdCompanyPrefix, companies]);
}, [effectiveOnboardingOpen, createdCompanyId, createdCompanyPrefix, companies]);
// Resize textarea when step 3 is shown or description changes
useEffect(() => {
@ -171,7 +193,7 @@ export function OnboardingWizard() {
? queryKeys.agents.adapterModels(createdCompanyId, adapterType)
: ["agents", "none", "adapter-models", adapterType],
queryFn: () => agentsApi.adapterModels(createdCompanyId!, adapterType),
enabled: Boolean(createdCompanyId) && onboardingOpen && step === 2
enabled: Boolean(createdCompanyId) && effectiveOnboardingOpen && step === 2
});
const isLocalAdapter =
adapterType === "claude_local" ||
@ -546,13 +568,16 @@ export function OnboardingWizard() {
}
}
if (!onboardingOpen) return null;
if (!effectiveOnboardingOpen) return null;
return (
<Dialog
open={onboardingOpen}
open={effectiveOnboardingOpen}
onOpenChange={(open) => {
if (!open) handleClose();
if (!open) {
setRouteDismissed(true);
handleClose();
}
}}
>
<DialogPortal>
@ -762,6 +787,12 @@ export function OnboardingWizard() {
icon: Gem,
desc: "Local Gemini agent"
},
{
value: "process" as const,
label: "Process",
icon: Terminal,
desc: "Run a local command"
},
{
value: "opencode_local" as const,
label: "OpenCode",