Files
backnotprop__plannotator/packages/review-editor/hooks/useCodeNav.ts
Michael Ramos 95750ab657 feat: search-based code navigation with peek view (#711)
* feat: search-based code navigation with peek view (#694)

Add IDE-like code navigation to the review UI. Cmd/Ctrl+click a token
in a diff to find its definitions and references across the repo via
ripgrep, displayed in a VS Code-style peek panel below the diff.

Backend: bounded rg search with language-aware definition patterns
(TS/JS, Python, Go, Rust), ranked results (same file > changed files >
same directory), confidence labels, and graceful degradation when rg
is not installed. Both Bun and Pi servers implement the endpoints.

Frontend: Dockview peek panel with syntax-highlighted full-file preview
on the left and grouped reference list on the right. Clicking a
reference scrolls the preview; double-clicking an in-diff result
navigates to the file with a gold line flash.

Closes #694

* fix: resolve Pi server TypeScript errors for code-nav

Add missing spawn import, type the close callback parameter,
and use double-cast for parseBody → CodeNavRequest.

* fix: clear loading state on cached preview hits

Without this, clicking a cached file while a fetch is in-flight
leaves isLoading stuck true — the spinner hides the preview.

* feat: show pointer cursor on Cmd/Ctrl+hover for navigable tokens

Adds pn-token-nav class with thicker underline and pointer cursor
when hovering a token while holding the modifier key, signaling
the token is Cmd+clickable for code navigation.

* refactor: remove go-to-diff navigation from peek panel

Strip the in-diff badge, double-click-to-jump, highlightDiffLine
wiring, and onCodeNavGoToDiff from the peek panel. The peek view
is the primary interaction — jump-to-diff adds complexity without
clear value at this stage.

* chore: remove dead code from code-nav cleanup

Delete unused highlightDiffLine.ts, remove codeNavChangedFiles
and codeNavActiveSide from context and App.tsx, drop stale
extractChangedFiles import.

* chore: add code-nav endpoints to AGENTS.md, remove dead activeSide state

* fix: don't classify bare indented calls as definitions

Change the TS/JS method pattern from zero-or-more (*) to
one-or-more (+) declaration keywords, so plain calls like
startServer(config) are no longer misclassified as definitions.

* feat: show toast when code-nav is unavailable in platform-only PR mode

Instead of opening the peek panel and showing misleading "No results",
Cmd+click in non-local PR mode shows a brief toast explaining that
code navigation requires a local checkout.

* fix: strip hljs hardcoded background from code-nav preview

The highlight.js github-dark theme sets a fixed dark background
on all .hljs elements. Apply transparent override for the entire
peek preview so code inherits the active theme's background.
2026-05-13 12:24:37 -07:00

50 lines
1.5 KiB
TypeScript

import { useState, useCallback, useRef } from 'react';
import type { CodeNavRequest, CodeNavResponse } from '@plannotator/shared/code-nav';
export type { CodeNavRequest, CodeNavResponse };
export function useCodeNav() {
const [result, setResult] = useState<CodeNavResponse | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [activeSymbol, setActiveSymbol] = useState<string | null>(null);
const abortRef = useRef<AbortController | null>(null);
const resolve = useCallback(async (request: CodeNavRequest) => {
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
setActiveSymbol(request.symbol);
setIsLoading(true);
setResult(null);
try {
const res = await fetch('/api/code-nav/resolve', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
signal: controller.signal,
});
if (!res.ok) throw new Error('Failed');
const data: CodeNavResponse = await res.json();
setResult(data);
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') return;
setResult(null);
} finally {
if (abortRef.current === controller) {
setIsLoading(false);
}
}
}, []);
const clear = useCallback(() => {
abortRef.current?.abort();
setResult(null);
setActiveSymbol(null);
setIsLoading(false);
}, []);
return { result, isLoading, activeSymbol, resolve, clear };
}