mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
Add a Codex-style global search dialog (Cmd+K / sidebar button) that full-text searches across all session transcripts, replacing the old title-only sidebar filter. Backend: rewrite searchService.searchSessions as a two-phase engine — ripgrep finds candidate files + matched lines, then those lines are parsed to keep only user/assistant text, re-confirmed against the cleaned text to drop JSON/UUID/base64 false positives, and windowed into highlighted snippets. Results carry real session titles (new sessionService.getSessionTitleAndMeta reusing the list title precedence), project path, mtime, role and match counts; falls back to a JS scan when ripgrep is unavailable. Frontend: new GlobalSearchModal (debounced, stale-response-safe, keyboard nav, role badges, highlighting, recent-chats empty state); Cmd+K now opens it; the sidebar input is replaced by a search trigger button. Tests: 15 backend cases (searchService.sessions) + 12 frontend cases (GlobalSearchModal); existing Sidebar/pages tests updated for the new trigger.
72 lines
2.5 KiB
TypeScript
72 lines
2.5 KiB
TypeScript
/**
|
|
* Search REST API
|
|
*
|
|
* POST /api/search — 全局工作区搜索
|
|
* POST /api/search/sessions — 搜索会话历史
|
|
*/
|
|
|
|
import { SearchService } from '../services/searchService.js'
|
|
import { ApiError, errorResponse } from '../middleware/errorHandler.js'
|
|
|
|
const searchService = new SearchService()
|
|
|
|
export async function handleSearchApi(
|
|
req: Request,
|
|
_url: URL,
|
|
segments: string[],
|
|
): Promise<Response> {
|
|
try {
|
|
const method = req.method
|
|
const sub = segments[2] // 'sessions' | undefined
|
|
|
|
if (method !== 'POST') {
|
|
throw new ApiError(
|
|
405,
|
|
`Method ${method} not allowed. Use POST.`,
|
|
'METHOD_NOT_ALLOWED',
|
|
)
|
|
}
|
|
|
|
const body = await parseJsonBody(req)
|
|
const query = body.query as string
|
|
if (!query || typeof query !== 'string') {
|
|
throw ApiError.badRequest('Missing or invalid "query" in request body')
|
|
}
|
|
|
|
// ── POST /api/search/sessions ──────────────────────────────────────────
|
|
if (sub === 'sessions') {
|
|
const { results, truncated } = await searchService.searchSessions(query, {
|
|
limit: body.limit as number | undefined,
|
|
matchesPerSession: body.matchesPerSession as number | undefined,
|
|
caseSensitive: body.caseSensitive as boolean | undefined,
|
|
})
|
|
return Response.json({ results, total: results.length, truncated })
|
|
}
|
|
|
|
// ── POST /api/search ───────────────────────────────────────────────────
|
|
if (!sub) {
|
|
const results = await searchService.searchWorkspace(query, {
|
|
cwd: body.cwd as string | undefined,
|
|
maxResults: body.maxResults as number | undefined,
|
|
glob: body.glob as string | undefined,
|
|
caseSensitive: body.caseSensitive as boolean | undefined,
|
|
})
|
|
return Response.json({ results, total: results.length })
|
|
}
|
|
|
|
throw ApiError.notFound(`Unknown search endpoint: ${sub}`)
|
|
} catch (error) {
|
|
return errorResponse(error)
|
|
}
|
|
}
|
|
|
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
|
|
async function parseJsonBody(req: Request): Promise<Record<string, unknown>> {
|
|
try {
|
|
return (await req.json()) as Record<string, unknown>
|
|
} catch {
|
|
throw ApiError.badRequest('Invalid JSON body')
|
|
}
|
|
}
|