fix(miyo): strip vault folder-name prefix from indexed paths (#2390)

Miyo returns indexed file paths prefixed with the owning folder name
(the vault name), but getVaultRelativeMiyoPath was stripping the
absolute filesystem path prefix instead. As a result, the "List
Indexed Files" command rendered every entry as a broken Obsidian link
(e.g. [[MyVault/notes/foo.md]] instead of [[notes/foo.md]]).

Switch the strip logic to match what Miyo actually returns. Files from
other vaults pass through unchanged so cross-vault search results stay
intact. Always return a forward-slash-normalized path so callers see a
consistent separator. Drop the now-unused getVaultBasePath helper.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wenzheng Jiang 2026-05-09 21:21:07 +09:00 committed by GitHub
parent 37bea9a65c
commit 94737297b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 69 additions and 47 deletions

View file

@ -2,7 +2,7 @@ jest.mock("@/plusUtils", () => ({
isSelfHostAccessValid: jest.fn(),
}));
import { getMiyoFolderName } from "@/miyo/miyoUtils";
import { getMiyoFolderName, getVaultRelativeMiyoPath } from "@/miyo/miyoUtils";
describe("getMiyoFolderName", () => {
it("uses the vault folder name even when an adapter exposes an absolute path", () => {
@ -18,3 +18,44 @@ describe("getMiyoFolderName", () => {
expect(folderName).toBe("graham-essays-main");
});
});
describe("getVaultRelativeMiyoPath", () => {
const buildApp = (vaultName: string) =>
({
vault: {
getName: () => vaultName,
},
}) as any;
it("strips the current vault folder-name prefix", () => {
expect(getVaultRelativeMiyoPath(buildApp("MyVault"), "MyVault/notes/foo.md")).toBe(
"notes/foo.md"
);
});
it("returns the normalized path unchanged when the prefix matches a different vault", () => {
expect(getVaultRelativeMiyoPath(buildApp("MyVault"), "OtherVault/notes/foo.md")).toBe(
"OtherVault/notes/foo.md"
);
});
it("normalizes separators even when the prefix does not match", () => {
expect(getVaultRelativeMiyoPath(buildApp("MyVault"), "OtherVault\\notes\\foo.md")).toBe(
"OtherVault/notes/foo.md"
);
});
it("only strips the leading prefix once", () => {
expect(getVaultRelativeMiyoPath(buildApp("Test"), "Test/Test/foo.md")).toBe("Test/foo.md");
});
it("normalizes backslash separators before stripping", () => {
expect(getVaultRelativeMiyoPath(buildApp("MyVault"), "MyVault\\notes\\foo.md")).toBe(
"notes/foo.md"
);
});
it("returns the normalized path when the vault folder name is empty", () => {
expect(getVaultRelativeMiyoPath(buildApp(""), "notes\\foo.md")).toBe("notes/foo.md");
});
});

View file

@ -2,6 +2,16 @@ import { isSelfHostAccessValid } from "@/plusUtils";
import { CopilotSettings } from "@/settings/model";
import { App, FileSystemAdapter, Platform } from "obsidian";
/**
* Convert backslashes to forward slashes and trim trailing slashes.
*
* @param path - Filesystem path.
* @returns Normalized path.
*/
function normalizeFilesystemPath(path: string): string {
return path.replace(/\\/g, "/").replace(/\/+$/, "");
}
/**
* Return the user-configured Miyo server URL, or "" to fall back to local service discovery.
* Uses `|| ""` to guard against undefined when loaded from older saved settings.
@ -67,57 +77,28 @@ export function getMiyoAbsolutePath(app: App, vaultRelativePath: string): string
}
/**
* Convert a Miyo file path back to a vault-relative path when it belongs to the current vault.
* Convert a Miyo file path to a vault-relative path when it belongs to the current vault.
*
* Miyo always returns paths prefixed with their owning folder name. When the
* prefix matches the current vault's folder name, strip it to get a
* vault-relative path. Files from other vaults pass through unchanged. The
* returned path is always normalized to forward slashes.
*
* @param app - Obsidian application instance.
* @param miyoPath - Path returned by Miyo.
* @returns Vault-relative path when the file is inside the current vault, otherwise the original path.
* @param miyoPath - Path returned by Miyo (e.g., "MyVault/notes/foo.md").
* @returns Normalized vault-relative path when inside the current vault, otherwise the normalized original path.
*/
export function getVaultRelativeMiyoPath(app: App, miyoPath: string): string {
const vaultPath = getVaultBasePath(app);
if (!vaultPath) {
return miyoPath;
}
const normalizedVaultPath = normalizeFilesystemPath(vaultPath);
const normalizedMiyoPath = normalizeFilesystemPath(miyoPath);
const prefix = `${normalizedVaultPath}/`;
if (normalizedMiyoPath.startsWith(prefix)) {
return normalizedMiyoPath.slice(prefix.length);
const folderName = getMiyoFolderName(app);
if (!folderName) {
return normalizedMiyoPath;
}
return miyoPath;
}
/**
* Resolve the base path for the current vault when available.
*
* @param app - Obsidian application instance.
* @returns Vault base path or undefined when unavailable.
*/
export function getVaultBasePath(app: App): string | undefined {
const adapter = app.vault.adapter;
if (adapter instanceof FileSystemAdapter) {
return adapter.getBasePath();
}
const adapterAny = adapter as unknown as { getBasePath?: () => string; basePath?: string };
if (typeof adapterAny.getBasePath === "function") {
return adapterAny.getBasePath();
}
if (typeof adapterAny.basePath === "string") {
return adapterAny.basePath;
}
return undefined;
}
/**
* Normalize a filesystem path for prefix comparisons across platforms.
*
* @param path - Filesystem path.
* @returns Normalized path with forward slashes and no trailing slash.
*/
function normalizeFilesystemPath(path: string): string {
return path.replace(/\\/g, "/").replace(/\/+$/, "");
const folderPrefix = `${folderName}/`;
if (normalizedMiyoPath.startsWith(folderPrefix)) {
return normalizedMiyoPath.slice(folderPrefix.length);
}
return normalizedMiyoPath;
}