mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
revert(cli): restore stable grep behavior
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Temporarily restore the default grep controls to prevent searches from stalling subagents.
|
||||
@@ -1,53 +0,0 @@
|
||||
import type { Match } from "../filesystem/schema"
|
||||
|
||||
export interface Options {
|
||||
readonly context?: number
|
||||
readonly literal?: boolean
|
||||
readonly ignoreCase?: boolean
|
||||
}
|
||||
|
||||
export type GrepMatch = Match & {
|
||||
readonly context: boolean
|
||||
readonly textTruncated: boolean
|
||||
}
|
||||
|
||||
export const flags = (input: Options) => [
|
||||
...(input.literal ? ["--fixed-strings"] : []),
|
||||
...(input.ignoreCase ? ["--ignore-case"] : []),
|
||||
...(input.context ? [`--context=${input.context}`] : []),
|
||||
]
|
||||
|
||||
export const stop = (limit: number) => {
|
||||
let matches = 0
|
||||
return (row: { readonly context: boolean }) => !row.context && ++matches > limit
|
||||
}
|
||||
|
||||
export const select = <
|
||||
A extends {
|
||||
readonly context: boolean
|
||||
readonly path: { readonly text: string }
|
||||
readonly line_number: number
|
||||
},
|
||||
>(
|
||||
input: { readonly limit: number; readonly context?: number },
|
||||
items: readonly A[],
|
||||
) => {
|
||||
let count = 0
|
||||
const overflow = items.findIndex((row) => !row.context && ++count > input.limit)
|
||||
const selected = items.slice(0, overflow === -1 ? items.length : overflow)
|
||||
const matches = selected.filter((row) => !row.context)
|
||||
return selected.filter(
|
||||
(row) =>
|
||||
!row.context ||
|
||||
matches.some(
|
||||
(match) =>
|
||||
match.path.text === row.path.text && Math.abs(match.line_number - row.line_number) <= (input.context ?? 0),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export const decorate = (match: Match, context: boolean, textTruncated: boolean): GrepMatch => ({
|
||||
...match,
|
||||
context,
|
||||
textTruncated,
|
||||
})
|
||||
@@ -6,7 +6,6 @@ import path from "path"
|
||||
import { LayerNode } from "./effect/layer-node"
|
||||
import { Entry, Match } from "./filesystem/schema"
|
||||
import { FSUtil } from "./fs-util"
|
||||
import * as KiloGrep from "./kilocode/ripgrep-grep" // kilocode_change
|
||||
import * as SpawnValidation from "./kilocode/spawn-validation" // kilocode_change
|
||||
import { AppProcess, collectStream, waitForAbort } from "./process"
|
||||
import { NonNegativeInt, PositiveInt, RelativePath } from "./schema"
|
||||
@@ -24,7 +23,7 @@ const MAX_RECORD_BYTES = 64 * 1024
|
||||
const MAX_SUBMATCHES = 100
|
||||
|
||||
const RawMatch = Schema.Struct({
|
||||
type: Schema.Literals(["match", "context"]), // kilocode_change - retain requested context records
|
||||
type: Schema.Literal("match"),
|
||||
data: Schema.Struct({
|
||||
path: Schema.Struct({ text: Schema.String }),
|
||||
lines: Schema.Struct({ text: Schema.String }),
|
||||
@@ -40,7 +39,7 @@ const RawMatch = Schema.Struct({
|
||||
}),
|
||||
})
|
||||
|
||||
type RawMatchData = (typeof RawMatch.Type)["data"] & { readonly context: boolean } // kilocode_change
|
||||
type RawMatchData = (typeof RawMatch.Type)["data"]
|
||||
|
||||
export class Error extends Schema.TaggedErrorClass<Error>()("Ripgrep.Error", {
|
||||
message: Schema.String,
|
||||
@@ -72,8 +71,7 @@ export interface GlobInput {
|
||||
readonly validate?: Effect.Effect<void, unknown> // kilocode_change - bind approved searches at spawn
|
||||
}
|
||||
|
||||
export interface GrepInput extends KiloGrep.Options {
|
||||
// kilocode_change
|
||||
export interface GrepInput {
|
||||
readonly cwd: string
|
||||
readonly pattern: string
|
||||
readonly file?: string
|
||||
@@ -86,7 +84,7 @@ export interface GrepInput extends KiloGrep.Options {
|
||||
export interface Interface {
|
||||
readonly find: (input: FindInput) => Effect.Effect<readonly Entry[], Error>
|
||||
readonly glob: (input: GlobInput) => Effect.Effect<SearchResult<Entry>, Error> // kilocode_change
|
||||
readonly grep: (input: GrepInput) => Effect.Effect<SearchResult<KiloGrep.GrepMatch>, Error | InvalidPatternError> // kilocode_change
|
||||
readonly grep: (input: GrepInput) => Effect.Effect<SearchResult<Match>, Error | InvalidPatternError> // kilocode_change
|
||||
}
|
||||
|
||||
// kilocode_change start - retain truncation state through model-facing tools
|
||||
@@ -118,7 +116,6 @@ export const layer = Layer.effect(
|
||||
readonly parse: (line: string) => Effect.Effect<A | undefined, Error>
|
||||
readonly pattern?: string
|
||||
readonly onItem?: (item: A) => Effect.Effect<void>
|
||||
readonly stop?: (item: A) => boolean // kilocode_change - stop bounded searches at the overflow match
|
||||
readonly validate?: Effect.Effect<void, unknown> // kilocode_change - spawn-bound target validation
|
||||
}) => {
|
||||
const program = Effect.scoped(
|
||||
@@ -139,13 +136,6 @@ export const layer = Layer.effect(
|
||||
Effect.forkScoped,
|
||||
)
|
||||
let observed = 0
|
||||
let stopped = false // kilocode_change
|
||||
const take = input.stop // kilocode_change start
|
||||
? Stream.takeUntil<A>((row) => {
|
||||
stopped = input.stop?.(row) ?? false
|
||||
return stopped
|
||||
})
|
||||
: Stream.take(input.limit + 1) // kilocode_change end
|
||||
const rows = yield* Stream.decodeText(handle.stdout).pipe(
|
||||
Stream.splitLines,
|
||||
Stream.filter((line) => line.length > 0),
|
||||
@@ -155,12 +145,11 @@ export const layer = Layer.effect(
|
||||
if (!input.onItem || observed++ >= input.limit) return Effect.void
|
||||
return input.onItem(row)
|
||||
}),
|
||||
take, // kilocode_change
|
||||
Stream.take(input.limit + 1),
|
||||
Stream.runCollect,
|
||||
Effect.map((chunk) => [...chunk]),
|
||||
)
|
||||
if (stopped) return { items: rows, truncated: true, partial: false } // kilocode_change
|
||||
const truncated = input.stop ? false : rows.length > input.limit // kilocode_change - custom stop predicates own truncation
|
||||
const truncated = rows.length > input.limit
|
||||
if (truncated) return { items: rows.slice(0, input.limit), truncated, partial: false }
|
||||
|
||||
const code = yield* handle.exitCode
|
||||
@@ -260,13 +249,11 @@ export const layer = Layer.effect(
|
||||
grep: (input) =>
|
||||
run<RawMatchData>({
|
||||
...input,
|
||||
stop: KiloGrep.stop(input.limit), // kilocode_change
|
||||
args: [
|
||||
"--no-config",
|
||||
"--json",
|
||||
"--hidden",
|
||||
"--no-messages",
|
||||
...KiloGrep.flags(input), // kilocode_change
|
||||
...(input.include ? [`--glob=${input.include}`] : []),
|
||||
"--glob=!**/.git/**",
|
||||
"--",
|
||||
@@ -282,19 +269,13 @@ export const layer = Layer.effect(
|
||||
})
|
||||
).pipe(
|
||||
Effect.flatMap((json) => {
|
||||
if (
|
||||
!json ||
|
||||
typeof json !== "object" ||
|
||||
!("type" in json) ||
|
||||
(json.type !== "match" && json.type !== "context") // kilocode_change
|
||||
)
|
||||
if (!json || typeof json !== "object" || !("type" in json) || json.type !== "match")
|
||||
return Effect.succeed(undefined)
|
||||
return Schema.decodeUnknownEffect(RawMatch)(json).pipe(
|
||||
Effect.map((match) => ({
|
||||
...match.data,
|
||||
path: { text: match.data.path.text.replace(/^\.[\\/]/, "") },
|
||||
submatches: match.data.submatches.slice(0, MAX_SUBMATCHES),
|
||||
context: match.type === "context", // kilocode_change
|
||||
})),
|
||||
Effect.mapError((cause) => failure("Invalid ripgrep match output", cause)),
|
||||
)
|
||||
@@ -304,13 +285,13 @@ export const layer = Layer.effect(
|
||||
// kilocode_change start - retain spawn metadata after mapping matches
|
||||
Effect.map((result) => ({
|
||||
...result,
|
||||
items: KiloGrep.select(input, result.items).map((match) => {
|
||||
items: result.items.map((match) => {
|
||||
const relative = match.path.text
|
||||
.replace(/^(?:\.[\\/])+/u, "")
|
||||
.replace(/^[\\/]+/u, "")
|
||||
.replaceAll("\\", "/")
|
||||
const absolute = path.resolve(input.cwd, relative)
|
||||
const item = new Match({
|
||||
return new Match({
|
||||
entry: new Entry({
|
||||
path: RelativePath.make(relative),
|
||||
type: "file",
|
||||
@@ -325,7 +306,6 @@ export const layer = Layer.effect(
|
||||
end: submatch.end,
|
||||
})),
|
||||
})
|
||||
return KiloGrep.decorate(item, match.context, match.lines.text.length > 2_000)
|
||||
}),
|
||||
})),
|
||||
// kilocode_change end
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import { NonNegativeInt, PositiveInt } from "@opencode-ai/core/schema"
|
||||
import { Schema } from "effect"
|
||||
|
||||
export const DEFAULT_LIMIT = 100
|
||||
|
||||
export const fields = {
|
||||
context: Schema.optional(NonNegativeInt).annotate({
|
||||
description: "Number of context lines to show before and after each match (default 0)",
|
||||
}),
|
||||
limit: Schema.optional(PositiveInt).annotate({
|
||||
description: "Maximum matching lines to return (default 100)",
|
||||
}),
|
||||
literal: Schema.optional(Schema.Boolean).annotate({
|
||||
description: "Treat pattern as plain text instead of a regex (default false)",
|
||||
}),
|
||||
ignoreCase: Schema.optional(Schema.Boolean).annotate({
|
||||
description: "Match without regard to letter case (default false)",
|
||||
}),
|
||||
}
|
||||
|
||||
type Input = {
|
||||
readonly context?: number
|
||||
readonly limit?: number
|
||||
readonly literal?: boolean
|
||||
readonly ignoreCase?: boolean
|
||||
}
|
||||
|
||||
export const metadata = (input: Input, limit: number, context: number) => ({
|
||||
context,
|
||||
limit,
|
||||
literal: input.literal,
|
||||
ignoreCase: input.ignoreCase,
|
||||
})
|
||||
|
||||
export const options = (input: Input, limit: number, context: number) => ({
|
||||
limit,
|
||||
context,
|
||||
literal: input.literal,
|
||||
ignoreCase: input.ignoreCase,
|
||||
})
|
||||
|
||||
export const describe = (description: string) => `${description}
|
||||
- Searches file contents using regular expressions by default; use literal=true for plain-text patterns
|
||||
- Use ignoreCase=true for case-insensitive matching, context=N for surrounding lines, and limit=N to bound matches (default 100)
|
||||
- Context lines are explicitly labeled when requested`
|
||||
|
||||
export const line = (
|
||||
row: { readonly line: number; readonly text: string; readonly context: boolean },
|
||||
context: number,
|
||||
) => {
|
||||
const label = context === 0 ? `Line ${row.line}` : `${row.context ? "[context]" : "[match]"} Line ${row.line}`
|
||||
return ` ${label}: ${row.text}`
|
||||
}
|
||||
|
||||
export const limitNotice = (limit: number) =>
|
||||
`${limit} matches limit reached. Use limit=${Math.min(Number.MAX_SAFE_INTEGER, limit * 2)} for more, or refine pattern.`
|
||||
|
||||
export const notices = (rows: readonly { readonly textTruncated: boolean }[]) => {
|
||||
const output: string[] = []
|
||||
if (rows.some((row) => row.textTruncated)) {
|
||||
output.push("", "Some matching or context lines were truncated. Use read for full lines.")
|
||||
}
|
||||
return output
|
||||
}
|
||||
@@ -3,20 +3,18 @@ import { Effect, Schema } from "effect"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { Ripgrep } from "@opencode-ai/core/ripgrep"
|
||||
import * as KiloGrep from "@/kilocode/tool/grep-signal-controls" // kilocode_change
|
||||
import { assertExternalDirectoryEffect } from "./external-directory"
|
||||
import DESCRIPTION from "./grep.txt"
|
||||
import * as Tool from "./tool"
|
||||
|
||||
export const Parameters = Schema.Struct({
|
||||
pattern: Schema.String.annotate({ description: "Pattern to search for in file contents (regex by default)" }), // kilocode_change
|
||||
pattern: Schema.String.annotate({ description: "The regex pattern to search for in file contents" }),
|
||||
path: Schema.optional(Schema.String).annotate({
|
||||
description: "The directory to search in. Defaults to the current working directory.",
|
||||
}),
|
||||
include: Schema.optional(Schema.String).annotate({
|
||||
description: 'File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")',
|
||||
}),
|
||||
...KiloGrep.fields, // kilocode_change
|
||||
})
|
||||
|
||||
export const GrepTool = Tool.define(
|
||||
@@ -25,12 +23,10 @@ export const GrepTool = Tool.define(
|
||||
const fs = yield* FSUtil.Service
|
||||
const ripgrep = yield* Ripgrep.Service
|
||||
return {
|
||||
description: KiloGrep.describe(DESCRIPTION), // kilocode_change
|
||||
description: DESCRIPTION,
|
||||
parameters: Parameters,
|
||||
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
|
||||
execute: (params: { pattern: string; path?: string; include?: string }, ctx: Tool.Context) =>
|
||||
Effect.gen(function* () {
|
||||
const limit = params.limit ?? KiloGrep.DEFAULT_LIMIT // kilocode_change
|
||||
const context = params.context ?? 0 // kilocode_change
|
||||
const empty = {
|
||||
title: params.pattern,
|
||||
metadata: { matches: 0, truncated: false },
|
||||
@@ -48,7 +44,6 @@ export const GrepTool = Tool.define(
|
||||
pattern: params.pattern,
|
||||
path: params.path,
|
||||
include: params.include,
|
||||
...KiloGrep.metadata(params, limit, context), // kilocode_change
|
||||
},
|
||||
})
|
||||
|
||||
@@ -71,7 +66,7 @@ export const GrepTool = Tool.define(
|
||||
file: info?.type === "File" ? path.basename(search) : undefined, // kilocode_change - constrain exact-file searches
|
||||
pattern: params.pattern,
|
||||
include: params.include,
|
||||
...KiloGrep.options(params, limit, context), // kilocode_change
|
||||
limit: 100,
|
||||
signal: ctx.abort, // kilocode_change - stop ripgrep when the tool call is cancelled
|
||||
})
|
||||
// kilocode_change start
|
||||
@@ -79,20 +74,18 @@ export const GrepTool = Tool.define(
|
||||
if (matches.length === 0) return empty
|
||||
// kilocode_change end
|
||||
|
||||
const rows = matches.map((item) => ({
|
||||
// kilocode_change
|
||||
const rows = matches.map((item) => ({ // kilocode_change
|
||||
path: path.resolve(cwd, item.entry.path),
|
||||
line: item.line,
|
||||
text: item.text,
|
||||
context: item.context, // kilocode_change
|
||||
textTruncated: item.textTruncated, // kilocode_change
|
||||
}))
|
||||
|
||||
const limit = 100
|
||||
const truncated = result.truncated // kilocode_change
|
||||
const final = rows
|
||||
if (final.length === 0) return empty
|
||||
|
||||
const total = rows.filter((row) => !row.context).length // kilocode_change
|
||||
const total = rows.length
|
||||
const hasMore = truncated // kilocode_change
|
||||
const output = [`Found ${total} matches${hasMore ? " (more matches available)" : ""}`]
|
||||
|
||||
@@ -103,14 +96,13 @@ export const GrepTool = Tool.define(
|
||||
current = match.path
|
||||
output.push(`${match.path}:`)
|
||||
}
|
||||
output.push(KiloGrep.line(match, context)) // kilocode_change
|
||||
output.push(` Line ${match.line}: ${match.text}`)
|
||||
}
|
||||
|
||||
if (truncated) {
|
||||
output.push("")
|
||||
output.push(KiloGrep.limitNotice(limit)) // kilocode_change
|
||||
output.push("(Results truncated. Consider using a more specific path or pattern.)")
|
||||
}
|
||||
output.push(...KiloGrep.notices(rows)) // kilocode_change
|
||||
if (result.partial) output.push("", "(Some paths were inaccessible.)") // kilocode_change
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Exit, Layer, Schema } from "effect"
|
||||
import path from "path"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { Ripgrep } from "@opencode-ai/core/ripgrep"
|
||||
import { Agent } from "../../../src/agent/agent"
|
||||
import { Git } from "../../../src/git"
|
||||
import { GrepTool, Parameters } from "../../../src/tool/grep"
|
||||
import { Truncate } from "../../../src/tool/truncate"
|
||||
import { MessageID, SessionID } from "../../../src/session/schema"
|
||||
import { TestInstance } from "../../fixture/fixture"
|
||||
import { testEffect } from "../../lib/effect"
|
||||
|
||||
const it = testEffect(
|
||||
Layer.mergeAll(
|
||||
CrossSpawnSpawner.defaultLayer,
|
||||
FSUtil.defaultLayer,
|
||||
Ripgrep.defaultLayer,
|
||||
Truncate.defaultLayer,
|
||||
Agent.defaultLayer,
|
||||
Git.defaultLayer,
|
||||
),
|
||||
)
|
||||
|
||||
const ctx = {
|
||||
sessionID: SessionID.make("ses_grep_signal_controls"),
|
||||
messageID: MessageID.make("msg_grep_signal_controls"),
|
||||
callID: "",
|
||||
agent: "code",
|
||||
abort: AbortSignal.any([]),
|
||||
messages: [],
|
||||
metadata: () => Effect.void,
|
||||
ask: () => Effect.void,
|
||||
}
|
||||
|
||||
const file = (test: { readonly directory: string }, name: string) => path.join(test.directory, name)
|
||||
|
||||
const init = Effect.gen(function* () {
|
||||
const info = yield* GrepTool
|
||||
return yield* info.init()
|
||||
})
|
||||
|
||||
describe("Kilo grep signal-to-noise controls", () => {
|
||||
it.effect("validates signal controls", () =>
|
||||
Effect.sync(() => {
|
||||
expect(Schema.decodeUnknownSync(Parameters)({ pattern: "needle", context: 0, limit: 1 })).toMatchObject({
|
||||
context: 0,
|
||||
limit: 1,
|
||||
})
|
||||
expect(() => Schema.decodeUnknownSync(Parameters)({ pattern: "needle", context: -1 })).toThrow()
|
||||
expect(() => Schema.decodeUnknownSync(Parameters)({ pattern: "needle", limit: 0 })).toThrow()
|
||||
expect(() => Schema.decodeUnknownSync(Parameters)({ pattern: "needle", limit: 1.5 })).toThrow()
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("preserves the default match output", () =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* TestInstance
|
||||
yield* Effect.promise(() => Bun.write(file(test, "default.txt"), "before\nneedle\nafter\n"))
|
||||
const grep = yield* init
|
||||
const result = yield* grep.execute({ pattern: "needle", path: test.directory }, ctx)
|
||||
|
||||
expect(result.metadata.matches).toBe(1)
|
||||
expect(result.output).toContain("Line 2: needle")
|
||||
expect(result.output).not.toContain("[match]")
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("stops after the custom match limit", () =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* TestInstance
|
||||
yield* Effect.promise(() =>
|
||||
Bun.write(file(test, "many.txt"), `${Array.from({ length: 20 }, () => "needle").join("\n")}\n`),
|
||||
)
|
||||
const grep = yield* init
|
||||
const result = yield* grep.execute({ pattern: "needle", path: test.directory, limit: 2 }, ctx)
|
||||
|
||||
expect(result.metadata.matches).toBe(2)
|
||||
expect(result.metadata.truncated).toBe(true)
|
||||
expect(result.output).toContain("2 matches limit reached. Use limit=4 for more, or refine pattern.")
|
||||
expect(result.output).not.toContain("(Results truncated")
|
||||
expect(result.output).not.toContain("Line 3: needle")
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("supports literal and case-insensitive matching", () =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* TestInstance
|
||||
yield* Effect.promise(() =>
|
||||
Promise.all([
|
||||
Bun.write(file(test, "literal.txt"), "a.*b\n"),
|
||||
Bun.write(file(test, "regex.txt"), "azb\n"),
|
||||
Bun.write(file(test, "case.txt"), "Needle\n"),
|
||||
]),
|
||||
)
|
||||
const grep = yield* init
|
||||
const literal = yield* grep.execute({ pattern: "a.*b", path: test.directory, literal: true }, ctx)
|
||||
const insensitive = yield* grep.execute({ pattern: "needle", path: test.directory, ignoreCase: true }, ctx)
|
||||
|
||||
expect(literal.metadata.matches).toBe(1)
|
||||
expect(literal.output).toContain("literal.txt")
|
||||
expect(literal.output).not.toContain("regex.txt")
|
||||
expect(insensitive.metadata.matches).toBe(1)
|
||||
expect(insensitive.output).toContain("case.txt")
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("formats only bounded context around returned matches", () =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* TestInstance
|
||||
yield* Effect.promise(() =>
|
||||
Bun.write(
|
||||
file(test, "context.txt"),
|
||||
["before", "needle", "after", "far", "later needle", "later after"].join("\n") + "\n",
|
||||
),
|
||||
)
|
||||
const grep = yield* init
|
||||
const result = yield* grep.execute({ pattern: "needle", path: test.directory, context: 1, limit: 1 }, ctx)
|
||||
|
||||
expect(result.metadata.matches).toBe(1)
|
||||
expect(result.output).toContain("[context] Line 1: before")
|
||||
expect(result.output).toContain("[match] Line 2: needle")
|
||||
expect(result.output).toContain("[context] Line 3: after")
|
||||
expect(result.output).not.toContain("Line 4: far")
|
||||
expect(result.output).not.toContain("later needle")
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("does not count context lines toward the match limit", () =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* TestInstance
|
||||
const content = Array.from(
|
||||
{ length: 35 },
|
||||
(_, index) => `before-${index}\nneedle-${index}\nafter-${index}\ngap-${index}`,
|
||||
).join("\n")
|
||||
yield* Effect.promise(() => Bun.write(file(test, "context-limit.txt"), `${content}\n`))
|
||||
const grep = yield* init
|
||||
const result = yield* grep.execute({ pattern: "needle", path: test.directory, context: 1, limit: 100 }, ctx)
|
||||
|
||||
expect(result.metadata.matches).toBe(35)
|
||||
expect(result.metadata.truncated).toBe(false)
|
||||
expect(result.output).toContain("needle-34")
|
||||
expect(result.output).not.toContain("matches limit reached")
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("guides the model to read truncated lines", () =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* TestInstance
|
||||
yield* Effect.promise(() => Bun.write(file(test, "long.txt"), `${"x".repeat(2_100)}needle\n`))
|
||||
const grep = yield* init
|
||||
const result = yield* grep.execute({ pattern: "needle", path: test.directory }, ctx)
|
||||
|
||||
expect(result.metadata.matches).toBe(1)
|
||||
expect(result.output).toContain("Some matching or context lines were truncated. Use read for full lines.")
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("retains include filtering", () =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* TestInstance
|
||||
yield* Effect.promise(() =>
|
||||
Promise.all([
|
||||
Bun.write(file(test, "included.ts"), "needle\n"),
|
||||
Bun.write(file(test, "excluded.txt"), "needle\n"),
|
||||
]),
|
||||
)
|
||||
const grep = yield* init
|
||||
const result = yield* grep.execute({ pattern: "needle", path: test.directory, include: "*.ts" }, ctx)
|
||||
|
||||
expect(result.metadata.matches).toBe(1)
|
||||
expect(result.output).toContain("included.ts")
|
||||
expect(result.output).not.toContain("excluded.txt")
|
||||
}),
|
||||
)
|
||||
|
||||
it.instance("honors cancellation", () =>
|
||||
Effect.gen(function* () {
|
||||
const test = yield* TestInstance
|
||||
yield* Effect.promise(() => Bun.write(file(test, "cancel.txt"), "needle\n".repeat(10_000)))
|
||||
const grep = yield* init
|
||||
const controller = new AbortController()
|
||||
controller.abort()
|
||||
const exit = yield* grep
|
||||
.execute({ pattern: "needle", path: test.directory }, { ...ctx, abort: controller.signal })
|
||||
.pipe(Effect.exit)
|
||||
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -114,37 +114,16 @@ exports[`tool parameters JSON Schema (wire shape) grep 1`] = `
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"properties": {
|
||||
"context": {
|
||||
"description": "Number of context lines to show before and after each match (default 0)",
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
},
|
||||
"ignoreCase": {
|
||||
"description": "Match without regard to letter case (default false)",
|
||||
"type": "boolean",
|
||||
},
|
||||
"include": {
|
||||
"description": "File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")",
|
||||
"type": "string",
|
||||
},
|
||||
"limit": {
|
||||
"description": "Maximum matching lines to return (default 100)",
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": -9007199254740991,
|
||||
"type": "integer",
|
||||
},
|
||||
"literal": {
|
||||
"description": "Treat pattern as plain text instead of a regex (default false)",
|
||||
"type": "boolean",
|
||||
},
|
||||
"path": {
|
||||
"description": "The directory to search in. Defaults to the current working directory.",
|
||||
"type": "string",
|
||||
},
|
||||
"pattern": {
|
||||
"description": "Pattern to search for in file contents (regex by default)",
|
||||
"description": "The regex pattern to search for in file contents",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -149,7 +149,7 @@ describe("tool.grep", () => {
|
||||
const grep = yield* info.init()
|
||||
const result = yield* grep.execute({ pattern: "needle", path: test.directory, include: "*.txt" }, ctx)
|
||||
|
||||
expect(result.output).toContain("100 matches limit reached. Use limit=200 for more, or refine pattern.") // kilocode_change
|
||||
expect(result.output).toContain("(Results truncated. Consider using a more specific path or pattern.)")
|
||||
expect(result.output).not.toMatch(/showing \d+ of \d+ matches/)
|
||||
}),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user