Redesign agent instructions tab (formerly Prompts)

- Rename Prompts tab to Instructions (with backwards-compatible URL routing)
- Update header/subheader text to "Instructions Bundle" / "Configure your agent's behavior with instructions"
- Remove standalone legacy prompt warning banner; move warning to deprecated virtual file badge with tooltip
- Move mode, root path, and entry file controls into a collapsible "Advanced" section below the file browser
- Add help tooltips (?) for mode, root path, and entry file fields
- Show full root path in managed mode with copy-to-clipboard icon
- Reorder: root path now appears to the left of entry file
- Remove HEARTBEAT.md, SOUL.md, TOOLS.md shortcut buttons from file browser
- Add key prop to MarkdownEditor to ensure proper re-mount on file selection change

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta
2026-03-18 06:47:53 -05:00
parent f98d821213
commit 6b355e1acf

View File

@@ -62,7 +62,10 @@ import {
ChevronRight,
ChevronDown,
ArrowLeft,
HelpCircle,
} from "lucide-react";
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible";
import { TooltipProvider } from "@/components/ui/tooltip";
import { Input } from "@/components/ui/input";
import { AgentIcon, AgentIconPicker } from "../components/AgentIconPicker";
import { RunTranscriptView, type TranscriptMode } from "../components/transcript/RunTranscriptView";
@@ -198,10 +201,10 @@ function scrollToContainerBottom(container: ScrollContainer, behavior: ScrollBeh
container.scrollTo({ top: container.scrollHeight, behavior });
}
type AgentDetailView = "dashboard" | "prompts" | "configuration" | "skills" | "runs" | "budget";
type AgentDetailView = "dashboard" | "instructions" | "configuration" | "skills" | "runs" | "budget";
function parseAgentDetailView(value: string | null): AgentDetailView {
if (value === "prompts") return value;
if (value === "instructions" || value === "prompts") return "instructions";
if (value === "configure" || value === "configuration") return "configuration";
if (value === "skills") return "skills";
if (value === "budget") return "budget";
@@ -601,8 +604,8 @@ export function AgentDetail() {
return;
}
const canonicalTab =
activeView === "prompts"
? "prompts"
activeView === "instructions"
? "instructions"
: activeView === "configuration"
? "configuration"
: activeView === "skills"
@@ -724,8 +727,8 @@ export function AgentDetail() {
if (urlRunId) {
crumbs.push({ label: "Runs", href: `/agents/${canonicalAgentRef}/runs` });
crumbs.push({ label: `Run ${urlRunId.slice(0, 8)}` });
} else if (activeView === "prompts") {
crumbs.push({ label: "Prompts" });
} else if (activeView === "instructions") {
crumbs.push({ label: "Instructions" });
} else if (activeView === "configuration") {
crumbs.push({ label: "Configuration" });
} else if (activeView === "skills") {
@@ -761,7 +764,7 @@ export function AgentDetail() {
return <Navigate to={`/agents/${canonicalAgentRef}/dashboard`} replace />;
}
const isPendingApproval = agent.status === "pending_approval";
const showConfigActionBar = (activeView === "configuration" || activeView === "prompts") && (configDirty || configSaving);
const showConfigActionBar = (activeView === "configuration" || activeView === "instructions") && (configDirty || configSaving);
return (
<div className={cn("space-y-6", isMobile && showConfigActionBar && "pb-24")}>
@@ -888,7 +891,7 @@ export function AgentDetail() {
<PageTabBar
items={[
{ value: "dashboard", label: "Dashboard" },
{ value: "prompts", label: "Prompts" },
{ value: "instructions", label: "Instructions" },
{ value: "skills", label: "Skills" },
{ value: "configuration", label: "Configuration" },
{ value: "budget", label: "Budget" },
@@ -974,7 +977,7 @@ export function AgentDetail() {
/>
)}
{activeView === "prompts" && (
{activeView === "instructions" && (
<PromptsTab
agent={agent}
companyId={resolvedCompanyId ?? undefined}
@@ -1734,7 +1737,7 @@ function PromptsTab({
<div>
<h3 className="text-sm font-medium">Instructions Bundle</h3>
<p className="mt-1 text-sm text-muted-foreground">
`AGENTS.md` is the entry file. Sibling files like `HEARTBEAT.md`, `SOUL.md`, `TOOLS.md`, and arbitrary custom files live in the same bundle.
Configure your agent's behavior with instructions
</p>
</div>
<div className="text-xs text-muted-foreground">
@@ -1742,81 +1745,12 @@ function PromptsTab({
</div>
</div>
{(bundle?.legacyPromptTemplateActive || bundle?.legacyBootstrapPromptTemplateActive) && (
<div className="rounded-md border border-amber-500/25 bg-amber-500/10 px-3 py-2 text-xs text-amber-100">
Legacy inline prompt state is still present. `promptTemplate` now appears as a deprecated virtual file entry so it is visible without masquerading as the live `AGENTS.md`.
</div>
)}
{(bundle?.warnings ?? []).map((warning) => (
<div key={warning} className="rounded-md border border-sky-500/25 bg-sky-500/10 px-3 py-2 text-xs text-sky-100">
{warning}
</div>
))}
<div className="grid gap-3 md:grid-cols-3">
<label className="space-y-1">
<span className="text-xs font-medium text-muted-foreground">Mode</span>
<div className="flex gap-2">
<Button
type="button"
size="sm"
variant={currentMode === "managed" ? "default" : "outline"}
onClick={() => setBundleDraft((current) => ({
mode: "managed",
rootPath: current?.rootPath ?? bundle?.rootPath ?? "",
entryFile: current?.entryFile ?? bundle?.entryFile ?? "AGENTS.md",
}))}
>
Managed
</Button>
<Button
type="button"
size="sm"
variant={currentMode === "external" ? "default" : "outline"}
onClick={() => setBundleDraft((current) => ({
mode: "external",
rootPath: current?.rootPath ?? bundle?.rootPath ?? "",
entryFile: current?.entryFile ?? bundle?.entryFile ?? "AGENTS.md",
}))}
>
External
</Button>
</div>
</label>
<label className="space-y-1">
<span className="text-xs font-medium text-muted-foreground">Entry file</span>
<Input
value={currentEntryFile}
onChange={(event) => {
const nextEntryFile = event.target.value || "AGENTS.md";
if (selectedOrEntryFile === currentEntryFile) {
setSelectedFile(nextEntryFile);
}
setBundleDraft((current) => ({
mode: current?.mode ?? bundle?.mode ?? "managed",
rootPath: current?.rootPath ?? bundle?.rootPath ?? "",
entryFile: nextEntryFile,
}));
}}
className="font-mono text-sm"
/>
</label>
<label className="space-y-1">
<span className="text-xs font-medium text-muted-foreground">Root path</span>
<Input
value={currentRootPath}
onChange={(event) => setBundleDraft((current) => ({
mode: current?.mode ?? bundle?.mode ?? "managed",
rootPath: event.target.value,
entryFile: current?.entryFile ?? bundle?.entryFile ?? "AGENTS.md",
}))}
disabled={currentMode === "managed"}
className="font-mono text-sm"
placeholder={currentMode === "managed" ? "Managed by Paperclip" : "/absolute/path/to/agent/prompts"}
/>
</label>
</div>
</div>
<div className="grid gap-4 lg:grid-cols-[260px_minmax(0,1fr)]">
@@ -1847,22 +1781,6 @@ function PromptsTab({
className="font-mono text-sm"
/>
</div>
<div className="flex flex-wrap gap-2">
{["HEARTBEAT.md", "SOUL.md", "TOOLS.md"].map((filePath) => (
<Button
key={filePath}
type="button"
size="sm"
variant="outline"
onClick={() => {
setSelectedFile(filePath);
if (!fileOptions.includes(filePath)) setDraft("");
}}
>
{filePath}
</Button>
))}
</div>
<PackageFileTree
nodes={fileTree}
selectedFile={selectedOrEntryFile}
@@ -1883,20 +1801,148 @@ function PromptsTab({
renderFileExtra={(node) => {
const file = bundle?.files.find((entry) => entry.path === node.path);
if (!file) return null;
if (file.deprecated) {
return (
<Tooltip>
<TooltipTrigger asChild>
<span className="ml-3 shrink-0 rounded border border-amber-500/40 bg-amber-500/10 text-amber-200 px-1.5 py-0.5 text-[10px] uppercase tracking-wide cursor-help">
virtual file
</span>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={4}>
Legacy inline prompt — this deprecated virtual file preserves the old promptTemplate content
</TooltipContent>
</Tooltip>
);
}
return (
<span
className={cn(
"ml-3 shrink-0 rounded border px-1.5 py-0.5 text-[10px] uppercase tracking-wide",
file.deprecated
? "border-amber-500/40 bg-amber-500/10 text-amber-200"
: "border-border text-muted-foreground",
)}
>
{file.deprecated ? "deprecated" : file.isEntryFile ? "entry" : `${file.size}b`}
<span className="ml-3 shrink-0 rounded border border-border text-muted-foreground px-1.5 py-0.5 text-[10px] uppercase tracking-wide">
{file.isEntryFile ? "entry" : `${file.size}b`}
</span>
);
}}
/>
<Collapsible>
<CollapsibleTrigger className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors group w-full">
<ChevronRight className="h-3 w-3 transition-transform group-data-[state=open]:rotate-90" />
Advanced
</CollapsibleTrigger>
<CollapsibleContent className="pt-3 space-y-3">
<TooltipProvider>
<label className="space-y-1">
<span className="text-xs font-medium text-muted-foreground flex items-center gap-1">
Mode
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="h-3 w-3 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent side="right" sideOffset={4}>
Managed: Paperclip stores and serves the instructions bundle. External: you provide a path on disk where the instructions live.
</TooltipContent>
</Tooltip>
</span>
<div className="flex gap-2">
<Button
type="button"
size="sm"
variant={currentMode === "managed" ? "default" : "outline"}
onClick={() => setBundleDraft((current) => ({
mode: "managed",
rootPath: current?.rootPath ?? bundle?.rootPath ?? "",
entryFile: current?.entryFile ?? bundle?.entryFile ?? "AGENTS.md",
}))}
>
Managed
</Button>
<Button
type="button"
size="sm"
variant={currentMode === "external" ? "default" : "outline"}
onClick={() => setBundleDraft((current) => ({
mode: "external",
rootPath: current?.rootPath ?? bundle?.rootPath ?? "",
entryFile: current?.entryFile ?? bundle?.entryFile ?? "AGENTS.md",
}))}
>
External
</Button>
</div>
</label>
<div className="grid gap-3 grid-cols-2">
<label className="space-y-1">
<span className="text-xs font-medium text-muted-foreground flex items-center gap-1">
Root path
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="h-3 w-3 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent side="right" sideOffset={4}>
The absolute directory on disk where the instructions bundle lives. In managed mode this is set by Paperclip automatically.
</TooltipContent>
</Tooltip>
</span>
{currentMode === "managed" ? (
<div className="flex items-center gap-1.5 font-mono text-sm text-muted-foreground">
<span className="truncate">{currentRootPath || "(managed)"}</span>
{currentRootPath && (
<CopyText text={currentRootPath} className="shrink-0">
<Copy className="h-3.5 w-3.5" />
</CopyText>
)}
</div>
) : (
<div className="flex items-center gap-1.5">
<Input
value={currentRootPath}
onChange={(event) => setBundleDraft((current) => ({
mode: current?.mode ?? bundle?.mode ?? "managed",
rootPath: event.target.value,
entryFile: current?.entryFile ?? bundle?.entryFile ?? "AGENTS.md",
}))}
className="font-mono text-sm"
placeholder="/absolute/path/to/agent/prompts"
/>
{currentRootPath && (
<CopyText text={currentRootPath} className="shrink-0">
<Copy className="h-3.5 w-3.5" />
</CopyText>
)}
</div>
)}
</label>
<label className="space-y-1">
<span className="text-xs font-medium text-muted-foreground flex items-center gap-1">
Entry file
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="h-3 w-3 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent side="right" sideOffset={4}>
The main file the agent reads first when loading instructions. Defaults to AGENTS.md.
</TooltipContent>
</Tooltip>
</span>
<Input
value={currentEntryFile}
onChange={(event) => {
const nextEntryFile = event.target.value || "AGENTS.md";
if (selectedOrEntryFile === currentEntryFile) {
setSelectedFile(nextEntryFile);
}
setBundleDraft((current) => ({
mode: current?.mode ?? bundle?.mode ?? "managed",
rootPath: current?.rootPath ?? bundle?.rootPath ?? "",
entryFile: nextEntryFile,
}));
}}
className="font-mono text-sm"
/>
</label>
</div>
</TooltipProvider>
</CollapsibleContent>
</Collapsible>
</div>
<div className="border border-border rounded-lg p-4 space-y-3">
@@ -1937,6 +1983,7 @@ function PromptsTab({
<PromptEditorSkeleton />
) : isMarkdown(selectedOrEntryFile) ? (
<MarkdownEditor
key={selectedOrEntryFile}
value={displayValue}
onChange={(value) => setDraft(value ?? "")}
placeholder="# Agent instructions"