chore: merge dev into v2 (#34788)

Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
Co-authored-by: Kit Langton <kit.langton@gmail.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Affan Ali <93028901+affanali2k3@users.noreply.github.com>
Co-authored-by: affanali2k3 <affanalikhanxx@gmail.com>
Co-authored-by: Frank <frank@anoma.ly>
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Jay V <air@live.ca>
Co-authored-by: Dax Raad <d@ironbay.co>
Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com>
Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com>
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
Co-authored-by: Ben Guthrie <benjee.012@gmail.com>
Co-authored-by: Dax <mail@thdxr.com>
Co-authored-by: Filip <34747899+neriousy@users.noreply.github.com>
Co-authored-by: Max Anderson <max.a.anderson95@gmail.com>
Co-authored-by: Brendan Allan <git@brendonovich.dev>
Co-authored-by: Jack <jack@anoma.ly>
Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>
Co-authored-by: Dustin Deus <deusdustin@gmail.com>
Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com>
Co-authored-by: Jay <53023+jayair@users.noreply.github.com>
Co-authored-by: runvip <164729189+runvip@users.noreply.github.com>
Co-authored-by: opencode <opencode@sst.dev>
Co-authored-by: Julian Coy <julian@ex-machina.co>
Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com>
This commit is contained in:
James Long 2026-07-01 17:12:00 -04:00 committed by GitHub
commit 8c94e9005f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
590 changed files with 15772 additions and 5530 deletions

View file

@ -4,11 +4,13 @@ import { ToolFailure } from "@opencode-ai/llm"
import { FileDiff } from "@opencode-ai/schema/file-diff"
import { createTwoFilesPatch, diffLines } from "diff"
import { Effect, Layer, Schema } from "effect"
import { makeLocationNode } from "../effect/app-node"
import { FileMutation } from "../file-mutation"
import { FSUtil } from "../fs-util"
import { LocationMutation } from "../location-mutation"
import { Patch } from "../patch"
import { PermissionV2 } from "../permission"
import { ToolRegistry } from "./registry"
import { Tool } from "./tool"
import { Tools } from "./tools"
@ -54,7 +56,7 @@ type Prepared =
readonly after: string
})
export const layer = Layer.effectDiscard(
const layer = Layer.effectDiscard(
Effect.gen(function* () {
const tools = yield* Tools.Service
const mutation = yield* LocationMutation.Service
@ -194,6 +196,12 @@ export const layer = Layer.effectDiscard(
}),
)
export const node = makeLocationNode({
name: "tool/apply-patch",
layer,
deps: [ToolRegistry.node, LocationMutation.node, FileMutation.node, FSUtil.node, PermissionV2.node],
})
function patchFile(change: Prepared): typeof FileDiff.Info.Type {
const counts = diffLines(change.before, change.after).reduce(
(result, item) => ({

View file

@ -14,18 +14,6 @@ import { TodoWriteTool } from "./todowrite"
import { WebFetchTool } from "./webfetch"
import { WebSearchTool } from "./websearch"
import { WriteTool } from "./write"
import { FSUtil } from "../fs-util"
import { Location } from "../location"
import { LocationMutation } from "../location-mutation"
import { FileMutation } from "../file-mutation"
import { PermissionV2 } from "../permission"
import { Ripgrep } from "../ripgrep"
import { Image } from "../image"
import { QuestionV2 } from "../question"
import { SkillV2 } from "../skill"
import { SessionTodo } from "../session/todo"
import { ToolRegistry } from "./registry"
import { httpClient } from "../effect/app-node-platform"
export class Service extends Context.Service<Service, Record<string, never>>()("@opencode/v2/BuiltInTools") {}
@ -42,38 +30,24 @@ export class Service extends Context.Service<Service, Record<string, never>>()("
* repo_clone, repo_overview, plan_exit, and Rune/code mode. Keep MCP and plugin
* transforms separate from this static built-in list.
*/
const registrations = Layer.mergeAll(
ApplyPatchTool.layer,
EditTool.layer,
GlobTool.layer,
GrepTool.layer,
QuestionTool.layer,
ReadTool.layer.pipe(Layer.provide(ReadToolFileSystem.layer)),
SkillTool.layer,
TodoWriteTool.layer,
WebFetchTool.layer,
WebSearchTool.layer.pipe(Layer.provide(WebSearchTool.defaultConfigLayer)),
WriteTool.layer,
)
export const locationLayer = Layer.succeed(Service, Service.of({})).pipe(Layer.provideMerge(registrations))
const layer = Layer.succeed(Service, Service.of({}))
export const node = makeLocationNode({
service: Service,
layer: locationLayer,
layer,
deps: [
ToolRegistry.toolsNode,
FSUtil.node,
Location.node,
LocationMutation.node,
FileMutation.node,
PermissionV2.node,
Ripgrep.node,
Image.node,
QuestionV2.node,
SkillV2.node,
SessionTodo.node,
ApplyPatchTool.node,
EditTool.node,
GlobTool.node,
GrepTool.node,
QuestionTool.node,
ReadTool.node,
ReadToolFileSystem.node,
httpClient,
SkillTool.node,
TodoWriteTool.node,
WebFetchTool.node,
WebSearchTool.node,
WebSearchTool.configNode,
WriteTool.node,
],
})

View file

@ -10,10 +10,12 @@ import { ToolFailure } from "@opencode-ai/llm"
import { FileDiff } from "@opencode-ai/schema/file-diff"
import { createTwoFilesPatch, diffLines } from "diff"
import { Effect, Layer, Schema } from "effect"
import { makeLocationNode } from "../effect/app-node"
import { FileMutation } from "../file-mutation"
import { FSUtil } from "../fs-util"
import { LocationMutation } from "../location-mutation"
import { PermissionV2 } from "../permission"
import { ToolRegistry } from "./registry"
import { Tool } from "./tool"
import { Tools } from "./tools"
@ -85,7 +87,7 @@ export const toModelOutput = (output: Output, oldString: string, newString: stri
// TODO: Add snapshots / undo after design exists.
// TODO: Add LSP notification and diagnostics after V2 LSP runtime exists.
export const layer = Layer.effectDiscard(
const layer = Layer.effectDiscard(
Effect.gen(function* () {
const tools = yield* Tools.Service
const mutation = yield* LocationMutation.Service
@ -213,3 +215,9 @@ export const layer = Layer.effectDiscard(
.pipe(Effect.orDie)
}),
)
export const node = makeLocationNode({
name: "tool/edit",
layer,
deps: [ToolRegistry.node, LocationMutation.node, FileMutation.node, FSUtil.node, PermissionV2.node],
})

View file

@ -3,11 +3,13 @@ export * as GlobTool from "./glob"
import { ToolFailure } from "@opencode-ai/llm"
import { Effect, Layer, Schema } from "effect"
import path from "path"
import { makeLocationNode } from "../effect/app-node"
import { FileSystem } from "../filesystem"
import { Location } from "../location"
import { Ripgrep } from "../ripgrep"
import { RelativePath } from "../schema"
import { PermissionV2 } from "../permission"
import { ToolRegistry } from "./registry"
import { Tool } from "./tool"
import { Tools } from "./tools"
@ -33,7 +35,7 @@ export const toModelOutput = (output: ModelOutput) => {
}
/** Glob leaf that defaults its filesystem root to the active Location. */
export const layer = Layer.effectDiscard(
const layer = Layer.effectDiscard(
Effect.gen(function* () {
const tools = yield* Tools.Service
const ripgrep = yield* Ripgrep.Service
@ -95,3 +97,9 @@ export const layer = Layer.effectDiscard(
.pipe(Effect.orDie)
}),
)
export const node = makeLocationNode({
name: "tool/glob",
layer,
deps: [ToolRegistry.node, Ripgrep.node, Location.node, PermissionV2.node],
})

View file

@ -3,12 +3,14 @@ export * as GrepTool from "./grep"
import { ToolFailure } from "@opencode-ai/llm"
import { Effect, Layer, Schema } from "effect"
import path from "path"
import { makeLocationNode } from "../effect/app-node"
import { FileSystem } from "../filesystem"
import { FSUtil } from "../fs-util"
import { Location } from "../location"
import { PermissionV2 } from "../permission"
import { Ripgrep } from "../ripgrep"
import { RelativePath } from "../schema"
import { ToolRegistry } from "./registry"
import { Tool } from "./tool"
import { Tools } from "./tools"
@ -48,7 +50,7 @@ export const toModelOutput = (output: ModelOutput) => {
}
/** Grep leaf that defaults its filesystem root to the active Location. */
export const layer = Layer.effectDiscard(
const layer = Layer.effectDiscard(
Effect.gen(function* () {
const tools = yield* Tools.Service
const fs = yield* FSUtil.Service
@ -127,3 +129,9 @@ export const layer = Layer.effectDiscard(
.pipe(Effect.orDie)
}),
)
export const node = makeLocationNode({
name: "tool/grep",
layer,
deps: [ToolRegistry.node, FSUtil.node, Ripgrep.node, Location.node, PermissionV2.node],
})

View file

@ -2,8 +2,10 @@ export * as QuestionTool from "./question"
import { ToolFailure } from "@opencode-ai/llm"
import { Effect, Layer, Schema } from "effect"
import { makeLocationNode } from "../effect/app-node"
import { PermissionV2 } from "../permission"
import { QuestionV2 } from "../question"
import { ToolRegistry } from "./registry"
import { Tool } from "./tool"
import { Tools } from "./tools"
@ -42,7 +44,7 @@ export const toModelOutput = (
return `User has answered your questions: ${formatted}. You can now continue with the user's answers in mind.`
}
export const layer = Layer.effectDiscard(
const layer = Layer.effectDiscard(
Effect.gen(function* () {
const tools = yield* Tools.Service
const question = yield* QuestionV2.Service
@ -84,3 +86,9 @@ export const layer = Layer.effectDiscard(
.pipe(Effect.orDie)
}),
)
export const node = makeLocationNode({
name: "tool/question",
layer,
deps: [ToolRegistry.node, PermissionV2.node, QuestionV2.node],
})

View file

@ -351,7 +351,7 @@ export const list = Effect.fn("ReadTool.list")(function* (fs: FSUtil.Interface,
return new ListPage({ entries: selected, truncated, ...(truncated ? { next: offset + selected.length } : {}) })
})
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const fs = yield* FSUtil.Service

View file

@ -2,12 +2,14 @@ export * as ReadTool from "./read"
import { ToolFailure } from "@opencode-ai/llm"
import { Effect, Layer, Schema } from "effect"
import { makeLocationNode } from "../effect/app-node"
import { FileSystem } from "../filesystem"
import { Image } from "../image"
import { LocationMutation } from "../location-mutation"
import { PermissionV2 } from "../permission"
import { AbsolutePath } from "../schema"
import { ReadToolFileSystem } from "./read-filesystem"
import { ToolRegistry } from "./registry"
import { Tool } from "./tool"
import { Tools } from "./tools"
@ -25,7 +27,7 @@ const LocationInput = Schema.Struct({
const Input = LocationInput
const Output = Schema.Union([FileSystem.Content, ReadToolFileSystem.TextPage, ReadToolFileSystem.ListPage])
export const layer = Layer.effectDiscard(
const layer = Layer.effectDiscard(
Effect.gen(function* () {
const tools = yield* Tools.Service
const reader = yield* ReadToolFileSystem.Service
@ -107,3 +109,9 @@ export const layer = Layer.effectDiscard(
.pipe(Effect.orDie)
}),
)
export const node = makeLocationNode({
name: "tool/read",
layer,
deps: [ToolRegistry.node, ReadToolFileSystem.node, LocationMutation.node, Image.node, PermissionV2.node],
})

View file

@ -130,7 +130,7 @@ const registryLayer = Layer.effect(
}),
)
export const layer = Layer.effect(
const layer = Layer.effect(
Tools.Service,
Service.use((registry) => Effect.succeed(Tools.Service.of({ register: registry.register }))),
).pipe(Layer.provideMerge(registryLayer))
@ -140,8 +140,6 @@ function whollyDisabled(action: string, rules: PermissionV2.Ruleset) {
return rule?.resource === "*" && rule.effect === "deny"
}
export const defaultLayer = layer.pipe(Layer.provide(ToolOutputStore.defaultLayer))
export const node = makeLocationNode({
service: Service,
layer,

View file

@ -3,9 +3,11 @@ export * as SkillTool from "./skill"
import path from "path"
import { ToolFailure } from "@opencode-ai/llm"
import { Effect, Layer, Schema } from "effect"
import { makeLocationNode } from "../effect/app-node"
import { FSUtil } from "../fs-util"
import { SkillV2 } from "../skill"
import { PermissionV2 } from "../permission"
import { ToolRegistry } from "./registry"
import { Tool } from "./tool"
import { Tools } from "./tools"
@ -52,7 +54,7 @@ export const toModelOutput = (skill: SkillV2.Info, files: ReadonlyArray<string>)
const unableToLoad = (name: string, error?: unknown) =>
new ToolFailure({ message: `Unable to load skill ${name}`, error })
export const layer = Layer.effectDiscard(
const layer = Layer.effectDiscard(
Effect.gen(function* () {
const tools = yield* Tools.Service
const fs = yield* FSUtil.Service
@ -99,3 +101,9 @@ export const layer = Layer.effectDiscard(
.pipe(Effect.orDie)
}),
)
export const node = makeLocationNode({
name: "tool/skill",
layer,
deps: [ToolRegistry.node, FSUtil.node, SkillV2.node, PermissionV2.node],
})

View file

@ -2,8 +2,10 @@ export * as TodoWriteTool from "./todowrite"
import { ToolFailure } from "@opencode-ai/llm"
import { Effect, Layer, Schema } from "effect"
import { makeLocationNode } from "../effect/app-node"
import { PermissionV2 } from "../permission"
import { SessionTodo } from "../session/todo"
import { ToolRegistry } from "./registry"
import { Tool } from "./tool"
import { Tools } from "./tools"
@ -20,7 +22,7 @@ export type Output = typeof Output.Type
export const toModelOutput = (output: Output) => JSON.stringify(output.todos, null, 2)
export const layer = Layer.effectDiscard(
const layer = Layer.effectDiscard(
Effect.gen(function* () {
const tools = yield* Tools.Service
const todos = yield* SessionTodo.Service
@ -52,3 +54,9 @@ export const layer = Layer.effectDiscard(
.pipe(Effect.orDie)
}),
)
export const node = makeLocationNode({
name: "tool/todowrite",
layer,
deps: [ToolRegistry.node, PermissionV2.node, SessionTodo.node],
})

View file

@ -5,8 +5,11 @@ import { Duration, Effect, Layer, Schema } from "effect"
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { Parser } from "htmlparser2"
import TurndownService from "turndown"
import { makeLocationNode } from "../effect/app-node"
import { LayerNodePlatform } from "../effect/app-node-platform"
import { PermissionV2 } from "../permission"
import { collectBoundedResponseBody } from "./http-body"
import { ToolRegistry } from "./registry"
import { Tool } from "./tool"
import { Tools } from "./tools"
@ -112,7 +115,7 @@ const convert = (content: string, contentType: string, format: Format) => {
return content
}
export const layer = Layer.effectDiscard(
const layer = Layer.effectDiscard(
Effect.gen(function* () {
const tools = yield* Tools.Service
const http = yield* HttpClient.HttpClient
@ -177,6 +180,12 @@ export const layer = Layer.effectDiscard(
}),
)
export const node = makeLocationNode({
name: "tool/webfetch",
layer,
deps: [ToolRegistry.node, PermissionV2.node, LayerNodePlatform.httpClient],
})
export function extractTextFromHTML(html: string) {
let text = ""
let skipDepth = 0

View file

@ -3,6 +3,8 @@ export * as WebSearchTool from "./websearch"
import { ToolFailure } from "@opencode-ai/llm"
import { Context, Duration, Effect, Layer, Schema } from "effect"
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
import { makeLocationNode } from "../effect/app-node"
import { LayerNodePlatform } from "../effect/app-node-platform"
import { truthy } from "../flag/flag"
import { InstallationVersion } from "../installation/version"
import { PositiveInt } from "../schema"
@ -11,6 +13,7 @@ import { Tool } from "./tool"
import { Tools } from "./tools"
import { collectBoundedResponseBody } from "./http-body"
import { checksum } from "../util/encode"
import { ToolRegistry } from "./registry"
export const name = "websearch"
export const NO_RESULTS = "No search results found. Please try a different query."
@ -80,6 +83,8 @@ export const defaultConfigLayer = Layer.sync(ConfigService, () =>
}),
)
export const configNode = makeLocationNode({ service: ConfigService, layer: defaultConfigLayer, deps: [] })
export function selectProvider(
sessionID: string,
flags: Pick<Config, "enableExa" | "enableParallel"> = { enableExa: false, enableParallel: false },
@ -184,7 +189,7 @@ const Output = Schema.Struct({
text: Schema.String,
})
export const layer = Layer.effectDiscard(
const layer = Layer.effectDiscard(
Effect.gen(function* () {
const tools = yield* Tools.Service
const http = yield* HttpClient.HttpClient
@ -247,3 +252,9 @@ export const layer = Layer.effectDiscard(
.pipe(Effect.orDie)
}),
)
export const node = makeLocationNode({
name: "tool/websearch",
layer,
deps: [ToolRegistry.node, PermissionV2.node, LayerNodePlatform.httpClient, configNode],
})

View file

@ -8,9 +8,11 @@ export * as WriteTool from "./write"
import { ToolFailure } from "@opencode-ai/llm"
import { Effect, Layer, Schema } from "effect"
import { makeLocationNode } from "../effect/app-node"
import { FileMutation } from "../file-mutation"
import { LocationMutation } from "../location-mutation"
import { PermissionV2 } from "../permission"
import { ToolRegistry } from "./registry"
import { Tool } from "./tool"
import { Tools } from "./tools"
@ -42,7 +44,7 @@ export const toModelOutput = (output: Output) =>
// TODO: Add snapshots / undo after design exists.
// TODO: Add LSP notification and diagnostics after V2 LSP runtime exists.
export const layer = Layer.effectDiscard(
const layer = Layer.effectDiscard(
Effect.gen(function* () {
const tools = yield* Tools.Service
const mutation = yield* LocationMutation.Service
@ -91,3 +93,9 @@ export const layer = Layer.effectDiscard(
.pipe(Effect.orDie)
}),
)
export const node = makeLocationNode({
name: "tool/write",
layer,
deps: [ToolRegistry.node, LocationMutation.node, FileMutation.node, PermissionV2.node],
})