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:
@@ -17,11 +17,13 @@ export interface AgentInstructionsFileSummary {
|
|||||||
language: string;
|
language: string;
|
||||||
markdown: boolean;
|
markdown: boolean;
|
||||||
isEntryFile: boolean;
|
isEntryFile: boolean;
|
||||||
|
editable: boolean;
|
||||||
|
deprecated: boolean;
|
||||||
|
virtual: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentInstructionsFileDetail extends AgentInstructionsFileSummary {
|
export interface AgentInstructionsFileDetail extends AgentInstructionsFileSummary {
|
||||||
content: string;
|
content: string;
|
||||||
editable: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentInstructionsBundle {
|
export interface AgentInstructionsBundle {
|
||||||
|
|||||||
@@ -109,7 +109,16 @@ describe("agent instructions bundle routes", () => {
|
|||||||
warnings: [],
|
warnings: [],
|
||||||
legacyPromptTemplateActive: false,
|
legacyPromptTemplateActive: false,
|
||||||
legacyBootstrapPromptTemplateActive: false,
|
legacyBootstrapPromptTemplateActive: false,
|
||||||
files: [{ path: "AGENTS.md", size: 12, language: "markdown", markdown: true, isEntryFile: true }],
|
files: [{
|
||||||
|
path: "AGENTS.md",
|
||||||
|
size: 12,
|
||||||
|
language: "markdown",
|
||||||
|
markdown: true,
|
||||||
|
isEntryFile: true,
|
||||||
|
editable: true,
|
||||||
|
deprecated: false,
|
||||||
|
virtual: false,
|
||||||
|
}],
|
||||||
});
|
});
|
||||||
mockAgentInstructionsService.readFile.mockResolvedValue({
|
mockAgentInstructionsService.readFile.mockResolvedValue({
|
||||||
path: "AGENTS.md",
|
path: "AGENTS.md",
|
||||||
@@ -118,6 +127,8 @@ describe("agent instructions bundle routes", () => {
|
|||||||
markdown: true,
|
markdown: true,
|
||||||
isEntryFile: true,
|
isEntryFile: true,
|
||||||
editable: true,
|
editable: true,
|
||||||
|
deprecated: false,
|
||||||
|
virtual: false,
|
||||||
content: "# Agent\n",
|
content: "# Agent\n",
|
||||||
});
|
});
|
||||||
mockAgentInstructionsService.writeFile.mockResolvedValue({
|
mockAgentInstructionsService.writeFile.mockResolvedValue({
|
||||||
@@ -129,6 +140,8 @@ describe("agent instructions bundle routes", () => {
|
|||||||
markdown: true,
|
markdown: true,
|
||||||
isEntryFile: true,
|
isEntryFile: true,
|
||||||
editable: true,
|
editable: true,
|
||||||
|
deprecated: false,
|
||||||
|
virtual: false,
|
||||||
content: "# Updated Agent\n",
|
content: "# Updated Agent\n",
|
||||||
},
|
},
|
||||||
adapterConfig: {
|
adapterConfig: {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const ENTRY_KEY = "instructionsEntryFile";
|
|||||||
const FILE_KEY = "instructionsFilePath";
|
const FILE_KEY = "instructionsFilePath";
|
||||||
const PROMPT_KEY = "promptTemplate";
|
const PROMPT_KEY = "promptTemplate";
|
||||||
const BOOTSTRAP_PROMPT_KEY = "bootstrapPromptTemplate";
|
const BOOTSTRAP_PROMPT_KEY = "bootstrapPromptTemplate";
|
||||||
|
const LEGACY_PROMPT_TEMPLATE_PATH = "promptTemplate.legacy.md";
|
||||||
|
|
||||||
type BundleMode = "managed" | "external";
|
type BundleMode = "managed" | "external";
|
||||||
|
|
||||||
@@ -26,6 +27,9 @@ type AgentInstructionsFileSummary = {
|
|||||||
language: string;
|
language: string;
|
||||||
markdown: boolean;
|
markdown: boolean;
|
||||||
isEntryFile: boolean;
|
isEntryFile: boolean;
|
||||||
|
editable: boolean;
|
||||||
|
deprecated: boolean;
|
||||||
|
virtual: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AgentInstructionsFileDetail = AgentInstructionsFileSummary & {
|
type AgentInstructionsFileDetail = AgentInstructionsFileSummary & {
|
||||||
@@ -171,6 +175,9 @@ async function readFileSummary(rootPath: string, relativePath: string, entryFile
|
|||||||
language: inferLanguage(relativePath),
|
language: inferLanguage(relativePath),
|
||||||
markdown: isMarkdown(relativePath),
|
markdown: isMarkdown(relativePath),
|
||||||
isEntryFile: relativePath === entryFile,
|
isEntryFile: relativePath === entryFile,
|
||||||
|
editable: true,
|
||||||
|
deprecated: false,
|
||||||
|
virtual: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,6 +246,21 @@ function deriveBundleState(agent: AgentLike): BundleState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toBundle(agent: AgentLike, state: BundleState, files: AgentInstructionsFileSummary[]): AgentInstructionsBundle {
|
function toBundle(agent: AgentLike, state: BundleState, files: AgentInstructionsFileSummary[]): AgentInstructionsBundle {
|
||||||
|
const nextFiles = [...files];
|
||||||
|
if (state.legacyPromptTemplateActive && !nextFiles.some((file) => file.path === LEGACY_PROMPT_TEMPLATE_PATH)) {
|
||||||
|
const legacyPromptTemplate = asString(state.config[PROMPT_KEY]) ?? "";
|
||||||
|
nextFiles.push({
|
||||||
|
path: LEGACY_PROMPT_TEMPLATE_PATH,
|
||||||
|
size: legacyPromptTemplate.length,
|
||||||
|
language: "markdown",
|
||||||
|
markdown: true,
|
||||||
|
isEntryFile: false,
|
||||||
|
editable: true,
|
||||||
|
deprecated: true,
|
||||||
|
virtual: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
nextFiles.sort((left, right) => left.path.localeCompare(right.path));
|
||||||
return {
|
return {
|
||||||
agentId: agent.id,
|
agentId: agent.id,
|
||||||
companyId: agent.companyId,
|
companyId: agent.companyId,
|
||||||
@@ -250,7 +272,7 @@ function toBundle(agent: AgentLike, state: BundleState, files: AgentInstructions
|
|||||||
warnings: state.warnings,
|
warnings: state.warnings,
|
||||||
legacyPromptTemplateActive: state.legacyPromptTemplateActive,
|
legacyPromptTemplateActive: state.legacyPromptTemplateActive,
|
||||||
legacyBootstrapPromptTemplateActive: state.legacyBootstrapPromptTemplateActive,
|
legacyBootstrapPromptTemplateActive: state.legacyBootstrapPromptTemplateActive,
|
||||||
files,
|
files: nextFiles,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,6 +339,21 @@ export function agentInstructionsService() {
|
|||||||
|
|
||||||
async function readFile(agent: AgentLike, relativePath: string): Promise<AgentInstructionsFileDetail> {
|
async function readFile(agent: AgentLike, relativePath: string): Promise<AgentInstructionsFileDetail> {
|
||||||
const state = deriveBundleState(agent);
|
const state = deriveBundleState(agent);
|
||||||
|
if (relativePath === LEGACY_PROMPT_TEMPLATE_PATH) {
|
||||||
|
const content = asString(state.config[PROMPT_KEY]);
|
||||||
|
if (content === null) throw notFound("Instructions file not found");
|
||||||
|
return {
|
||||||
|
path: LEGACY_PROMPT_TEMPLATE_PATH,
|
||||||
|
size: content.length,
|
||||||
|
language: "markdown",
|
||||||
|
markdown: true,
|
||||||
|
isEntryFile: false,
|
||||||
|
editable: true,
|
||||||
|
deprecated: true,
|
||||||
|
virtual: true,
|
||||||
|
content,
|
||||||
|
};
|
||||||
|
}
|
||||||
if (!state.rootPath) throw notFound("Agent instructions bundle is not configured");
|
if (!state.rootPath) throw notFound("Agent instructions bundle is not configured");
|
||||||
const absolutePath = resolvePathWithinRoot(state.rootPath, relativePath);
|
const absolutePath = resolvePathWithinRoot(state.rootPath, relativePath);
|
||||||
const [content, stat] = await Promise.all([
|
const [content, stat] = await Promise.all([
|
||||||
@@ -331,12 +368,14 @@ export function agentInstructionsService() {
|
|||||||
language: inferLanguage(normalizedPath),
|
language: inferLanguage(normalizedPath),
|
||||||
markdown: isMarkdown(normalizedPath),
|
markdown: isMarkdown(normalizedPath),
|
||||||
isEntryFile: normalizedPath === state.entryFile,
|
isEntryFile: normalizedPath === state.entryFile,
|
||||||
content,
|
|
||||||
editable: true,
|
editable: true,
|
||||||
|
deprecated: false,
|
||||||
|
virtual: false,
|
||||||
|
content,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureManagedBundle(
|
async function ensureWritableBundle(
|
||||||
agent: AgentLike,
|
agent: AgentLike,
|
||||||
options?: { clearLegacyPromptTemplate?: boolean },
|
options?: { clearLegacyPromptTemplate?: boolean },
|
||||||
): Promise<{ adapterConfig: Record<string, unknown>; state: BundleState }> {
|
): Promise<{ adapterConfig: Record<string, unknown>; state: BundleState }> {
|
||||||
@@ -421,7 +460,21 @@ export function agentInstructionsService() {
|
|||||||
file: AgentInstructionsFileDetail;
|
file: AgentInstructionsFileDetail;
|
||||||
adapterConfig: Record<string, unknown>;
|
adapterConfig: Record<string, unknown>;
|
||||||
}> {
|
}> {
|
||||||
const prepared = await ensureManagedBundle(agent, options);
|
const current = deriveBundleState(agent);
|
||||||
|
if (relativePath === LEGACY_PROMPT_TEMPLATE_PATH) {
|
||||||
|
const adapterConfig: Record<string, unknown> = {
|
||||||
|
...current.config,
|
||||||
|
[PROMPT_KEY]: content,
|
||||||
|
};
|
||||||
|
const nextAgent = { ...agent, adapterConfig };
|
||||||
|
const [bundle, file] = await Promise.all([
|
||||||
|
getBundle(nextAgent),
|
||||||
|
readFile(nextAgent, LEGACY_PROMPT_TEMPLATE_PATH),
|
||||||
|
]);
|
||||||
|
return { bundle, file, adapterConfig };
|
||||||
|
}
|
||||||
|
|
||||||
|
const prepared = await ensureWritableBundle(agent, options);
|
||||||
const absolutePath = resolvePathWithinRoot(prepared.state.rootPath!, relativePath);
|
const absolutePath = resolvePathWithinRoot(prepared.state.rootPath!, relativePath);
|
||||||
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
||||||
await fs.writeFile(absolutePath, content, "utf8");
|
await fs.writeFile(absolutePath, content, "utf8");
|
||||||
@@ -438,6 +491,9 @@ export function agentInstructionsService() {
|
|||||||
adapterConfig: Record<string, unknown>;
|
adapterConfig: Record<string, unknown>;
|
||||||
}> {
|
}> {
|
||||||
const state = deriveBundleState(agent);
|
const state = deriveBundleState(agent);
|
||||||
|
if (relativePath === LEGACY_PROMPT_TEMPLATE_PATH) {
|
||||||
|
throw unprocessable("Cannot delete the legacy promptTemplate pseudo-file");
|
||||||
|
}
|
||||||
if (!state.rootPath) throw notFound("Agent instructions bundle is not configured");
|
if (!state.rootPath) throw notFound("Agent instructions bundle is not configured");
|
||||||
const normalizedPath = normalizeRelativeFilePath(relativePath);
|
const normalizedPath = normalizeRelativeFilePath(relativePath);
|
||||||
if (normalizedPath === state.entryFile) {
|
if (normalizedPath === state.entryFile) {
|
||||||
@@ -525,7 +581,7 @@ export function agentInstructionsService() {
|
|||||||
writeFile,
|
writeFile,
|
||||||
deleteFile,
|
deleteFile,
|
||||||
exportFiles,
|
exportFiles,
|
||||||
ensureManagedBundle,
|
ensureManagedBundle: ensureWritableBundle,
|
||||||
materializeManagedBundle,
|
materializeManagedBundle,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,49 +175,57 @@ export function PackageFileTree({
|
|||||||
onToggleCheck,
|
onToggleCheck,
|
||||||
renderFileExtra,
|
renderFileExtra,
|
||||||
fileRowClassName,
|
fileRowClassName,
|
||||||
|
showCheckboxes = true,
|
||||||
depth = 0,
|
depth = 0,
|
||||||
}: {
|
}: {
|
||||||
nodes: FileTreeNode[];
|
nodes: FileTreeNode[];
|
||||||
selectedFile: string | null;
|
selectedFile: string | null;
|
||||||
expandedDirs: Set<string>;
|
expandedDirs: Set<string>;
|
||||||
checkedFiles: Set<string>;
|
checkedFiles?: Set<string>;
|
||||||
onToggleDir: (path: string) => void;
|
onToggleDir: (path: string) => void;
|
||||||
onSelectFile: (path: string) => void;
|
onSelectFile: (path: string) => void;
|
||||||
onToggleCheck: (path: string, kind: "file" | "dir") => void;
|
onToggleCheck?: (path: string, kind: "file" | "dir") => void;
|
||||||
/** Optional extra content rendered at the end of each file row (e.g. action badge) */
|
/** Optional extra content rendered at the end of each file row (e.g. action badge) */
|
||||||
renderFileExtra?: (node: FileTreeNode, checked: boolean) => ReactNode;
|
renderFileExtra?: (node: FileTreeNode, checked: boolean) => ReactNode;
|
||||||
/** Optional additional className for file rows */
|
/** Optional additional className for file rows */
|
||||||
fileRowClassName?: (node: FileTreeNode, checked: boolean) => string | undefined;
|
fileRowClassName?: (node: FileTreeNode, checked: boolean) => string | undefined;
|
||||||
|
showCheckboxes?: boolean;
|
||||||
depth?: number;
|
depth?: number;
|
||||||
}) {
|
}) {
|
||||||
|
const effectiveCheckedFiles = checkedFiles ?? new Set<string>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{nodes.map((node) => {
|
{nodes.map((node) => {
|
||||||
const expanded = node.kind === "dir" && expandedDirs.has(node.path);
|
const expanded = node.kind === "dir" && expandedDirs.has(node.path);
|
||||||
if (node.kind === "dir") {
|
if (node.kind === "dir") {
|
||||||
const childFiles = collectAllPaths(node.children, "file");
|
const childFiles = collectAllPaths(node.children, "file");
|
||||||
const allChecked = [...childFiles].every((p) => checkedFiles.has(p));
|
const allChecked = [...childFiles].every((p) => effectiveCheckedFiles.has(p));
|
||||||
const someChecked = [...childFiles].some((p) => checkedFiles.has(p));
|
const someChecked = [...childFiles].some((p) => effectiveCheckedFiles.has(p));
|
||||||
return (
|
return (
|
||||||
<div key={node.path}>
|
<div key={node.path}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"group grid w-full grid-cols-[auto_minmax(0,1fr)_2.25rem] items-center gap-x-1 pr-3 text-left text-sm text-muted-foreground hover:bg-accent/30 hover:text-foreground",
|
showCheckboxes
|
||||||
|
? "group grid w-full grid-cols-[auto_minmax(0,1fr)_2.25rem] items-center gap-x-1 pr-3 text-left text-sm text-muted-foreground hover:bg-accent/30 hover:text-foreground"
|
||||||
|
: "group grid w-full grid-cols-[minmax(0,1fr)_2.25rem] items-center gap-x-1 pr-3 text-left text-sm text-muted-foreground hover:bg-accent/30 hover:text-foreground",
|
||||||
TREE_ROW_HEIGHT_CLASS,
|
TREE_ROW_HEIGHT_CLASS,
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
paddingInlineStart: `${TREE_BASE_INDENT + depth * TREE_STEP_INDENT - 8}px`,
|
paddingInlineStart: `${TREE_BASE_INDENT + depth * TREE_STEP_INDENT - 8}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<label className="flex items-center pl-2">
|
{showCheckboxes && (
|
||||||
<input
|
<label className="flex items-center pl-2">
|
||||||
type="checkbox"
|
<input
|
||||||
checked={allChecked}
|
type="checkbox"
|
||||||
ref={(el) => { if (el) el.indeterminate = someChecked && !allChecked; }}
|
checked={allChecked}
|
||||||
onChange={() => onToggleCheck(node.path, "dir")}
|
ref={(el) => { if (el) el.indeterminate = someChecked && !allChecked; }}
|
||||||
className="mr-2 accent-foreground"
|
onChange={() => onToggleCheck?.(node.path, "dir")}
|
||||||
/>
|
className="mr-2 accent-foreground"
|
||||||
</label>
|
/>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex min-w-0 items-center gap-2 py-1 text-left"
|
className="flex min-w-0 items-center gap-2 py-1 text-left"
|
||||||
@@ -249,12 +257,13 @@ export function PackageFileTree({
|
|||||||
nodes={node.children}
|
nodes={node.children}
|
||||||
selectedFile={selectedFile}
|
selectedFile={selectedFile}
|
||||||
expandedDirs={expandedDirs}
|
expandedDirs={expandedDirs}
|
||||||
checkedFiles={checkedFiles}
|
checkedFiles={effectiveCheckedFiles}
|
||||||
onToggleDir={onToggleDir}
|
onToggleDir={onToggleDir}
|
||||||
onSelectFile={onSelectFile}
|
onSelectFile={onSelectFile}
|
||||||
onToggleCheck={onToggleCheck}
|
onToggleCheck={onToggleCheck}
|
||||||
renderFileExtra={renderFileExtra}
|
renderFileExtra={renderFileExtra}
|
||||||
fileRowClassName={fileRowClassName}
|
fileRowClassName={fileRowClassName}
|
||||||
|
showCheckboxes={showCheckboxes}
|
||||||
depth={depth + 1}
|
depth={depth + 1}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -263,7 +272,7 @@ export function PackageFileTree({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FileIcon = fileIcon(node.name);
|
const FileIcon = fileIcon(node.name);
|
||||||
const checked = checkedFiles.has(node.path);
|
const checked = effectiveCheckedFiles.has(node.path);
|
||||||
const extraClassName = fileRowClassName?.(node, checked);
|
const extraClassName = fileRowClassName?.(node, checked);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -278,14 +287,16 @@ export function PackageFileTree({
|
|||||||
paddingInlineStart: `${TREE_BASE_INDENT + depth * TREE_STEP_INDENT - 8}px`,
|
paddingInlineStart: `${TREE_BASE_INDENT + depth * TREE_STEP_INDENT - 8}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<label className="flex items-center pl-2">
|
{showCheckboxes && (
|
||||||
<input
|
<label className="flex items-center pl-2">
|
||||||
type="checkbox"
|
<input
|
||||||
checked={checked}
|
type="checkbox"
|
||||||
onChange={() => onToggleCheck(node.path, "file")}
|
checked={checked}
|
||||||
className="mr-2 accent-foreground"
|
onChange={() => onToggleCheck?.(node.path, "file")}
|
||||||
/>
|
className="mr-2 accent-foreground"
|
||||||
</label>
|
/>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex min-w-0 flex-1 items-center gap-2 py-1 text-left"
|
className="flex min-w-0 flex-1 items-center gap-2 py-1 text-left"
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { EntityRow } from "../components/EntityRow";
|
|||||||
import { Identity } from "../components/Identity";
|
import { Identity } from "../components/Identity";
|
||||||
import { PageSkeleton } from "../components/PageSkeleton";
|
import { PageSkeleton } from "../components/PageSkeleton";
|
||||||
import { BudgetPolicyCard } from "../components/BudgetPolicyCard";
|
import { BudgetPolicyCard } from "../components/BudgetPolicyCard";
|
||||||
|
import { PackageFileTree, buildFileTree } from "../components/PackageFileTree";
|
||||||
import { ScrollToBottom } from "../components/ScrollToBottom";
|
import { ScrollToBottom } from "../components/ScrollToBottom";
|
||||||
import { formatCents, formatDate, relativeTime, formatTokens, visibleRunCostUsd } from "../lib/utils";
|
import { formatCents, formatDate, relativeTime, formatTokens, visibleRunCostUsd } from "../lib/utils";
|
||||||
import { cn } from "../lib/utils";
|
import { cn } from "../lib/utils";
|
||||||
@@ -1495,6 +1496,7 @@ function PromptsTab({
|
|||||||
entryFile: string;
|
entryFile: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [newFilePath, setNewFilePath] = useState("");
|
const [newFilePath, setNewFilePath] = useState("");
|
||||||
|
const [expandedDirs, setExpandedDirs] = useState<Set<string>>(new Set());
|
||||||
const [awaitingRefresh, setAwaitingRefresh] = useState(false);
|
const [awaitingRefresh, setAwaitingRefresh] = useState(false);
|
||||||
const lastFileVersionRef = useRef<string | null>(null);
|
const lastFileVersionRef = useRef<string | null>(null);
|
||||||
|
|
||||||
@@ -1516,8 +1518,17 @@ function PromptsTab({
|
|||||||
const currentEntryFile = bundleDraft?.entryFile ?? bundle?.entryFile ?? "AGENTS.md";
|
const currentEntryFile = bundleDraft?.entryFile ?? bundle?.entryFile ?? "AGENTS.md";
|
||||||
const currentRootPath = bundleDraft?.rootPath ?? bundle?.rootPath ?? "";
|
const currentRootPath = bundleDraft?.rootPath ?? bundle?.rootPath ?? "";
|
||||||
const fileOptions = bundle?.files.map((file) => file.path) ?? [];
|
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 selectedOrEntryFile = selectedFile || currentEntryFile;
|
||||||
const selectedFileExists = fileOptions.includes(selectedOrEntryFile);
|
const selectedFileExists = fileOptions.includes(selectedOrEntryFile);
|
||||||
|
const selectedFileSummary = bundle?.files.find((file) => file.path === selectedOrEntryFile) ?? null;
|
||||||
|
|
||||||
const { data: selectedFileDetail, isLoading: fileLoading } = useQuery({
|
const { data: selectedFileDetail, isLoading: fileLoading } = useQuery({
|
||||||
queryKey: queryKeys.agents.instructionsFile(agent.id, selectedOrEntryFile),
|
queryKey: queryKeys.agents.instructionsFile(agent.id, selectedOrEntryFile),
|
||||||
@@ -1585,6 +1596,19 @@ function PromptsTab({
|
|||||||
}
|
}
|
||||||
}, [bundle, selectedFile]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
const versionKey = selectedFileDetail ? `${selectedFileDetail.path}:${selectedFileDetail.content}` : `draft:${selectedOrEntryFile}`;
|
const versionKey = selectedFileDetail ? `${selectedFileDetail.path}:${selectedFileDetail.content}` : `draft:${selectedOrEntryFile}`;
|
||||||
if (awaitingRefresh) {
|
if (awaitingRefresh) {
|
||||||
@@ -1708,7 +1732,7 @@ function PromptsTab({
|
|||||||
|
|
||||||
{(bundle?.legacyPromptTemplateActive || bundle?.legacyBootstrapPromptTemplateActive) && (
|
{(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">
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1827,30 +1851,40 @@ function PromptsTab({
|
|||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<PackageFileTree
|
||||||
{[...new Set([currentEntryFile, ...fileOptions])].map((filePath) => {
|
nodes={fileTree}
|
||||||
const file = bundle?.files.find((entry) => entry.path === filePath);
|
selectedFile={selectedOrEntryFile}
|
||||||
return (
|
expandedDirs={expandedDirs}
|
||||||
<button
|
checkedFiles={new Set()}
|
||||||
key={filePath}
|
onToggleDir={(dirPath) => setExpandedDirs((current) => {
|
||||||
type="button"
|
const next = new Set(current);
|
||||||
className={cn(
|
if (next.has(dirPath)) next.delete(dirPath);
|
||||||
"flex w-full items-center justify-between rounded-md border px-3 py-2 text-left text-sm",
|
else next.add(dirPath);
|
||||||
filePath === selectedOrEntryFile ? "border-foreground/30 bg-accent/30" : "border-border",
|
return next;
|
||||||
)}
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</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>
|
||||||
|
|
||||||
<div className="border border-border rounded-lg p-4 space-y-3">
|
<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>
|
<h4 className="text-sm font-medium font-mono">{selectedOrEntryFile}</h4>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{selectedFileExists
|
{selectedFileExists
|
||||||
? `${selectedFileDetail?.language ?? "text"} file`
|
? selectedFileSummary?.deprecated
|
||||||
|
? "Deprecated virtual file"
|
||||||
|
: `${selectedFileDetail?.language ?? "text"} file`
|
||||||
: "New file in this bundle"}
|
: "New file in this bundle"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{selectedFileExists && selectedOrEntryFile !== currentEntryFile && (
|
{selectedFileExists && !selectedFileSummary?.deprecated && selectedOrEntryFile !== currentEntryFile && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
Reference in New Issue
Block a user