Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2345,6 +2345,72 @@ it("toggles a Mermaid attachment between diagram and source through the lazy pre
expect(screen.container.querySelector('[role="alert"]')).toBeNull();
});

it("keeps a workspace PDF open across URL renewals and reloads it when the file changes", async () => {
const { WorkspaceBrowserPreview } = await import("./FilePreviewPanel");
using _native = vi.spyOn(navigator, "pdfViewerEnabled", "get").mockReturnValue(true);
const original = URL.createObjectURL(new Blob(["%PDF-1.4"], { type: "application/pdf" }));
const edited = URL.createObjectURL(new Blob(["%PDF-1.4 edited"], { type: "application/pdf" }));
const preview = (mutation: string) => (
<WorkspaceBrowserPreview
environmentId={EnvironmentId.make("test")}
threadRef={{ environmentId: EnvironmentId.make("test"), threadId: ThreadId.make("test") }}
draft={false}
workspaceRoot="/repo"
absolutePath="/repo/planning.pdf"
title="Planning PDF"
workspaceMutationId={mutation}
/>
);
assetUrlState.mockReturnValue({
_tag: "Success",
url: `${original}#initial`,
expiresAt: 0,
fileRevision: "one",
});
try {
await using screen = await render(preview("first"));
const opened = screen.container.querySelector("iframe");
expect(opened?.src.startsWith(original)).toBe(true);

// An agent command renews the signed URL but leaves the file alone.
assetUrlState.mockReturnValue({
_tag: "Success",
url: `${original}#renewed`,
expiresAt: 0,
fileRevision: "one",
});
await screen.rerender(preview("second"));
expect(assetUrlState).toHaveBeenLastCalledWith(expect.anything(), expect.anything(), "second");
expect(screen.container.querySelector("iframe")).toBe(opened);

assetUrlState.mockReturnValue({
_tag: "Success",
url: edited,
expiresAt: 0,
fileRevision: "two",
});
await screen.rerender(preview("third"));
const reloaded = screen.container.querySelector("iframe");
expect(reloaded).not.toBe(opened);
expect(reloaded?.src.startsWith(edited)).toBe(true);

// Servers without revisions hold the preview back until a fresh grant arrives.
assetUrlState.mockReturnValue({
_tag: "Success",
url: `${edited}#stale`,
expiresAt: 0,
waiting: true,
});
await screen.rerender(preview("fourth"));
expect(screen.container.querySelector("iframe")).toBeNull();
assetUrlState.mockReturnValue({ _tag: "Success", url: original, expiresAt: 0 });
await screen.rerender(preview("fourth"));
expect(screen.container.querySelector("iframe")?.src.startsWith(original)).toBe(true);
} finally {
URL.revokeObjectURL(original);
URL.revokeObjectURL(edited);
}
});
it("unwraps one text attachment without turning off the word wrap setting", async () => {
const file = new Blob(["a long line"], { type: "text/plain" });
await using screen = await render(
Expand Down
26 changes: 18 additions & 8 deletions apps/web/src/components/files/FilePreviewPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ function WorkspaceImagePreview(props: {
);
}

function WorkspaceBrowserPreview(props: {
export function WorkspaceBrowserPreview(props: {
readonly environmentId: EnvironmentId;
readonly threadRef: ScopedThreadRef;
readonly draft: boolean;
Expand All @@ -231,18 +231,25 @@ function WorkspaceBrowserPreview(props: {
readonly title: string;
readonly workspaceMutationId: string | null;
}) {
const pdf = isPdfPreviewFile(props.absolutePath);
const resource = workspaceAssetResource({
kind: "workspace-file",
kind: pdf ? "media-file" : "workspace-file",
threadRef: props.threadRef,
draft: props.draft,
workspaceRoot: props.workspaceRoot,
absolutePath: props.absolutePath,
});
const assetUrl = useAssetUrlState(props.environmentId, resource);
const assetUrl = useAssetUrlState(
props.environmentId,
resource,
pdf ? props.workspaceMutationId : undefined,
);
const fileRevision = pdf && assetUrl._tag === "Success" ? assetUrl.fileRevision : undefined;
const revision = fileRevision ?? props.workspaceMutationId;
const revisionSuffix =
props.workspaceMutationId === null
revision === null
? ""
: `${assetUrl._tag === "Success" && assetUrl.url.includes("?") ? "&" : "?"}workspace-revision=${encodeURIComponent(props.workspaceMutationId)}`;
: `${assetUrl._tag === "Success" && assetUrl.url.includes("?") ? "&" : "?"}${fileRevision !== undefined ? "file" : "workspace"}-revision=${encodeURIComponent(revision)}`;

if (assetUrl._tag === "Failure") {
return (
Expand All @@ -251,7 +258,10 @@ function WorkspaceBrowserPreview(props: {
</div>
);
}
if (assetUrl._tag !== "Success") {
if (
assetUrl._tag !== "Success" ||
(pdf && assetUrl.fileRevision === undefined && assetUrl.waiting)
) {
return (
<div className="flex min-h-0 flex-1 items-center justify-center text-muted-foreground">
<Spinner size="lg" />
Expand All @@ -261,9 +271,9 @@ function WorkspaceBrowserPreview(props: {
return (
<BrowserDocumentFrame
src={`${assetUrl.url}${revisionSuffix}`}
documentKey={`${props.environmentId}:${resource._tag}:${props.threadRef.threadId}:${props.absolutePath}:${props.workspaceMutationId}`}
documentKey={`${props.environmentId}:${resource._tag}:${props.threadRef.threadId}:${props.absolutePath}:${revision}`}
title={props.title}
pdf={isPdfPreviewFile(props.absolutePath)}
pdf={pdf}
/>
);
}
Expand Down
Loading