mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-16 21:10:40 -07:00
Merge branch 'main' into abhi/agent-factory.draft
This commit is contained in:
@@ -166,9 +166,11 @@ async function searchResourceCandidates(
|
||||
const fzf = new AsyncFzf(candidates, {
|
||||
selector: (candidate: ResourceSuggestionCandidate) => candidate.searchKey,
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const results = await fzf.find(normalizedPattern, {
|
||||
limit: MAX_SUGGESTIONS_TO_SHOW * 3,
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return results.map(
|
||||
(result: { item: ResourceSuggestionCandidate }) => result.item.suggestion,
|
||||
);
|
||||
@@ -188,9 +190,11 @@ async function searchAgentCandidates(
|
||||
const fzf = new AsyncFzf(candidates, {
|
||||
selector: (s: Suggestion) => s.label,
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const results = await fzf.find(normalizedPattern, {
|
||||
limit: MAX_SUGGESTIONS_TO_SHOW,
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return results.map((r: { item: Suggestion }) => r.item);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||
import {
|
||||
ExtensionRegistryClient,
|
||||
type RegistryExtension,
|
||||
} from '../../config/extensionRegistryClient.js';
|
||||
|
||||
export interface UseExtensionRegistryResult {
|
||||
extensions: RegistryExtension[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
search: (query: string) => void;
|
||||
}
|
||||
|
||||
export function useExtensionRegistry(
|
||||
initialQuery = '',
|
||||
): UseExtensionRegistryResult {
|
||||
const [extensions, setExtensions] = useState<RegistryExtension[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const client = useMemo(() => new ExtensionRegistryClient(), []);
|
||||
|
||||
// Ref to track the latest query to avoid race conditions
|
||||
const latestQueryRef = useRef(initialQuery);
|
||||
|
||||
// Ref for debounce timeout
|
||||
const debounceTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
||||
|
||||
const searchExtensions = useCallback(
|
||||
async (query: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const results = await client.searchExtensions(query);
|
||||
|
||||
// Only update if this is still the latest query
|
||||
if (query === latestQueryRef.current) {
|
||||
// Check if results are different from current extensions
|
||||
setExtensions((prev) => {
|
||||
if (
|
||||
prev.length === results.length &&
|
||||
prev.every((ext, i) => ext.id === results[i].id)
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
return results;
|
||||
});
|
||||
setError(null);
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
if (query === latestQueryRef.current) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
setExtensions([]);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[client],
|
||||
);
|
||||
|
||||
const search = useCallback(
|
||||
(query: string) => {
|
||||
latestQueryRef.current = query;
|
||||
|
||||
// Clear existing timeout
|
||||
if (debounceTimeoutRef.current) {
|
||||
clearTimeout(debounceTimeoutRef.current);
|
||||
}
|
||||
|
||||
// Debounce
|
||||
debounceTimeoutRef.current = setTimeout(() => {
|
||||
void searchExtensions(query);
|
||||
}, 300);
|
||||
},
|
||||
[searchExtensions],
|
||||
);
|
||||
|
||||
// Initial load
|
||||
useEffect(() => {
|
||||
void searchExtensions(initialQuery);
|
||||
|
||||
return () => {
|
||||
if (debounceTimeoutRef.current) {
|
||||
clearTimeout(debounceTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [initialQuery, searchExtensions]);
|
||||
|
||||
return {
|
||||
extensions,
|
||||
loading,
|
||||
error,
|
||||
search,
|
||||
};
|
||||
}
|
||||
@@ -36,6 +36,9 @@ vi.mock('@google/gemini-cli-core', async () => {
|
||||
return {
|
||||
...actual,
|
||||
isHeadlessMode: vi.fn().mockReturnValue(false),
|
||||
FolderTrustDiscoveryService: {
|
||||
discover: vi.fn(() => new Promise(() => {})),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,13 @@ import {
|
||||
} from '../../config/trustedFolders.js';
|
||||
import * as process from 'node:process';
|
||||
import { type HistoryItemWithoutId, MessageType } from '../types.js';
|
||||
import { coreEvents, ExitCodes, isHeadlessMode } from '@google/gemini-cli-core';
|
||||
import {
|
||||
coreEvents,
|
||||
ExitCodes,
|
||||
isHeadlessMode,
|
||||
FolderTrustDiscoveryService,
|
||||
type FolderDiscoveryResults,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { runExitCleanup } from '../../utils/cleanup.js';
|
||||
|
||||
export const useFolderTrust = (
|
||||
@@ -24,6 +30,8 @@ export const useFolderTrust = (
|
||||
) => {
|
||||
const [isTrusted, setIsTrusted] = useState<boolean | undefined>(undefined);
|
||||
const [isFolderTrustDialogOpen, setIsFolderTrustDialogOpen] = useState(false);
|
||||
const [discoveryResults, setDiscoveryResults] =
|
||||
useState<FolderDiscoveryResults | null>(null);
|
||||
const [isRestarting, setIsRestarting] = useState(false);
|
||||
const startupMessageSent = useRef(false);
|
||||
|
||||
@@ -33,6 +41,19 @@ export const useFolderTrust = (
|
||||
let isMounted = true;
|
||||
const { isTrusted: trusted } = isWorkspaceTrusted(settings.merged);
|
||||
|
||||
if (trusted === undefined || trusted === false) {
|
||||
void FolderTrustDiscoveryService.discover(process.cwd())
|
||||
.then((results) => {
|
||||
if (isMounted) {
|
||||
setDiscoveryResults(results);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Silently ignore discovery errors as they are handled within the service
|
||||
// and reported via results.discoveryErrors if successful.
|
||||
});
|
||||
}
|
||||
|
||||
const showUntrustedMessage = () => {
|
||||
if (trusted === false && !startupMessageSent.current) {
|
||||
addItem(
|
||||
@@ -100,8 +121,6 @@ export const useFolderTrust = (
|
||||
onTrustChange(currentIsTrusted);
|
||||
setIsTrusted(currentIsTrusted);
|
||||
|
||||
// logic: we restart if the trust state *effectively* changes from the previous state.
|
||||
// previous state was `isTrusted`. If undefined, we assume false (untrusted).
|
||||
const wasTrusted = isTrusted ?? false;
|
||||
|
||||
if (wasTrusted !== currentIsTrusted) {
|
||||
@@ -117,6 +136,7 @@ export const useFolderTrust = (
|
||||
return {
|
||||
isTrusted,
|
||||
isFolderTrustDialogOpen,
|
||||
discoveryResults,
|
||||
handleFolderTrustSelect,
|
||||
isRestarting,
|
||||
};
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { AsyncFzf } from 'fzf';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import {
|
||||
useTextBuffer,
|
||||
type TextBuffer,
|
||||
} from '../components/shared/text-buffer.js';
|
||||
import { getCachedStringWidth } from '../utils/textUtils.js';
|
||||
|
||||
interface FzfResult {
|
||||
item: string;
|
||||
start: number;
|
||||
end: number;
|
||||
score: number;
|
||||
positions?: number[];
|
||||
}
|
||||
|
||||
export interface GenericListItem {
|
||||
key: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
scopeMessage?: string;
|
||||
}
|
||||
|
||||
export interface UseFuzzyListProps<T extends GenericListItem> {
|
||||
items: T[];
|
||||
initialQuery?: string;
|
||||
onSearch?: (query: string) => void;
|
||||
}
|
||||
|
||||
export interface UseFuzzyListResult<T extends GenericListItem> {
|
||||
filteredItems: T[];
|
||||
searchBuffer: TextBuffer | undefined;
|
||||
searchQuery: string;
|
||||
setSearchQuery: (query: string) => void;
|
||||
maxLabelWidth: number;
|
||||
}
|
||||
|
||||
export function useFuzzyList<T extends GenericListItem>({
|
||||
items,
|
||||
initialQuery = '',
|
||||
onSearch,
|
||||
}: UseFuzzyListProps<T>): UseFuzzyListResult<T> {
|
||||
// Search state
|
||||
const [searchQuery, setSearchQuery] = useState(initialQuery);
|
||||
const [filteredKeys, setFilteredKeys] = useState<string[]>(() =>
|
||||
items.map((i) => i.key),
|
||||
);
|
||||
|
||||
// FZF instance for fuzzy searching
|
||||
const { fzfInstance, searchMap } = useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
const searchItems: string[] = [];
|
||||
|
||||
items.forEach((item) => {
|
||||
searchItems.push(item.label);
|
||||
map.set(item.label.toLowerCase(), item.key);
|
||||
});
|
||||
|
||||
const fzf = new AsyncFzf(searchItems, {
|
||||
fuzzy: 'v2',
|
||||
casing: 'case-insensitive',
|
||||
});
|
||||
return { fzfInstance: fzf, searchMap: map };
|
||||
}, [items]);
|
||||
|
||||
// Perform search
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
if (!searchQuery.trim() || !fzfInstance) {
|
||||
setFilteredKeys(items.map((i) => i.key));
|
||||
return;
|
||||
}
|
||||
|
||||
const doSearch = async () => {
|
||||
const results = await fzfInstance.find(searchQuery);
|
||||
|
||||
if (!active) return;
|
||||
|
||||
const matchedKeys = new Set<string>();
|
||||
results.forEach((res: FzfResult) => {
|
||||
const key = searchMap.get(res.item.toLowerCase());
|
||||
if (key) matchedKeys.add(key);
|
||||
});
|
||||
setFilteredKeys(Array.from(matchedKeys));
|
||||
onSearch?.(searchQuery);
|
||||
};
|
||||
|
||||
void doSearch().catch((error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Search failed:', error);
|
||||
setFilteredKeys(items.map((i) => i.key)); // Reset to all items on error
|
||||
});
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [searchQuery, fzfInstance, searchMap, items, onSearch]);
|
||||
|
||||
// Get mainAreaWidth for search buffer viewport from UIState
|
||||
const { mainAreaWidth } = useUIState();
|
||||
const viewportWidth = Math.max(20, mainAreaWidth - 8);
|
||||
|
||||
// Search input buffer
|
||||
const searchBuffer = useTextBuffer({
|
||||
initialText: searchQuery,
|
||||
initialCursorOffset: searchQuery.length,
|
||||
viewport: {
|
||||
width: viewportWidth,
|
||||
height: 1,
|
||||
},
|
||||
singleLine: true,
|
||||
onChange: (text) => setSearchQuery(text),
|
||||
});
|
||||
|
||||
// Filtered items to display
|
||||
const filteredItems = useMemo(() => {
|
||||
if (!searchQuery) return items;
|
||||
return items.filter((item) => filteredKeys.includes(item.key));
|
||||
}, [items, filteredKeys, searchQuery]);
|
||||
|
||||
// Calculate max label width for alignment
|
||||
const maxLabelWidth = useMemo(() => {
|
||||
let max = 0;
|
||||
// We use all items for consistent alignment even when filtered
|
||||
items.forEach((item) => {
|
||||
const labelFull =
|
||||
item.label + (item.scopeMessage ? ` ${item.scopeMessage}` : '');
|
||||
const lWidth = getCachedStringWidth(labelFull);
|
||||
const dWidth = item.description
|
||||
? getCachedStringWidth(item.description)
|
||||
: 0;
|
||||
max = Math.max(max, lWidth, dWidth);
|
||||
});
|
||||
return max;
|
||||
}, [items]);
|
||||
|
||||
return {
|
||||
filteredItems,
|
||||
searchBuffer,
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
maxLabelWidth,
|
||||
};
|
||||
}
|
||||
@@ -970,6 +970,10 @@ export const useGeminiStream = (
|
||||
'Response stopped due to prohibited image content.',
|
||||
[FinishReason.NO_IMAGE]:
|
||||
'Response stopped because no image was generated.',
|
||||
[FinishReason.IMAGE_RECITATION]:
|
||||
'Response stopped due to image recitation policy.',
|
||||
[FinishReason.IMAGE_OTHER]:
|
||||
'Response stopped due to other image-related reasons.',
|
||||
};
|
||||
|
||||
const message = finishReasonMessages[finishReason];
|
||||
|
||||
@@ -155,9 +155,10 @@ describe('useQuotaAndFallback', () => {
|
||||
expect(request?.isTerminalQuotaError).toBe(true);
|
||||
|
||||
const message = request!.message;
|
||||
expect(message).toContain('Usage limit reached for gemini-pro.');
|
||||
expect(message).toContain('Usage limit reached for all Pro models.');
|
||||
expect(message).toContain('Access resets at'); // From getResetTimeMessage
|
||||
expect(message).toContain('/stats model for usage details');
|
||||
expect(message).toContain('/model to switch models.');
|
||||
expect(message).toContain('/auth to switch to API key.');
|
||||
|
||||
expect(mockHistoryManager.addItem).not.toHaveBeenCalled();
|
||||
@@ -176,6 +177,77 @@ describe('useQuotaAndFallback', () => {
|
||||
expect(mockHistoryManager.addItem).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should show the model name for a terminal quota error on a non-pro model', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useQuotaAndFallback({
|
||||
config: mockConfig,
|
||||
historyManager: mockHistoryManager,
|
||||
userTier: UserTierId.FREE,
|
||||
setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
|
||||
onShowAuthSelection: mockOnShowAuthSelection,
|
||||
}),
|
||||
);
|
||||
|
||||
const handler = setFallbackHandlerSpy.mock
|
||||
.calls[0][0] as FallbackModelHandler;
|
||||
|
||||
let promise: Promise<FallbackIntent | null>;
|
||||
const error = new TerminalQuotaError(
|
||||
'flash quota',
|
||||
mockGoogleApiError,
|
||||
1000 * 60 * 5,
|
||||
);
|
||||
act(() => {
|
||||
promise = handler('gemini-flash', 'gemini-pro', error);
|
||||
});
|
||||
|
||||
const request = result.current.proQuotaRequest;
|
||||
expect(request).not.toBeNull();
|
||||
expect(request?.failedModel).toBe('gemini-flash');
|
||||
|
||||
const message = request!.message;
|
||||
expect(message).toContain('Usage limit reached for gemini-flash.');
|
||||
expect(message).not.toContain('all Pro models');
|
||||
|
||||
act(() => {
|
||||
result.current.handleProQuotaChoice('retry_later');
|
||||
});
|
||||
|
||||
await promise!;
|
||||
});
|
||||
|
||||
it('should handle terminal quota error without retry delay', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useQuotaAndFallback({
|
||||
config: mockConfig,
|
||||
historyManager: mockHistoryManager,
|
||||
userTier: UserTierId.FREE,
|
||||
setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
|
||||
onShowAuthSelection: mockOnShowAuthSelection,
|
||||
}),
|
||||
);
|
||||
|
||||
const handler = setFallbackHandlerSpy.mock
|
||||
.calls[0][0] as FallbackModelHandler;
|
||||
|
||||
let promise: Promise<FallbackIntent | null>;
|
||||
const error = new TerminalQuotaError('no delay', mockGoogleApiError);
|
||||
act(() => {
|
||||
promise = handler('gemini-pro', 'gemini-flash', error);
|
||||
});
|
||||
|
||||
const request = result.current.proQuotaRequest;
|
||||
const message = request!.message;
|
||||
expect(message).not.toContain('Access resets at');
|
||||
expect(message).toContain('Usage limit reached for all Pro models.');
|
||||
|
||||
act(() => {
|
||||
result.current.handleProQuotaChoice('retry_later');
|
||||
});
|
||||
|
||||
await promise!;
|
||||
});
|
||||
|
||||
it('should handle race conditions by stopping subsequent requests', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useQuotaAndFallback({
|
||||
|
||||
@@ -14,9 +14,9 @@ import {
|
||||
TerminalQuotaError,
|
||||
ModelNotFoundError,
|
||||
type UserTierId,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
VALID_GEMINI_MODELS,
|
||||
isProModel,
|
||||
getDisplayString,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { type UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
@@ -67,11 +67,9 @@ export function useQuotaAndFallback({
|
||||
let message: string;
|
||||
let isTerminalQuotaError = false;
|
||||
let isModelNotFoundError = false;
|
||||
const usageLimitReachedModel =
|
||||
failedModel === DEFAULT_GEMINI_MODEL ||
|
||||
failedModel === PREVIEW_GEMINI_MODEL
|
||||
? 'all Pro models'
|
||||
: failedModel;
|
||||
const usageLimitReachedModel = isProModel(failedModel)
|
||||
? 'all Pro models'
|
||||
: failedModel;
|
||||
if (error instanceof TerminalQuotaError) {
|
||||
isTerminalQuotaError = true;
|
||||
// Common part of the message for both tiers
|
||||
@@ -87,7 +85,7 @@ export function useQuotaAndFallback({
|
||||
isModelNotFoundError = true;
|
||||
if (VALID_GEMINI_MODELS.has(failedModel)) {
|
||||
const messageLines = [
|
||||
`It seems like you don't have access to ${failedModel}.`,
|
||||
`It seems like you don't have access to ${getDisplayString(failedModel)}.`,
|
||||
`Your admin might have disabled the access. Contact them to enable the Preview Release Channel.`,
|
||||
];
|
||||
message = messageLines.join('\n');
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import type { TextBuffer } from '../components/shared/text-buffer.js';
|
||||
import type { GenericListItem } from '../components/shared/SearchableList.js';
|
||||
import { useSearchBuffer } from './useSearchBuffer.js';
|
||||
|
||||
export interface UseRegistrySearchResult<T extends GenericListItem> {
|
||||
filteredItems: T[];
|
||||
searchBuffer: TextBuffer | undefined;
|
||||
searchQuery: string;
|
||||
setSearchQuery: (query: string) => void;
|
||||
maxLabelWidth: number;
|
||||
}
|
||||
|
||||
export function useRegistrySearch<T extends GenericListItem>(props: {
|
||||
items: T[];
|
||||
initialQuery?: string;
|
||||
onSearch?: (query: string) => void;
|
||||
}): UseRegistrySearchResult<T> {
|
||||
const { items, initialQuery = '', onSearch } = props;
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState(initialQuery);
|
||||
const isFirstRender = useRef(true);
|
||||
const onSearchRef = useRef(onSearch);
|
||||
|
||||
onSearchRef.current = onSearch;
|
||||
|
||||
useEffect(() => {
|
||||
if (isFirstRender.current) {
|
||||
isFirstRender.current = false;
|
||||
return;
|
||||
}
|
||||
onSearchRef.current?.(searchQuery);
|
||||
}, [searchQuery]);
|
||||
|
||||
const searchBuffer = useSearchBuffer({
|
||||
initialText: searchQuery,
|
||||
onChange: setSearchQuery,
|
||||
});
|
||||
|
||||
const maxLabelWidth = 0;
|
||||
|
||||
const filteredItems = items;
|
||||
|
||||
return {
|
||||
filteredItems,
|
||||
searchBuffer,
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
maxLabelWidth,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
useTextBuffer,
|
||||
type TextBuffer,
|
||||
} from '../components/shared/text-buffer.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
|
||||
const MIN_VIEWPORT_WIDTH = 20;
|
||||
const VIEWPORT_WIDTH_OFFSET = 8;
|
||||
|
||||
export interface UseSearchBufferProps {
|
||||
initialText?: string;
|
||||
onChange: (text: string) => void;
|
||||
}
|
||||
|
||||
export function useSearchBuffer({
|
||||
initialText = '',
|
||||
onChange,
|
||||
}: UseSearchBufferProps): TextBuffer {
|
||||
const { mainAreaWidth } = useUIState();
|
||||
const viewportWidth = Math.max(
|
||||
MIN_VIEWPORT_WIDTH,
|
||||
mainAreaWidth - VIEWPORT_WIDTH_OFFSET,
|
||||
);
|
||||
|
||||
return useTextBuffer({
|
||||
initialText,
|
||||
initialCursorOffset: initialText.length,
|
||||
viewport: {
|
||||
width: viewportWidth,
|
||||
height: 1,
|
||||
},
|
||||
singleLine: true,
|
||||
onChange,
|
||||
});
|
||||
}
|
||||
@@ -60,6 +60,7 @@ export const useSessionBrowser = (
|
||||
const originalFilePath = path.join(chatsDir, fileName);
|
||||
|
||||
// Load up the conversation.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const conversation: ConversationRecord = JSON.parse(
|
||||
await fs.readFile(originalFilePath, 'utf8'),
|
||||
);
|
||||
|
||||
@@ -271,6 +271,7 @@ function useCommandSuggestions(
|
||||
const fzfInstance = getFzfForCommands(commandsToSearch);
|
||||
if (fzfInstance) {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const fzfResults = await fzfInstance.fzf.find(partial);
|
||||
if (signal.aborted) return;
|
||||
const uniqueCommands = new Set<SlashCommand>();
|
||||
|
||||
@@ -22,6 +22,7 @@ export const useStateAndRef = <
|
||||
(newStateOrCallback) => {
|
||||
let newValue: T;
|
||||
if (typeof newStateOrCallback === 'function') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
newValue = newStateOrCallback(ref.current);
|
||||
} else {
|
||||
newValue = newStateOrCallback;
|
||||
|
||||
Reference in New Issue
Block a user