{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../src/core/tools/search.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAe,IAAI,EAAE,MAAM,SAAS,CAAC;AAE5C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAC1E,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AAEtF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAW7D,QAAA,MAAM,YAAY;;;;;EAoBhB,CAAC;AAIH,MAAM,WAAW,iBAAiB;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,kBAAkB,CAAC;IAClC,QAAQ,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3C;AAED,MAAM,WAAW,iBAAiB;IACjC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,gBAAgB,GAAG,SAAS,CAAC;CAChD;AAqCD,wBAAgB,0BAA0B,CACzC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,iBAAiB,GACzB,cAAc,CAAC,OAAO,YAAY,EAAE,iBAAiB,GAAG,SAAS,CAAC,CAoEpE","sourcesContent":["/**\n * The unified `SearchCodebase` tool: ranked code discovery in lexical, semantic, or\n * hybrid (rank-fused) mode. Replaces the old `semantic_search` tool — see\n * docs/hybrid-retrieval-design.md, Decision 1. It is the only dedicated\n * discovery tool: this tool answers \"find where X lives\", and exact\n * line-level mechanics are a shell job (rg/find/ls via bash).\n */\n\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport type { EmbsearchService } from \"../embsearch/embsearch-service.js\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.js\";\nimport { runSearch } from \"../search/hybrid-search.js\";\nimport type { ResolvedSearchMode } from \"../search/types.js\";\nimport { PEEK_LINES } from \"../tool-output-view.js\";\nimport { getTextOutput, invalidArgText, str } from \"./render-utils.js\";\n\n// Default of 5 balances recall against context cost. Measured on the 62-query\n// eval: limit=5 costs ~1000 tokens and puts a gold span in the results 68% of\n// the time; limit=10 costs ~1600 tokens for 74%. The extra 6 points are real\n// but not worth 60% more context on every call, and the model can ask for more.\nconst DEFAULT_RESULTS = 5;\nconst MAX_RESULTS = 30;\n\nconst searchSchema = Type.Object({\n\tquery: Type.String({\n\t\tdescription:\n\t\t\t\"What to find: an identifier, error text, or a natural-language description of the code, e.g. 'where sessions are persisted to disk'\",\n\t}),\n\tmode: Type.Optional(\n\t\tType.Union([Type.Literal(\"auto\"), Type.Literal(\"lexical\"), Type.Literal(\"semantic\"), Type.Literal(\"hybrid\")], {\n\t\t\tdescription:\n\t\t\t\t\"Retrieval mode (default: auto, which is almost always right). auto = hybrid when the index is available, else lexical; hybrid = keyword and meaning over the index plus exact text for anything indexed later; semantic = index only, skipping exact text; lexical = exact text only, the one mode that works with no index.\",\n\t\t}),\n\t),\n\tglob: Type.Optional(\n\t\tType.String({\n\t\t\tdescription:\n\t\t\t\t\"Optional glob filter applied to file paths. Only file paths matching the glob are searched. Supports both slashless patterns (match base name anywhere) and slash patterns (match full path).\",\n\t\t}),\n\t),\n\tlimit: Type.Optional(\n\t\tType.Number({ description: `Maximum number of results (default: ${DEFAULT_RESULTS}, max: ${MAX_RESULTS})` }),\n\t),\n});\n\ntype SearchToolInput = Static<typeof searchSchema>;\n\nexport interface SearchToolDetails {\n\tresultCount?: number;\n\tresolvedMode?: ResolvedSearchMode;\n\tindexing?: { done: number; total: number };\n}\n\nexport interface SearchToolOptions {\n\t/**\n\t * Provider for the per-session embsearch service. Resolved lazily at call\n\t * time because the service is created (and its index built) after tool\n\t * registration. When absent/unavailable the tool degrades to lexical mode.\n\t */\n\tgetService?: () => EmbsearchService | undefined;\n}\n\nfunction formatSearchCall(\n\targs: { query?: string; mode?: string; limit?: number } | undefined,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n): string {\n\tconst query = str(args?.query);\n\tconst queryDisplay = query === null ? invalidArgText(theme) : `\"${query}\"`;\n\tlet text = theme.fg(\"toolTitle\", theme.bold(\"SearchCodebase \")) + theme.fg(\"accent\", queryDisplay);\n\tconst extras: string[] = [];\n\tif (args?.mode && args.mode !== \"auto\") extras.push(args.mode);\n\tif (args?.limit !== undefined) extras.push(String(args.limit));\n\tif (extras.length > 0) text += theme.fg(\"muted\", ` (${extras.join(\", \")})`);\n\treturn text;\n}\n\nfunction formatSearchResult(\n\tresult: { content: Array<{ type: string; text?: string }>; details?: SearchToolDetails },\n\toptions: ToolRenderResultOptions,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result as any, showImages).trim();\n\tlet text = \"\";\n\tif (output) {\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst maxLines = options.expanded ? lines.length : PEEK_LINES;\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - maxLines;\n\t\ttext += `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t}\n\t}\n\treturn text;\n}\n\nexport function createSearchToolDefinition(\n\tcwd: string,\n\toptions?: SearchToolOptions,\n): ToolDefinition<typeof searchSchema, SearchToolDetails | undefined> {\n\treturn {\n\t\tname: \"SearchCodebase\",\n\t\tlabel: \"SearchCodebase\",\n\t\t// The search-vs-bash split is stated once, by buildSystemPrompt, whenever both\n\t\t// are registered — so it is deliberately absent here. What stays is what only\n\t\t// this tool knows: that the query is not a regex, and that it degrades to\n\t\t// exact-text when the index is missing.\n\t\tdescription:\n\t\t\t\"Find where code lives: ranked file:line-range results, fusing keyword and semantic retrieval over a local index with exact-text search of files the index has not read yet. The query is plain text, not a regex — regex metacharacters are matched literally. Falls back to exact-text retrieval automatically when the index is unavailable, and still finds code written moments ago that no index has seen.\",\n\t\tpromptSnippet: \"Ranked code search (keyword + semantic, rank-fused)\",\n\t\tpromptGuidelines: [\n\t\t\t\"SearchCodebase defaults to mode=auto, which is almost always right. Use limit=3 for targeted lookups, 10–20 when exploring a broad topic — past ~15 results the deeper ones arrive as ranked file:line-range headers without a snippet, which is still enough to choose what to read.\",\n\t\t],\n\t\tparameters: searchSchema,\n\t\tasync execute(_toolCallId, { query, mode, glob, limit }: SearchToolInput, signal?: AbortSignal) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\n\t\t\tconst result = await runSearch({\n\t\t\t\tcwd,\n\t\t\t\tquery,\n\t\t\t\tmode,\n\t\t\t\tglob,\n\t\t\t\tlimit: Math.min(MAX_RESULTS, Math.max(1, limit ?? DEFAULT_RESULTS)),\n\t\t\t\tservice: options?.getService?.(),\n\t\t\t\tsignal,\n\t\t\t});\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\n\t\t\tconst details: SearchToolDetails = {\n\t\t\t\tresultCount: result.resultCount,\n\t\t\t\tresolvedMode: result.resolvedMode,\n\t\t\t\tindexing: result.indexing,\n\t\t\t};\n\n\t\t\tconst notices: string[] = [];\n\t\t\tif (result.degradedReason) notices.push(result.degradedReason);\n\t\t\tif (result.indexing) {\n\t\t\t\tnotices.push(\n\t\t\t\t\t`index still building: ${result.indexing.done}/${result.indexing.total} chunks embedded — results may be incomplete`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst notice = notices.length > 0 ? `\\n\\n[${notices.join(\". \")}]` : \"\";\n\n\t\t\tif (result.resultCount === 0) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{ type: \"text\" as const, text: `No results for \"${query}\" (${result.resolvedMode})${notice}` },\n\t\t\t\t\t],\n\t\t\t\t\tdetails,\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text: result.text + notice }],\n\t\t\t\tdetails,\n\t\t\t};\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatSearchCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatSearchResult(result as any, options, theme, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n"]}