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:
@@ -62,7 +62,10 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
|
HelpCircle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible";
|
||||||
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { AgentIcon, AgentIconPicker } from "../components/AgentIconPicker";
|
import { AgentIcon, AgentIconPicker } from "../components/AgentIconPicker";
|
||||||
import { RunTranscriptView, type TranscriptMode } from "../components/transcript/RunTranscriptView";
|
import { RunTranscriptView, type TranscriptMode } from "../components/transcript/RunTranscriptView";
|
||||||
@@ -198,10 +201,10 @@ function scrollToContainerBottom(container: ScrollContainer, behavior: ScrollBeh
|
|||||||
container.scrollTo({ top: container.scrollHeight, behavior });
|
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 {
|
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 === "configure" || value === "configuration") return "configuration";
|
||||||
if (value === "skills") return "skills";
|
if (value === "skills") return "skills";
|
||||||
if (value === "budget") return "budget";
|
if (value === "budget") return "budget";
|
||||||
@@ -601,8 +604,8 @@ export function AgentDetail() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const canonicalTab =
|
const canonicalTab =
|
||||||
activeView === "prompts"
|
activeView === "instructions"
|
||||||
? "prompts"
|
? "instructions"
|
||||||
: activeView === "configuration"
|
: activeView === "configuration"
|
||||||
? "configuration"
|
? "configuration"
|
||||||
: activeView === "skills"
|
: activeView === "skills"
|
||||||
@@ -724,8 +727,8 @@ export function AgentDetail() {
|
|||||||
if (urlRunId) {
|
if (urlRunId) {
|
||||||
crumbs.push({ label: "Runs", href: `/agents/${canonicalAgentRef}/runs` });
|
crumbs.push({ label: "Runs", href: `/agents/${canonicalAgentRef}/runs` });
|
||||||
crumbs.push({ label: `Run ${urlRunId.slice(0, 8)}` });
|
crumbs.push({ label: `Run ${urlRunId.slice(0, 8)}` });
|
||||||
} else if (activeView === "prompts") {
|
} else if (activeView === "instructions") {
|
||||||
crumbs.push({ label: "Prompts" });
|
crumbs.push({ label: "Instructions" });
|
||||||
} else if (activeView === "configuration") {
|
} else if (activeView === "configuration") {
|
||||||
crumbs.push({ label: "Configuration" });
|
crumbs.push({ label: "Configuration" });
|
||||||
} else if (activeView === "skills") {
|
} else if (activeView === "skills") {
|
||||||
@@ -761,7 +764,7 @@ export function AgentDetail() {
|
|||||||
return <Navigate to={`/agents/${canonicalAgentRef}/dashboard`} replace />;
|
return <Navigate to={`/agents/${canonicalAgentRef}/dashboard`} replace />;
|
||||||
}
|
}
|
||||||
const isPendingApproval = agent.status === "pending_approval";
|
const isPendingApproval = agent.status === "pending_approval";
|
||||||
const showConfigActionBar = (activeView === "configuration" || activeView === "prompts") && (configDirty || configSaving);
|
const showConfigActionBar = (activeView === "configuration" || activeView === "instructions") && (configDirty || configSaving);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("space-y-6", isMobile && showConfigActionBar && "pb-24")}>
|
<div className={cn("space-y-6", isMobile && showConfigActionBar && "pb-24")}>
|
||||||
@@ -888,7 +891,7 @@ export function AgentDetail() {
|
|||||||
<PageTabBar
|
<PageTabBar
|
||||||
items={[
|
items={[
|
||||||
{ value: "dashboard", label: "Dashboard" },
|
{ value: "dashboard", label: "Dashboard" },
|
||||||
{ value: "prompts", label: "Prompts" },
|
{ value: "instructions", label: "Instructions" },
|
||||||
{ value: "skills", label: "Skills" },
|
{ value: "skills", label: "Skills" },
|
||||||
{ value: "configuration", label: "Configuration" },
|
{ value: "configuration", label: "Configuration" },
|
||||||
{ value: "budget", label: "Budget" },
|
{ value: "budget", label: "Budget" },
|
||||||
@@ -974,7 +977,7 @@ export function AgentDetail() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeView === "prompts" && (
|
{activeView === "instructions" && (
|
||||||
<PromptsTab
|
<PromptsTab
|
||||||
agent={agent}
|
agent={agent}
|
||||||
companyId={resolvedCompanyId ?? undefined}
|
companyId={resolvedCompanyId ?? undefined}
|
||||||
@@ -1734,7 +1737,7 @@ function PromptsTab({
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-medium">Instructions Bundle</h3>
|
<h3 className="text-sm font-medium">Instructions Bundle</h3>
|
||||||
<p className="mt-1 text-sm text-muted-foreground">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
@@ -1742,81 +1745,12 @@ function PromptsTab({
|
|||||||
</div>
|
</div>
|
||||||
</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) => (
|
{(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">
|
<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}
|
{warning}
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div className="grid gap-4 lg:grid-cols-[260px_minmax(0,1fr)]">
|
<div className="grid gap-4 lg:grid-cols-[260px_minmax(0,1fr)]">
|
||||||
@@ -1847,22 +1781,6 @@ function PromptsTab({
|
|||||||
className="font-mono text-sm"
|
className="font-mono text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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
|
<PackageFileTree
|
||||||
nodes={fileTree}
|
nodes={fileTree}
|
||||||
selectedFile={selectedOrEntryFile}
|
selectedFile={selectedOrEntryFile}
|
||||||
@@ -1883,20 +1801,148 @@ function PromptsTab({
|
|||||||
renderFileExtra={(node) => {
|
renderFileExtra={(node) => {
|
||||||
const file = bundle?.files.find((entry) => entry.path === node.path);
|
const file = bundle?.files.find((entry) => entry.path === node.path);
|
||||||
if (!file) return null;
|
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 (
|
return (
|
||||||
<span
|
<span className="ml-3 shrink-0 rounded border border-border text-muted-foreground px-1.5 py-0.5 text-[10px] uppercase tracking-wide">
|
||||||
className={cn(
|
{file.isEntryFile ? "entry" : `${file.size}b`}
|
||||||
"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>
|
</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>
|
||||||
|
|
||||||
<div className="border border-border rounded-lg p-4 space-y-3">
|
<div className="border border-border rounded-lg p-4 space-y-3">
|
||||||
@@ -1937,6 +1983,7 @@ function PromptsTab({
|
|||||||
<PromptEditorSkeleton />
|
<PromptEditorSkeleton />
|
||||||
) : isMarkdown(selectedOrEntryFile) ? (
|
) : isMarkdown(selectedOrEntryFile) ? (
|
||||||
<MarkdownEditor
|
<MarkdownEditor
|
||||||
|
key={selectedOrEntryFile}
|
||||||
value={displayValue}
|
value={displayValue}
|
||||||
onChange={(value) => setDraft(value ?? "")}
|
onChange={(value) => setDraft(value ?? "")}
|
||||||
placeholder="# Agent instructions"
|
placeholder="# Agent instructions"
|
||||||
|
|||||||
Reference in New Issue
Block a user