@mosaic/mosaic is now the single package providing both: - 'mosaic' binary (CLI: yolo, coord, prdy, tui, gateway, etc.) - 'mosaic-wizard' binary (installation wizard) Changes: - Move packages/cli/src/* into packages/mosaic/src/ - Convert dynamic @mosaic/mosaic imports to static relative imports - Add CLI deps (ink, react, socket.io-client, @mosaic/config) to mosaic - Add jsx: react-jsx to mosaic's tsconfig - Exclude packages/cli from workspace (pnpm-workspace.yaml) - Update install.sh to install @mosaic/mosaic instead of @mosaic/cli - Bump version to 0.0.17 This eliminates the circular dependency between @mosaic/cli and @mosaic/mosaic that was blocking the build graph.
77 lines
1.9 KiB
TypeScript
77 lines
1.9 KiB
TypeScript
import { useState, useMemo, useCallback } from 'react';
|
|
import type { Message } from './use-socket.js';
|
|
|
|
export interface SearchMatch {
|
|
messageIndex: number;
|
|
charOffset: number;
|
|
}
|
|
|
|
export interface UseSearchReturn {
|
|
query: string;
|
|
setQuery: (q: string) => void;
|
|
matches: SearchMatch[];
|
|
currentMatchIndex: number;
|
|
nextMatch: () => void;
|
|
prevMatch: () => void;
|
|
clear: () => void;
|
|
totalMatches: number;
|
|
}
|
|
|
|
export function useSearch(messages: Message[]): UseSearchReturn {
|
|
const [query, setQuery] = useState('');
|
|
const [currentMatchIndex, setCurrentMatchIndex] = useState(0);
|
|
|
|
const matches = useMemo<SearchMatch[]>(() => {
|
|
if (query.length < 2) return [];
|
|
|
|
const lowerQuery = query.toLowerCase();
|
|
const result: SearchMatch[] = [];
|
|
|
|
for (let i = 0; i < messages.length; i++) {
|
|
const msg = messages[i];
|
|
if (!msg) continue;
|
|
const content = msg.content.toLowerCase();
|
|
let offset = 0;
|
|
while (true) {
|
|
const idx = content.indexOf(lowerQuery, offset);
|
|
if (idx === -1) break;
|
|
result.push({ messageIndex: i, charOffset: idx });
|
|
offset = idx + 1;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}, [query, messages]);
|
|
|
|
// Reset match index when matches change
|
|
useMemo(() => {
|
|
setCurrentMatchIndex(0);
|
|
}, [matches]);
|
|
|
|
const nextMatch = useCallback(() => {
|
|
if (matches.length === 0) return;
|
|
setCurrentMatchIndex((prev) => (prev + 1) % matches.length);
|
|
}, [matches.length]);
|
|
|
|
const prevMatch = useCallback(() => {
|
|
if (matches.length === 0) return;
|
|
setCurrentMatchIndex((prev) => (prev - 1 + matches.length) % matches.length);
|
|
}, [matches.length]);
|
|
|
|
const clear = useCallback(() => {
|
|
setQuery('');
|
|
setCurrentMatchIndex(0);
|
|
}, []);
|
|
|
|
return {
|
|
query,
|
|
setQuery,
|
|
matches,
|
|
currentMatchIndex,
|
|
nextMatch,
|
|
prevMatch,
|
|
clear,
|
|
totalMatches: matches.length,
|
|
};
|
|
}
|