Skip to content
Closed
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
# Changelog

## Unreleased

- Allow Manage to resolve raw Roam UIDs and `((block reference))` inputs directly.
11 changes: 10 additions & 1 deletion src/utils/quickSwitcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import type {
QuickSwitcherCommandPaletteSettings,
QuickSwitcherTargetType,
} from "~/types/quickSwitcher";
import { BLOCK_REF_REGEX } from "roamjs-components/dom/constants";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the block-reference capture group

The imported BLOCK_REF_REGEX is global, so value.match(BLOCK_REF_REGEX) returns only complete matches rather than capture groups. For a single input such as ((acW-i9uMD)), match[1] is therefore undefined, causing parseRoamUid to return null and preventing the new direct-resolution path; with multiple references it can even return the second full reference as the UID. Use a non-global regex or extract the capture with an appropriate exec/match-all strategy.

Useful? React with 👍 / 👎.


const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null;

const BLOCK_REF_REGEX = /\(\(([A-Za-z0-9_-]+)\)\)/;
const ROAM_UID_REGEX = /^[\w-]{9,10}$/;
export const DEFAULT_COMMAND_PALETTE_PREFIX = "QS: ";
const LEGACY_COMMAND_PALETTE_PREFIX = "Q S - ";

Expand Down Expand Up @@ -315,5 +316,13 @@ export const extractBlockRefUid = ({
return match?.[1] || null;
};

export const parseRoamUid = ({ value }: { value: string }): string | null => {
const normalizedValue = value.trim();
if (ROAM_UID_REGEX.test(normalizedValue)) {
return normalizedValue;
}
return extractBlockRefUid({ value: normalizedValue });
};

export const createBookmarkId = (): string =>
`${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
19 changes: 16 additions & 3 deletions src/utils/quickSwitcherEntries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import {
buildRoamPageUrl,
createBookmarkId,
deriveBlockTitle,
extractBlockRefUid,
getBookmarkTargetType,
getBookmarkTargetUid,
parseRoamUid,
parsePageUidFromUrl,
} from "~/utils/quickSwitcher";

Expand Down Expand Up @@ -87,10 +87,10 @@ const isPageUrlInput = ({ entry }: { entry: string }): boolean =>
const getUidFromEntryInput = ({ value }: { value: string }): string => {
const normalizedValue = value.trim();
return (
extractBlockRefUid({ value: normalizedValue }) ||
parseRoamUid({ value: normalizedValue }) ||
(isPageUrlInput({ entry: normalizedValue })
? parsePageUidFromUrl({ url: normalizedValue })
: normalizedValue) ||
: null) ||
Comment on lines +90 to +93

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Adding an entry by a custom-length Roam ID or block reference no longer works

Inputs are now only accepted as IDs when they are exactly 9-10 characters long (ROAM_UID_REGEX at src/utils/quickSwitcher.ts:11 combined with the imported reference pattern), so entries whose ID has a different length can no longer be added by pasting the ID or its reference.
Impact: Users with custom (non 9-10 character) Roam identifiers get "No page or block matched that input" for inputs that previously worked.

How the stricter ID validation removes previously working paths

Before this change, getUidFromEntryInput (src/utils/quickSwitcherEntries.ts:87-96) returned the raw trimmed input for any non-URL text, so resolveEntryInput (src/utils/quickSwitcherEntries.ts:406-412) would attempt a direct UID lookup for arbitrary-length IDs, and the old local BLOCK_REF_REGEX (/\(\(([A-Za-z0-9_-]+)\)\)/) matched block references of any length.

Now the non-URL branch returns null unless parseRoamUid matches, and parseRoamUid only accepts ^[\w-]{9,10}$ or the roamjs-components BLOCK_REF_REGEX, which is /\(\(([\w\d-]{9,10})\)\)/. Roam allows user-specified custom block/page UIDs of other lengths (e.g. ((my-custom-uid))), which now fall through to resolvePageTitleToSuggestion (src/utils/quickSwitcherEntries.ts:376-393) with the literal text and fail.

Prompt for agents
parseRoamUid in src/utils/quickSwitcher.ts restricts identifiers to 9-10 characters (ROAM_UID_REGEX) and the newly imported roamjs-components BLOCK_REF_REGEX also enforces {9,10}. Combined with getUidFromEntryInput in src/utils/quickSwitcherEntries.ts now returning null instead of the raw input for non-URL text, custom Roam UIDs of other lengths (which users can set explicitly, and which previously resolved through resolveEntryInput) can no longer be added. Consider keeping a permissive fallback for the explicit add path (resolveEntryInput): e.g. still attempt a direct UID lookup with the raw input when strict parsing fails, or relax the block-reference pattern to accept any [\w-]+ inside the double parens while keeping the strict pattern for the live search short-circuit.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

""
);
};
Expand Down Expand Up @@ -245,6 +245,19 @@ export const searchEntries = async ({
savedTargetKeys: Set<string>;
searchApi?: RoamSearchApi;
}): Promise<QuickSwitcherEntrySuggestion[]> => {
const directUid = getUidFromEntryInput({ value: query });
if (directUid) {
const directSuggestion = await resolveUidToSuggestion({ uid: directUid });
if (
directSuggestion &&
!savedTargetKeys.has(
getSuggestionTargetKey({ suggestion: directSuggestion }),
)
) {
return [directSuggestion];
}
}
Comment on lines +248 to +259

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Blocks found by pasting an ID are saved without their parent trail

A block found directly from a pasted identifier is returned immediately (return [directSuggestion] at src/utils/quickSwitcherEntries.ts:257) without collecting its parent trail, so it is shown and saved without the page/parent context that normal search results include.
Impact: Entries added by pasting a block identifier appear without their page and parent context, unlike identical entries added via text search.

Breadcrumb enrichment is skipped on the direct path

The text-search path ends with addBreadcrumbsToBlockSuggestions({ suggestions }) (src/utils/quickSwitcherEntries.ts:319), which pulls :block/page / :block/parents per block suggestion and attaches breadcrumbs. The new early return at src/utils/quickSwitcherEntries.ts:248-259 skips that step entirely, and createBookmarkFromSuggestion (src/utils/quickSwitcherEntries.ts:145-159) only stores breadcrumbs when the suggestion carries them, so the persisted bookmark permanently lacks the trail.

Suggested change
const directUid = getUidFromEntryInput({ value: query });
if (directUid) {
const directSuggestion = await resolveUidToSuggestion({ uid: directUid });
if (
directSuggestion &&
!savedTargetKeys.has(
getSuggestionTargetKey({ suggestion: directSuggestion }),
)
) {
return [directSuggestion];
}
}
const directUid = getUidFromEntryInput({ value: query });
if (directUid) {
const directSuggestion = await resolveUidToSuggestion({ uid: directUid });
if (
directSuggestion &&
!savedTargetKeys.has(
getSuggestionTargetKey({ suggestion: directSuggestion }),
)
) {
return addBreadcrumbsToBlockSuggestions({
suggestions: [directSuggestion],
});
}
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


const options: RoamSearchOptions = {
"hide-code-blocks": false,
limit: MAX_SEARCH_RESULTS,
Expand Down
54 changes: 54 additions & 0 deletions tests/quickSwitcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
getCommandPaletteCommandLabel,
normalizeCommandPaletteSettings,
parsePageUidFromUrl,
parseRoamUid,
parseStoredBookmarks,
parseStoredCommandPaletteSettings,
toAbsoluteUrl,
Expand Down Expand Up @@ -322,6 +323,59 @@ test("extracts block uids from roam block refs", () => {
expect(extractBlockRefUid({ value: "not a block ref" })).toBeNull();
});

test("resolves raw and block reference uid searches directly", async () => {
const originalWindow = globalThis.window;
Object.defineProperty(globalThis, "window", {
configurable: true,
value: {
location: {
href: "https://roamresearch.com/#/app/test-graph/daily-notes",
origin: "https://roamresearch.com",
},
roamAlphaAPI: {
data: {
backend: {
q: async (query: string): Promise<[string][]> =>
query.includes(":node/title") ? [["Direct UID page"]] : [],
},
},
},
},
writable: true,
});

try {
const searchApi = async (): Promise<never> => {
throw new Error("Text search should not run for a resolvable UID");
};

for (const query of ["acW-i9uMD", "((acW-i9uMD))"]) {
await expect(
searchEntries({ query, savedTargetKeys: new Set(), searchApi }),
).resolves.toEqual([
expect.objectContaining({
targetType: "page",
title: "Direct UID page",
uid: "acW-i9uMD",
}),
]);
}
} finally {
Object.defineProperty(globalThis, "window", {
configurable: true,
value: originalWindow,
writable: true,
});
}
});

test("parses raw and block reference Roam uids", () => {
expect(parseRoamUid({ value: "acW-i9uMD" })).toBe("acW-i9uMD");
expect(parseRoamUid({ value: "((acW-i9uMD))" })).toBe("acW-i9uMD");
expect(parseRoamUid({ value: "a page title" })).toBeNull();
expect(parseRoamUid({ value: "short" })).toBeNull();
});

test("resolves relative urls to absolute urls", () => {
expect(toAbsoluteUrl({ url: "/#/app/graph/page/abc" })).toContain(
"/#/app/graph/page/abc",
Expand Down
Loading