Skip to content
Merged
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
15 changes: 15 additions & 0 deletions frontend/app/element/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const SearchComponent = ({
caseSensitive: caseSensitiveAtom,
wholeWord: wholeWordAtom,
isOpen: isOpenAtom,
focusInput: focusInputAtom,
anchorRef,
offsetX = 10,
offsetY = 10,
Expand All @@ -37,6 +38,8 @@ const SearchComponent = ({
const [search, setSearch] = useAtom<string>(searchAtom);
const [index, setIndex] = useAtom<number>(indexAtom);
const [numResults, setNumResults] = useAtom<number>(numResultsAtom);
const [focusInputCounter, setFocusInputCounter] = useAtom<number>(focusInputAtom);
const inputRef = useRef<HTMLInputElement>(null);

const handleOpenChange = useCallback((open: boolean) => {
setIsOpen(open);
Expand All @@ -47,6 +50,7 @@ const SearchComponent = ({
setSearch("");
setIndex(0);
setNumResults(0);
setFocusInputCounter(0);
}
}, [isOpen]);

Expand All @@ -56,6 +60,15 @@ const SearchComponent = ({
onSearch?.(search);
}, [search]);

// When activateSearch fires while already open, it increments focusInputCounter
// to signal this specific instance to grab focus (avoids global DOM queries).
useEffect(() => {
if (focusInputCounter > 0 && isOpen) {
inputRef.current?.focus();
inputRef.current?.select();
}
}, [focusInputCounter]);

const middleware: Middleware[] = [];
const offsetCallback = useCallback(
({ rects }) => {
Expand Down Expand Up @@ -146,6 +159,7 @@ const SearchComponent = ({
<FloatingPortal>
<div className="search-container" style={{ ...floatingStyles }} ref={refs.setFloating}>
<Input
ref={inputRef}
placeholder="Search"
value={search}
onChange={setSearch}
Expand Down Expand Up @@ -197,6 +211,7 @@ export function useSearch(options?: SearchOptions): SearchProps {
resultsIndex: atom(0),
resultsCount: atom(0),
isOpen: atom(false),
focusInput: atom(0),
regex: options?.regex !== undefined ? atom(options.regex) : undefined,
caseSensitive: options?.caseSensitive !== undefined ? atom(options.caseSensitive) : undefined,
wholeWord: options?.wholeWord !== undefined ? atom(options.wholeWord) : undefined,
Expand Down
10 changes: 9 additions & 1 deletion frontend/app/store/keymodel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -695,7 +695,15 @@ function registerGlobalKeys() {
return false;
}
if (bcm.viewModel.searchAtoms) {
globalStore.set(bcm.viewModel.searchAtoms.isOpen, true);
if (globalStore.get(bcm.viewModel.searchAtoms.isOpen)) {
// Already open — increment the focusInput counter so this block's
// SearchComponent focuses its own input (avoids a global DOM query
// that could target the wrong block when multiple searches are open).
const cur = globalStore.get(bcm.viewModel.searchAtoms.focusInput) as number;
globalStore.set(bcm.viewModel.searchAtoms.focusInput, cur + 1);
} else {
globalStore.set(bcm.viewModel.searchAtoms.isOpen, true);
}
return true;
}
return false;
Expand Down
6 changes: 6 additions & 0 deletions frontend/app/view/term/termwrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,12 @@ export class TermWrap {
if (!globalStore.get(copyOnSelectAtom)) {
return;
}
// Don't copy-on-select when the search bar has focus — navigating
// search results changes the terminal selection programmatically.
const active = document.activeElement;
if (active != null && active.closest(".search-container") != null) {
return;
}
const selectedText = this.terminal.getSelection();
if (selectedText.length > 0) {
navigator.clipboard.writeText(selectedText);
Expand Down
3 changes: 2 additions & 1 deletion frontend/types/custom.d.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// Copyright 2026, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

import type { WaveEnv } from "@/app/waveenv/waveenv";
import { type Placement } from "@floating-ui/react";
import type * as jotai from "jotai";
import type * as rxjs from "rxjs";
import type { WaveEnv } from "@/app/waveenv/waveenv";

declare global {
type GlobalAtomsType = {
Expand Down Expand Up @@ -276,6 +276,7 @@ declare global {
resultsIndex: PrimitiveAtom<number>;
resultsCount: PrimitiveAtom<number>;
isOpen: PrimitiveAtom<boolean>;
focusInput: PrimitiveAtom<number>;
regex?: PrimitiveAtom<boolean>;
caseSensitive?: PrimitiveAtom<boolean>;
wholeWord?: PrimitiveAtom<boolean>;
Expand Down