Skip to content

Commit bdf0b8b

Browse files
backnotpropclaude
andcommitted
fix: restore React hook dep arrays and harden biome rules
Reverts unsafe biome lint --write --unsafe changes that modified React hook dependency arrays, potentially introducing stale closures and infinite re-render bugs. Promotes useExhaustiveDependencies, noAssignInExpressions, and noInvalidUseBeforeDeclaration from off/warn to error with targeted inline suppressions. Reverts _onUpdate rename in pi-extension. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 31fca7a commit bdf0b8b

File tree

14 files changed

+55
-41
lines changed

14 files changed

+55
-41
lines changed

apps/pi-extension/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ export default function plannotator(pi: ExtensionAPI): void {
286286
),
287287
}),
288288

289-
async execute(_toolCallId, _params, _signal, _update, ctx) {
289+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
290290
// Guard: must be in planning phase
291291
if (phase !== 'planning') {
292292
return {

biome.jsonc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@
1919
"suspicious": {
2020
"noArrayIndexKey": "warn",
2121
"noExplicitAny": "warn",
22-
"noAssignInExpressions": "off",
22+
"noAssignInExpressions": "error",
2323
"useIterableCallbackReturn": "warn"
2424
},
2525
"correctness": {
26-
"useExhaustiveDependencies": "warn",
26+
"useExhaustiveDependencies": "error",
2727
"noUnusedFunctionParameters": "warn",
28-
"noInvalidUseBeforeDeclaration": "off"
28+
"noInvalidUseBeforeDeclaration": "error"
2929
},
3030
"security": {
3131
"noDangerouslySetInnerHtml": "warn"

packages/editor/App.tsx

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -420,20 +420,22 @@ const App: React.FC = () => {
420420
const sidebar = useSidebar(getUIPreferences().tocEnabled);
421421

422422
// Sync sidebar open state when preference changes in Settings
423+
// biome-ignore lint/correctness/useExhaustiveDependencies: sidebar methods are stable
423424
useEffect(() => {
424425
if (uiPrefs.tocEnabled) {
425426
sidebar.open('toc');
426427
} else {
427428
sidebar.close();
428429
}
429-
}, [uiPrefs.tocEnabled, sidebar.close, sidebar.open]);
430+
}, [uiPrefs.tocEnabled]);
430431

431432
// Clear diff view when switching away from versions tab
433+
// biome-ignore lint/correctness/useExhaustiveDependencies: isPlanDiffActive is read, not a trigger
432434
useEffect(() => {
433435
if (sidebar.activeTab === 'toc' && isPlanDiffActive) {
434436
setIsPlanDiffActive(false);
435437
}
436-
}, [sidebar.activeTab, isPlanDiffActive]);
438+
}, [sidebar.activeTab]);
437439

438440
// Clear diff view on Escape key
439441
useEffect(() => {
@@ -467,19 +469,23 @@ const App: React.FC = () => {
467469
// Obsidian vault browser
468470
const vaultBrowser = useVaultBrowser();
469471

470-
const showVaultTab = useMemo(() => isVaultBrowserEnabled(), []);
472+
// biome-ignore lint/correctness/useExhaustiveDependencies: recompute when user changes settings
473+
const showVaultTab = useMemo(() => isVaultBrowserEnabled(), [uiPrefs]);
474+
// biome-ignore lint/correctness/useExhaustiveDependencies: recompute when user changes settings
471475
const vaultPath = useMemo(() => {
472476
if (!showVaultTab) return '';
473477
const settings = getObsidianSettings();
474478
return getEffectiveVaultPath(settings);
475-
}, [showVaultTab]);
479+
}, [showVaultTab, uiPrefs]);
476480

477481
// Clear active file when vault browser is disabled
482+
// biome-ignore lint/correctness/useExhaustiveDependencies: vaultBrowser.setActiveFile is stable
478483
useEffect(() => {
479484
if (!showVaultTab) vaultBrowser.setActiveFile(null);
480-
}, [showVaultTab, vaultBrowser.setActiveFile]);
485+
}, [showVaultTab]);
481486

482487
// Auto-fetch vault tree when vault tab is first opened
488+
// biome-ignore lint/correctness/useExhaustiveDependencies: vaultBrowser methods/state are read inside, not triggers
483489
useEffect(() => {
484490
if (
485491
sidebar.activeTab === 'vault' &&
@@ -490,14 +496,7 @@ const App: React.FC = () => {
490496
) {
491497
vaultBrowser.fetchTree(vaultPath);
492498
}
493-
}, [
494-
sidebar.activeTab,
495-
showVaultTab,
496-
vaultPath,
497-
vaultBrowser.fetchTree,
498-
vaultBrowser.isLoading,
499-
vaultBrowser.tree.length,
500-
]);
499+
}, [sidebar.activeTab, showVaultTab, vaultPath]);
501500

502501
const buildVaultDocUrl = React.useCallback(
503502
(vp: string) => (path: string) =>
@@ -842,6 +841,7 @@ const App: React.FC = () => {
842841
};
843842

844843
// Global keyboard shortcuts (Cmd/Ctrl+Enter to submit)
844+
// biome-ignore lint/correctness/useExhaustiveDependencies: handler functions are stable enough — wrapping in useCallback would be a larger refactor
845845
useEffect(() => {
846846
const handleKeyDown = (e: KeyboardEvent) => {
847847
// Only handle Cmd/Ctrl+Enter
@@ -923,9 +923,6 @@ const App: React.FC = () => {
923923
annotateMode,
924924
origin,
925925
getAgentWarning,
926-
handleAnnotateFeedback,
927-
handleApprove,
928-
handleDeny,
929926
]);
930927

931928
const handleAddAnnotation = (ann: Annotation) => {
@@ -1042,6 +1039,7 @@ const App: React.FC = () => {
10421039
};
10431040

10441041
// Cmd/Ctrl+S keyboard shortcut — save to default notes app
1042+
// biome-ignore lint/correctness/useExhaustiveDependencies: handler functions are stable enough
10451043
useEffect(() => {
10461044
const handleSaveShortcut = (e: KeyboardEvent) => {
10471045
if (e.key !== 's' || !(e.metaKey || e.ctrlKey)) return;
@@ -1094,8 +1092,6 @@ const App: React.FC = () => {
10941092
pendingPasteImage,
10951093
submitted,
10961094
isApiMode,
1097-
handleDownloadAnnotations,
1098-
handleQuickSaveToNotes,
10991095
]);
11001096

11011097
// Close export dropdown on click outside

packages/review-editor/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ const ReviewApp: React.FC = () => {
173173
const isResizing = panelResize.isDragging || fileTreeResize.isDragging;
174174

175175
// Global keyboard shortcuts
176+
// biome-ignore lint/correctness/useExhaustiveDependencies: handleCopyDiff is stable enough
176177
useEffect(() => {
177178
const handleKeyDown = (e: KeyboardEvent) => {
178179
// Escape closes modals
@@ -190,8 +191,7 @@ const ReviewApp: React.FC = () => {
190191

191192
window.addEventListener('keydown', handleKeyDown);
192193
return () => window.removeEventListener('keydown', handleKeyDown);
193-
// eslint-disable-next-line react-hooks/exhaustive-deps
194-
}, [showExportModal, handleCopyDiff]);
194+
}, [showExportModal]);
195195

196196
// Get annotations for active file
197197
const activeFileAnnotations = useMemo(() => {

packages/review-editor/utils/renderInlineMarkdown.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ function renderInline(text: string, startKey: number): React.ReactNode[] {
4141
let lastIndex = 0;
4242
let match: RegExpExecArray | null;
4343

44+
// biome-ignore lint/suspicious/noAssignInExpressions: standard regex exec loop pattern
4445
while ((match = regex.exec(text)) !== null) {
4546
// Text before match
4647
if (match.index > lastIndex) {

packages/ui/components/AnnotationToolbar.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
7373
};
7474

7575
// Focus input when entering input step (including on mount with initialStep='input')
76+
// biome-ignore lint/correctness/useExhaustiveDependencies: element triggers refocus on new selection
7677
useEffect(() => {
7778
if (step === 'input') {
7879
// Use setTimeout to ensure DOM is fully ready (portals can have timing issues)
@@ -86,16 +87,17 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
8687
}, 0);
8788
return () => clearTimeout(timeoutId);
8889
}
89-
}, [step]); // Also re-run when element changes (new selection)
90+
}, [step, element]);
9091

9192
// Reset state when element changes
93+
// biome-ignore lint/correctness/useExhaustiveDependencies: element triggers reset on new selection
9294
useEffect(() => {
9395
setStep(initialStep);
9496
setActiveType(initialType ?? null);
9597
setInputValue('');
9698
setImages([]);
9799
setCopied(false);
98-
}, [initialStep, initialType]);
100+
}, [element, initialStep, initialType]);
99101

100102
// Notify parent when locked (in input mode)
101103
useEffect(() => {

packages/ui/components/ImageAnnotator/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export const ImageAnnotator: React.FC<ImageAnnotatorProps> = ({
3737
}, [isOpen, initialName]);
3838

3939
// Keyboard shortcuts
40+
// biome-ignore lint/correctness/useExhaustiveDependencies: handlers are stable useCallbacks with empty deps
4041
useEffect(() => {
4142
if (!isOpen) return;
4243

@@ -74,7 +75,7 @@ export const ImageAnnotator: React.FC<ImageAnnotatorProps> = ({
7475

7576
window.addEventListener('keydown', handleKeyDown);
7677
return () => window.removeEventListener('keydown', handleKeyDown);
77-
}, [isOpen, handleAccept, handleUndo]);
78+
}, [isOpen, state.strokes]);
7879

7980
const handleStrokeStart = useCallback((point: Point) => {
8081
const id = crypto.randomUUID();

packages/ui/components/MermaidBlock.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,12 @@ export const MermaidBlock: React.FC<{ block: Block }> = ({ block }) => {
117117
}, [block.content, block.id]);
118118

119119
// Reset zoom and pan when content changes
120+
// biome-ignore lint/correctness/useExhaustiveDependencies: block.content triggers zoom reset
120121
useEffect(() => {
121122
zoomLevelRef.current = 1.0;
122123
baseViewBoxRef.current = null;
123124
panOffsetRef.current = { x: 0, y: 0 };
124-
}, []);
125+
}, [block.content]);
125126

126127
// Reset zoom and pan when switching from source back to diagram
127128
useEffect(() => {

packages/ui/components/Settings.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export const Settings: React.FC<SettingsProps> = ({
9696
return t;
9797
}, [mode]);
9898

99+
// biome-ignore lint/correctness/useExhaustiveDependencies: re-read settings when availableAgents loads
99100
useEffect(() => {
100101
if (showDialog) {
101102
setIdentity(getIdentity());
@@ -113,7 +114,7 @@ export const Settings: React.FC<SettingsProps> = ({
113114
setAgentWarning(getAgentWarning());
114115
}
115116
}
116-
}, [showDialog, origin, getAgentWarning]);
117+
}, [showDialog, availableAgents, origin, getAgentWarning]);
117118

118119
// Fetch detected vaults when Obsidian is enabled
119120
useEffect(() => {
@@ -125,6 +126,7 @@ export const Settings: React.FC<SettingsProps> = ({
125126
setDetectedVaults(data.vaults || []);
126127
// Auto-select first vault if none set
127128
if (data.vaults?.length > 0 && !obsidian.vaultPath) {
129+
// biome-ignore lint/correctness/noInvalidUseBeforeDeclaration: handleObsidianChange defined after this effect but stable
128130
handleObsidianChange({ vaultPath: data.vaults[0] });
129131
}
130132
})
@@ -134,6 +136,8 @@ export const Settings: React.FC<SettingsProps> = ({
134136
}, [
135137
obsidian.enabled,
136138
detectedVaults.length,
139+
// biome-ignore lint/correctness/useExhaustiveDependencies: handleObsidianChange is defined below but stable
140+
// biome-ignore lint/correctness/noInvalidUseBeforeDeclaration: handleObsidianChange is defined below but stable
137141
handleObsidianChange,
138142
obsidian.vaultPath,
139143
vaultsLoading,

packages/ui/components/Viewer.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(
279279
const walker = document.createTreeWalker(containerRef.current, NodeFilter.SHOW_TEXT, null);
280280

281281
let node: Text | null;
282+
// biome-ignore lint/suspicious/noAssignInExpressions: standard TreeWalker loop pattern
282283
while ((node = walker.nextNode() as Text | null)) {
283284
const text = node.textContent || '';
284285
const index = text.indexOf(searchText);
@@ -308,6 +309,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(
308309
let endNode: Text | null = null;
309310
let endOffset = 0;
310311

312+
// biome-ignore lint/suspicious/noAssignInExpressions: standard TreeWalker loop pattern
311313
while ((node = walker2.nextNode() as Text | null)) {
312314
const nodeLength = node.textContent?.length || 0;
313315

@@ -335,6 +337,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(
335337
return null;
336338
}, []);
337339

340+
// biome-ignore lint/correctness/useExhaustiveDependencies: blocks.find changes every render, blocks is stable enough
338341
useImperativeHandle(
339342
ref,
340343
() => ({
@@ -435,6 +438,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(
435438
let node: Text | null;
436439
let inRange = false;
437440

441+
// biome-ignore lint/suspicious/noAssignInExpressions: standard TreeWalker loop pattern
438442
while ((node = walker.nextNode() as Text | null)) {
439443
// Check if this node is the start container
440444
if (node === range.startContainer) {
@@ -505,7 +509,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(
505509
});
506510
},
507511
}),
508-
[findTextInDOM, onSelectAnnotation, blocks.find],
512+
[findTextInDOM, onSelectAnnotation],
509513
);
510514

511515
useEffect(() => {
@@ -565,6 +569,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(
565569
return () => highlighter.dispose();
566570
}, [
567571
onSelectAnnotation, // Auto-delete in redline mode
572+
// biome-ignore lint/correctness/useExhaustiveDependencies: createAnnotationFromSource is stable (only depends on refs and props)
568573
createAnnotationFromSource,
569574
]);
570575

@@ -1322,14 +1327,15 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ block, onHover, onLeave, isHovere
13221327
const codeRef = useRef<HTMLElement>(null);
13231328

13241329
// Highlight code block on mount and when content/language changes
1330+
// biome-ignore lint/correctness/useExhaustiveDependencies: block.content triggers re-highlight when code changes
13251331
useEffect(() => {
13261332
if (codeRef.current) {
13271333
// Reset any previous highlighting
13281334
codeRef.current.removeAttribute('data-highlighted');
13291335
codeRef.current.className = `hljs font-mono${block.language ? ` language-${block.language}` : ''}`;
13301336
hljs.highlightElement(codeRef.current);
13311337
}
1332-
}, [block.language]);
1338+
}, [block.content, block.language]);
13331339

13341340
const handleCopy = useCallback(async () => {
13351341
try {

0 commit comments

Comments
 (0)