Skip to content

Commit 59724b7

Browse files
dgrissen2claude
andauthored
fix: enable linked doc navigation in annotate mode (#276)
The annotate server was missing the /api/doc endpoint, so clicking .md links (e.g., [see other](sibling.md)) in annotated files showed "Failed to connect to server" — the request fell through to the SPA catch-all and returned HTML instead of JSON. Three changes: 1. annotate.ts — Add /api/doc route, auto-injecting the source file's parent directory as ?base= so relative links resolve from the correct location. 2. reference-handlers.ts — handleDoc() now accepts an optional ?base= query param and tries resolving relative paths against it before falling back to the project-root glob search. 3. App.tsx — handleOpenLinkedDoc passes the current file's directory as ?base= when navigating linked-doc → linked-doc, so chained relative links resolve correctly. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2d1016a commit 59724b7

3 files changed

Lines changed: 39 additions & 2 deletions

File tree

packages/editor/App.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -529,9 +529,19 @@ const App: React.FC = () => {
529529
if (vaultBrowser.activeFile && vaultPath) {
530530
linkedDocHook.open(docPath, buildVaultDocUrl(vaultPath));
531531
} else {
532-
linkedDocHook.open(docPath);
532+
// Pass the current file's directory as base for relative path resolution
533+
const baseDir = linkedDocHook.filepath
534+
? linkedDocHook.filepath.replace(/\/[^/]+$/, '')
535+
: imageBaseDir;
536+
if (baseDir) {
537+
linkedDocHook.open(docPath, (path) =>
538+
`/api/doc?path=${encodeURIComponent(path)}&base=${encodeURIComponent(baseDir)}`
539+
);
540+
} else {
541+
linkedDocHook.open(docPath);
542+
}
533543
}
534-
}, [vaultBrowser.activeFile, vaultPath, linkedDocHook, buildVaultDocUrl]);
544+
}, [vaultBrowser.activeFile, vaultPath, linkedDocHook, buildVaultDocUrl, imageBaseDir]);
535545

536546
// Wrap linked doc back to also clear vault active file
537547
const handleLinkedDocBack = React.useCallback(() => {

packages/server/annotate.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
import { isRemoteSession, getServerPort } from "./remote";
1515
import { getRepoInfo } from "./repo";
1616
import { handleImage, handleUpload, handleServerReady, handleDraftSave, handleDraftLoad, handleDraftDelete } from "./shared-handlers";
17+
import { handleDoc } from "./reference-handlers";
1718
import { contentHash, deleteDraft } from "./draft";
19+
import { dirname } from "path";
1820

1921
// Re-export utilities
2022
export { isRemoteSession, getServerPort } from "./remote";
@@ -130,6 +132,17 @@ export async function startAnnotateServer(
130132
return handleImage(req);
131133
}
132134

135+
// API: Serve a linked markdown document
136+
// Inject source file's directory as base for relative path resolution
137+
if (url.pathname === "/api/doc" && req.method === "GET") {
138+
if (!url.searchParams.has("base")) {
139+
const docUrl = new URL(req.url);
140+
docUrl.searchParams.set("base", dirname(filePath));
141+
return handleDoc(new Request(docUrl.toString()));
142+
}
143+
return handleDoc(req);
144+
}
145+
133146
// API: Upload image -> save to temp -> return path
134147
if (url.pathname === "/api/upload" && req.method === "POST") {
135148
return handleUpload(req);

packages/server/reference-handlers.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,20 @@ export async function handleDoc(req: Request): Promise<Response> {
7373
return Response.json({ error: "Missing path parameter" }, { status: 400 });
7474
}
7575

76+
// If a base directory is provided, try resolving relative to it first
77+
// (used by annotate mode to resolve paths relative to the source file)
78+
const base = url.searchParams.get("base");
79+
if (base && !requestedPath.startsWith("/") && /\.mdx?$/i.test(requestedPath)) {
80+
const fromBase = resolve(base, requestedPath);
81+
try {
82+
const file = Bun.file(fromBase);
83+
if (await file.exists()) {
84+
const markdown = await file.text();
85+
return Response.json({ markdown, filepath: fromBase });
86+
}
87+
} catch { /* fall through to standard resolution */ }
88+
}
89+
7690
const projectRoot = process.cwd();
7791
const result = await resolveMarkdownFile(requestedPath, projectRoot);
7892

0 commit comments

Comments
 (0)