summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-04-01 12:13:13 -0400
committerGitHub <[email protected]>2026-04-01 16:13:13 +0000
commit2f405daa983c950794aa3982584f59411f89bc50 (patch)
treee33511b889de861f1584d3d49dd27061a2ba890c
parenta9c85b7c2789f9363cbfeb9c1adceaddfbdbbdc3 (diff)
downloadopencode-2f405daa983c950794aa3982584f59411f89bc50.tar.gz
opencode-2f405daa983c950794aa3982584f59411f89bc50.zip
refactor: use Effect services instead of async facades in provider, auth, and file (#20480)
-rw-r--r--bun.lock10
-rw-r--r--package.json4
-rw-r--r--packages/opencode/src/effect/instance-state.ts8
-rw-r--r--packages/opencode/src/file/index.ts111
-rw-r--r--packages/opencode/src/provider/auth.ts33
-rw-r--r--packages/opencode/src/provider/provider.ts9
6 files changed, 86 insertions, 89 deletions
diff --git a/bun.lock b/bun.lock
index 767cb6da2..8d1144fa2 100644
--- a/bun.lock
+++ b/bun.lock
@@ -612,7 +612,7 @@
},
"catalog": {
"@cloudflare/workers-types": "4.20251008.0",
- "@effect/platform-node": "4.0.0-beta.42",
+ "@effect/platform-node": "4.0.0-beta.43",
"@hono/zod-validator": "0.4.2",
"@kobalte/core": "0.13.11",
"@octokit/rest": "22.0.0",
@@ -636,7 +636,7 @@
"dompurify": "3.3.1",
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
- "effect": "4.0.0-beta.42",
+ "effect": "4.0.0-beta.43",
"fuzzysort": "3.1.0",
"hono": "4.10.7",
"hono-openapi": "1.1.2",
@@ -995,9 +995,9 @@
"@effect/language-service": ["@effect/[email protected]", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-DEmIOsg1GjjP6s9HXH1oJrW+gDmzkhVv9WOZl6to5eNyyCrjz1S2PDqQ7aYrW/HuifhfwI5Bik1pK4pj7Z+lrg=="],
- "@effect/platform-node": ["@effect/[email protected]", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.42", "mime": "^4.1.0", "undici": "^7.24.0" }, "peerDependencies": { "effect": "^4.0.0-beta.42", "ioredis": "^5.7.0" } }, "sha512-kbdRML2FBa4q8U8rZQcnmLKZ5zN/z1bAA7t5D1/UsBHZqJgnfRgu1CP6kaEfb1Nie6YyaWshxTktZQryjvW/Yg=="],
+ "@effect/platform-node": ["@effect/[email protected]", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.43", "mime": "^4.1.0", "undici": "^7.24.0" }, "peerDependencies": { "effect": "^4.0.0-beta.43", "ioredis": "^5.7.0" } }, "sha512-Uq6E1rjaIpjHauzjwoB2HzAg3battYt2Boy8XO50GoHiWCXKE6WapYZ0/AnaBx5v5qg2sOfqpuiLsUf9ZgxOkA=="],
- "@effect/platform-node-shared": ["@effect/[email protected]", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.19.0" }, "peerDependencies": { "effect": "^4.0.0-beta.42" } }, "sha512-PC+lxLsrwob3+nBChAPrQq32olCeyApgXBvs1NrRsoArLViNT76T/68CttuCAksCZj5e1bZ1ZibLPel3vUmx2g=="],
+ "@effect/platform-node-shared": ["@effect/[email protected]", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.19.0" }, "peerDependencies": { "effect": "^4.0.0-beta.43" } }, "sha512-A9q0GEb61pYcQ06Dr6gXj1nKlDI3KHsar1sk3qb1ZY+kVSR64tBAylI8zGon23KY+NPtTUj/sEIToB7jc3Qt5w=="],
"@electron/asar": ["@electron/[email protected]", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
@@ -2771,7 +2771,7 @@
"ee-first": ["[email protected]", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
- "effect": ["[email protected]", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-c1UrRP+tLzyHb4Fepl8XBDJlLQLkrcMXrRBba441GQRxMbeQ/aIOSFcBwSda1iMJ5l9F0lYc3Bhe33/whrmavQ=="],
+ "effect": ["[email protected]", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-AJYyDimIwJOn87uUz/JzmgDc5GfjxJbXvEbTvNzMa+M3Uer344bLo/O5mMRkqc1vBleA+Ygs4+dbE3QsqOkKTQ=="],
"ejs": ["[email protected]", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
diff --git a/package.json b/package.json
index 2bb1a9539..cc2d3f4c2 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,7 @@
"packages/slack"
],
"catalog": {
- "@effect/platform-node": "4.0.0-beta.42",
+ "@effect/platform-node": "4.0.0-beta.43",
"@types/bun": "1.3.11",
"@octokit/rest": "22.0.0",
"@hono/zod-validator": "0.4.2",
@@ -45,7 +45,7 @@
"dompurify": "3.3.1",
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
- "effect": "4.0.0-beta.42",
+ "effect": "4.0.0-beta.43",
"ai": "6.0.138",
"hono": "4.10.7",
"hono-openapi": "1.1.2",
diff --git a/packages/opencode/src/effect/instance-state.ts b/packages/opencode/src/effect/instance-state.ts
index b073cf0a4..cc5901fb5 100644
--- a/packages/opencode/src/effect/instance-state.ts
+++ b/packages/opencode/src/effect/instance-state.ts
@@ -24,9 +24,9 @@ export namespace InstanceState {
return ((...args: any[]) => Instance.restore(ctx, () => fn(...args))) as F
}
- export const context = Effect.fnUntraced(function* () {
+ export const context = Effect.gen(function* () {
return (yield* InstanceRef) ?? Instance.current
- })()
+ })
export const directory = Effect.map(context, (ctx) => ctx.directory)
@@ -37,9 +37,9 @@ export namespace InstanceState {
const cache = yield* ScopedCache.make<string, A, E, R>({
capacity: Number.POSITIVE_INFINITY,
lookup: () =>
- Effect.fnUntraced(function* () {
+ Effect.gen(function* () {
return yield* init(yield* context)
- })(),
+ }),
})
const off = registerDisposer((directory) => Effect.runPromise(ScopedCache.invalidate(cache, directory)))
diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts
index 08b2faf6b..353f02c31 100644
--- a/packages/opencode/src/file/index.ts
+++ b/packages/opencode/src/file/index.ts
@@ -5,7 +5,6 @@ import { AppFileSystem } from "@/filesystem"
import { git } from "@/util/git"
import { Effect, Layer, ServiceMap } from "effect"
import { formatPatch, structuredPatch } from "diff"
-import fs from "fs"
import fuzzysort from "fuzzysort"
import ignore from "ignore"
import path from "path"
@@ -359,49 +358,46 @@ export namespace File {
const isGlobalHome = Instance.directory === Global.Path.home && Instance.project.id === "global"
const next: Entry = { files: [], dirs: [] }
- yield* Effect.promise(async () => {
- if (isGlobalHome) {
- const dirs = new Set<string>()
- const protectedNames = Protected.names()
- const ignoreNested = new Set(["node_modules", "dist", "build", "target", "vendor"])
- const shouldIgnoreName = (name: string) => name.startsWith(".") || protectedNames.has(name)
- const shouldIgnoreNested = (name: string) => name.startsWith(".") || ignoreNested.has(name)
- const top = await fs.promises
- .readdir(Instance.directory, { withFileTypes: true })
- .catch(() => [] as fs.Dirent[])
-
- for (const entry of top) {
- if (!entry.isDirectory()) continue
- if (shouldIgnoreName(entry.name)) continue
- dirs.add(entry.name + "/")
-
- const base = path.join(Instance.directory, entry.name)
- const children = await fs.promises.readdir(base, { withFileTypes: true }).catch(() => [] as fs.Dirent[])
- for (const child of children) {
- if (!child.isDirectory()) continue
- if (shouldIgnoreNested(child.name)) continue
- dirs.add(entry.name + "/" + child.name + "/")
- }
+ if (isGlobalHome) {
+ const dirs = new Set<string>()
+ const protectedNames = Protected.names()
+ const ignoreNested = new Set(["node_modules", "dist", "build", "target", "vendor"])
+ const shouldIgnoreName = (name: string) => name.startsWith(".") || protectedNames.has(name)
+ const shouldIgnoreNested = (name: string) => name.startsWith(".") || ignoreNested.has(name)
+ const top = yield* appFs.readDirectoryEntries(Instance.directory).pipe(Effect.orElseSucceed(() => []))
+
+ for (const entry of top) {
+ if (entry.type !== "directory") continue
+ if (shouldIgnoreName(entry.name)) continue
+ dirs.add(entry.name + "/")
+
+ const base = path.join(Instance.directory, entry.name)
+ const children = yield* appFs.readDirectoryEntries(base).pipe(Effect.orElseSucceed(() => []))
+ for (const child of children) {
+ if (child.type !== "directory") continue
+ if (shouldIgnoreNested(child.name)) continue
+ dirs.add(entry.name + "/" + child.name + "/")
}
+ }
- next.dirs = Array.from(dirs).toSorted()
- } else {
- const seen = new Set<string>()
- for await (const file of Ripgrep.files({ cwd: Instance.directory })) {
- next.files.push(file)
- let current = file
- while (true) {
- const dir = path.dirname(current)
- if (dir === ".") break
- if (dir === current) break
- current = dir
- if (seen.has(dir)) continue
- seen.add(dir)
- next.dirs.push(dir + "/")
- }
+ next.dirs = Array.from(dirs).toSorted()
+ } else {
+ const files = yield* Effect.promise(() => Array.fromAsync(Ripgrep.files({ cwd: Instance.directory })))
+ const seen = new Set<string>()
+ for (const file of files) {
+ next.files.push(file)
+ let current = file
+ while (true) {
+ const dir = path.dirname(current)
+ if (dir === ".") break
+ if (dir === current) break
+ current = dir
+ if (seen.has(dir)) continue
+ seen.add(dir)
+ next.dirs.push(dir + "/")
}
}
- })
+ }
const s = yield* InstanceState.get(state)
s.cache = next
@@ -636,30 +632,27 @@ export namespace File {
yield* ensure()
const { cache } = yield* InstanceState.get(state)
- return yield* Effect.promise(async () => {
- const query = input.query.trim()
- const limit = input.limit ?? 100
- const kind = input.type ?? (input.dirs === false ? "file" : "all")
- log.info("search", { query, kind })
+ const query = input.query.trim()
+ const limit = input.limit ?? 100
+ const kind = input.type ?? (input.dirs === false ? "file" : "all")
+ log.info("search", { query, kind })
- const result = cache
- const preferHidden = query.startsWith(".") || query.includes("/.")
+ const preferHidden = query.startsWith(".") || query.includes("/.")
- if (!query) {
- if (kind === "file") return result.files.slice(0, limit)
- return sortHiddenLast(result.dirs.toSorted(), preferHidden).slice(0, limit)
- }
+ if (!query) {
+ if (kind === "file") return cache.files.slice(0, limit)
+ return sortHiddenLast(cache.dirs.toSorted(), preferHidden).slice(0, limit)
+ }
- const items =
- kind === "file" ? result.files : kind === "directory" ? result.dirs : [...result.files, ...result.dirs]
+ const items =
+ kind === "file" ? cache.files : kind === "directory" ? cache.dirs : [...cache.files, ...cache.dirs]
- const searchLimit = kind === "directory" && !preferHidden ? limit * 20 : limit
- const sorted = fuzzysort.go(query, items, { limit: searchLimit }).map((item) => item.target)
- const output = kind === "directory" ? sortHiddenLast(sorted, preferHidden).slice(0, limit) : sorted
+ const searchLimit = kind === "directory" && !preferHidden ? limit * 20 : limit
+ const sorted = fuzzysort.go(query, items, { limit: searchLimit }).map((item) => item.target)
+ const output = kind === "directory" ? sortHiddenLast(sorted, preferHidden).slice(0, limit) : sorted
- log.info("search", { query, kind, results: output.length })
- return output
- })
+ log.info("search", { query, kind, results: output.length })
+ return output
})
log.info("init")
diff --git a/packages/opencode/src/provider/auth.ts b/packages/opencode/src/provider/auth.ts
index fbfab6c3b..38ef4b11f 100644
--- a/packages/opencode/src/provider/auth.ts
+++ b/packages/opencode/src/provider/auth.ts
@@ -111,26 +111,25 @@ export namespace ProviderAuth {
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/ProviderAuth") {}
- export const layer = Layer.effect(
+ export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> = Layer.effect(
Service,
Effect.gen(function* () {
const auth = yield* Auth.Service
+ const plugin = yield* Plugin.Service
const state = yield* InstanceState.make<State>(
- Effect.fn("ProviderAuth.state")(() =>
- Effect.promise(async () => {
- const plugins = await Plugin.list()
- return {
- hooks: Record.fromEntries(
- Arr.filterMap(plugins, (x) =>
- x.auth?.provider !== undefined
- ? Result.succeed([ProviderID.make(x.auth.provider), x.auth] as const)
- : Result.failVoid,
- ),
+ Effect.fn("ProviderAuth.state")(function* () {
+ const plugins = yield* plugin.list()
+ return {
+ hooks: Record.fromEntries(
+ Arr.filterMap(plugins, (x) =>
+ x.auth?.provider !== undefined
+ ? Result.succeed([ProviderID.make(x.auth.provider), x.auth] as const)
+ : Result.failVoid,
),
- pending: new Map<ProviderID, AuthOAuthResult>(),
- }
- }),
- ),
+ ),
+ pending: new Map<ProviderID, AuthOAuthResult>(),
+ }
+ }),
)
const methods = Effect.fn("ProviderAuth.methods")(function* () {
@@ -230,7 +229,9 @@ export namespace ProviderAuth {
}),
)
- export const defaultLayer = layer.pipe(Layer.provide(Auth.defaultLayer))
+ export const defaultLayer = Layer.suspend(() =>
+ layer.pipe(Layer.provide(Auth.defaultLayer), Layer.provide(Plugin.defaultLayer)),
+ )
const { runPromise } = makeRuntime(Service, defaultLayer)
diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts
index 40ab69e0f..861b34a7a 100644
--- a/packages/opencode/src/provider/provider.ts
+++ b/packages/opencode/src/provider/provider.ts
@@ -961,11 +961,12 @@ export namespace Provider {
}
}
- const layer: Layer.Layer<Service, never, Config.Service | Auth.Service> = Layer.effect(
+ const layer: Layer.Layer<Service, never, Config.Service | Auth.Service | Plugin.Service> = Layer.effect(
Service,
Effect.gen(function* () {
const config = yield* Config.Service
const auth = yield* Auth.Service
+ const plugin = yield* Plugin.Service
const state = yield* InstanceState.make<State>(() =>
Effect.gen(function* () {
@@ -1128,7 +1129,7 @@ export namespace Provider {
}
}
- const plugins = yield* Effect.promise(() => Plugin.list())
+ const plugins = yield* plugin.list()
for (const plugin of plugins) {
if (!plugin.auth) continue
const providerID = ProviderID.make(plugin.auth.provider)
@@ -1541,7 +1542,9 @@ export namespace Provider {
}),
)
- export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(Auth.defaultLayer))
+ export const defaultLayer = Layer.suspend(() =>
+ layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(Auth.defaultLayer), Layer.provide(Plugin.defaultLayer)),
+ )
const { runPromise } = makeRuntime(Service, defaultLayer)