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