From f2a83a0a005f70aca7298685aec58ec2d6e39afe Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 13 Apr 2026 10:14:19 -0400 Subject: [PATCH 1/4] refactor(file): stream ripgrep search parsing --- packages/opencode/src/file/ripgrep.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index 81cd2bf0dc..1d2d4b56d3 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -408,6 +408,14 @@ export namespace Ripgrep { }) { return yield* Effect.scoped( Effect.gen(function* () { + const parse = Effect.fn("Ripgrep.parse")(function* (line: string) { + const row = yield* Effect.try({ + try: () => Result.parse(JSON.parse(line)), + catch: (cause) => new Error(`invalid ripgrep output: ${cause}`), + }) + if (row.type !== "match") return undefined + return row.data + }) const cmd = yield* args({ mode: "search", glob: input.glob, @@ -428,11 +436,8 @@ export namespace Ripgrep { Stream.decodeText(handle.stdout).pipe( Stream.splitLines, Stream.filter((line) => line.length > 0), - Stream.mapEffect((line) => - decode(line).pipe(Effect.mapError((cause) => new Error("invalid ripgrep output", { cause }))), - ), - Stream.filter((row): row is Schema.Schema.Type => row.type === "match"), - Stream.map((row): Item => row.data), + Stream.mapEffect(parse), + Stream.filter((item): item is Item => item !== undefined), Stream.runCollect, Effect.map((chunk) => [...chunk]), ), From 2a10e4e89e149b570e73e979922bbc78c0e558a3 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 13 Apr 2026 10:32:39 -0400 Subject: [PATCH 2/4] refactor(file): decode ripgrep rows with schema --- packages/opencode/src/file/ripgrep.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index 1d2d4b56d3..81cd2bf0dc 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -408,14 +408,6 @@ export namespace Ripgrep { }) { return yield* Effect.scoped( Effect.gen(function* () { - const parse = Effect.fn("Ripgrep.parse")(function* (line: string) { - const row = yield* Effect.try({ - try: () => Result.parse(JSON.parse(line)), - catch: (cause) => new Error(`invalid ripgrep output: ${cause}`), - }) - if (row.type !== "match") return undefined - return row.data - }) const cmd = yield* args({ mode: "search", glob: input.glob, @@ -436,8 +428,11 @@ export namespace Ripgrep { Stream.decodeText(handle.stdout).pipe( Stream.splitLines, Stream.filter((line) => line.length > 0), - Stream.mapEffect(parse), - Stream.filter((item): item is Item => item !== undefined), + Stream.mapEffect((line) => + decode(line).pipe(Effect.mapError((cause) => new Error("invalid ripgrep output", { cause }))), + ), + Stream.filter((row): row is Schema.Schema.Type => row.type === "match"), + Stream.map((row): Item => row.data), Stream.runCollect, Effect.map((chunk) => [...chunk]), ), From 75a87ffc5ec977b79691000f84769e356da2a4cc Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 13 Apr 2026 11:18:31 -0400 Subject: [PATCH 3/4] refactor(file): decode ripgrep chunks synchronously --- packages/opencode/src/file/ripgrep.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index 81cd2bf0dc..ebc3f12a22 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -3,7 +3,7 @@ import path from "path" import { Global } from "../global" import fs from "fs/promises" import z from "zod" -import { Effect, Layer, Context, Schema } from "effect" +import { Array as Arr, Effect, Layer, Context, Schema } from "effect" import * as Stream from "effect/Stream" import { ChildProcess } from "effect/unstable/process" import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" @@ -126,7 +126,7 @@ export namespace Ripgrep { Schema.Struct({ type: Schema.Literal("summary"), data: Schema.Unknown }), ]) - const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(Row)) + const decode = Schema.decodeUnknownSync(Schema.fromJsonString(Row)) export type Result = z.infer export type Match = z.infer @@ -428,8 +428,11 @@ export namespace Ripgrep { Stream.decodeText(handle.stdout).pipe( Stream.splitLines, Stream.filter((line) => line.length > 0), - Stream.mapEffect((line) => - decode(line).pipe(Effect.mapError((cause) => new Error("invalid ripgrep output", { cause }))), + Stream.mapArrayEffect((lines) => + Effect.try({ + try: () => Arr.map(lines, (line) => decode(line)), + catch: (cause) => new Error("invalid ripgrep output", { cause }), + }), ), Stream.filter((row): row is Schema.Schema.Type => row.type === "match"), Stream.map((row): Item => row.data), From 573a10e2f4120930ba7c2a5ec9ce07e70bd6cfd4 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 13 Apr 2026 12:29:34 -0400 Subject: [PATCH 4/4] refactor(file): derive ripgrep zod from effect schema --- packages/opencode/src/file/ripgrep.ts | 172 ++++++++---------- packages/opencode/src/server/instance/file.ts | 2 +- 2 files changed, 78 insertions(+), 96 deletions(-) diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index ebc3f12a22..18101f191a 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -20,117 +20,99 @@ import { text } from "node:stream/consumers" import { ZipReader, BlobReader, BlobWriter } from "@zip.js/zip.js" import { Log } from "@/util/log" +import { zod } from "@/util/effect-zod" export namespace Ripgrep { const log = Log.create({ service: "ripgrep" }) - const Stats = z.object({ - elapsed: z.object({ - secs: z.number(), - nanos: z.number(), - human: z.string(), + const stats = Schema.Struct({ + elapsed: Schema.Struct({ + secs: Schema.Number, + nanos: Schema.Number, + human: Schema.String, }), - searches: z.number(), - searches_with_match: z.number(), - bytes_searched: z.number(), - bytes_printed: z.number(), - matched_lines: z.number(), - matches: z.number(), + searches: Schema.Number, + searches_with_match: Schema.Number, + bytes_searched: Schema.Number, + bytes_printed: Schema.Number, + matched_lines: Schema.Number, + matches: Schema.Number, }) - const Begin = z.object({ - type: z.literal("begin"), - data: z.object({ - path: z.object({ - text: z.string(), - }), - }), - }) - - export const Match = z.object({ - type: z.literal("match"), - data: z.object({ - path: z.object({ - text: z.string(), - }), - lines: z.object({ - text: z.string(), - }), - line_number: z.number(), - absolute_offset: z.number(), - submatches: z.array( - z.object({ - match: z.object({ - text: z.string(), - }), - start: z.number(), - end: z.number(), - }), - ), - }), - }) - - const End = z.object({ - type: z.literal("end"), - data: z.object({ - path: z.object({ - text: z.string(), - }), - binary_offset: z.number().nullable(), - stats: Stats, - }), - }) - - const Summary = z.object({ - type: z.literal("summary"), - data: z.object({ - elapsed_total: z.object({ - human: z.string(), - nanos: z.number(), - secs: z.number(), - }), - stats: Stats, - }), - }) - - const Result = z.union([Begin, Match, End, Summary]) - - const Hit = Schema.Struct({ - type: Schema.Literal("match"), + const begin = Schema.Struct({ + type: Schema.Literal("begin"), data: Schema.Struct({ path: Schema.Struct({ text: Schema.String, }), - lines: Schema.Struct({ - text: Schema.String, - }), - line_number: Schema.Number, - absolute_offset: Schema.Number, - submatches: Schema.mutable( - Schema.Array( - Schema.Struct({ - match: Schema.Struct({ - text: Schema.String, - }), - start: Schema.Number, - end: Schema.Number, - }), - ), - ), }), }) - const Row = Schema.Union([ - Schema.Struct({ type: Schema.Literal("begin"), data: Schema.Unknown }), - Hit, - Schema.Struct({ type: Schema.Literal("end"), data: Schema.Unknown }), - Schema.Struct({ type: Schema.Literal("summary"), data: Schema.Unknown }), - ]) + const item = Schema.Struct({ + path: Schema.Struct({ + text: Schema.String, + }), + lines: Schema.Struct({ + text: Schema.String, + }), + line_number: Schema.Number, + absolute_offset: Schema.Number, + submatches: Schema.mutable( + Schema.Array( + Schema.Struct({ + match: Schema.Struct({ + text: Schema.String, + }), + start: Schema.Number, + end: Schema.Number, + }), + ), + ), + }) - const decode = Schema.decodeUnknownSync(Schema.fromJsonString(Row)) + const match = Schema.Struct({ + type: Schema.Literal("match"), + data: item, + }) + const end = Schema.Struct({ + type: Schema.Literal("end"), + data: Schema.Struct({ + path: Schema.Struct({ + text: Schema.String, + }), + binary_offset: Schema.NullOr(Schema.Number), + stats, + }), + }) + + const summary = Schema.Struct({ + type: Schema.Literal("summary"), + data: Schema.Struct({ + elapsed_total: Schema.Struct({ + human: Schema.String, + nanos: Schema.Number, + secs: Schema.Number, + }), + stats, + }), + }) + + const row = Schema.Union([begin, match, end, summary]) + + const decode = Schema.decodeUnknownSync(Schema.fromJsonString(row)) + + export const Stats = zod(stats) + export const Begin = zod(begin) + export const Item = zod(item) + export const Match = zod(match) + export const End = zod(end) + export const Summary = zod(summary) + export const Result = zod(row) + + export type Stats = z.infer export type Result = z.infer export type Match = z.infer - export type Item = Match["data"] + export type Item = z.infer export type Begin = z.infer export type End = z.infer export type Summary = z.infer @@ -434,7 +416,7 @@ export namespace Ripgrep { catch: (cause) => new Error("invalid ripgrep output", { cause }), }), ), - Stream.filter((row): row is Schema.Schema.Type => row.type === "match"), + Stream.filter((row): row is Schema.Schema.Type => row.type === "match"), Stream.map((row): Item => row.data), Stream.runCollect, Effect.map((chunk) => [...chunk]), diff --git a/packages/opencode/src/server/instance/file.ts b/packages/opencode/src/server/instance/file.ts index 713513b38d..390e868e5b 100644 --- a/packages/opencode/src/server/instance/file.ts +++ b/packages/opencode/src/server/instance/file.ts @@ -21,7 +21,7 @@ export const FileRoutes = lazy(() => description: "Matches", content: { "application/json": { - schema: resolver(Ripgrep.Match.shape.data.array()), + schema: resolver(Ripgrep.Item.array()), }, }, },