fix(studio): copy absolute paths in agent prompts

This commit is contained in:
Miguel Ángel
2026-05-10 18:35:58 +00:00
parent 664c215f8b
commit 767e2e25b2
4 changed files with 67 additions and 5 deletions
@@ -25,6 +25,6 @@ export function registerProjectRoutes(api: Hono, adapter: StudioApiAdapter): voi
const project = await adapter.resolveProject(c.req.param("id"));
if (!project) return c.json({ error: "not found" }, 404);
const files = walkDir(project.dir);
return c.json({ id: project.id, files });
return c.json({ id: project.id, dir: project.dir, title: project.title, files });
});
}
+25 -3
View File
@@ -181,6 +181,23 @@ function toRelativeProjectAssetPath(sourceFile: string, assetPath: string): stri
return [...fromParts.map(() => ".."), ...targetParts].join("/") || assetPath;
}
function isAbsoluteFilePath(value: string): boolean {
return /^(?:\/|[A-Za-z]:[\\/]|\\\\)/.test(value);
}
function toProjectAbsolutePath(projectDir: string | null, sourceFile: string): string | undefined {
const trimmedSource = sourceFile.trim();
if (!trimmedSource) return undefined;
const normalizedSource = trimmedSource.replace(/\\/g, "/");
if (isAbsoluteFilePath(normalizedSource)) return normalizedSource;
const normalizedRoot = projectDir?.trim().replace(/\\/g, "/").replace(/\/+$/, "");
if (!normalizedRoot) return undefined;
return `${normalizedRoot}/${normalizedSource.replace(/^\.?\//, "")}`;
}
function ensureImportedFontFace(
html: string,
asset: ImportedFontAsset,
@@ -589,6 +606,7 @@ export function StudioApp() {
});
const [editingFile, setEditingFile] = useState<EditingFile | null>(null);
const [projectDir, setProjectDir] = useState<string | null>(null);
const [activeCompPath, setActiveCompPath] = useState<string | null>(null);
const [fileTree, setFileTree] = useState<string[]>([]);
const [compIdToSrc, setCompIdToSrc] = useState<Map<string, string>>(new Map());
@@ -1094,10 +1112,13 @@ export function StudioApp() {
let cancelled = false;
fetch(`/api/projects/${projectId}`)
.then((r) => r.json())
.then((data: { files?: string[] }) => {
.then((data: { files?: string[]; dir?: string }) => {
if (!cancelled && data.files) setFileTree(data.files);
if (!cancelled) setProjectDir(typeof data.dir === "string" ? data.dir : null);
})
.catch(() => {});
.catch(() => {
if (!cancelled) setProjectDir(null);
});
return () => {
cancelled = true;
};
@@ -2009,6 +2030,7 @@ export function StudioApp() {
currentTime,
tagSnippet,
userInstruction,
sourceFilePath: toProjectAbsolutePath(projectDir, targetPath),
});
try {
@@ -2030,7 +2052,7 @@ export function StudioApp() {
setCopiedAgentPrompt(true);
copiedAgentTimerRef.current = setTimeout(() => setCopiedAgentPrompt(false), 1600);
},
[activeCompPath, currentTime, domEditSelection],
[activeCompPath, currentTime, domEditSelection, projectDir],
);
const handlePreviewIframeRef = useCallback(
@@ -467,6 +467,43 @@ describe("patch builders and prompt builder", () => {
expect(prompt).toContain("Do not modify other elements' data-* attributes or positioning.");
});
it("uses an absolute source path in copied agent prompts when provided", () => {
const selection = {
element: {} as HTMLElement,
id: "editable-card",
selector: "#editable-card",
selectorIndex: undefined,
sourceFile: "index.html",
compositionPath: "index.html",
compositionSrc: undefined,
isCompositionHost: false,
label: "Drag me first",
tagName: "div",
boundingBox: { x: 108, y: 112, width: 380, height: 196 },
textContent: "Drag me first",
dataAttributes: {},
inlineStyles: {},
computedStyles: {},
textFields: [],
capabilities: {
canSelect: true,
canEditStyles: true,
canMove: true,
canResize: true,
canDetachFromLayout: false,
},
} satisfies DomEditSelection;
const prompt = buildElementAgentPrompt({
selection,
currentTime: 1.25,
sourceFilePath: "/tmp/hf-studio-project/index.html",
});
expect(prompt).toContain("Source file: /tmp/hf-studio-project/index.html");
expect(prompt).not.toContain("Source file: index.html");
});
it("serializes child text fields back into HTML", () => {
expect(
serializeDomEditTextFields([
@@ -697,12 +697,15 @@ export function buildElementAgentPrompt({
currentTime,
tagSnippet,
userInstruction,
sourceFilePath,
}: {
selection: DomEditSelection;
currentTime: number;
tagSnippet?: string;
userInstruction?: string;
sourceFilePath?: string;
}): string {
const displayedSourceFile = sourceFilePath?.trim() || selection.sourceFile;
const lines = [
"## HyperFrames element edit request v1",
"Schema version: 1",
@@ -711,7 +714,7 @@ export function buildElementAgentPrompt({
"",
`Composition: ${selection.compositionPath}`,
`Playback time: ${formatTime(currentTime)}`,
`Source file: ${selection.sourceFile}`,
`Source file: ${displayedSourceFile}`,
`DOM id: ${selection.id ?? "(none)"}`,
`Selector: ${selection.selector ?? "(none)"}`,
`Selector index: ${selection.selectorIndex ?? 0}`,