Improve instructions bundle mode switching
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -31,6 +31,7 @@ export interface AgentInstructionsBundle {
|
|||||||
companyId: string;
|
companyId: string;
|
||||||
mode: AgentInstructionsBundleMode | null;
|
mode: AgentInstructionsBundleMode | null;
|
||||||
rootPath: string | null;
|
rootPath: string | null;
|
||||||
|
managedRootPath: string;
|
||||||
entryFile: string;
|
entryFile: string;
|
||||||
resolvedEntryPath: string | null;
|
resolvedEntryPath: string | null;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ describe("agent instructions bundle routes", () => {
|
|||||||
companyId: "company-1",
|
companyId: "company-1",
|
||||||
mode: "managed",
|
mode: "managed",
|
||||||
rootPath: "/tmp/agent-1",
|
rootPath: "/tmp/agent-1",
|
||||||
|
managedRootPath: "/tmp/agent-1",
|
||||||
entryFile: "AGENTS.md",
|
entryFile: "AGENTS.md",
|
||||||
resolvedEntryPath: "/tmp/agent-1/AGENTS.md",
|
resolvedEntryPath: "/tmp/agent-1/AGENTS.md",
|
||||||
editable: true,
|
editable: true,
|
||||||
@@ -161,6 +162,7 @@ describe("agent instructions bundle routes", () => {
|
|||||||
expect(res.body).toMatchObject({
|
expect(res.body).toMatchObject({
|
||||||
mode: "managed",
|
mode: "managed",
|
||||||
rootPath: "/tmp/agent-1",
|
rootPath: "/tmp/agent-1",
|
||||||
|
managedRootPath: "/tmp/agent-1",
|
||||||
entryFile: "AGENTS.md",
|
entryFile: "AGENTS.md",
|
||||||
});
|
});
|
||||||
expect(mockAgentInstructionsService.getBundle).toHaveBeenCalled();
|
expect(mockAgentInstructionsService.getBundle).toHaveBeenCalled();
|
||||||
|
|||||||
123
server/src/__tests__/agent-instructions-service.test.ts
Normal file
123
server/src/__tests__/agent-instructions-service.test.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
|
import { agentInstructionsService } from "../services/agent-instructions.js";
|
||||||
|
|
||||||
|
type TestAgent = {
|
||||||
|
id: string;
|
||||||
|
companyId: string;
|
||||||
|
name: string;
|
||||||
|
adapterConfig: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function makeTempDir(prefix: string) {
|
||||||
|
return fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeAgent(adapterConfig: Record<string, unknown>): TestAgent {
|
||||||
|
return {
|
||||||
|
id: "agent-1",
|
||||||
|
companyId: "company-1",
|
||||||
|
name: "Agent 1",
|
||||||
|
adapterConfig,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("agent instructions service", () => {
|
||||||
|
const originalPaperclipHome = process.env.PAPERCLIP_HOME;
|
||||||
|
const originalPaperclipInstanceId = process.env.PAPERCLIP_INSTANCE_ID;
|
||||||
|
const cleanupDirs = new Set<string>();
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (originalPaperclipHome === undefined) delete process.env.PAPERCLIP_HOME;
|
||||||
|
else process.env.PAPERCLIP_HOME = originalPaperclipHome;
|
||||||
|
if (originalPaperclipInstanceId === undefined) delete process.env.PAPERCLIP_INSTANCE_ID;
|
||||||
|
else process.env.PAPERCLIP_INSTANCE_ID = originalPaperclipInstanceId;
|
||||||
|
|
||||||
|
await Promise.all([...cleanupDirs].map(async (dir) => {
|
||||||
|
await fs.rm(dir, { recursive: true, force: true });
|
||||||
|
cleanupDirs.delete(dir);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("copies the existing bundle into the managed root when switching to managed mode", async () => {
|
||||||
|
const paperclipHome = await makeTempDir("paperclip-agent-instructions-home-");
|
||||||
|
const externalRoot = await makeTempDir("paperclip-agent-instructions-external-");
|
||||||
|
cleanupDirs.add(paperclipHome);
|
||||||
|
cleanupDirs.add(externalRoot);
|
||||||
|
process.env.PAPERCLIP_HOME = paperclipHome;
|
||||||
|
process.env.PAPERCLIP_INSTANCE_ID = "test-instance";
|
||||||
|
|
||||||
|
await fs.writeFile(path.join(externalRoot, "AGENTS.md"), "# External Agent\n", "utf8");
|
||||||
|
await fs.mkdir(path.join(externalRoot, "docs"), { recursive: true });
|
||||||
|
await fs.writeFile(path.join(externalRoot, "docs", "TOOLS.md"), "## Tools\n", "utf8");
|
||||||
|
|
||||||
|
const svc = agentInstructionsService();
|
||||||
|
const agent = makeAgent({
|
||||||
|
instructionsBundleMode: "external",
|
||||||
|
instructionsRootPath: externalRoot,
|
||||||
|
instructionsEntryFile: "AGENTS.md",
|
||||||
|
instructionsFilePath: path.join(externalRoot, "AGENTS.md"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await svc.updateBundle(agent, { mode: "managed" });
|
||||||
|
|
||||||
|
expect(result.bundle.mode).toBe("managed");
|
||||||
|
expect(result.bundle.managedRootPath).toBe(
|
||||||
|
path.join(
|
||||||
|
paperclipHome,
|
||||||
|
"instances",
|
||||||
|
"test-instance",
|
||||||
|
"companies",
|
||||||
|
"company-1",
|
||||||
|
"agents",
|
||||||
|
"agent-1",
|
||||||
|
"instructions",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(result.bundle.files.map((file) => file.path)).toEqual(["AGENTS.md", "docs/TOOLS.md"]);
|
||||||
|
await expect(fs.readFile(path.join(result.bundle.managedRootPath, "AGENTS.md"), "utf8")).resolves.toBe("# External Agent\n");
|
||||||
|
await expect(fs.readFile(path.join(result.bundle.managedRootPath, "docs", "TOOLS.md"), "utf8")).resolves.toBe("## Tools\n");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates the target entry file when switching to a new external root", async () => {
|
||||||
|
const paperclipHome = await makeTempDir("paperclip-agent-instructions-home-");
|
||||||
|
const managedRoot = path.join(
|
||||||
|
paperclipHome,
|
||||||
|
"instances",
|
||||||
|
"test-instance",
|
||||||
|
"companies",
|
||||||
|
"company-1",
|
||||||
|
"agents",
|
||||||
|
"agent-1",
|
||||||
|
"instructions",
|
||||||
|
);
|
||||||
|
const externalRoot = await makeTempDir("paperclip-agent-instructions-new-external-");
|
||||||
|
cleanupDirs.add(paperclipHome);
|
||||||
|
cleanupDirs.add(externalRoot);
|
||||||
|
process.env.PAPERCLIP_HOME = paperclipHome;
|
||||||
|
process.env.PAPERCLIP_INSTANCE_ID = "test-instance";
|
||||||
|
|
||||||
|
await fs.mkdir(managedRoot, { recursive: true });
|
||||||
|
await fs.writeFile(path.join(managedRoot, "AGENTS.md"), "# Managed Agent\n", "utf8");
|
||||||
|
|
||||||
|
const svc = agentInstructionsService();
|
||||||
|
const agent = makeAgent({
|
||||||
|
instructionsBundleMode: "managed",
|
||||||
|
instructionsRootPath: managedRoot,
|
||||||
|
instructionsEntryFile: "AGENTS.md",
|
||||||
|
instructionsFilePath: path.join(managedRoot, "AGENTS.md"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await svc.updateBundle(agent, {
|
||||||
|
mode: "external",
|
||||||
|
rootPath: externalRoot,
|
||||||
|
entryFile: "docs/AGENTS.md",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.bundle.mode).toBe("external");
|
||||||
|
expect(result.bundle.rootPath).toBe(externalRoot);
|
||||||
|
await expect(fs.readFile(path.join(externalRoot, "docs", "AGENTS.md"), "utf8")).resolves.toBe("# Managed Agent\n");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -42,6 +42,7 @@ type AgentInstructionsBundle = {
|
|||||||
companyId: string;
|
companyId: string;
|
||||||
mode: BundleMode | null;
|
mode: BundleMode | null;
|
||||||
rootPath: string | null;
|
rootPath: string | null;
|
||||||
|
managedRootPath: string;
|
||||||
entryFile: string;
|
entryFile: string;
|
||||||
resolvedEntryPath: string | null;
|
resolvedEntryPath: string | null;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
@@ -266,6 +267,7 @@ function toBundle(agent: AgentLike, state: BundleState, files: AgentInstructions
|
|||||||
companyId: agent.companyId,
|
companyId: agent.companyId,
|
||||||
mode: state.mode,
|
mode: state.mode,
|
||||||
rootPath: state.rootPath,
|
rootPath: state.rootPath,
|
||||||
|
managedRootPath: resolveManagedInstructionsRoot(agent),
|
||||||
entryFile: state.entryFile,
|
entryFile: state.entryFile,
|
||||||
resolvedEntryPath: state.resolvedEntryPath,
|
resolvedEntryPath: state.resolvedEntryPath,
|
||||||
editable: Boolean(state.rootPath),
|
editable: Boolean(state.rootPath),
|
||||||
@@ -299,6 +301,21 @@ function applyBundleConfig(
|
|||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function writeBundleFiles(
|
||||||
|
rootPath: string,
|
||||||
|
files: Record<string, string>,
|
||||||
|
options?: { overwriteExisting?: boolean },
|
||||||
|
) {
|
||||||
|
for (const [relativePath, content] of Object.entries(files)) {
|
||||||
|
const normalizedPath = normalizeRelativeFilePath(relativePath);
|
||||||
|
const absolutePath = resolvePathWithinRoot(rootPath, normalizedPath);
|
||||||
|
const existingStat = await statIfExists(absolutePath);
|
||||||
|
if (existingStat?.isFile() && !options?.overwriteExisting) continue;
|
||||||
|
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
||||||
|
await fs.writeFile(absolutePath, content, "utf8");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function syncInstructionsBundleConfigFromFilePath(
|
export function syncInstructionsBundleConfigFromFilePath(
|
||||||
agent: AgentLike,
|
agent: AgentLike,
|
||||||
adapterConfig: Record<string, unknown>,
|
adapterConfig: Record<string, unknown>,
|
||||||
@@ -440,6 +457,17 @@ export function agentInstructionsService() {
|
|||||||
|
|
||||||
await fs.mkdir(nextRootPath, { recursive: true });
|
await fs.mkdir(nextRootPath, { recursive: true });
|
||||||
|
|
||||||
|
const existingFiles = await listFilesRecursive(nextRootPath);
|
||||||
|
const exported = await exportFiles(agent);
|
||||||
|
if (existingFiles.length === 0) {
|
||||||
|
await writeBundleFiles(nextRootPath, exported.files);
|
||||||
|
}
|
||||||
|
const refreshedFiles = existingFiles.length === 0 ? await listFilesRecursive(nextRootPath) : existingFiles;
|
||||||
|
if (!refreshedFiles.includes(nextEntryFile)) {
|
||||||
|
const nextEntryContent = exported.files[nextEntryFile] ?? exported.files[exported.entryFile] ?? "";
|
||||||
|
await writeBundleFiles(nextRootPath, { [nextEntryFile]: nextEntryContent });
|
||||||
|
}
|
||||||
|
|
||||||
const nextConfig = applyBundleConfig(state.config, {
|
const nextConfig = applyBundleConfig(state.config, {
|
||||||
mode: nextMode,
|
mode: nextMode,
|
||||||
rootPath: nextRootPath,
|
rootPath: nextRootPath,
|
||||||
|
|||||||
@@ -1513,6 +1513,11 @@ function PromptsTab({
|
|||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [awaitingRefresh, setAwaitingRefresh] = useState(false);
|
const [awaitingRefresh, setAwaitingRefresh] = useState(false);
|
||||||
const lastFileVersionRef = useRef<string | null>(null);
|
const lastFileVersionRef = useRef<string | null>(null);
|
||||||
|
const externalBundleRef = useRef<{
|
||||||
|
rootPath: string;
|
||||||
|
entryFile: string;
|
||||||
|
selectedFile: string;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
const isLocal =
|
const isLocal =
|
||||||
agent.adapterType === "claude_local" ||
|
agent.adapterType === "claude_local" ||
|
||||||
@@ -1528,23 +1533,35 @@ function PromptsTab({
|
|||||||
enabled: Boolean(companyId && isLocal),
|
enabled: Boolean(companyId && isLocal),
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentMode = bundleDraft?.mode ?? bundle?.mode ?? "managed";
|
const persistedMode = bundle?.mode ?? "managed";
|
||||||
|
const persistedRootPath = persistedMode === "managed"
|
||||||
|
? (bundle?.managedRootPath ?? bundle?.rootPath ?? "")
|
||||||
|
: (bundle?.rootPath ?? "");
|
||||||
|
const currentMode = bundleDraft?.mode ?? persistedMode;
|
||||||
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 ?? persistedRootPath;
|
||||||
const fileOptions = useMemo(
|
const fileOptions = useMemo(
|
||||||
() => bundle?.files.map((file) => file.path) ?? [],
|
() => bundle?.files.map((file) => file.path) ?? [],
|
||||||
[bundle],
|
[bundle],
|
||||||
);
|
);
|
||||||
|
const bundleMatchesDraft = Boolean(
|
||||||
|
bundle &&
|
||||||
|
currentMode === persistedMode &&
|
||||||
|
currentEntryFile === bundle.entryFile &&
|
||||||
|
currentRootPath === persistedRootPath,
|
||||||
|
);
|
||||||
const visibleFilePaths = useMemo(
|
const visibleFilePaths = useMemo(
|
||||||
() => [...new Set([currentEntryFile, ...fileOptions])],
|
() => bundleMatchesDraft
|
||||||
[currentEntryFile, fileOptions],
|
? [...new Set([currentEntryFile, ...fileOptions])]
|
||||||
|
: [currentEntryFile],
|
||||||
|
[bundleMatchesDraft, currentEntryFile, fileOptions],
|
||||||
);
|
);
|
||||||
const fileTree = useMemo(
|
const fileTree = useMemo(
|
||||||
() => buildFileTree(Object.fromEntries(visibleFilePaths.map((filePath) => [filePath, ""]))),
|
() => buildFileTree(Object.fromEntries(visibleFilePaths.map((filePath) => [filePath, ""]))),
|
||||||
[visibleFilePaths],
|
[visibleFilePaths],
|
||||||
);
|
);
|
||||||
const selectedOrEntryFile = selectedFile || currentEntryFile;
|
const selectedOrEntryFile = selectedFile || currentEntryFile;
|
||||||
const selectedFileExists = fileOptions.includes(selectedOrEntryFile);
|
const selectedFileExists = bundleMatchesDraft && fileOptions.includes(selectedOrEntryFile);
|
||||||
const selectedFileSummary = bundle?.files.find((file) => file.path === selectedOrEntryFile) ?? null;
|
const selectedFileSummary = bundle?.files.find((file) => file.path === selectedOrEntryFile) ?? null;
|
||||||
|
|
||||||
const { data: selectedFileDetail, isLoading: fileLoading } = useQuery({
|
const { data: selectedFileDetail, isLoading: fileLoading } = useQuery({
|
||||||
@@ -1603,6 +1620,10 @@ function PromptsTab({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!bundle) return;
|
if (!bundle) return;
|
||||||
|
if (!bundleMatchesDraft) {
|
||||||
|
if (selectedFile !== currentEntryFile) setSelectedFile(currentEntryFile);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const availablePaths = bundle.files.map((file) => file.path);
|
const availablePaths = bundle.files.map((file) => file.path);
|
||||||
if (availablePaths.length === 0) {
|
if (availablePaths.length === 0) {
|
||||||
if (selectedFile !== bundle.entryFile) setSelectedFile(bundle.entryFile);
|
if (selectedFile !== bundle.entryFile) setSelectedFile(bundle.entryFile);
|
||||||
@@ -1611,7 +1632,7 @@ function PromptsTab({
|
|||||||
if (!availablePaths.includes(selectedFile)) {
|
if (!availablePaths.includes(selectedFile)) {
|
||||||
setSelectedFile(availablePaths.includes(bundle.entryFile) ? bundle.entryFile : availablePaths[0]!);
|
setSelectedFile(availablePaths.includes(bundle.entryFile) ? bundle.entryFile : availablePaths[0]!);
|
||||||
}
|
}
|
||||||
}, [bundle, selectedFile]);
|
}, [bundle, bundleMatchesDraft, currentEntryFile, selectedFile]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const nextExpanded = new Set<string>();
|
const nextExpanded = new Set<string>();
|
||||||
@@ -1627,7 +1648,9 @@ function PromptsTab({
|
|||||||
}, [visibleFilePaths]);
|
}, [visibleFilePaths]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const versionKey = selectedFileDetail ? `${selectedFileDetail.path}:${selectedFileDetail.content}` : `draft:${selectedOrEntryFile}`;
|
const versionKey = selectedFileExists && selectedFileDetail
|
||||||
|
? `${selectedFileDetail.path}:${selectedFileDetail.content}`
|
||||||
|
: `draft:${currentMode}:${currentRootPath}:${selectedOrEntryFile}`;
|
||||||
if (awaitingRefresh) {
|
if (awaitingRefresh) {
|
||||||
setAwaitingRefresh(false);
|
setAwaitingRefresh(false);
|
||||||
setBundleDraft(null);
|
setBundleDraft(null);
|
||||||
@@ -1639,27 +1662,36 @@ function PromptsTab({
|
|||||||
setDraft(null);
|
setDraft(null);
|
||||||
lastFileVersionRef.current = versionKey;
|
lastFileVersionRef.current = versionKey;
|
||||||
}
|
}
|
||||||
}, [awaitingRefresh, selectedFileDetail, selectedOrEntryFile]);
|
}, [awaitingRefresh, currentMode, currentRootPath, selectedFileDetail, selectedFileExists, selectedOrEntryFile]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!bundle) return;
|
if (!bundle) return;
|
||||||
setBundleDraft((current) => {
|
setBundleDraft((current) => {
|
||||||
if (current) return current;
|
if (current) return current;
|
||||||
return {
|
return {
|
||||||
mode: bundle.mode ?? "managed",
|
mode: persistedMode,
|
||||||
rootPath: bundle.rootPath ?? "",
|
rootPath: persistedRootPath,
|
||||||
entryFile: bundle.entryFile,
|
entryFile: bundle.entryFile,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [bundle]);
|
}, [bundle, persistedMode, persistedRootPath]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!bundle || currentMode !== "external") return;
|
||||||
|
externalBundleRef.current = {
|
||||||
|
rootPath: currentRootPath,
|
||||||
|
entryFile: currentEntryFile,
|
||||||
|
selectedFile: selectedOrEntryFile,
|
||||||
|
};
|
||||||
|
}, [bundle, currentEntryFile, currentMode, currentRootPath, selectedOrEntryFile]);
|
||||||
|
|
||||||
const currentContent = selectedFileExists ? (selectedFileDetail?.content ?? "") : "";
|
const currentContent = selectedFileExists ? (selectedFileDetail?.content ?? "") : "";
|
||||||
const displayValue = draft ?? currentContent;
|
const displayValue = draft ?? currentContent;
|
||||||
const bundleDirty = Boolean(
|
const bundleDirty = Boolean(
|
||||||
bundleDraft &&
|
bundleDraft &&
|
||||||
(
|
(
|
||||||
bundleDraft.mode !== (bundle?.mode ?? "managed") ||
|
bundleDraft.mode !== persistedMode ||
|
||||||
bundleDraft.rootPath !== (bundle?.rootPath ?? "") ||
|
bundleDraft.rootPath !== persistedRootPath ||
|
||||||
bundleDraft.entryFile !== (bundle?.entryFile ?? "AGENTS.md")
|
bundleDraft.entryFile !== (bundle?.entryFile ?? "AGENTS.md")
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1710,13 +1742,13 @@ function PromptsTab({
|
|||||||
setDraft(null);
|
setDraft(null);
|
||||||
if (bundle) {
|
if (bundle) {
|
||||||
setBundleDraft({
|
setBundleDraft({
|
||||||
mode: bundle.mode ?? "managed",
|
mode: persistedMode,
|
||||||
rootPath: bundle.rootPath ?? "",
|
rootPath: persistedRootPath,
|
||||||
entryFile: bundle.entryFile,
|
entryFile: bundle.entryFile,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} : null);
|
} : null);
|
||||||
}, [bundle, isDirty, onCancelActionChange]);
|
}, [bundle, isDirty, onCancelActionChange, persistedMode, persistedRootPath]);
|
||||||
|
|
||||||
const handleSeparatorDrag = useCallback((event: React.MouseEvent) => {
|
const handleSeparatorDrag = useCallback((event: React.MouseEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -1754,7 +1786,7 @@ function PromptsTab({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-6xl space-y-4">
|
<div className="max-w-6xl space-y-6">
|
||||||
{(bundle?.warnings ?? []).length > 0 && (
|
{(bundle?.warnings ?? []).length > 0 && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{(bundle?.warnings ?? []).map((warning) => (
|
{(bundle?.warnings ?? []).map((warning) => (
|
||||||
@@ -1907,7 +1939,7 @@ function PromptsTab({
|
|||||||
<ChevronRight className="h-3 w-3 transition-transform group-data-[state=open]:rotate-90" />
|
<ChevronRight className="h-3 w-3 transition-transform group-data-[state=open]:rotate-90" />
|
||||||
Advanced
|
Advanced
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent className="pt-3 space-y-3">
|
<CollapsibleContent className="pt-4 pb-4 space-y-4">
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<label className="space-y-1">
|
<label className="space-y-1">
|
||||||
<span className="text-xs font-medium text-muted-foreground flex items-center gap-1">
|
<span className="text-xs font-medium text-muted-foreground flex items-center gap-1">
|
||||||
@@ -1926,11 +1958,22 @@ function PromptsTab({
|
|||||||
type="button"
|
type="button"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant={currentMode === "managed" ? "default" : "outline"}
|
variant={currentMode === "managed" ? "default" : "outline"}
|
||||||
onClick={() => setBundleDraft((current) => ({
|
onClick={() => {
|
||||||
mode: "managed",
|
if (currentMode === "external") {
|
||||||
rootPath: current?.rootPath ?? bundle?.rootPath ?? "",
|
externalBundleRef.current = {
|
||||||
entryFile: current?.entryFile ?? bundle?.entryFile ?? "AGENTS.md",
|
rootPath: currentRootPath,
|
||||||
}))}
|
entryFile: currentEntryFile,
|
||||||
|
selectedFile: selectedOrEntryFile,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const nextEntryFile = currentEntryFile || "AGENTS.md";
|
||||||
|
setBundleDraft({
|
||||||
|
mode: "managed",
|
||||||
|
rootPath: bundle?.managedRootPath ?? currentRootPath,
|
||||||
|
entryFile: nextEntryFile,
|
||||||
|
});
|
||||||
|
setSelectedFile(nextEntryFile);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Managed
|
Managed
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1938,17 +1981,22 @@ function PromptsTab({
|
|||||||
type="button"
|
type="button"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant={currentMode === "external" ? "default" : "outline"}
|
variant={currentMode === "external" ? "default" : "outline"}
|
||||||
onClick={() => setBundleDraft((current) => ({
|
onClick={() => {
|
||||||
mode: "external",
|
const externalBundle = externalBundleRef.current;
|
||||||
rootPath: current?.rootPath ?? bundle?.rootPath ?? "",
|
const nextEntryFile = externalBundle?.entryFile ?? currentEntryFile ?? "AGENTS.md";
|
||||||
entryFile: current?.entryFile ?? bundle?.entryFile ?? "AGENTS.md",
|
setBundleDraft({
|
||||||
}))}
|
mode: "external",
|
||||||
|
rootPath: externalBundle?.rootPath ?? (bundle?.mode === "external" ? (bundle.rootPath ?? "") : ""),
|
||||||
|
entryFile: nextEntryFile,
|
||||||
|
});
|
||||||
|
setSelectedFile(externalBundle?.selectedFile ?? nextEntryFile);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
External
|
External
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<div className="grid gap-3 sm:grid-cols-3">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<label className="space-y-1">
|
<label className="space-y-1">
|
||||||
<span className="text-xs font-medium text-muted-foreground flex items-center gap-1">
|
<span className="text-xs font-medium text-muted-foreground flex items-center gap-1">
|
||||||
Root path
|
Root path
|
||||||
@@ -1963,7 +2011,7 @@ function PromptsTab({
|
|||||||
</span>
|
</span>
|
||||||
{currentMode === "managed" ? (
|
{currentMode === "managed" ? (
|
||||||
<div className="flex items-center gap-1.5 font-mono text-sm text-muted-foreground">
|
<div className="flex items-center gap-1.5 font-mono text-sm text-muted-foreground">
|
||||||
<span className="truncate">{currentRootPath || "(managed)"}</span>
|
<span className="min-w-0 break-all">{currentRootPath || "(managed)"}</span>
|
||||||
{currentRootPath && (
|
{currentRootPath && (
|
||||||
<CopyText text={currentRootPath} className="shrink-0">
|
<CopyText text={currentRootPath} className="shrink-0">
|
||||||
<Copy className="h-3.5 w-3.5" />
|
<Copy className="h-3.5 w-3.5" />
|
||||||
@@ -1974,11 +2022,19 @@ function PromptsTab({
|
|||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<Input
|
<Input
|
||||||
value={currentRootPath}
|
value={currentRootPath}
|
||||||
onChange={(event) => setBundleDraft((current) => ({
|
onChange={(event) => {
|
||||||
mode: current?.mode ?? bundle?.mode ?? "managed",
|
const nextRootPath = event.target.value;
|
||||||
rootPath: event.target.value,
|
externalBundleRef.current = {
|
||||||
entryFile: current?.entryFile ?? bundle?.entryFile ?? "AGENTS.md",
|
rootPath: nextRootPath,
|
||||||
}))}
|
entryFile: currentEntryFile,
|
||||||
|
selectedFile: selectedOrEntryFile,
|
||||||
|
};
|
||||||
|
setBundleDraft({
|
||||||
|
mode: "external",
|
||||||
|
rootPath: nextRootPath,
|
||||||
|
entryFile: currentEntryFile,
|
||||||
|
});
|
||||||
|
}}
|
||||||
className="font-mono text-sm"
|
className="font-mono text-sm"
|
||||||
placeholder="/absolute/path/to/agent/prompts"
|
placeholder="/absolute/path/to/agent/prompts"
|
||||||
/>
|
/>
|
||||||
@@ -2006,14 +2062,22 @@ function PromptsTab({
|
|||||||
value={currentEntryFile}
|
value={currentEntryFile}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const nextEntryFile = event.target.value || "AGENTS.md";
|
const nextEntryFile = event.target.value || "AGENTS.md";
|
||||||
if (selectedOrEntryFile === currentEntryFile) {
|
const nextSelectedFile = selectedOrEntryFile === currentEntryFile
|
||||||
setSelectedFile(nextEntryFile);
|
? nextEntryFile
|
||||||
|
: selectedOrEntryFile;
|
||||||
|
if (currentMode === "external") {
|
||||||
|
externalBundleRef.current = {
|
||||||
|
rootPath: currentRootPath,
|
||||||
|
entryFile: nextEntryFile,
|
||||||
|
selectedFile: nextSelectedFile,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
setBundleDraft((current) => ({
|
if (selectedOrEntryFile === currentEntryFile) setSelectedFile(nextEntryFile);
|
||||||
mode: current?.mode ?? bundle?.mode ?? "managed",
|
setBundleDraft({
|
||||||
rootPath: current?.rootPath ?? bundle?.rootPath ?? "",
|
mode: currentMode,
|
||||||
|
rootPath: currentRootPath,
|
||||||
entryFile: nextEntryFile,
|
entryFile: nextEntryFile,
|
||||||
}));
|
});
|
||||||
}}
|
}}
|
||||||
className="font-mono text-sm"
|
className="font-mono text-sm"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user