Added extension search (#5283)

This commit is contained in:
Zane
2025-10-21 12:54:56 -07:00
committed by GitHub
parent ebac0cfb4f
commit aab127e18d
9 changed files with 84 additions and 23 deletions
+1 -1
View File
@@ -366,7 +366,7 @@ function BaseChatContent({
/>
) : (
// Render messages with SearchView wrapper when search is enabled
<SearchView>
<SearchView placeholder="Search conversation...">
<ProgressiveMessageList
messages={filteredMessages}
chat={chat}
@@ -23,6 +23,8 @@ interface SearchBarProps {
inputRef?: React.RefObject<HTMLInputElement>;
/** Initial search term */
initialSearchTerm?: string;
/** Placeholder text for the search input */
placeholder?: string;
}
/**
@@ -35,6 +37,7 @@ export const SearchBar: React.FC<SearchBarProps> = ({
searchResults,
inputRef: externalInputRef,
initialSearchTerm = '',
placeholder = 'Search conversation...',
}: SearchBarProps) => {
const [searchTerm, setSearchTerm] = useState(initialSearchTerm);
const [caseSensitive, setCaseSensitive] = useState(false);
@@ -158,7 +161,7 @@ export const SearchBar: React.FC<SearchBarProps> = ({
value={searchTerm}
onChange={handleSearch}
onKeyDown={handleKeyDown}
placeholder="Search conversation..."
placeholder={placeholder}
className="no-drag w-full text-sm pl-9 pr-24 py-3 bg-background-inverse text-text-inverse
placeholder:text-text-inverse/50 focus:outline-none
active:border-border-strong"
@@ -19,6 +19,8 @@ interface SearchViewProps {
count: number;
currentIndex: number;
} | null;
/** Placeholder text for the search input */
placeholder?: string;
}
interface SearchContainerElement extends HTMLDivElement {
@@ -36,6 +38,7 @@ export const SearchView: React.FC<PropsWithChildren<SearchViewProps>> = ({
onSearch,
onNavigate,
searchResults,
placeholder,
}) => {
const [isSearchVisible, setIsSearchVisible] = useState(false);
const [initialSearchTerm, setInitialSearchTerm] = useState('');
@@ -376,6 +379,7 @@ export const SearchView: React.FC<PropsWithChildren<SearchViewProps>> = ({
searchResults={searchResults || internalSearchResults || undefined}
inputRef={searchInputRef}
initialSearchTerm={initialSearchTerm}
placeholder={placeholder}
/>
)}
{children}
@@ -16,6 +16,7 @@ import {
} from '../settings/extensions/utils';
import { activateExtension } from '../settings/extensions';
import { useConfig } from '../ConfigContext';
import { SearchView } from '../conversation/SearchView';
export type ExtensionsViewOptions = {
deepLinkConfig?: ExtensionConfig;
@@ -31,6 +32,7 @@ export default function ExtensionsView({
}) {
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [refreshKey, setRefreshKey] = useState(0);
const [searchTerm, setSearchTerm] = useState('');
const { addExtension } = useConfig();
const chatContext = useChatContext();
const sessionId = chatContext?.chat.sessionId || '';
@@ -102,7 +104,10 @@ export default function ExtensionsView({
return (
<MainPanelLayout>
<div className="flex flex-col min-w-0 flex-1 overflow-y-auto relative">
<div
className="flex flex-col min-w-0 flex-1 overflow-y-auto relative"
data-search-scroll-area
>
<div className="bg-background-default px-8 pb-4 pt-16">
<div className="flex flex-col page-transition">
<div className="flex justify-between items-center mb-1">
@@ -110,7 +115,8 @@ export default function ExtensionsView({
</div>
<p className="text-sm text-text-muted mb-6">
These extensions use the Model Context Protocol (MCP). They can expand Goose's
capabilities using three main components: Prompts, Resources, and Tools.
capabilities using three main components: Prompts, Resources, and Tools. ⌘F/Ctrl+F to
search.
</p>
{/* Action Buttons */}
@@ -138,16 +144,19 @@ export default function ExtensionsView({
</div>
<div className="px-8 pb-16">
<ExtensionsSection
key={refreshKey}
sessionId={sessionId}
deepLinkConfig={viewOptions.deepLinkConfig}
showEnvVars={viewOptions.showEnvVars}
hideButtons={true}
onModalClose={(extensionName: string) => {
scrollToExtension(extensionName);
}}
/>
<SearchView onSearch={(term) => setSearchTerm(term)} placeholder="Search extensions...">
<ExtensionsSection
key={refreshKey}
sessionId={sessionId}
deepLinkConfig={viewOptions.deepLinkConfig}
showEnvVars={viewOptions.showEnvVars}
hideButtons={true}
searchTerm={searchTerm}
onModalClose={(extensionName: string) => {
scrollToExtension(extensionName);
}}
/>
</SearchView>
</div>
{/* Bottom padding space - same as in hub.tsx */}
@@ -103,7 +103,7 @@ const SessionMessages: React.FC<{
) : filteredMessages?.length > 0 ? (
<ContextManagerProvider>
<div className="max-w-4xl mx-auto w-full">
<SearchView>
<SearchView placeholder="Search history...">
<ProgressiveMessageList
messages={filteredMessages}
chat={{
@@ -713,7 +713,7 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
</Button>
</div>
<p className="text-sm text-text-muted mb-4">
View and search your past conversations with Goose.
View and search your past conversations with Goose. F/Ctrl+F to search.
</p>
</div>
</div>
@@ -726,6 +726,7 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
onNavigate={handleSearchNavigation}
searchResults={searchResults}
className="relative"
placeholder="Search history..."
>
{/* Skeleton layer - always rendered but conditionally visible */}
<div
@@ -24,6 +24,7 @@ interface ExtensionSectionProps {
customToggle?: (extension: FixedExtensionEntry) => Promise<boolean | void>;
selectedExtensions?: string[]; // Add controlled state
onModalClose?: (extensionName: string) => void;
searchTerm?: string;
}
export default function ExtensionsSection({
@@ -35,6 +36,7 @@ export default function ExtensionsSection({
customToggle,
selectedExtensions = [],
onModalClose,
searchTerm = '',
}: ExtensionSectionProps) {
const { getExtensions, addExtension, removeExtension, extensionsList } = useConfig();
const [selectedExtension, setSelectedExtension] = useState<FixedExtensionEntry | null>(null);
@@ -199,6 +201,7 @@ export default function ExtensionsSection({
onToggle={handleExtensionToggle}
onConfigure={handleConfigureClick}
disableConfiguration={disableConfiguration}
searchTerm={searchTerm}
/>
{!hideButtons && (
@@ -10,6 +10,7 @@ interface ExtensionListProps {
onConfigure?: (extension: FixedExtensionEntry) => void;
isStatic?: boolean;
disableConfiguration?: boolean;
searchTerm?: string;
}
export default function ExtensionList({
@@ -18,10 +19,26 @@ export default function ExtensionList({
onConfigure,
isStatic,
disableConfiguration: _disableConfiguration,
searchTerm = '',
}: ExtensionListProps) {
// Separate enabled and disabled extensions
const enabledExtensions = extensions.filter((ext) => ext.enabled);
const disabledExtensions = extensions.filter((ext) => !ext.enabled);
const matchesSearch = (extension: FixedExtensionEntry): boolean => {
if (!searchTerm) return true;
const searchLower = searchTerm.toLowerCase();
const title = getFriendlyTitle(extension).toLowerCase();
const name = extension.name.toLowerCase();
const subtitle = getSubtitle(extension);
const description = subtitle.description?.toLowerCase() || '';
return (
title.includes(searchLower) || name.includes(searchLower) || description.includes(searchLower)
);
};
// Separate enabled and disabled extensions, then filter by search term
const enabledExtensions = extensions.filter((ext) => ext.enabled && matchesSearch(ext));
const disabledExtensions = extensions.filter((ext) => !ext.enabled && matchesSearch(ext));
// Sort each group alphabetically by their friendly title
const sortedEnabledExtensions = [...enabledExtensions].sort((a, b) =>
getFriendlyTitle(a).localeCompare(getFriendlyTitle(b))
+28 -4
View File
@@ -14,6 +14,7 @@ export class SearchHighlighter {
private onMatchesChange?: (count: number) => void;
private currentMatchIndex: number = -1;
private isScrollingProgrammatically: boolean = false;
private highlightTimeout?: ReturnType<typeof setTimeout>;
constructor(container: HTMLElement, onMatchesChange?: (count: number) => void) {
this.container = container;
@@ -33,10 +34,10 @@ export class SearchHighlighter {
`;
// Find scroll container (look for our custom data attribute first, then fallback to radix)
const searchScrollArea = container.closest('[data-search-scroll-area]');
this.scrollContainer =
container
.closest('[data-search-scroll-area]')
?.querySelector('[data-radix-scroll-area-viewport]') ||
searchScrollArea?.querySelector('[data-radix-scroll-area-viewport]') ||
(searchScrollArea as HTMLElement) ||
container.closest('[data-radix-scroll-area-viewport]');
if (this.scrollContainer) {
@@ -72,12 +73,32 @@ export class SearchHighlighter {
let shouldUpdate = false;
for (const mutation of mutations) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
// Ignore mutations from our own overlay
if (mutation.target === this.overlay || this.overlay.contains(mutation.target as Node)) {
continue;
}
// Ignore mutations that only add/remove our highlight elements
const isOnlyHighlights = Array.from(mutation.addedNodes).every(
(node) =>
node instanceof HTMLElement &&
(node.classList.contains('search-highlight') ||
node.classList.contains('search-highlight-container'))
);
if (isOnlyHighlights) {
continue;
}
shouldUpdate = true;
break;
}
}
if (shouldUpdate && this.currentTerm) {
this.highlight(this.currentTerm, this.caseSensitive);
// Debounce the highlight update to avoid rapid re-highlighting
if (this.highlightTimeout) {
clearTimeout(this.highlightTimeout);
}
this.highlightTimeout = setTimeout(() => {
this.highlight(this.currentTerm, this.caseSensitive);
}, 100);
}
});
this.mutationObserver.observe(container, { childList: true, subtree: true });
@@ -259,6 +280,9 @@ export class SearchHighlighter {
}
destroy() {
if (this.highlightTimeout) {
clearTimeout(this.highlightTimeout);
}
this.resizeObserver.disconnect();
this.mutationObserver.disconnect();
this.overlay.remove();