diff --git a/packages/shared/src/types/agent.ts b/packages/shared/src/types/agent.ts index 394de331..e33a1fb4 100644 --- a/packages/shared/src/types/agent.ts +++ b/packages/shared/src/types/agent.ts @@ -17,11 +17,13 @@ export interface AgentInstructionsFileSummary { language: string; markdown: boolean; isEntryFile: boolean; + editable: boolean; + deprecated: boolean; + virtual: boolean; } export interface AgentInstructionsFileDetail extends AgentInstructionsFileSummary { content: string; - editable: boolean; } export interface AgentInstructionsBundle { diff --git a/server/src/__tests__/agent-instructions-routes.test.ts b/server/src/__tests__/agent-instructions-routes.test.ts index 783daed7..3924fc04 100644 --- a/server/src/__tests__/agent-instructions-routes.test.ts +++ b/server/src/__tests__/agent-instructions-routes.test.ts @@ -109,7 +109,16 @@ describe("agent instructions bundle routes", () => { warnings: [], legacyPromptTemplateActive: 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({ path: "AGENTS.md", @@ -118,6 +127,8 @@ describe("agent instructions bundle routes", () => { markdown: true, isEntryFile: true, editable: true, + deprecated: false, + virtual: false, content: "# Agent\n", }); mockAgentInstructionsService.writeFile.mockResolvedValue({ @@ -129,6 +140,8 @@ describe("agent instructions bundle routes", () => { markdown: true, isEntryFile: true, editable: true, + deprecated: false, + virtual: false, content: "# Updated Agent\n", }, adapterConfig: { diff --git a/server/src/services/agent-instructions.ts b/server/src/services/agent-instructions.ts index 7df8006d..197fe960 100644 --- a/server/src/services/agent-instructions.ts +++ b/server/src/services/agent-instructions.ts @@ -10,6 +10,7 @@ const ENTRY_KEY = "instructionsEntryFile"; const FILE_KEY = "instructionsFilePath"; const PROMPT_KEY = "promptTemplate"; const BOOTSTRAP_PROMPT_KEY = "bootstrapPromptTemplate"; +const LEGACY_PROMPT_TEMPLATE_PATH = "promptTemplate.legacy.md"; type BundleMode = "managed" | "external"; @@ -26,6 +27,9 @@ type AgentInstructionsFileSummary = { language: string; markdown: boolean; isEntryFile: boolean; + editable: boolean; + deprecated: boolean; + virtual: boolean; }; type AgentInstructionsFileDetail = AgentInstructionsFileSummary & { @@ -171,6 +175,9 @@ async function readFileSummary(rootPath: string, relativePath: string, entryFile language: inferLanguage(relativePath), markdown: isMarkdown(relativePath), 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 { + 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 { agentId: agent.id, companyId: agent.companyId, @@ -250,7 +272,7 @@ function toBundle(agent: AgentLike, state: BundleState, files: AgentInstructions warnings: state.warnings, legacyPromptTemplateActive: state.legacyPromptTemplateActive, legacyBootstrapPromptTemplateActive: state.legacyBootstrapPromptTemplateActive, - files, + files: nextFiles, }; } @@ -317,6 +339,21 @@ export function agentInstructionsService() { async function readFile(agent: AgentLike, relativePath: string): Promise { 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"); const absolutePath = resolvePathWithinRoot(state.rootPath, relativePath); const [content, stat] = await Promise.all([ @@ -331,12 +368,14 @@ export function agentInstructionsService() { language: inferLanguage(normalizedPath), markdown: isMarkdown(normalizedPath), isEntryFile: normalizedPath === state.entryFile, - content, editable: true, + deprecated: false, + virtual: false, + content, }; } - async function ensureManagedBundle( + async function ensureWritableBundle( agent: AgentLike, options?: { clearLegacyPromptTemplate?: boolean }, ): Promise<{ adapterConfig: Record; state: BundleState }> { @@ -421,7 +460,21 @@ export function agentInstructionsService() { file: AgentInstructionsFileDetail; adapterConfig: Record; }> { - const prepared = await ensureManagedBundle(agent, options); + const current = deriveBundleState(agent); + if (relativePath === LEGACY_PROMPT_TEMPLATE_PATH) { + const adapterConfig: Record = { + ...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); await fs.mkdir(path.dirname(absolutePath), { recursive: true }); await fs.writeFile(absolutePath, content, "utf8"); @@ -438,6 +491,9 @@ export function agentInstructionsService() { adapterConfig: Record; }> { 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"); const normalizedPath = normalizeRelativeFilePath(relativePath); if (normalizedPath === state.entryFile) { @@ -525,7 +581,7 @@ export function agentInstructionsService() { writeFile, deleteFile, exportFiles, - ensureManagedBundle, + ensureManagedBundle: ensureWritableBundle, materializeManagedBundle, }; } diff --git a/ui/src/components/PackageFileTree.tsx b/ui/src/components/PackageFileTree.tsx index 0a449022..76fbb04c 100644 --- a/ui/src/components/PackageFileTree.tsx +++ b/ui/src/components/PackageFileTree.tsx @@ -175,49 +175,57 @@ export function PackageFileTree({ onToggleCheck, renderFileExtra, fileRowClassName, + showCheckboxes = true, depth = 0, }: { nodes: FileTreeNode[]; selectedFile: string | null; expandedDirs: Set; - checkedFiles: Set; + checkedFiles?: Set; onToggleDir: (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) */ renderFileExtra?: (node: FileTreeNode, checked: boolean) => ReactNode; /** Optional additional className for file rows */ fileRowClassName?: (node: FileTreeNode, checked: boolean) => string | undefined; + showCheckboxes?: boolean; depth?: number; }) { + const effectiveCheckedFiles = checkedFiles ?? new Set(); + return (
{nodes.map((node) => { const expanded = node.kind === "dir" && expandedDirs.has(node.path); if (node.kind === "dir") { const childFiles = collectAllPaths(node.children, "file"); - const allChecked = [...childFiles].every((p) => checkedFiles.has(p)); - const someChecked = [...childFiles].some((p) => checkedFiles.has(p)); + const allChecked = [...childFiles].every((p) => effectiveCheckedFiles.has(p)); + const someChecked = [...childFiles].some((p) => effectiveCheckedFiles.has(p)); return (
- + {showCheckboxes && ( + + )} ))}
-
- {[...new Set([currentEntryFile, ...fileOptions])].map((filePath) => { - const file = bundle?.files.find((entry) => entry.path === filePath); - return ( - - ); + setExpandedDirs((current) => { + const next = new Set(current); + if (next.has(dirPath)) next.delete(dirPath); + else next.add(dirPath); + return next; })} -
+ 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 ( + + {file.deprecated ? "deprecated" : file.isEntryFile ? "entry" : `${file.size}b`} + + ); + }} + />
@@ -1859,11 +1893,13 @@ function PromptsTab({

{selectedOrEntryFile}

{selectedFileExists - ? `${selectedFileDetail?.language ?? "text"} file` + ? selectedFileSummary?.deprecated + ? "Deprecated virtual file" + : `${selectedFileDetail?.language ?? "text"} file` : "New file in this bundle"}

- {selectedFileExists && selectedOrEntryFile !== currentEntryFile && ( + {selectedFileExists && !selectedFileSummary?.deprecated && selectedOrEntryFile !== currentEntryFile && (