Added extension search (#5283)
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user