Skip to content
Merged
Changes from 2 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
17 changes: 17 additions & 0 deletions frontend/app/block/block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => {
const focusElemRef = useRef<HTMLInputElement>(null);
const blockRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
const pendingFocusRafRef = useRef<number | null>(null);
const [blockClicked, setBlockClicked] = useState(false);
const blockView = useAtomValue(waveEnv.getBlockMetaKeyAtom(nodeModel.blockId, "view")) ?? "";
const isFocused = useAtomValue(nodeModel.isFocused);
Expand All @@ -156,6 +157,14 @@ const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => {
const innerRect = useDebouncedNodeInnerRect(nodeModel);
const noPadding = useAtomValueSafe(viewModel.noPadding);

useEffect(() => {
return () => {
if (pendingFocusRafRef.current != null) {
cancelAnimationFrame(pendingFocusRafRef.current);
}
};
}, []);

useLayoutEffect(() => {
setBlockClicked(isFocused);
}, [isFocused]);
Expand Down Expand Up @@ -221,11 +230,19 @@ const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => {
);

const setFocusTarget = useCallback(() => {
if (pendingFocusRafRef.current != null) {
cancelAnimationFrame(pendingFocusRafRef.current);
pendingFocusRafRef.current = null;
}
const ok = viewModel?.giveFocus?.();
if (ok) {
return;
}
focusElemRef.current?.focus({ preventScroll: true });
pendingFocusRafRef.current = requestAnimationFrame(() => {
pendingFocusRafRef.current = null;
viewModel?.giveFocus?.();
});
}, [viewModel]);
Comment on lines 232 to 248
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find all giveFocus implementations to assess side-effect patterns

# Search for giveFocus method definitions
ast-grep --pattern $'giveFocus($_) {
  $$$
}'

# Also search for giveFocus with no params
ast-grep --pattern $'giveFocus() {
  $$$
}'

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

# First, find the block.tsx file and examine the code around lines 232-246
find . -name "block.tsx" -path "*/frontend/*" | head -5

Repository: wavetermdev/waveterm

Length of output: 94


🏁 Script executed:

# Search for giveFocus implementations using rg
rg "giveFocus\s*\(" --type ts --type tsx -A 5 -B 1 | head -100

Repository: wavetermdev/waveterm

Length of output: 91


🏁 Script executed:

# Also search for giveFocus method definitions more broadly
rg "giveFocus\s*[:\(]" --type ts --type tsx | head -50

Repository: wavetermdev/waveterm

Length of output: 91


🏁 Script executed:

# Check file size first
wc -l ./frontend/app/block/block.tsx

Repository: wavetermdev/waveterm

Length of output: 98


🏁 Script executed:

# Read the specific lines from block.tsx
sed -n '232,246p' ./frontend/app/block/block.tsx

Repository: wavetermdev/waveterm

Length of output: 619


🏁 Script executed:

# Search for giveFocus without type filters
rg "giveFocus\s*\(" -A 5 -B 1 | head -150

Repository: wavetermdev/waveterm

Length of output: 8613


🏁 Script executed:

# Also find webview.tsx file
find . -name "webview.tsx" | head -5

Repository: wavetermdev/waveterm

Length of output: 103


🏁 Script executed:

# Find the giveFocus method in webview.tsx and get more context
rg "giveFocus" ./frontend/app/view/webview/webview.tsx -A 30 -B 2

Repository: wavetermdev/waveterm

Length of output: 1248


🏁 Script executed:

# Check the webview.tsx file size first
wc -l ./frontend/app/view/webview/webview.tsx

Repository: wavetermdev/waveterm

Length of output: 108


🏁 Script executed:

# Read lines around 504-527 as mentioned in the review
sed -n '500,540p' ./frontend/app/view/webview/webview.tsx

Repository: wavetermdev/waveterm

Length of output: 1439


🏁 Script executed:

# Search for focusedBlockId function
rg "focusedBlockId" --type-list

Repository: wavetermdev/waveterm

Length of output: 453


🏁 Script executed:

# Search more broadly for focusedBlockId
rg "focusedBlockId" -A 3 -B 1 | head -50

Repository: wavetermdev/waveterm

Length of output: 3132


🏁 Script executed:

# Check the setFocusTarget context in block.tsx - get more lines to see available variables
sed -n '200,250p' ./frontend/app/block/block.tsx

Repository: wavetermdev/waveterm

Length of output: 2003


🏁 Script executed:

# Search for nodeModel usage in block.tsx
rg "nodeModel" ./frontend/app/block/block.tsx -B 1 -A 1 | head -30

Repository: wavetermdev/waveterm

Length of output: 1182


Double giveFocus() invocation creates duplicate subscriptions in webview.

The retry logic calls giveFocus() twice: immediately (line 237) and in the RAF callback (line 244). The webview implementation has problematic side effects when ctrlShiftState is true—it sets up a globalStore.sub() subscription and returns false. If the RAF callback fires while ctrlShiftState is still true, a second subscription is created. Both subscriptions respond to the state change, causing duplicate unsubscribe calls, double focus() invocations, and potential memory leaks.

Guard the RAF callback to skip the retry if focus is already on the block:

Suggested guard to prevent redundant retry
         focusElemRef.current?.focus({ preventScroll: true });
         pendingFocusRafRef.current = requestAnimationFrame(() => {
             pendingFocusRafRef.current = null;
+            if (focusedBlockId() === nodeModel.blockId) {
+                return;
+            }
             viewModel?.giveFocus?.();
         });
-    }, [viewModel]);
+    }, [viewModel, nodeModel.blockId]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/block/block.tsx` around lines 232 - 246, The retry RAF currently
calls viewModel?.giveFocus?() unconditionally, causing duplicate subscriptions;
update setFocusTarget so the RAF callback first checks whether focus is already
on the block (e.g. check focusElemRef.current and document.activeElement or a
viewModel method) and only calls viewModel?.giveFocus?() if focus is still not
within focusElemRef.current; keep the immediate giveFocus call as-is, still
clear pendingFocusRafRef, and ensure pendingFocusRafRef is nulled whether or not
the retry runs to avoid dangling refs.


const focusFromPointerEnter = useCallback(
Expand Down