fix(app): show local images in Markdown file previews

Carry forward document-relative image loading and preserve the host home
directory when resolving images beside home-relative Markdown files.
Decode file URIs once, and require decoded images in the file-pane regression.

Co-authored-by: Ouyang Xingyuan <galaxyroaming@qq.com>
This commit is contained in:
paseo-bot[bot]
2026-10-12 21:03:21 +02:00
co-authored by Ouyang Xingyuan
parent 301d8618cd
commit f518e18a72
4 changed files with 47 additions and 17 deletions
+15 -9
View File
@@ -71,6 +71,14 @@ async function selectFileView(page: Page, view: "Preview" | "Source"): Promise<v
await expect(option).toHaveAttribute("aria-selected", "true");
}
async function expectMarkdownImageLoaded(page: Page, name: string): Promise<void> {
const image = page.getByRole("img", { name, exact: true });
await expect(image).toBeVisible();
await expect
.poll(() => image.locator("img").evaluate((element: HTMLImageElement) => element.naturalWidth))
.toBeGreaterThan(0);
}
function watchRequestsTo(page: Page, origin: string): string[] {
const requests: string[] = [];
page.on("request", (request) => {
@@ -700,20 +708,18 @@ test.describe("CodeMirror workspace file editing", () => {
"utf8",
);
await workspace.navigateTo();
const tree = page.getByTestId("file-explorer-tree-scroll");
if (!(await tree.isVisible())) await openFileExplorer(page);
await openFileExplorer(page);
await expandFolder(page, "docs");
await openFileFromExplorer(page, "guide.md");
await expectFileTabOpen(page, "guide.md");
await expectFileTabOpen(page, "docs/guide.md");
const visibleFilePane = page.getByTestId("workspace-file-pane").filter({ visible: true });
await expect(visibleFilePane.getByText("Guide", { exact: true })).toBeVisible();
const previewImages = visibleFilePane.getByTestId("markdown-preview-image");
await expect(previewImages).toHaveCount(2);
for (const image of await previewImages.locator("img").all()) {
await expect(image).toBeVisible();
await expect.poll(async () => (await image.getAttribute("src")) ?? "").not.toBe("");
}
await expectMarkdownImageLoaded(page, "sibling");
await expectMarkdownImageLoaded(page, "hero");
await visibleFilePane.screenshot({
path: test.info().outputPath("markdown-relative-images.png"),
});
});
test("previews and refreshes an HTML plan while preserving source access", async ({
@@ -21,13 +21,13 @@ describe("resolveMarkdownFileDirectory", () => {
).toBe("/Users/test/project/docs/星谷云");
});
it("returns null for home-relative markdown paths", () => {
it("preserves the host home directory for home-relative markdown paths", () => {
expect(
resolveMarkdownFileDirectory({
markdownPath: "~/.paseo/plans/note.md",
workspaceRoot: "/Users/test/project",
}),
).toBeNull();
).toBe("~/.paseo/plans");
});
});
@@ -96,14 +96,34 @@ describe("resolveMarkdownPreviewImageSource", () => {
).toBe("/tmp/paseo-preview.png");
});
it("falls back to the raw relative path when the markdown directory is unknown", () => {
it("resolves images beside a home-relative document without using the workspace root", () => {
expect(
resolveMarkdownPreviewImageSource({
source: "screenshots/output.png",
markdownPath: "~/.paseo/plans/note.md",
workspaceRoot: "/Users/test/project",
}),
).toBe("screenshots/output.png");
).toBe("~/.paseo/plans/screenshots/output.png");
});
it("decodes a file URI exactly once", () => {
expect(
resolveMarkdownPreviewImageSource({
source: "file:///tmp/hero%2520image.png",
markdownPath: "docs/guide.md",
workspaceRoot: "/workspace",
}),
).toBe("/tmp/hero%20image.png");
});
it("resolves parent segments from a home-relative document", () => {
expect(
resolveMarkdownPreviewImageSource({
source: "../images/hero.png",
markdownPath: "~/docs/guide.md",
workspaceRoot: "/workspace",
}),
).toBe("~/images/hero.png");
});
it("resolves Windows relative images against a drive-rooted markdown file", () => {
@@ -27,6 +27,9 @@ function normalizePathSegments(value: string): string {
function normalizeAbsolutePath(value: string): string | null {
const normalizedInput = trimTrailingSeparators(value.replace(/\\/g, "/"));
if (isHomeRelativePath(normalizedInput)) {
return `~/${normalizePathSegments(normalizedInput.slice(2))}`;
}
if (!isAbsolutePath(normalizedInput)) {
return null;
}
@@ -91,11 +94,11 @@ export function resolveMarkdownFileDirectory(input: {
workspaceRoot?: string;
}): string | null {
const markdownPath = input.markdownPath.trim().replace(/\\/g, "/");
if (!markdownPath || isHomeRelativePath(markdownPath)) {
if (!markdownPath) {
return null;
}
if (isAbsolutePath(markdownPath)) {
if (isAbsolutePath(markdownPath) || isHomeRelativePath(markdownPath)) {
const absolute = normalizeAbsolutePath(markdownPath);
return absolute ? parentDirectory(absolute) : null;
}
@@ -123,7 +126,9 @@ export function resolveMarkdownPreviewImageSource(input: {
return source;
}
const path = decodeRelativeSource(localFileSourceToPath(source));
const path = localFileSourceToPath(
source.startsWith("file://") ? source : decodeRelativeSource(source),
);
if (!path) {
return null;
}
@@ -8,7 +8,6 @@ import {
MarkdownRenderer,
type MarkdownStyles,
} from "@/components/markdown/renderer";
import { MAX_CONTENT_WIDTH } from "@/constants/layout";
import { parseMarkdownPreviewDocument } from "./document";
import { resolveMarkdownPreviewImageSource } from "./image-source";
import { FileMarkdownPreviewImage } from "./preview-image";