mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
Merge pull request #12306 from Kilo-Org/custom-file-extensions-for-indexing
feat(indexing): support custom file extensions
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@kilocode/cli": minor
|
||||
"@kilocode/kilo-indexing": minor
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Configure a custom file extension allowlist for codebase indexing to limit scans to relevant project files and support additional text formats.
|
||||
@@ -166,6 +166,7 @@
|
||||
"name": "@kilocode/kilo-console",
|
||||
"version": "7.4.11",
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-indexing": "workspace:*",
|
||||
"@kilocode/kilo-web-ui": "workspace:*",
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@lottiefiles/dotlottie-web": "0.74.0",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"typecheck": "tsgo --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kilocode/kilo-indexing": "workspace:*",
|
||||
"@kilocode/kilo-web-ui": "workspace:*",
|
||||
"@kilocode/sdk": "workspace:*",
|
||||
"@lottiefiles/dotlottie-web": "0.74.0",
|
||||
|
||||
@@ -6,7 +6,16 @@ import { CustomSelect, type SelectOption } from "../../components/CustomSelect"
|
||||
import { loadEmbeddingModels } from "../../client"
|
||||
import { useConfig } from "../../context/config"
|
||||
import { ConfigPage, ConfigTag as Tag, SourceBadge } from "./ConfigPage"
|
||||
import { clean, clone, merge, providerPatch, removed, shouldSync, validate } from "./state/indexing"
|
||||
import {
|
||||
clean,
|
||||
clone,
|
||||
merge,
|
||||
parseFileExtensions,
|
||||
providerPatch,
|
||||
removed,
|
||||
shouldSync,
|
||||
validate,
|
||||
} from "./state/indexing"
|
||||
|
||||
type Provider = NonNullable<IndexingConfig["provider"]>
|
||||
type ProviderValue = Provider | ""
|
||||
@@ -111,6 +120,7 @@ export function IndexingRoute() {
|
||||
const [draft, setDraft] = createSignal<IndexingConfig>({})
|
||||
const [source, setSource] = createSignal("")
|
||||
const [dirty, setDirty] = createSignal(false)
|
||||
const [extensionText, setExtensionText] = createSignal("")
|
||||
const scope = () => ctx.query()?.scope ?? "global"
|
||||
const [selected, setSelected] = createSignal(scope())
|
||||
const project = () => scope() === "project"
|
||||
@@ -149,6 +159,8 @@ export function IndexingRoute() {
|
||||
setSelected(current)
|
||||
setSource(key)
|
||||
setDraft(clone(next))
|
||||
const extensions = project() ? merge(global(), next).fileExtensions : next.fileExtensions
|
||||
setExtensionText(extensions?.join(", ") ?? "")
|
||||
setDirty(false)
|
||||
})
|
||||
|
||||
@@ -478,6 +490,28 @@ export function IndexingRoute() {
|
||||
</div>
|
||||
</header>
|
||||
<div class="ui-form agent-builder-form">
|
||||
<FieldCard
|
||||
label="File extensions"
|
||||
description="Comma-separated allowlist. Leave empty to use the built-in defaults."
|
||||
actions={
|
||||
<SourceBadge
|
||||
source={field("fileExtensions")?.source}
|
||||
inherited={field("fileExtensions")?.inherited}
|
||||
overridden={field("fileExtensions")?.overridden}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<input
|
||||
value={extensionText()}
|
||||
placeholder=".php, .js, .css"
|
||||
disabled={Boolean(ctx.saving())}
|
||||
onInput={(event) => {
|
||||
const value = event.currentTarget.value
|
||||
setExtensionText(value)
|
||||
update({ fileExtensions: parseFileExtensions(value) })
|
||||
}}
|
||||
/>
|
||||
</FieldCard>
|
||||
<FieldCard
|
||||
label="Minimum search score"
|
||||
description="Similarity threshold from 0 to 1. Default is model-specific or 0.4."
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { clean, merge, providerPatch, removed, shouldSync, validate } from "./indexing"
|
||||
import { clean, merge, parseFileExtensions, providerPatch, removed, shouldSync, validate } from "./indexing"
|
||||
|
||||
describe("indexing config state", () => {
|
||||
test("merges project settings over nested global settings", () => {
|
||||
@@ -59,4 +59,13 @@ describe("indexing config state", () => {
|
||||
"Embedding batch size must be a positive integer.",
|
||||
])
|
||||
})
|
||||
|
||||
test("parses and cleans file extension allowlists", () => {
|
||||
expect(parseFileExtensions(" PHP, , .JS, js, C++, c++ ")).toEqual([".c++", ".js", ".php"])
|
||||
expect(parseFileExtensions(" , , ")).toBeUndefined()
|
||||
expect(clean({ fileExtensions: [] })).toEqual({})
|
||||
expect(validate({ fileExtensions: ["*.js", ".d.ts", "src/php"] })).toEqual([
|
||||
"File extensions must contain only a name with an optional leading dot.",
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { IndexingConfig } from "@kilocode/sdk/v2/client"
|
||||
import { isFileExtension } from "@kilocode/kilo-indexing/file-extensions"
|
||||
|
||||
export { parseFileExtensions } from "@kilocode/kilo-indexing/file-extensions"
|
||||
|
||||
function record(input: unknown): input is Record<string, unknown> {
|
||||
return typeof input === "object" && input !== null && !Array.isArray(input)
|
||||
@@ -26,6 +29,7 @@ export function merge(base: IndexingConfig | undefined, patch: IndexingConfig |
|
||||
|
||||
function prune(input: unknown): unknown {
|
||||
if (typeof input === "string") return input.trim() || undefined
|
||||
if (Array.isArray(input)) return input.length > 0 ? input : undefined
|
||||
if (!record(input)) return input ?? undefined
|
||||
const entries = Object.entries(input).flatMap(([key, value]) => {
|
||||
const next = prune(value)
|
||||
@@ -64,6 +68,9 @@ export function removed(before: IndexingConfig, after: IndexingConfig): string[]
|
||||
|
||||
export function validate(input: IndexingConfig): string[] {
|
||||
const errors: string[] = []
|
||||
if (input.fileExtensions?.some((item) => !isFileExtension(item))) {
|
||||
errors.push("File extensions must contain only a name with an optional leading dot.")
|
||||
}
|
||||
if (input.dimension !== undefined && input.dimension !== null) {
|
||||
if (!Number.isInteger(input.dimension) || input.dimension <= 0)
|
||||
errors.push("Vector dimension must be a positive integer.")
|
||||
|
||||
@@ -51,6 +51,7 @@ You can also edit the `indexing` section in `kilo.jsonc` directly:
|
||||
"enabled": true,
|
||||
"provider": "openai",
|
||||
"model": "text-embedding-3-small",
|
||||
"fileExtensions": [".php", ".js", ".css"],
|
||||
"vectorStore": "lancedb",
|
||||
"openai": { "apiKey": "sk-..." },
|
||||
"lancedb": {}
|
||||
@@ -129,6 +130,7 @@ You can also edit the `indexing` section directly. This is the full shape of the
|
||||
"apiKey": "pa-..."
|
||||
},
|
||||
"lancedb": {},
|
||||
"fileExtensions": [".php", ".js", ".css"],
|
||||
"searchMinScore": 0.4,
|
||||
"searchMaxResults": 50,
|
||||
"embeddingBatchSize": 60,
|
||||
@@ -227,6 +229,18 @@ The interface shows real-time status:
|
||||
|
||||
### Automatic File Filtering
|
||||
|
||||
Set `indexing.fileExtensions` to a non-empty array to index only the listed file extensions. Values are case-insensitive and may be written with or without a leading dot. When this setting is omitted, Kilo uses its built-in language list. Configured text formats without a Tree-sitter parser use line-based fallback chunking.
|
||||
|
||||
```json
|
||||
{
|
||||
"indexing": {
|
||||
"fileExtensions": [".php", ".js", ".css"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The configured list replaces the built-in defaults rather than adding to them. Clear the field in the settings UI to restore the defaults, or to inherit the global list from project scope.
|
||||
|
||||
The indexer automatically excludes:
|
||||
|
||||
- Binary files and images
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"./detect": "./src/detect.ts",
|
||||
"./embedding-models": "./src/kilo-embedding-models.ts",
|
||||
"./engine": "./src/indexing/index.ts",
|
||||
"./file-extensions": "./src/file-extensions.ts",
|
||||
"./server": "./src/server/routes.ts",
|
||||
"./status": "./src/status.ts"
|
||||
},
|
||||
|
||||
@@ -3,8 +3,10 @@ import z from "zod"
|
||||
import type { IndexingConfigInput } from "./indexing/config-manager"
|
||||
import { DEFAULT_VECTOR_STORE } from "./indexing/constants"
|
||||
import type { EmbedderProvider } from "./indexing/interfaces/manager"
|
||||
import { FILE_EXTENSION_PATTERN, normalizeFileExtensions } from "./file-extensions"
|
||||
|
||||
export { DEFAULT_VECTOR_STORE } from "./indexing/constants"
|
||||
export { isFileExtension, normalizeFileExtensions, parseFileExtensions } from "./file-extensions"
|
||||
|
||||
const providers = [
|
||||
"kilo",
|
||||
@@ -128,6 +130,11 @@ export const IndexingConfig = z
|
||||
.positive()
|
||||
.optional()
|
||||
.describe("Maximum retry attempts for failed embedding batches (default: 3)"),
|
||||
fileExtensions: z
|
||||
.array(z.string().trim().regex(FILE_EXTENSION_PATTERN))
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe("File extension allowlist for codebase indexing (uses built-in defaults if omitted)"),
|
||||
})
|
||||
.strict()
|
||||
.meta({ ref: "IndexingConfig" })
|
||||
@@ -229,6 +236,15 @@ export const IndexingSchema = Schema.Struct({
|
||||
scannerMaxBatchRetries: Schema.optional(PositiveInt).annotate({
|
||||
description: "Maximum retry attempts for failed embedding batches (default: 3)",
|
||||
}),
|
||||
fileExtensions: Schema.optional(
|
||||
Schema.mutable(
|
||||
Schema.Array(Schema.String.check(Schema.isPattern(/^\s*\.?[A-Za-z0-9][A-Za-z0-9_+-]*\s*$/))).check(
|
||||
Schema.isMinLength(1),
|
||||
),
|
||||
),
|
||||
).annotate({
|
||||
description: "File extension allowlist for codebase indexing (uses built-in defaults if omitted)",
|
||||
}),
|
||||
}).annotate({
|
||||
identifier: "IndexingConfig",
|
||||
description: "Codebase indexing configuration",
|
||||
@@ -250,6 +266,7 @@ export function toIndexingConfigInput(cfg: IndexingConfig | undefined): Indexing
|
||||
searchMaxResults: cfg?.searchMaxResults,
|
||||
embeddingBatchSize: cfg?.embeddingBatchSize,
|
||||
scannerMaxBatchRetries: cfg?.scannerMaxBatchRetries,
|
||||
fileExtensions: normalizeFileExtensions(cfg?.fileExtensions),
|
||||
kiloApiKey: cfg?.kilo?.apiKey,
|
||||
kiloBaseUrl: cfg?.kilo?.baseUrl,
|
||||
kiloOrganizationId: cfg?.kilo?.organizationId,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
export const FILE_EXTENSION_PATTERN = /^\.?[A-Za-z0-9][A-Za-z0-9_+-]*$/
|
||||
|
||||
export function isFileExtension(input: string): boolean {
|
||||
return FILE_EXTENSION_PATTERN.test(input.trim())
|
||||
}
|
||||
|
||||
export function normalizeFileExtensions(input: readonly string[] | undefined): string[] | undefined {
|
||||
if (!input) return undefined
|
||||
const values = new Set<string>()
|
||||
for (const raw of input) {
|
||||
const item = raw.trim().toLowerCase()
|
||||
if (!item) continue
|
||||
values.add(item.startsWith(".") ? item : `.${item}`)
|
||||
}
|
||||
return values.size > 0 ? [...values].sort() : undefined
|
||||
}
|
||||
|
||||
export function parseFileExtensions(input: string): string[] | undefined {
|
||||
const values = input
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
return values.length > 0 ? normalizeFileExtensions(values) : undefined
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import type { CodeIndexConfig, PreviousConfigSnapshot } from "./interfaces/confi
|
||||
import { DEFAULT_SEARCH_MIN_SCORE, DEFAULT_MAX_SEARCH_RESULTS, DEFAULT_VECTOR_STORE } from "./constants"
|
||||
import { getDefaultModelId, getModelDimension, getModelScoreThreshold } from "./model-registry"
|
||||
import { isEmbeddingProfileEqual, resolveEmbeddingProfile } from "./embedding-profile"
|
||||
import { resolveFileExtensions } from "./shared/supported-extensions"
|
||||
|
||||
/**
|
||||
* Raw input fed to CodeIndexConfigManager from the host environment.
|
||||
@@ -22,6 +23,7 @@ export interface IndexingConfigInput {
|
||||
searchMaxResults?: number
|
||||
embeddingBatchSize?: number
|
||||
scannerMaxBatchRetries?: number
|
||||
fileExtensions?: string[]
|
||||
kiloApiKey?: string
|
||||
kiloBaseUrl?: string
|
||||
kiloOrganizationId?: string
|
||||
@@ -70,6 +72,7 @@ export class CodeIndexConfigManager {
|
||||
private searchMaxResults?: number
|
||||
private embeddingBatchSize?: number
|
||||
private scannerMaxBatchRetries?: number
|
||||
private fileExtensions: string[] = resolveFileExtensions(undefined)
|
||||
|
||||
constructor(input: IndexingConfigInput) {
|
||||
this.applyInput(input)
|
||||
@@ -96,6 +99,7 @@ export class CodeIndexConfigManager {
|
||||
this.searchMaxResults = input.searchMaxResults
|
||||
this.embeddingBatchSize = input.embeddingBatchSize
|
||||
this.scannerMaxBatchRetries = input.scannerMaxBatchRetries
|
||||
this.fileExtensions = resolveFileExtensions(input.fileExtensions)
|
||||
this.modelId = input.modelId
|
||||
|
||||
// Validate and set model dimension
|
||||
@@ -153,6 +157,7 @@ export class CodeIndexConfigManager {
|
||||
voyageApiKey: this.voyageOptions?.apiKey ?? "",
|
||||
qdrantUrl: this.qdrantUrl ?? "",
|
||||
qdrantApiKey: this.qdrantApiKey ?? "",
|
||||
fileExtensions: [...this.fileExtensions],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,6 +235,8 @@ export class CodeIndexConfigManager {
|
||||
if ((prev.qdrantUrl ?? "") !== (this.qdrantUrl ?? "") || (prev.qdrantApiKey ?? "") !== (this.qdrantApiKey ?? ""))
|
||||
return true
|
||||
|
||||
if (prev.fileExtensions.join("\0") !== this.fileExtensions.join("\0")) return true
|
||||
|
||||
if (this.hasEmbeddingProfileChanged(prevProvider, prev.modelId, prev.modelDimension)) return true
|
||||
|
||||
return false
|
||||
@@ -276,6 +283,7 @@ export class CodeIndexConfigManager {
|
||||
searchMaxResults: this.currentSearchMaxResults,
|
||||
embeddingBatchSize: this.currentEmbeddingBatchSize,
|
||||
scannerMaxBatchRetries: this.currentScannerMaxBatchRetries,
|
||||
fileExtensions: [...this.fileExtensions],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ export interface CodeIndexConfig {
|
||||
searchMaxResults?: number
|
||||
embeddingBatchSize?: number
|
||||
scannerMaxBatchRetries?: number
|
||||
fileExtensions: string[]
|
||||
}
|
||||
|
||||
export type PreviousConfigSnapshot = {
|
||||
@@ -57,4 +58,5 @@ export type PreviousConfigSnapshot = {
|
||||
voyageApiKey?: string
|
||||
qdrantUrl?: string
|
||||
qdrantApiKey?: string
|
||||
fileExtensions: string[]
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { scannerExtensions } from "../shared/supported-extensions"
|
||||
import {
|
||||
type IFileWatcher,
|
||||
type ICodeParser,
|
||||
type FileProcessingResult,
|
||||
type IEmbedder,
|
||||
type IVectorStore,
|
||||
@@ -33,6 +34,7 @@ import { Log } from "../../util/log"
|
||||
import type { WorktreeOverlay } from "../worktree-overlay"
|
||||
import { sanitizeErrorMessage } from "../shared/validation-helpers"
|
||||
import type { IgnoreMatcher } from "../shared/load-ignore"
|
||||
import { isBinary } from "../shared/is-binary"
|
||||
|
||||
const log = Log.create({ service: "file-watcher" })
|
||||
|
||||
@@ -56,6 +58,7 @@ export class FileWatcher implements IFileWatcher {
|
||||
private drainTask?: Promise<void>
|
||||
private ready?: Promise<void>
|
||||
private overlay?: WorktreeOverlay
|
||||
private readonly extensions: ReadonlySet<string>
|
||||
|
||||
public readonly onDidStartBatchProcessing = new Emitter<string[]>()
|
||||
public readonly onBatchProgressUpdate = new Emitter<{
|
||||
@@ -75,12 +78,15 @@ export class FileWatcher implements IFileWatcher {
|
||||
maxBatchRetries?: number,
|
||||
private readonly onTelemetry?: IndexingTelemetryReporter,
|
||||
private readonly telemetryMeta?: IndexingTelemetryMeta,
|
||||
extensions: readonly string[] = scannerExtensions,
|
||||
private readonly parser: ICodeParser = codeParser,
|
||||
) {
|
||||
if (ignoreInstance) {
|
||||
this.ignoreInstance = ignoreInstance
|
||||
}
|
||||
this.batchSegmentThreshold = batchSegmentThreshold ?? BATCH_SEGMENT_THRESHOLD
|
||||
this.maxBatchRetries = maxBatchRetries ?? MAX_BATCH_RETRIES
|
||||
this.extensions = new Set(extensions)
|
||||
}
|
||||
|
||||
private emitRetry(attempt: number, batchSize: number, err: unknown): void {
|
||||
@@ -271,7 +277,7 @@ export class FileWatcher implements IFileWatcher {
|
||||
const ext = path.extname(filePath).toLowerCase()
|
||||
if (FileIgnore.match(relativeFilePath)) return false
|
||||
if (this.ignoreInstance?.ignores(relativeFilePath)) return false
|
||||
return scannerExtensions.includes(ext) || !path.extname(filePath)
|
||||
return this.extensions.has(ext)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -657,6 +663,14 @@ export class FileWatcher implements IFileWatcher {
|
||||
*/
|
||||
async processFile(filePath: string): Promise<FileProcessingResult> {
|
||||
try {
|
||||
if (!this.extensions.has(path.extname(filePath).toLowerCase())) {
|
||||
return {
|
||||
path: filePath,
|
||||
status: "skipped" as const,
|
||||
reason: "File extension is not configured for indexing",
|
||||
}
|
||||
}
|
||||
|
||||
// Check if file is in an ignored directory
|
||||
const relativeFilePath = generateRelativeIgnorePath(filePath, this.workspacePath)
|
||||
if (!relativeFilePath) {
|
||||
@@ -695,7 +709,16 @@ export class FileWatcher implements IFileWatcher {
|
||||
}
|
||||
|
||||
// Read file content
|
||||
const content = await readFile(filePath, "utf-8")
|
||||
const bytes = await readFile(filePath)
|
||||
if (isBinary(bytes)) {
|
||||
this.cacheManager.deleteHash(filePath)
|
||||
return {
|
||||
path: filePath,
|
||||
status: "skipped" as const,
|
||||
reason: "File is binary",
|
||||
}
|
||||
}
|
||||
const content = bytes.toString("utf-8")
|
||||
|
||||
// Calculate hash
|
||||
const newHash = createHash("sha256").update(content).digest("hex")
|
||||
@@ -710,7 +733,7 @@ export class FileWatcher implements IFileWatcher {
|
||||
}
|
||||
|
||||
// Parse file
|
||||
const blocks = await codeParser.parseFile(filePath, { content, fileHash: newHash })
|
||||
const blocks = await this.parser.parseFile(filePath, { content, fileHash: newHash })
|
||||
|
||||
// Prepare points for batch processing
|
||||
let pointsToUpsert: PointStruct[] = []
|
||||
|
||||
@@ -20,6 +20,11 @@ export class CodeParser implements ICodeParser {
|
||||
private pendingLoads: Map<string, Promise<LanguageParser>> = new Map()
|
||||
private failedParsers: Set<string> = new Set()
|
||||
private parserFallbackNotified: Set<string> = new Set()
|
||||
private readonly extensions: ReadonlySet<string>
|
||||
|
||||
constructor(extensions: readonly string[] = scannerExtensions) {
|
||||
this.extensions = new Set(extensions)
|
||||
}
|
||||
// Markdown files are now supported using the custom markdown parser
|
||||
// which extracts headers and sections for semantic indexing
|
||||
|
||||
@@ -93,7 +98,7 @@ export class CodeParser implements ICodeParser {
|
||||
* @returns Boolean indicating if the language is supported
|
||||
*/
|
||||
private isSupportedLanguage(extension: string): boolean {
|
||||
return scannerExtensions.includes(extension)
|
||||
return this.extensions.has(extension)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,6 +28,7 @@ import { Log } from "../../util/log"
|
||||
import { sanitizeErrorMessage } from "../shared/validation-helpers"
|
||||
import type { IndexingTelemetryMeta, IndexingTelemetryMode, IndexingTelemetryReporter } from "../interfaces/telemetry"
|
||||
import type { IgnoreMatcher } from "../shared/load-ignore"
|
||||
import { isBinary } from "../shared/is-binary"
|
||||
|
||||
const log = Log.create({ service: "indexing-scanner" })
|
||||
|
||||
@@ -35,6 +36,7 @@ export class DirectoryScanner implements IDirectoryScanner {
|
||||
private _cancelled = false
|
||||
private batchSegmentThreshold: number
|
||||
private maxBatchRetries: number
|
||||
private readonly extensions: ReadonlySet<string>
|
||||
|
||||
constructor(
|
||||
private readonly embedder: IEmbedder,
|
||||
@@ -46,9 +48,11 @@ export class DirectoryScanner implements IDirectoryScanner {
|
||||
maxBatchRetries?: number,
|
||||
private readonly onTelemetry?: IndexingTelemetryReporter,
|
||||
private readonly telemetryMeta?: IndexingTelemetryMeta,
|
||||
extensions: readonly string[] = scannerExtensions,
|
||||
) {
|
||||
this.batchSegmentThreshold = batchSegmentThreshold ?? BATCH_SEGMENT_THRESHOLD
|
||||
this.maxBatchRetries = maxBatchRetries ?? MAX_BATCH_RETRIES
|
||||
this.extensions = new Set(extensions)
|
||||
}
|
||||
|
||||
private emitFileCount(mode: IndexingTelemetryMode, discovered: number, candidate: number): void {
|
||||
@@ -166,7 +170,7 @@ export class DirectoryScanner implements IDirectoryScanner {
|
||||
return false
|
||||
}
|
||||
|
||||
return scannerExtensions.includes(ext) && !this.ignoreInstance.ignores(relativeFilePath)
|
||||
return this.extensions.has(ext) && !this.ignoreInstance.ignores(relativeFilePath)
|
||||
})
|
||||
log.info("discovered candidate files for indexing", {
|
||||
workspacePath: scanWorkspace,
|
||||
@@ -265,7 +269,12 @@ export class DirectoryScanner implements IDirectoryScanner {
|
||||
}
|
||||
|
||||
// Read file content using fs/promises
|
||||
const content = await readFile(filePath, "utf-8")
|
||||
const bytes = await readFile(filePath)
|
||||
if (isBinary(bytes)) {
|
||||
skippedCount++
|
||||
return
|
||||
}
|
||||
const content = bytes.toString("utf-8")
|
||||
|
||||
if (this._cancelled) {
|
||||
return
|
||||
|
||||
@@ -23,6 +23,12 @@ export class CodeIndexSearchService {
|
||||
private readonly baseline?: BaselineSearch,
|
||||
) {}
|
||||
|
||||
private allowed(result: VectorStoreSearchResult, extensions: ReadonlySet<string>): boolean {
|
||||
const file = result.payload?.filePath
|
||||
if (typeof file !== "string") return false
|
||||
return extensions.has(path.extname(file).toLowerCase())
|
||||
}
|
||||
|
||||
public async searchIndex(query: string, directoryPrefix?: string): Promise<VectorStoreSearchResult[]> {
|
||||
if (!this.configManager.isFeatureEnabled || !this.configManager.isFeatureConfigured) {
|
||||
throw new Error("Code index feature is disabled or not configured.")
|
||||
@@ -44,7 +50,11 @@ export class CodeIndexSearchService {
|
||||
}
|
||||
|
||||
const normalizedPrefix = directoryPrefix ? path.normalize(directoryPrefix) : undefined
|
||||
if (!this.baseline) return await this.vectorStore.search(vector, normalizedPrefix, minScore, maxResults)
|
||||
const extensions = new Set(this.configManager.getConfig().fileExtensions)
|
||||
if (!this.baseline) {
|
||||
const results = await this.vectorStore.search(vector, normalizedPrefix, minScore, maxResults)
|
||||
return results.filter((result) => this.allowed(result, extensions))
|
||||
}
|
||||
if (!this.baseline.overlay.ready) throw new Error("Worktree index reconciliation is not complete.")
|
||||
|
||||
const ceiling = Math.max(maxResults, Math.min(maxResults * 16, 1000))
|
||||
@@ -77,9 +87,12 @@ export class CodeIndexSearchService {
|
||||
"\0",
|
||||
)
|
||||
|
||||
for (const result of baseline) merged.set(key(result), result)
|
||||
for (const result of baseline) {
|
||||
if (this.allowed(result, extensions)) merged.set(key(result), result)
|
||||
}
|
||||
for (const result of current) {
|
||||
if (this.baseline.overlay.deltaResult(result)) merged.set(key(result), result)
|
||||
if (this.allowed(result, extensions) && this.baseline.overlay.deltaResult(result))
|
||||
merged.set(key(result), result)
|
||||
}
|
||||
return [...merged.values()].sort((left, right) => right.score - left.score).slice(0, maxResults)
|
||||
} catch (err) {
|
||||
|
||||
@@ -15,7 +15,7 @@ import { OpenRouterEmbedder } from "./embedders/openrouter"
|
||||
import { VoyageEmbedder } from "./embedders/voyage"
|
||||
import { QdrantVectorStore } from "./vector-store/qdrant-client"
|
||||
import { LanceDBVectorStore } from "./vector-store/lancedb-vector-store"
|
||||
import { codeParser, DirectoryScanner, FileWatcher } from "./processors"
|
||||
import { CodeParser, DirectoryScanner, FileWatcher } from "./processors"
|
||||
import type { AvailableEmbedders, ICodeParser, IEmbedder, IFileWatcher, IVectorStore } from "./interfaces"
|
||||
import type { CodeIndexConfigManager } from "./config-manager"
|
||||
import type { CacheManager } from "./cache-manager"
|
||||
@@ -240,6 +240,7 @@ export class CodeIndexServiceFactory {
|
||||
config.scannerMaxBatchRetries,
|
||||
this.onTelemetry,
|
||||
meta,
|
||||
config.fileExtensions,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -248,6 +249,7 @@ export class CodeIndexServiceFactory {
|
||||
vectorStore: IVectorStore,
|
||||
cacheManager: CacheManager,
|
||||
ignoreInstance: IgnoreMatcher,
|
||||
parser: ICodeParser,
|
||||
): IFileWatcher {
|
||||
const config = this.configManager.getConfig()
|
||||
const meta = this.getTelemetryMeta()
|
||||
@@ -261,6 +263,8 @@ export class CodeIndexServiceFactory {
|
||||
config.scannerMaxBatchRetries,
|
||||
this.onTelemetry,
|
||||
meta,
|
||||
config.fileExtensions,
|
||||
parser,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -289,9 +293,9 @@ export class CodeIndexServiceFactory {
|
||||
|
||||
const embedder = this.createEmbedder()
|
||||
const vectorStore = this.createVectorStore()
|
||||
const parser = codeParser
|
||||
const parser = new CodeParser(config.fileExtensions)
|
||||
const scanner = this.createDirectoryScanner(embedder, vectorStore, parser, ignoreInstance)
|
||||
const fileWatcher = this.createFileWatcher(embedder, vectorStore, cacheManager, ignoreInstance)
|
||||
const fileWatcher = this.createFileWatcher(embedder, vectorStore, cacheManager, ignoreInstance, parser)
|
||||
|
||||
log.info("indexing services created", {
|
||||
workspacePath: this.workspacePath,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
const SAMPLE_BYTES = 4096
|
||||
|
||||
export function isBinary(input: Uint8Array): boolean {
|
||||
const length = Math.min(input.length, SAMPLE_BYTES)
|
||||
if (length === 0) return false
|
||||
|
||||
let control = 0
|
||||
for (let index = 0; index < length; index++) {
|
||||
const byte = input[index]
|
||||
if (byte === 0) return true
|
||||
if (byte < 9 || (byte > 13 && byte < 32)) control++
|
||||
}
|
||||
return control / length > 0.3
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
import { extensions as allExtensions } from "../../tree-sitter"
|
||||
import { normalizeFileExtensions } from "../../file-extensions"
|
||||
|
||||
// Include all extensions including markdown for the scanner
|
||||
export const scannerExtensions = allExtensions
|
||||
|
||||
export function resolveFileExtensions(input: readonly string[] | undefined): string[] {
|
||||
return normalizeFileExtensions(input) ?? [...scannerExtensions]
|
||||
}
|
||||
|
||||
/**
|
||||
* Extensions that should always use fallback chunking instead of tree-sitter parsing.
|
||||
*
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { toIndexingConfigInput } from "../../../src/config"
|
||||
import {
|
||||
IndexingConfig,
|
||||
normalizeFileExtensions,
|
||||
parseFileExtensions,
|
||||
toIndexingConfigInput,
|
||||
} from "../../../src/config"
|
||||
import { CodeIndexConfigManager, type IndexingConfigInput } from "../../../src/indexing/config-manager"
|
||||
|
||||
function createInput(input: Partial<IndexingConfigInput> = {}): IndexingConfigInput {
|
||||
@@ -72,6 +77,25 @@ describe("CodeIndexConfigManager", () => {
|
||||
expect(cfg.getConfig().vectorStoreProvider).toBe("qdrant")
|
||||
})
|
||||
|
||||
test("normalizes configured file extensions", () => {
|
||||
expect(normalizeFileExtensions([" PHP ", ".JS", "js", "css"])).toEqual([".css", ".js", ".php"])
|
||||
expect(parseFileExtensions(" PHP, .JS, js, css ")).toEqual([".css", ".js", ".php"])
|
||||
expect(parseFileExtensions(" ")).toBeUndefined()
|
||||
expect(toIndexingConfigInput({ fileExtensions: ["PHP", ".JS"] }).fileExtensions).toEqual([".js", ".php"])
|
||||
expect(normalizeFileExtensions(["", " "])).toBeUndefined()
|
||||
expect(
|
||||
normalizeFileExtensions(Array.from({ length: 10_000 }, (_, index) => (index % 2 ? " PHP " : ".JS"))),
|
||||
).toEqual([".js", ".php"])
|
||||
})
|
||||
|
||||
test("validates file extension tokens", () => {
|
||||
expect(IndexingConfig.safeParse({ fileExtensions: ["php", " .JS "] }).success).toBe(true)
|
||||
expect(IndexingConfig.safeParse({ fileExtensions: [] }).success).toBe(false)
|
||||
expect(IndexingConfig.safeParse({ fileExtensions: ["*.js"] }).success).toBe(false)
|
||||
expect(IndexingConfig.safeParse({ fileExtensions: ["src/php"] }).success).toBe(false)
|
||||
expect(IndexingConfig.safeParse({ fileExtensions: [".d.ts"] }).success).toBe(false)
|
||||
})
|
||||
|
||||
test("configures Kilo with hosted auth options and explicit model metadata", () => {
|
||||
const cfg = new CodeIndexConfigManager(
|
||||
createInput({
|
||||
@@ -206,5 +230,13 @@ describe("CodeIndexConfigManager", () => {
|
||||
|
||||
expect(result.requiresRestart).toBe(true)
|
||||
})
|
||||
|
||||
test("restarts only when the normalized file extension allowlist changes", () => {
|
||||
const cfg = new CodeIndexConfigManager(createInput({ fileExtensions: ["php", ".JS"] }))
|
||||
|
||||
expect(cfg.getConfig().fileExtensions).toEqual([".js", ".php"])
|
||||
expect(cfg.loadConfiguration(createInput({ fileExtensions: [".js", ".PHP", "php"] })).requiresRestart).toBe(false)
|
||||
expect(cfg.loadConfiguration(createInput({ fileExtensions: [".css"] })).requiresRestart).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
VectorStoreSearchResult,
|
||||
} from "../../../../src/indexing/interfaces"
|
||||
import { FileWatcher } from "../../../../src/indexing/processors/file-watcher"
|
||||
import { CodeParser } from "../../../../src/indexing/processors/parser"
|
||||
import { loadIgnore } from "../../../../src/indexing/shared/load-ignore"
|
||||
import { WorktreeOverlay } from "../../../../src/indexing/worktree-overlay"
|
||||
|
||||
@@ -318,6 +319,46 @@ describe("FileWatcher", () => {
|
||||
expect(result.reason).toBe("File is ignored by .gitignore or .kilocodeignore")
|
||||
})
|
||||
|
||||
test("processFile uses the configured extension allowlist", async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "file-watcher-test-"))
|
||||
const cacheDir = path.join(root, ".cache")
|
||||
const custom = path.join(root, "source.custom")
|
||||
const excluded = path.join(root, "source.ts")
|
||||
const content = "custom source content ".repeat(20)
|
||||
await mkdir(cacheDir, { recursive: true })
|
||||
await writeFile(custom, content)
|
||||
await writeFile(excluded, content)
|
||||
|
||||
const cache = new CacheManager(cacheDir, root)
|
||||
await cache.initialize()
|
||||
const watcher = new FileWatcher(
|
||||
root,
|
||||
cache,
|
||||
createEmbedder(),
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
[".custom"],
|
||||
new CodeParser([".custom"]),
|
||||
)
|
||||
|
||||
const first = await watcher.processFile(custom)
|
||||
expect(first.status).toBe("processed_for_batching")
|
||||
if (first.status === "processed_for_batching" && first.newHash) cache.updateHash(custom, first.newHash)
|
||||
expect(await watcher.processFile(excluded)).toMatchObject({
|
||||
status: "skipped",
|
||||
reason: "File extension is not configured for indexing",
|
||||
})
|
||||
await writeFile(custom, new Uint8Array([0, 1, 2, 3]))
|
||||
expect(await watcher.processFile(custom)).toMatchObject({ status: "skipped", reason: "File is binary" })
|
||||
expect(cache.getHash(custom)).toBeUndefined()
|
||||
await writeFile(custom, content)
|
||||
expect((await watcher.processFile(custom)).status).toBe("processed_for_batching")
|
||||
})
|
||||
|
||||
test("processFile skips files matched by nested .gitignore during incremental updates", async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "file-watcher-test-"))
|
||||
try {
|
||||
|
||||
@@ -71,6 +71,21 @@ describe("CodeParser", () => {
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
test("uses fallback chunking for configured custom extensions", async () => {
|
||||
const custom = new CodeParser([".custom"])
|
||||
const result = await custom.parseFile("test.custom", { content: "custom source line ".repeat(20) })
|
||||
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
expect(result[0]?.type).toBe("fallback_chunk")
|
||||
})
|
||||
|
||||
test("excludes built-in extensions outside a configured allowlist", async () => {
|
||||
const custom = new CodeParser([".php"])
|
||||
const result = await custom.parseFile("test.js", { content: "const value = 1;".repeat(20) })
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
test("should use provided content instead of reading file when options.content is provided", async () => {
|
||||
const content = `/* This is a long test content string that exceeds 100 characters to properly test the parser's behavior with large inputs.
|
||||
It includes multiple lines and various JavaScript constructs to simulate real-world code.
|
||||
|
||||
@@ -309,6 +309,63 @@ describe("DirectoryScanner", () => {
|
||||
expect(cache.getHash(open)).toBeDefined()
|
||||
})
|
||||
|
||||
test("uses a configured extension allowlist and removes excluded cached files", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "scanner-test-"))
|
||||
const cacheDir = await mkdtemp(join(tmpdir(), "scanner-cache-"))
|
||||
const custom = join(root, "main.custom")
|
||||
const excluded = join(root, "main.ts")
|
||||
await Bun.write(custom, "custom source content\n")
|
||||
await Bun.write(excluded, "export const excluded = 1\n")
|
||||
|
||||
const cache = new CacheManager(cacheDir, root)
|
||||
await cache.initialize()
|
||||
cache.updateHash(excluded, "old-hash")
|
||||
const scan = new DirectoryScanner(
|
||||
new Emb(),
|
||||
new Store(),
|
||||
new Parser(),
|
||||
cache,
|
||||
ignore(),
|
||||
1,
|
||||
1,
|
||||
undefined,
|
||||
undefined,
|
||||
[".custom"],
|
||||
)
|
||||
|
||||
const result = await scan.scanDirectory(root)
|
||||
|
||||
expect(result.stats.processed).toBe(1)
|
||||
expect(cache.getHash(custom)).toBeDefined()
|
||||
expect(cache.getHash(excluded)).toBeUndefined()
|
||||
})
|
||||
|
||||
test("skips binary files admitted by a custom extension", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "scanner-test-"))
|
||||
const cacheDir = await mkdtemp(join(tmpdir(), "scanner-cache-"))
|
||||
const file = join(root, "data.custom")
|
||||
await Bun.write(file, new Uint8Array([0, 1, 2, 3]))
|
||||
const cache = new CacheManager(cacheDir, root)
|
||||
await cache.initialize()
|
||||
const scan = new DirectoryScanner(
|
||||
new Emb(),
|
||||
new Store(),
|
||||
new Parser(),
|
||||
cache,
|
||||
ignore(),
|
||||
1,
|
||||
1,
|
||||
undefined,
|
||||
undefined,
|
||||
[".custom"],
|
||||
)
|
||||
|
||||
const result = await scan.scanDirectory(root)
|
||||
|
||||
expect(result.stats).toEqual({ processed: 0, skipped: 1 })
|
||||
expect(cache.getHash(file)).toBeUndefined()
|
||||
})
|
||||
|
||||
test("skips files matched by nested .kilocodeignore during full scans", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "scanner-test-"))
|
||||
const cacheDir = await mkdtemp(join(tmpdir(), "scanner-cache-"))
|
||||
|
||||
@@ -36,16 +36,34 @@ const embedder = (calls: string[][]): IEmbedder => ({
|
||||
},
|
||||
})
|
||||
|
||||
const config = () =>
|
||||
const config = (fileExtensions?: string[]) =>
|
||||
new CodeIndexConfigManager({
|
||||
enabled: true,
|
||||
embedderProvider: "openai",
|
||||
openAiKey: "test",
|
||||
vectorStoreProvider: "lancedb",
|
||||
searchMaxResults: 2,
|
||||
fileExtensions,
|
||||
})
|
||||
|
||||
describe("CodeIndexSearchService worktree search", () => {
|
||||
test("filters stale results using one vector query", async () => {
|
||||
const limits: number[] = []
|
||||
const state = new CodeIndexStateManager()
|
||||
state.setSystemState("Indexed")
|
||||
const service = new CodeIndexSearchService(
|
||||
config([".php"]),
|
||||
state,
|
||||
embedder([]),
|
||||
store([result("src/old.ts", 0.99), result("src/first.php", 0.9), result("src/second.php", 0.8)], limits),
|
||||
)
|
||||
|
||||
const results = await service.searchIndex("query")
|
||||
|
||||
expect(limits).toEqual([2])
|
||||
expect(results.map((item) => item.payload?.filePath)).toEqual(["src/first.php"])
|
||||
})
|
||||
|
||||
test("embeds once, hides baseline paths, and merges the current delta", async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "search-worktree-"))
|
||||
const main = await mkdtemp(path.join(tmpdir(), "search-main-"))
|
||||
|
||||
@@ -126,4 +126,13 @@ describe("indexing tab scope state", () => {
|
||||
qdrant: { url: "http://project", apiKey: "global-secret" },
|
||||
})
|
||||
})
|
||||
|
||||
it("replaces inherited file extensions with a project allowlist", () => {
|
||||
const global = { fileExtensions: [".ts", ".tsx"] }
|
||||
|
||||
expect(indexingConfig("project", global, {}).fileExtensions).toEqual([".ts", ".tsx"])
|
||||
expect(indexingConfig("project", global, { fileExtensions: [".php"] }).fileExtensions).toEqual([".php"])
|
||||
expect(indexingInheritance("project", global, {}, [["fileExtensions"]])).toBe("inherited")
|
||||
expect(indexingInheritance("project", global, { fileExtensions: [".php"] }, [["fileExtensions"]])).toBe("none")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, For, Show, createMemo, createSignal } from "solid-js"
|
||||
import { Button } from "@kilocode/kilo-ui/button"
|
||||
import { Card } from "@kilocode/kilo-ui/card"
|
||||
import { DEFAULT_VECTOR_STORE } from "@kilocode/kilo-indexing/config"
|
||||
import { DEFAULT_VECTOR_STORE, isFileExtension, parseFileExtensions } from "@kilocode/kilo-indexing/config"
|
||||
import { formatKiloEmbeddingModelLabel, getKiloEmbeddingModel } from "@kilocode/kilo-indexing/embedding-models"
|
||||
import { Select } from "@kilocode/kilo-ui/select"
|
||||
import { Switch } from "@kilocode/kilo-ui/switch"
|
||||
@@ -102,6 +102,8 @@ const IndexingTab: Component = () => {
|
||||
const [providerDrafts, setProviderDrafts] = createSignal<Record<string, string>>({})
|
||||
const [storeDrafts, setStoreDrafts] = createSignal<Record<string, string>>({})
|
||||
const [tuningDrafts, setTuningDrafts] = createSignal<Record<string, string>>({})
|
||||
const [extensionDrafts, setExtensionDrafts] = createSignal<Record<string, string>>({})
|
||||
const [extensionErrors, setExtensionErrors] = createSignal<Record<string, string>>({})
|
||||
const [scope, setScope] = createSignal<IndexingScope>("global")
|
||||
|
||||
const globalCfg = createMemo<IndexingConfig>(() => globalConfig().indexing ?? {})
|
||||
@@ -247,6 +249,30 @@ const IndexingTab: Component = () => {
|
||||
return value === undefined ? "" : String(value)
|
||||
}
|
||||
|
||||
const extensionValue = () => {
|
||||
const draft = extensionDrafts()[scope()]
|
||||
if (draft !== undefined) return draft
|
||||
return cfg().fileExtensions?.join(", ") ?? ""
|
||||
}
|
||||
|
||||
const saveExtensions = (value: string) => {
|
||||
const values = value
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
const invalid = values.find((item) => !isFileExtension(item))
|
||||
if (invalid) {
|
||||
setExtensionErrors((prev) => ({
|
||||
...prev,
|
||||
[scope()]: language.t("settings.indexing.fileExtensions.invalid", { extension: invalid }),
|
||||
}))
|
||||
return
|
||||
}
|
||||
updateIndexing({ fileExtensions: parseFileExtensions(value) })
|
||||
setExtensionDrafts((prev) => Object.fromEntries(Object.entries(prev).filter(([entry]) => entry !== scope())))
|
||||
setExtensionErrors((prev) => Object.fromEntries(Object.entries(prev).filter(([entry]) => entry !== scope())))
|
||||
}
|
||||
|
||||
const content = (_scope: IndexingScope) => (
|
||||
<div style={{ display: "flex", "flex-direction": "column", gap: "16px" }}>
|
||||
<Card>
|
||||
@@ -517,6 +543,26 @@ const IndexingTab: Component = () => {
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<SettingsRow
|
||||
title={language.t("settings.indexing.fileExtensions.title")}
|
||||
description={description(language.t("settings.indexing.fileExtensions.description"), [["fileExtensions"]])}
|
||||
tag={() => tag(scope(), [["fileExtensions"]])}
|
||||
>
|
||||
<TextField
|
||||
value={extensionValue()}
|
||||
error={extensionErrors()[scope()]}
|
||||
validationState={extensionErrors()[scope()] ? "invalid" : "valid"}
|
||||
placeholder=".php, .js, .css"
|
||||
onInput={(e: InputEvent) => {
|
||||
const target = e.currentTarget as HTMLInputElement
|
||||
setExtensionDrafts((prev) => ({ ...prev, [scope()]: target.value }))
|
||||
}}
|
||||
onBlur={(e: FocusEvent) => {
|
||||
const target = e.currentTarget as HTMLInputElement
|
||||
saveExtensions(target.value)
|
||||
}}
|
||||
/>
|
||||
</SettingsRow>
|
||||
<For each={tuning}>
|
||||
{(item, index) => (
|
||||
<SettingsRow
|
||||
|
||||
+4
@@ -1243,6 +1243,10 @@ export const dict = {
|
||||
"settings.indexing.qdrantApiKey.title": "مفتاح API لـ Qdrant",
|
||||
"settings.indexing.qdrantApiKey.description": "مفتاح API اختياري لمثيل Qdrant.",
|
||||
"settings.indexing.qdrantApiKey.placeholder": "مفتاح API اختياري",
|
||||
"settings.indexing.fileExtensions.title": "امتدادات الملفات",
|
||||
"settings.indexing.fileExtensions.description":
|
||||
"قائمة سماح مفصولة بفواصل. اتركها فارغة لاستخدام الإعدادات الافتراضية المضمنة.",
|
||||
"settings.indexing.fileExtensions.invalid": "امتداد غير صالح: {{extension}}",
|
||||
"settings.indexing.status.title": "الحالة",
|
||||
"settings.indexing.tuning.description": "معامل متقدم للبحث والدفعات.",
|
||||
"settings.indexing.providerField.description": "إعداد اتصال خاص بالموفر.",
|
||||
|
||||
+4
@@ -1266,6 +1266,10 @@ export const dict = {
|
||||
"settings.indexing.qdrantApiKey.title": "Chave de API do Qdrant",
|
||||
"settings.indexing.qdrantApiKey.description": "Chave de API opcional para a instância do Qdrant.",
|
||||
"settings.indexing.qdrantApiKey.placeholder": "Chave de API opcional",
|
||||
"settings.indexing.fileExtensions.title": "Extensões de arquivo",
|
||||
"settings.indexing.fileExtensions.description":
|
||||
"Lista de permissões separada por vírgulas. Deixe em branco para usar os padrões integrados.",
|
||||
"settings.indexing.fileExtensions.invalid": "Extensão inválida: {{extension}}",
|
||||
"settings.indexing.dimension.title": "Dimensão do vetor",
|
||||
"settings.indexing.dimension.description":
|
||||
"Deixe vazio para detectar automaticamente a dimensão de embedding do modelo.",
|
||||
|
||||
+4
@@ -921,6 +921,10 @@ export const dict = {
|
||||
"settings.indexing.providerField.description": "Postavka veze specifična za provajdera.",
|
||||
"settings.indexing.qdrantApiKey.description": "Opcionalni API ključ za Qdrant instancu.",
|
||||
"settings.indexing.qdrantApiKey.placeholder": "Opcionalni API ključ",
|
||||
"settings.indexing.fileExtensions.title": "Ekstenzije datoteka",
|
||||
"settings.indexing.fileExtensions.description":
|
||||
"Lista dozvoljenih stavki odvojena zarezima. Ostavite prazno da biste koristili ugrađene zadane postavke.",
|
||||
"settings.indexing.fileExtensions.invalid": "Neispravna ekstenzija datoteke: {{extension}}",
|
||||
"settings.indexing.qdrantApiKey.title": "Qdrant API ključ",
|
||||
"settings.indexing.qdrantUrl.description": "URL servera za Qdrant instancu.",
|
||||
"settings.indexing.qdrantUrl.title": "Qdrant URL",
|
||||
|
||||
+4
@@ -932,6 +932,10 @@ export const dict = {
|
||||
"settings.indexing.qdrantApiKey.title": "Qdrant API-nøgle",
|
||||
"settings.indexing.qdrantApiKey.description": "Valgfri API-nøgle til Qdrant-instansen.",
|
||||
"settings.indexing.qdrantApiKey.placeholder": "Valgfri API-nøgle",
|
||||
"settings.indexing.fileExtensions.title": "Filtypenavne",
|
||||
"settings.indexing.fileExtensions.description":
|
||||
"Kommaadskilt tilladelsesliste. Lad feltet være tomt for at bruge de indbyggede standardindstillinger.",
|
||||
"settings.indexing.fileExtensions.invalid": "Ugyldig filtype: {{extension}}",
|
||||
"settings.indexing.lancedbDirectory.title": "LanceDB-mappe",
|
||||
"settings.indexing.lancedbDirectory.description": "Valgfri mappe til det lokale LanceDB-lager.",
|
||||
"settings.indexing.lancedbDirectory.placeholder": "Lad være tom for standard",
|
||||
|
||||
@@ -932,6 +932,10 @@ export const dict = {
|
||||
"settings.indexing.providerField.description": "Anbieterspezifische Verbindungseinstellung.",
|
||||
"settings.indexing.qdrantApiKey.description": "Optionaler API-Schlüssel für die Qdrant-Instanz.",
|
||||
"settings.indexing.qdrantApiKey.placeholder": "Optionaler API-Schlüssel",
|
||||
"settings.indexing.fileExtensions.title": "Dateierweiterungen",
|
||||
"settings.indexing.fileExtensions.description":
|
||||
"Durch Kommas getrennte Zulassungsliste. Leer lassen, um die integrierten Standardwerte zu verwenden.",
|
||||
"settings.indexing.fileExtensions.invalid": "Ungültige Erweiterung: {{extension}}",
|
||||
"settings.indexing.qdrantApiKey.title": "Qdrant API-Schlüssel",
|
||||
"settings.indexing.qdrantUrl.description": "Server-URL für die Qdrant-Instanz.",
|
||||
"settings.indexing.qdrantUrl.title": "Qdrant URL",
|
||||
|
||||
@@ -1216,6 +1216,10 @@ export const dict = {
|
||||
"settings.indexing.qdrantApiKey.title": "Qdrant API key",
|
||||
"settings.indexing.qdrantApiKey.description": "Optional API key for the Qdrant instance.",
|
||||
"settings.indexing.qdrantApiKey.placeholder": "Optional API key",
|
||||
"settings.indexing.fileExtensions.title": "File Extensions",
|
||||
"settings.indexing.fileExtensions.description":
|
||||
"Comma-separated allowlist. Leave empty to use the built-in defaults.",
|
||||
"settings.indexing.fileExtensions.invalid": "Invalid extension: {{extension}}",
|
||||
"settings.indexing.tuning.description": "Advanced search and batching parameter.",
|
||||
"settings.experimental.title": "Experimental",
|
||||
"settings.language.title": "Language",
|
||||
|
||||
+4
@@ -929,6 +929,10 @@ export const dict = {
|
||||
"settings.indexing.qdrantApiKey.title": "Clave API de Qdrant",
|
||||
"settings.indexing.qdrantApiKey.description": "Clave API opcional para la instancia de Qdrant.",
|
||||
"settings.indexing.qdrantApiKey.placeholder": "Clave API opcional",
|
||||
"settings.indexing.fileExtensions.title": "Extensiones de archivo",
|
||||
"settings.indexing.fileExtensions.description":
|
||||
"Lista de permitidos separada por comas. Déjala vacía para usar los valores predeterminados integrados.",
|
||||
"settings.indexing.fileExtensions.invalid": "Extensión no válida: {{extension}}",
|
||||
"settings.indexing.providerField.description": "Configuración de conexión específica del proveedor.",
|
||||
"settings.indexing.status.title": "Estado",
|
||||
"settings.indexing.tuning.description": "Parámetro avanzado de búsqueda y procesamiento por lotes.",
|
||||
|
||||
+4
@@ -848,6 +848,10 @@ export const dict = {
|
||||
"settings.indexing.providerField.description": "Paramètre de connexion spécifique au fournisseur.",
|
||||
"settings.indexing.qdrantApiKey.description": "Clé API optionnelle pour l'instance Qdrant.",
|
||||
"settings.indexing.qdrantApiKey.placeholder": "Clé API optionnelle",
|
||||
"settings.indexing.fileExtensions.title": "Extensions de fichier",
|
||||
"settings.indexing.fileExtensions.description":
|
||||
"Liste blanche séparée par des virgules. Laissez vide pour utiliser les valeurs par défaut intégrées.",
|
||||
"settings.indexing.fileExtensions.invalid": "Extension non valide : {{extension}}",
|
||||
"settings.indexing.qdrantApiKey.title": "Clé API Qdrant",
|
||||
"settings.indexing.qdrantUrl.description": "URL du serveur pour l'instance Qdrant.",
|
||||
"settings.indexing.qdrantUrl.title": "URL Qdrant",
|
||||
|
||||
+4
@@ -1055,6 +1055,10 @@ export const dict = {
|
||||
"settings.indexing.qdrantApiKey.title": "API key Qdrant",
|
||||
"settings.indexing.qdrantApiKey.description": "API key opzionale per l'istanza Qdrant.",
|
||||
"settings.indexing.qdrantApiKey.placeholder": "API key opzionale",
|
||||
"settings.indexing.fileExtensions.title": "Estensioni file",
|
||||
"settings.indexing.fileExtensions.description":
|
||||
"Elenco di elementi consentiti separati da virgole. Lasciare vuoto per utilizzare i valori predefiniti integrati.",
|
||||
"settings.indexing.fileExtensions.invalid": "Estensione non valida: {{extension}}",
|
||||
"settings.indexing.tuning.description": "Parametro avanzato per ricerca e batching.",
|
||||
"settings.experimental.title": "Sperimentale",
|
||||
"settings.language.title": "Lingua",
|
||||
|
||||
+4
@@ -912,6 +912,10 @@ export const dict = {
|
||||
"settings.indexing.providerField.description": "プロバイダー固有の接続設定。",
|
||||
"settings.indexing.qdrantApiKey.description": "QdrantインスタンスのオプションのAPIキー。",
|
||||
"settings.indexing.qdrantApiKey.placeholder": "オプションのAPIキー",
|
||||
"settings.indexing.fileExtensions.title": "ファイル拡張子",
|
||||
"settings.indexing.fileExtensions.description":
|
||||
"カンマ区切りの許可リストです。空欄のままにすると、組み込みのデフォルトが使用されます。",
|
||||
"settings.indexing.fileExtensions.invalid": "無効な拡張子: {{extension}}",
|
||||
"settings.indexing.qdrantApiKey.title": "Qdrant APIキー",
|
||||
"settings.indexing.qdrantUrl.description": "QdrantインスタンスのサーバーURL。",
|
||||
"settings.indexing.qdrantUrl.title": "Qdrant URL",
|
||||
|
||||
+4
@@ -1253,6 +1253,10 @@ export const dict = {
|
||||
"settings.indexing.providerField.description": "공급자별 연결 설정.",
|
||||
"settings.indexing.qdrantApiKey.description": "Qdrant 인스턴스에 대한 선택적 API 키입니다.",
|
||||
"settings.indexing.qdrantApiKey.placeholder": "선택적 API 키",
|
||||
"settings.indexing.fileExtensions.title": "파일 확장자",
|
||||
"settings.indexing.fileExtensions.description":
|
||||
"쉼표로 구분된 허용 목록입니다. 기본 제공 기본값을 사용하려면 비워 두세요.",
|
||||
"settings.indexing.fileExtensions.invalid": "잘못된 확장자: {{extension}}",
|
||||
"settings.indexing.qdrantApiKey.title": "Qdrant API 키",
|
||||
"settings.indexing.qdrantUrl.description": "Qdrant 인스턴스의 서버 URL입니다.",
|
||||
"settings.indexing.qdrantUrl.title": "Qdrant URL",
|
||||
|
||||
+4
@@ -1260,6 +1260,10 @@ export const dict = {
|
||||
"settings.indexing.qdrantApiKey.title": "Qdrant API-sleutel",
|
||||
"settings.indexing.qdrantApiKey.description": "Optionele API-sleutel voor de Qdrant-instantie.",
|
||||
"settings.indexing.qdrantApiKey.placeholder": "Optionele API-sleutel",
|
||||
"settings.indexing.fileExtensions.title": "Bestandsextensies",
|
||||
"settings.indexing.fileExtensions.description":
|
||||
"Door komma's gescheiden lijst met toegestane items. Laat leeg om de ingebouwde standaardwaarden te gebruiken.",
|
||||
"settings.indexing.fileExtensions.invalid": "Ongeldige extensie: {{extension}}",
|
||||
"settings.indexing.tuning.description": "Geavanceerde parameter voor zoeken en batching.",
|
||||
|
||||
"settings.experimental.title": "Experimenteel",
|
||||
|
||||
+4
@@ -1465,6 +1465,10 @@ export const dict = {
|
||||
"settings.indexing.qdrantApiKey.title": "Qdrant API-nøkkel",
|
||||
"settings.indexing.qdrantApiKey.description": "Valgfri API-nøkkel for Qdrant-instansen.",
|
||||
"settings.indexing.qdrantApiKey.placeholder": "Valgfri API-nøkkel",
|
||||
"settings.indexing.fileExtensions.title": "Filutvidelser",
|
||||
"settings.indexing.fileExtensions.description":
|
||||
"Kommaseparert tillatelsesliste. La stå tomt for å bruke de innebygde standardinnstillingene.",
|
||||
"settings.indexing.fileExtensions.invalid": "Ugyldig filutvidelse: {{extension}}",
|
||||
"settings.indexing.dimension.title": "Vektordimensjon",
|
||||
"settings.indexing.dimension.description": "La stå tom for automatisk å oppdage embedding-dimensjonen fra modellen.",
|
||||
"settings.indexing.dimension.placeholder": "Auto",
|
||||
|
||||
+4
@@ -1465,6 +1465,10 @@ export const dict = {
|
||||
"settings.indexing.providerField.description": "Ustawienie połączenia specyficzne dla dostawcy.",
|
||||
"settings.indexing.qdrantApiKey.description": "Opcjonalny klucz API dla instancji Qdrant.",
|
||||
"settings.indexing.qdrantApiKey.placeholder": "Opcjonalny klucz API",
|
||||
"settings.indexing.fileExtensions.title": "Rozszerzenia plików",
|
||||
"settings.indexing.fileExtensions.description":
|
||||
"Lista dozwolonych wartości rozdzielona przecinkami. Pozostaw puste, aby użyć wbudowanych wartości domyślnych.",
|
||||
"settings.indexing.fileExtensions.invalid": "Nieprawidłowe rozszerzenie: {{extension}}",
|
||||
"settings.indexing.qdrantApiKey.title": "Klucz API Qdranta",
|
||||
"settings.indexing.qdrantUrl.description": "Adres URL serwera dla instancji Qdrant.",
|
||||
"settings.indexing.qdrantUrl.title": "Adres URL Qdranta",
|
||||
|
||||
+4
@@ -924,6 +924,10 @@ export const dict = {
|
||||
"settings.indexing.qdrantApiKey.title": "API-ключ Qdrant",
|
||||
"settings.indexing.qdrantApiKey.description": "Необязательный API-ключ для экземпляра Qdrant.",
|
||||
"settings.indexing.qdrantApiKey.placeholder": "Необязательный API-ключ",
|
||||
"settings.indexing.fileExtensions.title": "Расширения файлов",
|
||||
"settings.indexing.fileExtensions.description":
|
||||
"Список разрешённых значений, разделённых запятыми. Оставьте пустым, чтобы использовать встроенные значения по умолчанию.",
|
||||
"settings.indexing.fileExtensions.invalid": "Недопустимое расширение: {{extension}}",
|
||||
"settings.indexing.providerField.description": "Настройка подключения, специфичная для провайдера.",
|
||||
"settings.indexing.tuning.description": "Параметры расширенного поиска и пакетной обработки.",
|
||||
|
||||
|
||||
+4
@@ -911,6 +911,10 @@ export const dict = {
|
||||
"settings.indexing.qdrantApiKey.title": "คีย์ API Qdrant",
|
||||
"settings.indexing.qdrantApiKey.description": "คีย์ API เสริมสำหรับอินสแตนซ์ Qdrant",
|
||||
"settings.indexing.qdrantApiKey.placeholder": "คีย์ API เสริม",
|
||||
"settings.indexing.fileExtensions.title": "นามสกุลไฟล์",
|
||||
"settings.indexing.fileExtensions.description":
|
||||
"รายการที่อนุญาตคั่นด้วยเครื่องหมายจุลภาค ปล่อยว่างไว้เพื่อใช้ค่าเริ่มต้นที่มีมาให้",
|
||||
"settings.indexing.fileExtensions.invalid": "นามสกุลไฟล์ไม่ถูกต้อง: {{extension}}",
|
||||
"settings.indexing.providerField.description": "การตั้งค่าการเชื่อมต่อเฉพาะผู้ให้บริการ",
|
||||
"settings.indexing.status.title": "สถานะ",
|
||||
"settings.indexing.tuning.description": "พารามิเตอร์การค้นหาขั้นสูงและการประมวลผลแบทช์",
|
||||
|
||||
+4
@@ -1256,6 +1256,10 @@ export const dict = {
|
||||
"settings.indexing.qdrantApiKey.title": "Qdrant API anahtarı",
|
||||
"settings.indexing.qdrantApiKey.description": "Qdrant örneği için isteğe bağlı API anahtarı.",
|
||||
"settings.indexing.qdrantApiKey.placeholder": "İsteğe bağlı API anahtarı",
|
||||
"settings.indexing.fileExtensions.title": "Dosya Uzantıları",
|
||||
"settings.indexing.fileExtensions.description":
|
||||
"Virgülle ayrılmış izin listesi. Yerleşik varsayılanları kullanmak için boş bırakın.",
|
||||
"settings.indexing.fileExtensions.invalid": "Geçersiz uzantı: {{extension}}",
|
||||
"settings.indexing.tuning.description": "Gelişmiş arama ve toplu işlem parametresi.",
|
||||
|
||||
"settings.experimental.title": "Deneysel",
|
||||
|
||||
+4
@@ -1253,6 +1253,10 @@ export const dict = {
|
||||
"settings.indexing.qdrantApiKey.title": "Ключ API Qdrant",
|
||||
"settings.indexing.qdrantApiKey.description": "Необов'язковий ключ API для екземпляра Qdrant.",
|
||||
"settings.indexing.qdrantApiKey.placeholder": "Необов'язковий ключ API",
|
||||
"settings.indexing.fileExtensions.title": "Розширення файлів",
|
||||
"settings.indexing.fileExtensions.description":
|
||||
"Список дозволених елементів, розділений комами. Залиште порожнім, щоб використовувати вбудовані значення за замовчуванням.",
|
||||
"settings.indexing.fileExtensions.invalid": "Недійсне розширення: {{extension}}",
|
||||
"settings.indexing.tuning.description": "Розширений параметр пошуку та пакетної обробки.",
|
||||
|
||||
"settings.experimental.title": "Експериментальне",
|
||||
|
||||
+3
@@ -892,6 +892,9 @@ export const dict = {
|
||||
"settings.indexing.qdrantApiKey.title": "Qdrant API 密钥",
|
||||
"settings.indexing.qdrantApiKey.description": "Qdrant 实例的可选 API 密钥。",
|
||||
"settings.indexing.qdrantApiKey.placeholder": "可选 API 密钥",
|
||||
"settings.indexing.fileExtensions.title": "文件扩展名",
|
||||
"settings.indexing.fileExtensions.description": "以逗号分隔的允许列表。留空以使用内置默认值。",
|
||||
"settings.indexing.fileExtensions.invalid": "无效的扩展名:{{extension}}",
|
||||
"settings.indexing.dimension.title": "向量维度",
|
||||
"settings.indexing.dimension.description": "留空以从模型自动检测嵌入维度。",
|
||||
"settings.indexing.dimension.placeholder": "自动",
|
||||
|
||||
+3
@@ -1657,6 +1657,9 @@ export const dict = {
|
||||
"settings.indexing.providerField.description": "供應商特定的連線設定。",
|
||||
"settings.indexing.qdrantApiKey.description": "Qdrant 執行個體的可選 API 金鑰。",
|
||||
"settings.indexing.qdrantApiKey.placeholder": "可選 API 金鑰",
|
||||
"settings.indexing.fileExtensions.title": "檔案副檔名",
|
||||
"settings.indexing.fileExtensions.description": "以逗號分隔的允許清單。留空以使用內建預設值。",
|
||||
"settings.indexing.fileExtensions.invalid": "無效的副檔名:{{extension}}",
|
||||
"settings.indexing.qdrantApiKey.title": "Qdrant API 金鑰",
|
||||
"settings.indexing.qdrantUrl.description": "Qdrant 執行個體的伺服器 URL。",
|
||||
"settings.indexing.qdrantUrl.title": "Qdrant URL",
|
||||
|
||||
@@ -97,6 +97,7 @@ export interface IndexingConfig {
|
||||
searchMaxResults?: number
|
||||
embeddingBatchSize?: number
|
||||
scannerMaxBatchRetries?: number
|
||||
fileExtensions?: string[]
|
||||
}
|
||||
|
||||
export type KiloEmbeddingModel = {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import { useDialog } from "@tui/ui/dialog"
|
||||
import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"
|
||||
import { DialogPrompt } from "@tui/ui/dialog-prompt"
|
||||
import { DEFAULT_VECTOR_STORE } from "@kilocode/kilo-indexing/config"
|
||||
import { DEFAULT_VECTOR_STORE, isFileExtension, parseFileExtensions } from "@kilocode/kilo-indexing/config"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { useToast } from "@tui/ui/toast"
|
||||
import { createEffect, createMemo, createResource, createSignal, Show } from "solid-js"
|
||||
@@ -579,6 +579,12 @@ export function DialogIndexing(props: DialogIndexingProps) {
|
||||
category: "Storage",
|
||||
description: mark(storeLabel, [["vectorStore"]]),
|
||||
},
|
||||
{
|
||||
value: "fileExtensions",
|
||||
title: "File Extensions",
|
||||
category: "Advanced",
|
||||
description: mark(indexing.fileExtensions?.join(", ") ?? "built-in defaults", [["fileExtensions"]]),
|
||||
},
|
||||
{
|
||||
value: "tuning",
|
||||
title: "Tuning Parameters",
|
||||
@@ -680,6 +686,33 @@ export function DialogIndexing(props: DialogIndexingProps) {
|
||||
<VectorStoreSelect useSDK={props.useSDK} scope={scope()} indexing={indexing} raw={raw} />
|
||||
))
|
||||
break
|
||||
case "fileExtensions": {
|
||||
const result = await DialogPrompt.show(dialog, "File Extensions", {
|
||||
value: indexing.fileExtensions?.join(", ") ?? "",
|
||||
placeholder: ".php, .js, .css (empty uses built-in defaults)",
|
||||
})
|
||||
if (result !== null) {
|
||||
const values = result
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
const invalid = values.find((item) => !isFileExtension(item))
|
||||
if (invalid) {
|
||||
toast.show({ message: `Invalid file extension: "${invalid}"`, variant: "error" })
|
||||
} else {
|
||||
await saveScopedIndexing(
|
||||
sdk,
|
||||
sync,
|
||||
scope(),
|
||||
raw,
|
||||
{ ...raw, fileExtensions: parseFileExtensions(result) },
|
||||
toast,
|
||||
)
|
||||
}
|
||||
}
|
||||
dialog.replace(() => <DialogIndexing useSDK={props.useSDK} scope={scope()} />)
|
||||
break
|
||||
}
|
||||
case "tuning":
|
||||
dialog.replace(() => (
|
||||
<TuningMenu useSDK={props.useSDK} scope={scope()} indexing={indexing} raw={raw} global={globalCfg()} />
|
||||
|
||||
@@ -115,6 +115,7 @@ export namespace KilocodeConfigOverlay {
|
||||
["indexing", "searchMaxResults"],
|
||||
["indexing", "embeddingBatchSize"],
|
||||
["indexing", "scannerMaxBatchRetries"],
|
||||
["indexing", "fileExtensions"],
|
||||
] as const
|
||||
|
||||
const collectionPaths = ["provider", "mcp", "permission", "agent", "formatter", "lsp"] as const
|
||||
|
||||
@@ -107,6 +107,15 @@ describe("indexing dialog state", () => {
|
||||
expect(indexingInheritance("project", global, project, [["searchMinScore"]])).toBe("inherited")
|
||||
})
|
||||
|
||||
test("replaces inherited file extensions with a project allowlist", () => {
|
||||
const global: IndexingConfig = { fileExtensions: [".ts", ".tsx"] }
|
||||
const project: IndexingConfig = { fileExtensions: [".php"] }
|
||||
|
||||
expect(mergeIndexingConfig(global, project).fileExtensions).toEqual([".php"])
|
||||
expect(indexingInheritance("project", global, {}, [["fileExtensions"]])).toBe("inherited")
|
||||
expect(indexingInheritance("project", global, project, [["fileExtensions"]])).toBe("none")
|
||||
})
|
||||
|
||||
test("isolates global auth config from project indexing values", () => {
|
||||
const project: IndexingConfig = { kilo: { apiKey: "project-key", baseUrl: "https://project.test" } }
|
||||
const inherited: IndexingConfig = { enabled: true }
|
||||
|
||||
@@ -208,6 +208,7 @@ describe("config overlay routes", () => {
|
||||
indexing: {
|
||||
enabled: true,
|
||||
provider: "ollama",
|
||||
fileExtensions: [".php", ".js"],
|
||||
ollama: { baseUrl: "http://localhost:11434" },
|
||||
},
|
||||
})
|
||||
@@ -216,6 +217,11 @@ describe("config overlay routes", () => {
|
||||
|
||||
expect(body.fields["indexing.enabled"]).toMatchObject({ source: "global", inherited: true, value: true })
|
||||
expect(body.fields["indexing.provider"]).toMatchObject({ source: "global", inherited: true, value: "ollama" })
|
||||
expect(body.fields["indexing.fileExtensions"]).toMatchObject({
|
||||
source: "global",
|
||||
inherited: true,
|
||||
value: [".php", ".js"],
|
||||
})
|
||||
expect(body.fields["indexing.ollama.baseUrl"]).toMatchObject({
|
||||
source: "global",
|
||||
inherited: true,
|
||||
|
||||
@@ -1267,6 +1267,7 @@ export type IndexingConfig = {
|
||||
searchMaxResults?: number
|
||||
embeddingBatchSize?: number
|
||||
scannerMaxBatchRetries?: number
|
||||
fileExtensions?: Array<string>
|
||||
}
|
||||
|
||||
export type PermissionActionConfig = "ask" | "allow" | "deny"
|
||||
|
||||
@@ -25844,6 +25844,14 @@
|
||||
"scannerMaxBatchRetries": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0
|
||||
},
|
||||
"fileExtensions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^\\s*\\.?[A-Za-z0-9][A-Za-z0-9_+-]*\\s*$"
|
||||
},
|
||||
"minItems": 1
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
||||
Reference in New Issue
Block a user