mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-18 11:40:39 +09:00
Merge public-gh/master into paperclip-company-import-export
This commit is contained in:
commit
9e19f1d005
49 changed files with 3997 additions and 2501 deletions
|
|
@ -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'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"]);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue