Refine external instructions bundle handling

Keep existing instructionsFilePath agents in external-bundle mode during edits, expose legacy promptTemplate as a deprecated virtual file, and reuse the shared PackageFileTree component in the Prompts view.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta
2026-03-17 16:15:45 -05:00
parent 4fdcfe5515
commit 9d452eb120
5 changed files with 175 additions and 57 deletions

View File

@@ -29,6 +29,7 @@ import { EntityRow } from "../components/EntityRow";
import { Identity } from "../components/Identity";
import { PageSkeleton } from "../components/PageSkeleton";
import { BudgetPolicyCard } from "../components/BudgetPolicyCard";
import { PackageFileTree, buildFileTree } from "../components/PackageFileTree";
import { ScrollToBottom } from "../components/ScrollToBottom";
import { formatCents, formatDate, relativeTime, formatTokens, visibleRunCostUsd } from "../lib/utils";
import { cn } from "../lib/utils";
@@ -1495,6 +1496,7 @@ function PromptsTab({
entryFile: string;
} | null>(null);
const [newFilePath, setNewFilePath] = useState("");
const [expandedDirs, setExpandedDirs] = useState<Set<string>>(new Set());
const [awaitingRefresh, setAwaitingRefresh] = useState(false);
const lastFileVersionRef = useRef<string | null>(null);
@@ -1516,8 +1518,17 @@ function PromptsTab({
const currentEntryFile = bundleDraft?.entryFile ?? bundle?.entryFile ?? "AGENTS.md";
const currentRootPath = bundleDraft?.rootPath ?? bundle?.rootPath ?? "";
const fileOptions = bundle?.files.map((file) => file.path) ?? [];
const visibleFilePaths = useMemo(
() => [...new Set([currentEntryFile, ...fileOptions])],
[currentEntryFile, fileOptions],
);
const fileTree = useMemo(
() => buildFileTree(Object.fromEntries(visibleFilePaths.map((filePath) => [filePath, ""]))),
[visibleFilePaths],
);
const selectedOrEntryFile = selectedFile || currentEntryFile;
const selectedFileExists = fileOptions.includes(selectedOrEntryFile);
const selectedFileSummary = bundle?.files.find((file) => file.path === selectedOrEntryFile) ?? null;
const { data: selectedFileDetail, isLoading: fileLoading } = useQuery({
queryKey: queryKeys.agents.instructionsFile(agent.id, selectedOrEntryFile),
@@ -1585,6 +1596,19 @@ function PromptsTab({
}
}, [bundle, selectedFile]);
useEffect(() => {
const nextExpanded = new Set<string>();
for (const filePath of visibleFilePaths) {
const parts = filePath.split("/");
let currentPath = "";
for (let i = 0; i < parts.length - 1; i++) {
currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i]!;
nextExpanded.add(currentPath);
}
}
setExpandedDirs(nextExpanded);
}, [visibleFilePaths]);
useEffect(() => {
const versionKey = selectedFileDetail ? `${selectedFileDetail.path}:${selectedFileDetail.content}` : `draft:${selectedOrEntryFile}`;
if (awaitingRefresh) {
@@ -1708,7 +1732,7 @@ function PromptsTab({
{(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 fields are still active for this agent. The next bundle save will migrate behavior into file-backed instructions and clear those legacy fields.
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>
)}
@@ -1827,30 +1851,40 @@ function PromptsTab({
</Button>
))}
</div>
<div className="space-y-1">
{[...new Set([currentEntryFile, ...fileOptions])].map((filePath) => {
const file = bundle?.files.find((entry) => entry.path === filePath);
return (
<button
key={filePath}
type="button"
className={cn(
"flex w-full items-center justify-between rounded-md border px-3 py-2 text-left text-sm",
filePath === selectedOrEntryFile ? "border-foreground/30 bg-accent/30" : "border-border",
)}
onClick={() => {
setSelectedFile(filePath);
if (!fileOptions.includes(filePath)) setDraft("");
}}
>
<span className="truncate font-mono">{filePath}</span>
<span className="ml-3 shrink-0 text-[11px] text-muted-foreground">
{file?.isEntryFile ? "entry" : file ? `${file.size}b` : "new"}
</span>
</button>
);
<PackageFileTree
nodes={fileTree}
selectedFile={selectedOrEntryFile}
expandedDirs={expandedDirs}
checkedFiles={new Set()}
onToggleDir={(dirPath) => setExpandedDirs((current) => {
const next = new Set(current);
if (next.has(dirPath)) next.delete(dirPath);
else next.add(dirPath);
return next;
})}
</div>
onSelectFile={(filePath) => {
setSelectedFile(filePath);
if (!fileOptions.includes(filePath)) setDraft("");
}}
onToggleCheck={() => {}}
showCheckboxes={false}
renderFileExtra={(node) => {
const file = bundle?.files.find((entry) => entry.path === node.path);
if (!file) return null;
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>
);
}}
/>
</div>
<div className="border border-border rounded-lg p-4 space-y-3">
@@ -1859,11 +1893,13 @@ function PromptsTab({
<h4 className="text-sm font-medium font-mono">{selectedOrEntryFile}</h4>
<p className="text-xs text-muted-foreground">
{selectedFileExists
? `${selectedFileDetail?.language ?? "text"} file`
? selectedFileSummary?.deprecated
? "Deprecated virtual file"
: `${selectedFileDetail?.language ?? "text"} file`
: "New file in this bundle"}
</p>
</div>
{selectedFileExists && selectedOrEntryFile !== currentEntryFile && (
{selectedFileExists && !selectedFileSummary?.deprecated && selectedOrEntryFile !== currentEntryFile && (
<Button
type="button"
size="sm"