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

@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.17.11",
"version": "1.17.13",
"name": "opencode",
"type": "module",
"license": "MIT",
@ -120,7 +120,7 @@
"drizzle-orm": "catalog:",
"effect": "catalog:",
"fuzzysort": "3.1.0",
"gitlab-ai-provider": "6.9.3",
"gitlab-ai-provider": "6.10.0",
"glob": "13.0.5",
"google-auth-library": "10.5.0",
"gray-matter": "4.0.3",
@ -144,7 +144,7 @@
"tree-sitter-powershell": "0.25.10",
"turndown": "7.2.0",
"ulid": "catalog:",
"venice-ai-sdk-provider": "2.0.2",
"venice-ai-sdk-provider": "2.1.1",
"vscode-jsonrpc": "8.2.1",
"web-tree-sitter": "0.25.10",
"ws": "8.21.0",

View file

@ -186,7 +186,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Ac
export const use = serviceUse(Service)
export const layer: Layer.Layer<Service, never, AccountRepo.Service | HttpClient.HttpClient> = Layer.effect(
const layer: Layer.Layer<Service, never, AccountRepo.Service | HttpClient.HttpClient> = Layer.effect(
Service,
Effect.gen(function* () {
const repo = yield* AccountRepo.Service
@ -456,8 +456,6 @@ export const layer: Layer.Layer<Service, never, AccountRepo.Service | HttpClient
}),
)
export const defaultLayer = layer.pipe(Layer.provide(AccountRepo.defaultLayer), Layer.provide(FetchHttpClient.layer))
export const node = LayerNode.make({ service: Service, layer: layer, deps: [AccountRepo.node, httpClient] })
export * as Account from "./account"

View file

@ -39,7 +39,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Ac
export const use = serviceUse(Service)
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const { db } = yield* Database.Service
@ -166,8 +166,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))
export const node = LayerNode.make({ service: Service, layer: layer, deps: [Database.node] })
export * as AccountRepo from "./repo"

View file

@ -1,7 +1,9 @@
import { Agent } from "@/agent/agent"
import { Command } from "@/command"
import { InstanceRef } from "@/effect/instance-ref"
import { InstanceBootstrap } from "@/project/bootstrap"
import { InstanceStore } from "@/project/instance-store"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { Provider } from "@/provider/provider"
@ -139,7 +141,7 @@ export const loaderLayer = Layer.effect(
}),
)
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const loader = yield* Loader
@ -199,12 +201,12 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(loaderLayer),
Layer.provide(Provider.defaultLayer),
Layer.provide(Agent.defaultLayer),
Layer.provide(Command.defaultLayer),
Layer.provide(InstanceStore.defaultLayer),
)
export const loaderNode = LayerNode.make({
service: Loader,
layer: loaderLayer,
deps: [Provider.node, Agent.node, Command.node, InstanceStore.node],
})
export const node = LayerNode.make({ service: Service, layer, deps: [loaderNode] })
export * as Directory from "./directory"

View file

@ -30,6 +30,7 @@ import {
type SetSessionModeResponse,
} from "@agentclientprotocol/sdk"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import type { AssistantMessage, Message, OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2"
import { Context, Effect, Layer, ManagedRuntime } from "effect"
import * as ACPError from "./error"
@ -569,23 +570,24 @@ export function make(input: {
}
function makeSessionService() {
return ManagedRuntime.make(ACPSession.defaultLayer).runSync(
return ManagedRuntime.make(AppNodeBuilder.build(ACPSession.node)).runSync(
ACPSession.Service.use((service) => Effect.succeed(service)),
)
}
function makeDirectoryService(sdk: OpencodeClient) {
return ManagedRuntime.make(
Directory.layer.pipe(
Layer.provide(
AppNodeBuilder.build(Directory.node, [
[
Directory.loaderNode,
Layer.succeed(
Directory.Loader,
Directory.Loader.of({
load: (directory) => request(() => loadDirectorySnapshot(sdk, directory), "directory"),
}),
),
),
),
],
]),
).runSync(Directory.Service.use((service) => Effect.succeed(service)))
}

View file

@ -1,5 +1,6 @@
import type { McpServer } from "@agentclientprotocol/sdk"
import type { Message, Part } from "@opencode-ai/sdk/v2"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { Context, Effect, Layer, Ref } from "effect"
@ -93,7 +94,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/AC
type State = Map<string, Info>
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const sessions = yield* Ref.make<State>(new Map())
@ -200,7 +201,7 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer
export const node = LayerNode.make({ service: Service, layer, deps: [] })
function makeSession(input: StoreInput): Info {
return {

View file

@ -1,7 +1,10 @@
import type { AgentSideConnection, Usage } from "@agentclientprotocol/sdk"
import type { AssistantMessage as OpenCodeAssistantMessage, Message } from "@opencode-ai/sdk/v2"
import { InstanceRef } from "@/effect/instance-ref"
import { InstanceBootstrap } from "@/project/bootstrap"
import { InstanceStore } from "@/project/instance-store"
import { makeGlobalNode, Node } from "@opencode-ai/core/effect/app-node"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { Provider } from "@/provider/provider"
@ -132,7 +135,7 @@ export const contextLimitLoaderLayer = Layer.effect(
}),
)
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const messageLoader = yield* MessageLoader
@ -223,10 +226,14 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(contextLimitLoaderLayer),
Layer.provide(Provider.defaultLayer),
Layer.provide(InstanceStore.defaultLayer),
)
export const messageLoaderNode = LayerNode.unbound(MessageLoader, Node.tags.values.global)
export const contextLimitLoaderNode = makeGlobalNode({
service: ContextLimitLoader,
layer: contextLimitLoaderLayer,
deps: [Provider.node, InstanceStore.node],
})
export const node = makeGlobalNode({ service: Service, layer, deps: [messageLoaderNode, contextLimitLoaderNode] })
export * as UsageService from "./usage"

View file

@ -85,7 +85,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Ag
export const use = serviceUse(Service)
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const config = yield* Config.Service
@ -438,15 +438,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(Plugin.defaultLayer),
Layer.provide(Provider.defaultLayer),
Layer.provide(Auth.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(Skill.defaultLayer),
Layer.provide(locationServiceMapLayer),
)
const locationServiceMapNode = LayerNode.make({
service: LocationServiceMap.Service,
layer: locationServiceMapLayer,

View file

@ -49,7 +49,7 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/Auth") {}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const fsys = yield* FSUtil.Service
@ -92,8 +92,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(Layer.provide(FSUtil.defaultLayer))
export const node = LayerNode.make({ service: Service, layer: layer, deps: [FSUtil.node] })
export * as Auth from "."

View file

@ -7,8 +7,9 @@ export const ScrapCommand = cmd({
builder: (yargs) => yargs,
async handler() {
const { Project } = await import("@/project/project")
const { AppNodeBuilder } = await import("@opencode-ai/core/effect/app-node-builder")
const { makeRuntime } = await import("@opencode-ai/core/effect/runtime")
const runtime = makeRuntime(Project.Service, Project.defaultLayer)
const runtime = makeRuntime(Project.Service, AppNodeBuilder.build(Project.node))
const list = await runtime.runPromise((project) => project.list())
process.stdout.write(JSON.stringify(list, null, 2) + EOL)
},

View file

@ -761,7 +761,7 @@ export const McpDebugCommand = effectCmd({
}
if (response.status === 401) {
prompts.log.warn("Server returned 401 Unauthorized")
prompts.log.info("Initial unauthenticated check returned 401, so this server requires OAuth")
// Try to discover OAuth metadata
const oauthConfig = typeof serverConfig.oauth === "object" ? serverConfig.oauth : undefined

View file

@ -233,11 +233,27 @@ export const RunCommand = effectCmd({
hidden: true,
describe: "cap visible interactive replay to the newest N messages",
})
.option("dangerously-skip-permissions", {
.option("interactive", {
alias: ["i"],
type: "boolean",
describe: "run in direct interactive split-footer mode",
default: false,
})
.option("auto", {
type: "boolean",
describe: "auto-approve permissions that are not explicitly denied (dangerous!)",
default: false,
})
.option("yolo", {
type: "boolean",
hidden: true,
default: false,
})
.option("dangerously-skip-permissions", {
type: "boolean",
hidden: true,
default: false,
})
.option("demo", {
type: "boolean",
default: false,
@ -255,6 +271,7 @@ export const RunCommand = effectCmd({
yield* Effect.promise(async () => {
const rawMessage = [...args.message, ...(args["--"] || [])].join(" ")
const interactive = args.mini
const auto = args.auto || args.yolo || args["dangerously-skip-permissions"]
const thinking = interactive ? (args.thinking ?? true) : (args.thinking ?? false)
const die = (message: string): never => {
UI.error(message)
@ -780,7 +797,7 @@ export const RunCommand = effectCmd({
const permission = event.properties
if (permission.sessionID !== sessionID) continue
if (args["dangerously-skip-permissions"]) {
if (auto) {
await client.permission.reply({
requestID: permission.id,
reply: "once",
@ -981,9 +998,12 @@ export async function runMini(input: MiniCommandInput) {
variant: undefined,
thinking: undefined,
mini: true,
interactive: false,
replay: input.replay ?? true,
"replay-limit": input.replayLimit,
replayLimit: input.replayLimit,
auto: false,
yolo: false,
"dangerously-skip-permissions": false,
dangerouslySkipPermissions: false,
demo: input.demo ?? false,

View file

@ -8,6 +8,7 @@
// variant and the persisted file.
import path from "path"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { Context, Effect, Layer } from "effect"
import { makeRuntime } from "@/effect/run-service"
import { Global } from "@opencode-ai/core/global"
@ -135,7 +136,7 @@ function state(value: unknown): ModelState {
}
}
function createLayer(fs = FSUtil.defaultLayer) {
function createLayer(fs = AppNodeBuilder.build(FSUtil.node)) {
return Layer.fresh(
Layer.effect(
Service,
@ -196,7 +197,7 @@ function createLayer(fs = FSUtil.defaultLayer) {
}
/** @internal Exported for testing. */
export function createVariantRuntime(fs = FSUtil.defaultLayer): VariantRuntime {
export function createVariantRuntime(fs = AppNodeBuilder.build(FSUtil.node)): VariantRuntime {
const runtime = makeRuntime(Service, createLayer(fs))
return {
resolveSavedVariant: (model) => runtime.runPromise((svc) => svc.resolveSavedVariant(model)).catch(() => undefined),

View file

@ -6,11 +6,12 @@ import { fileURLToPath } from "url"
import { UI } from "@/cli/ui"
import { errorMessage } from "@opencode-ai/tui/util/error"
import { withTimeout } from "@/util/timeout"
import { withNetworkOptions, resolveNetworkOptionsNoConfig } from "@/cli/network"
import { withNetworkOptions, resolveNetworkOptionsNoConfig, hasArg } from "@/cli/network"
import { Filesystem } from "@/util/filesystem"
import { OpenCode } from "@opencode-ai/client"
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
import { writeHeapSnapshot } from "v8"
import { ServerAuth } from "@/server/auth"
import { validateSession } from "../tui/validate-session"
import { win32InstallCtrlCGuard } from "@opencode-ai/tui/terminal-win32"
@ -74,6 +75,21 @@ export const TuiThreadCommand = cmd({
type: "string",
describe: "agent to use",
})
.option("auto", {
type: "boolean",
describe: "auto-approve permissions that are not explicitly denied (dangerous!)",
default: false,
})
.option("yolo", {
type: "boolean",
hidden: true,
default: false,
})
.option("dangerously-skip-permissions", {
type: "boolean",
hidden: true,
default: false,
})
.option("mini", {
type: "boolean",
describe: "start the minimal interactive interface",
@ -221,6 +237,7 @@ export const TuiThreadCommand = cmd({
model: args.model,
prompt,
fork: args.fork,
auto: args.auto || args.yolo || args["dangerously-skip-permissions"],
},
}),
)

View file

@ -37,6 +37,22 @@ export type NetworkOptions = InferredOptionTypes<typeof options>
export function withNetworkOptions<T>(yargs: Argv<T>) {
return yargs.options(options)
}
export function hasArg(name: string) {
return networkArgs().some((arg) => arg === name || arg.startsWith(name + "="))
}
function hasBooleanArg(name: string) {
return networkArgs().some(
(arg) => arg === name || arg === name + "=true" || arg === name + "=false" || arg === "--no-" + name.slice(2),
)
}
function networkArgs() {
const separator = process.argv.indexOf("--")
return process.argv.slice(2, separator === -1 ? undefined : separator)
}
export const resolveNetworkOptions = Effect.fn("Cli.resolveNetworkOptions")(function* (args: NetworkOptions) {
const { Config } = yield* Effect.promise(() => import("@/config/config"))
const config = yield* Config.Service.use((cfg) => cfg.getGlobal())
@ -44,10 +60,10 @@ export const resolveNetworkOptions = Effect.fn("Cli.resolveNetworkOptions")(func
})
export function resolveNetworkOptionsNoConfig(args: NetworkOptions, config?: ConfigV1.Info) {
const portExplicitlySet = process.argv.includes("--port")
const hostnameExplicitlySet = process.argv.includes("--hostname")
const mdnsExplicitlySet = process.argv.includes("--mdns")
const mdnsDomainExplicitlySet = process.argv.includes("--mdns-domain")
const portExplicitlySet = hasArg("--port")
const hostnameExplicitlySet = hasArg("--hostname")
const mdnsExplicitlySet = hasBooleanArg("--mdns")
const mdnsDomainExplicitlySet = hasArg("--mdns-domain")
const mdns = mdnsExplicitlySet ? args.mdns : (config?.server?.mdns ?? args.mdns)
const mdnsDomain = mdnsDomainExplicitlySet ? args["mdns-domain"] : (config?.server?.mdnsDomain ?? args["mdns-domain"])
const port = portExplicitlySet ? args.port : (config?.server?.port ?? args.port)

View file

@ -1,7 +1,8 @@
import { run as runTui, type TuiInput } from "@opencode-ai/tui"
import { Global } from "@opencode-ai/core/global"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { Effect } from "effect"
export function run(input: TuiInput) {
return runTui(input).pipe(Effect.provide(Global.defaultLayer))
return runTui(input).pipe(Effect.provide(AppNodeBuilder.build(Global.node)))
}

View file

@ -55,7 +55,7 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/Command") {}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const config = yield* Config.Service
@ -172,12 +172,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(Config.defaultLayer),
Layer.provide(MCP.defaultLayer),
Layer.provide(Skill.defaultLayer),
)
export const node = LayerNode.make({ service: Service, layer: layer, deps: [Config.node, MCP.node, Skill.node] })
export * as Command from "."

View file

@ -172,7 +172,7 @@ function writableGlobal(info: Info) {
return next
}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const fs = yield* FSUtil.Service
@ -671,16 +671,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(EffectFlock.defaultLayer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Env.defaultLayer),
Layer.provide(Auth.defaultLayer),
Layer.provide(Account.defaultLayer),
Layer.provide(Npm.defaultLayer),
Layer.provide(FetchHttpClient.layer),
)
export const node = LayerNode.make({
service: Service,
layer: layer,

View file

@ -2,6 +2,8 @@ export * as TuiConfig from "./tui"
import path from "path"
import { mergeDeep, unique } from "remeda"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Cause, Context, Effect, Fiber, Layer } from "effect"
import { ConfigParse } from "@/config/parse"
import * as ConfigPaths from "@/config/paths"
@ -223,7 +225,7 @@ const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory:
}
})
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const directory = yield* CurrentWorkingDirectory
@ -257,9 +259,9 @@ export const layer = Layer.effect(
}).pipe(Effect.withSpan("TuiConfig.layer")),
)
export const defaultLayer = layer.pipe(Layer.provide(Npm.defaultLayer), Layer.provide(FSUtil.defaultLayer))
export const node = LayerNode.make({ service: Service, layer, deps: [Npm.node, FSUtil.node] })
const { runPromise } = makeRuntime(Service, defaultLayer)
const { runPromise } = makeRuntime(Service, AppNodeBuilder.build(node))
export async function waitForDependencies() {
await runPromise((svc) => svc.waitForDependencies())

View file

@ -31,8 +31,8 @@ import { waitEvent } from "./util"
import { WorkspaceRef } from "@/effect/instance-ref"
import { Vcs } from "@/project/vcs"
import { InstanceStore } from "@/project/instance-store"
import { InstanceBootstrap } from "@/project/bootstrap"
import { WorkspaceAdapterRuntime } from "./workspace-adapter-runtime"
import { AppNodeBuilderV1 } from "@/effect/app-node-builder-v1"
import { WorkspaceEvent } from "@opencode-ai/schema/workspace-event"
export const Info = Schema.Struct({
@ -150,7 +150,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Wo
export const use = serviceUse(Service)
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const auth = yield* Auth.Service
@ -601,7 +601,7 @@ export const layer = Layer.effect(
}),
fallback: "",
response: "text",
}).pipe(Effect.provide(InstanceStore.defaultLayer.pipe(Layer.provide(InstanceBootstrap.defaultLayer))))
}).pipe(Effect.provide(AppNodeBuilderV1.build(InstanceStore.node)))
: ""
if (sourcePatch) {
@ -617,7 +617,7 @@ export const layer = Layer.effect(
body: HttpBody.jsonUnsafe({ patch: sourcePatch }),
}),
fallback: { applied: false },
}).pipe(Effect.provide(InstanceStore.defaultLayer.pipe(Layer.provide(InstanceBootstrap.defaultLayer))))
}).pipe(Effect.provide(AppNodeBuilderV1.build(InstanceStore.node)))
}
if (input.workspaceID === null) {
@ -885,19 +885,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(Auth.defaultLayer),
Layer.provide(Session.defaultLayer),
Layer.provide(SessionPrompt.defaultLayer),
Layer.provide(Project.defaultLayer),
Layer.provide(Vcs.defaultLayer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Database.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(FetchHttpClient.layer),
Layer.provide(RuntimeFlags.defaultLayer),
)
const TIMEOUT = 5000
type HistoryEvent = {

View file

@ -0,0 +1,12 @@
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { InstanceBootstrap } from "@/project/bootstrap"
import { InstanceStore } from "@/project/instance-store"
const bootstrapReplacement = [InstanceStore.bootstrapNode, InstanceBootstrap.node] as const
export function build<A, E>(root: LayerNode.Node<A, E, any>, replacements: LayerNode.Replacements = []) {
return AppNodeBuilder.build(root, replacements.concat([bootstrapReplacement]))
}
export * as AppNodeBuilderV1 from "./app-node-builder-v1"

View file

@ -38,7 +38,7 @@ import { Command } from "@/command"
import { Truncate } from "@/tool/truncate"
import { ToolRegistry } from "@/tool/registry"
import { Format } from "@/format"
import { InstanceLayer } from "@/project/instance-layer"
import { InstanceStore } from "@/project/instance-store"
import { Project } from "@/project/project"
import { Vcs } from "@/project/vcs"
import { Workspace } from "@/control-plane/workspace"
@ -51,59 +51,62 @@ import { memoMap } from "@opencode-ai/core/effect/memo-map"
import { Job } from "@/job"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { EventV2Bridge } from "@/event-v2-bridge"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { AppNodeBuilderV1 } from "./app-node-builder-v1"
import { SessionProjector } from "@opencode-ai/core/session/projector"
export const AppLayer = Layer.mergeAll(
Npm.defaultLayer,
FSUtil.defaultLayer,
Database.defaultLayer,
Auth.defaultLayer,
Account.defaultLayer,
Config.defaultLayer,
Git.defaultLayer,
Storage.defaultLayer,
Snapshot.defaultLayer,
Plugin.defaultLayer,
ModelsDev.defaultLayer,
Provider.defaultLayer,
ProviderAuth.defaultLayer,
Agent.defaultLayer,
Skill.defaultLayer,
Discovery.defaultLayer,
Question.defaultLayer,
Permission.defaultLayer,
Todo.defaultLayer,
Session.defaultLayer,
SessionStatus.defaultLayer,
Job.defaultLayer,
RuntimeFlags.defaultLayer,
EventV2Bridge.defaultLayer,
SessionRunState.defaultLayer,
SessionProcessor.defaultLayer,
SessionCompaction.defaultLayer,
SessionRevert.defaultLayer,
SessionSummary.defaultLayer,
SessionPrompt.defaultLayer,
Instruction.defaultLayer,
LLM.defaultLayer,
LSP.defaultLayer,
MCP.defaultLayer,
McpAuth.defaultLayer,
Command.defaultLayer,
Truncate.defaultLayer,
ToolRegistry.defaultLayer,
Format.defaultLayer,
Project.defaultLayer,
Vcs.defaultLayer,
Workspace.defaultLayer,
Worktree.appLayer,
Installation.defaultLayer,
ShareNext.defaultLayer,
SessionShare.defaultLayer,
).pipe(
Layer.provideMerge(Ripgrep.defaultLayer),
Layer.provideMerge(InstanceLayer.layer),
Layer.provideMerge(Observability.layer),
)
export const AppLayer = AppNodeBuilderV1.build(
LayerNode.group([
Npm.node,
FSUtil.node,
Database.node,
Auth.node,
Account.node,
Config.node,
Git.node,
Storage.node,
Snapshot.node,
Plugin.node,
ModelsDev.node,
Provider.node,
ProviderAuth.node,
Agent.node,
Skill.node,
Discovery.node,
Question.node,
Permission.node,
Todo.node,
Session.node,
SessionProjector.node,
SessionStatus.node,
Job.node,
RuntimeFlags.node,
EventV2Bridge.node,
SessionRunState.node,
SessionProcessor.node,
SessionCompaction.node,
SessionRevert.node,
SessionSummary.node,
SessionPrompt.node,
Instruction.node,
LLM.node,
LSP.node,
MCP.node,
McpAuth.node,
Command.node,
Truncate.node,
ToolRegistry.node,
Format.node,
InstanceStore.node,
Project.node,
Vcs.node,
Workspace.node,
Worktree.node,
Installation.node,
ShareNext.node,
SessionShare.node,
]),
).pipe(Layer.provideMerge(AppNodeBuilderV1.build(Ripgrep.node)), Layer.provideMerge(Observability.layer))
const rt = ManagedRuntime.make(AppLayer, { memoMap })
type Runtime = Pick<typeof rt, "runSync" | "runPromise" | "runPromiseExit" | "runFork" | "runCallback" | "dispose">

View file

@ -1,4 +1,6 @@
import { Layer, ManagedRuntime } from "effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Plugin } from "@/plugin"
import { LSP } from "@/lsp/lsp"
@ -10,14 +12,8 @@ import { Config } from "@/config/config"
import * as Observability from "@opencode-ai/core/observability"
import { memoMap } from "@opencode-ai/core/effect/memo-map"
export const BootstrapLayer = Layer.mergeAll(
Config.defaultLayer,
Plugin.defaultLayer,
ShareNext.defaultLayer,
Format.defaultLayer,
LSP.defaultLayer,
Vcs.defaultLayer,
Snapshot.defaultLayer,
export const BootstrapLayer = AppNodeBuilder.build(
LayerNode.group([Config.node, Plugin.node, ShareNext.node, Format.node, LSP.node, Vcs.node, Snapshot.node]),
).pipe(Layer.provide(Observability.layer))
export const BootstrapRuntime = ManagedRuntime.make(BootstrapLayer, { memoMap })

View file

@ -14,9 +14,9 @@ export type Shape<Fields extends ConfigMap> = {
*/
export type ServiceClass<Self, Id extends string, Service> = Context.ServiceClass<Self, Id, Service> & {
/** Provide already-parsed config, useful in tests. */
readonly layer: (input: Service) => Layer.Layer<Self>
readonly configLayer: (input: Service) => Layer.Layer<Self>
/** Parse config once from the active Effect ConfigProvider and provide the service. */
readonly defaultLayer: Layer.Layer<Self, Config.ConfigError>
readonly layer: Layer.Layer<Self, Config.ConfigError>
}
/**
@ -35,19 +35,19 @@ export type ServiceClass<Self, Id extends string, Service> = Context.ServiceClas
* },
* ) {}
*
* const live = ServerAuthConfig.defaultLayer
* const test = ServerAuthConfig.layer({ password: Option.some("secret"), username: "kit" })
* const live = ServerAuthConfig.layer
* const test = ServerAuthConfig.configLayer({ password: Option.some("secret"), username: "kit" })
* ```
*/
export const Service =
<Self>() =>
<const Id extends string, const Fields extends ConfigMap>(id: Id, fields: Fields) => {
class ConfigTag extends Context.Service<Self, Shape<Fields>>()(id) {
static layer(input: Shape<Fields>) {
static configLayer(input: Shape<Fields>) {
return Layer.succeed(this, this.of(input))
}
static get defaultLayer() {
static get layer() {
const tag = this
return Layer.effect(
tag,

View file

@ -57,7 +57,7 @@ export class Service extends ConfigService.Service<Service>()("@opencode/Runtime
export type Info = Context.Service.Shape<typeof Service>
const emptyConfigLayer = Service.defaultLayer.pipe(
const emptyConfigLayer = Service.layer.pipe(
Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown({}))),
Layer.orDie,
)
@ -71,9 +71,7 @@ export const layer = (overrides: Partial<Info> = {}) =>
}),
).pipe(Layer.provide(emptyConfigLayer))
export const defaultLayer = Service.defaultLayer.pipe(Layer.orDie)
export const node = LayerNode.make({ service: Service, layer: defaultLayer, deps: [] })
export const node = LayerNode.make({ service: Service, layer: Service.layer.pipe(Layer.orDie), deps: [] })
export * as RuntimeFlags from "./runtime-flags"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"

View file

@ -16,7 +16,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/En
export const use = serviceUse(Service)
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const state = yield* InstanceState.make<State>(Effect.fn("Env.state")(() => Effect.succeed({ ...process.env })))
@ -36,8 +36,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer
export const node = LayerNode.make({ service: Service, layer: layer, deps: [] })
export * as Env from "."

View file

@ -11,7 +11,7 @@ import { Context, Effect, Layer } from "effect"
export class Service extends Context.Service<Service, EventV2.Interface>()("@opencode/EventV2Bridge") {}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2.Service
@ -66,8 +66,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(Layer.provide(EventV2.defaultLayer))
export const node = LayerNode.make({ service: Service, layer: layer, deps: [EventV2.node] })
export * as EventV2Bridge from "./event-v2-bridge"

View file

@ -28,7 +28,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Fo
export const use = serviceUse(Service)
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const config = yield* Config.Service
@ -194,12 +194,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(Config.defaultLayer),
Layer.provide(AppProcess.defaultLayer),
Layer.provide(RuntimeFlags.defaultLayer),
)
export const node = LayerNode.make({
service: Service,
layer: layer,

View file

@ -100,7 +100,7 @@ const kind = (code: string): Kind => {
export class Service extends Context.Service<Service, Interface>()("@opencode/Git") {}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const appProcess = yield* AppProcess.Service
@ -343,8 +343,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(Layer.provide(AppProcess.defaultLayer))
export const node = LayerNode.make({ service: Service, layer: layer, deps: [AppProcess.node] })
export * as Git from "."

View file

@ -56,7 +56,7 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/Image") {}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const config = yield* Config.Service
@ -167,8 +167,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer))
export const node = LayerNode.make({ service: Service, layer: layer, deps: [Config.node] })
export * as Image from "./image"

View file

@ -1,8 +1,9 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
import { Effect, Layer, Schema, Context, Stream } from "effect"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { withTransientReadRetry } from "@/util/effect-http-client"
import { errorMessage } from "@/util/error"
import { ChildProcess } from "effect/unstable/process"
@ -82,7 +83,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/In
export const use = serviceUse(Service)
export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | AppProcess.Service> = Layer.effect(
const layer: Layer.Layer<Service, never, HttpClient.HttpClient | AppProcess.Service> = Layer.effect(
Service,
Effect.gen(function* () {
const http = yield* HttpClient.HttpClient
@ -324,14 +325,12 @@ export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | AppProce
}),
)
export const defaultLayer = layer.pipe(Layer.provide(FetchHttpClient.layer), Layer.provide(AppProcess.defaultLayer))
export const node = LayerNode.make({ service: Service, layer: layer, deps: [httpClient, AppProcess.node] })
const { runPromise } = makeRuntime(Service, defaultLayer)
const { runPromise } = makeRuntime(Service, AppNodeBuilder.build(node))
export const latest = (...args: Parameters<Interface["latest"]>) => runPromise((s) => s.latest(...args))
export const method = () => runPromise((s) => s.method())
export const upgrade = (...args: Parameters<Interface["upgrade"]>) => runPromise((s) => s.upgrade(...args))
export const node = LayerNode.make({ service: Service, layer: layer, deps: [httpClient, AppProcess.node] })
export * as Installation from "."

View file

@ -34,8 +34,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer
export const node = LayerNode.make({ service: Service, layer, deps: [] })
export * as Job from "./job"

View file

@ -135,7 +135,7 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/LSP") {}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const config = yield* Config.Service
@ -496,12 +496,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(Config.defaultLayer),
Layer.provide(RuntimeFlags.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer),
)
export * as Diagnostic from "./diagnostic"
export const node = LayerNode.make({

View file

@ -56,7 +56,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Mc
export const use = serviceUse(Service)
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const fs = yield* FSUtil.Service
@ -158,8 +158,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(Layer.provide(EffectFlock.defaultLayer), Layer.provide(FSUtil.defaultLayer))
export const node = LayerNode.make({ service: Service, layer: layer, deps: [FSUtil.node, EffectFlock.node] })
export * as McpAuth from "./auth"

View file

@ -72,7 +72,8 @@ export function convertTool(mcpTool: MCPToolDef, client: Client, timeout?: numbe
.filter((text) => text.trim())
.join("\n\n") || "MCP tool returned an error",
)
if (result.structuredContent === undefined || result.structuredContent === null) return result
if (result.content.length > 0 || result.structuredContent === undefined || result.structuredContent === null)
return result
return {
...result,
content: [{ type: "text" as const, text: JSON.stringify(result.structuredContent) }],

View file

@ -194,7 +194,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/MC
export const use = serviceUse(Service)
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
@ -946,7 +946,7 @@ export const layer = Layer.effect(
const mcpConfig = yield* requireMcpConfig(mcpName)
return yield* createAndStore(mcpName, mcpConfig)
return yield* createAndStore(mcpName, { ...mcpConfig, enabled: true })
})
const removeAuth = Effect.fn("MCP.removeAuth")(function* (mcpName: string) {
@ -1003,16 +1003,6 @@ export const layer = Layer.effect(
export type AuthStatus = "authenticated" | "expired" | "not_authenticated"
// --- Per-service runtime ---
export const defaultLayer = layer.pipe(
Layer.provide(McpAuth.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(CrossSpawnSpawner.defaultLayer),
Layer.provide(FSUtil.defaultLayer),
)
export const node = LayerNode.make({
service: Service,
layer: layer,

View file

@ -39,7 +39,7 @@ export function evaluate(permission: string, pattern: string, ...rulesets: Permi
export class Service extends Context.Service<Service, Interface>()("@opencode/Permission") {}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2Bridge.Service
@ -213,8 +213,6 @@ export function disabled(tools: string[], ruleset: PermissionV1.Ruleset): Set<st
)
}
export const defaultLayer = layer.pipe(Layer.provide(EventV2Bridge.defaultLayer))
export const node = LayerNode.make({ service: Service, layer: layer, deps: [EventV2Bridge.node] })
export * as Permission from "."

View file

@ -120,7 +120,7 @@ async function applyPlugin(load: PluginLoader.Loaded, input: PluginInput, hooks:
}
}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2Bridge.Service
@ -305,12 +305,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(RuntimeFlags.defaultLayer),
)
export const node = LayerNode.make({
service: Service,
layer: layer,

View file

@ -14,6 +14,7 @@ import {
import path from "path"
import { fileURLToPath } from "url"
import { TuiConfig } from "@/config/tui"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { errorData, errorMessage } from "@opencode-ai/tui/util/error"
import { isRecord } from "@opencode-ai/tui/util/record"
import { resolveHostAttentionSoundPaths } from "@/config/tui-host-attention"
@ -1082,7 +1083,7 @@ async function load(input: {
const flags = await Effect.runPromise(
Effect.gen(function* () {
return yield* RuntimeFlags.Service
}).pipe(Effect.provide(RuntimeFlags.defaultLayer)),
}).pipe(Effect.provide(AppNodeBuilder.build(RuntimeFlags.node))),
)
const pluginOrigins = config.plugin_origins ?? (await TuiConfig.pluginOrigins())
const records = Flag.OPENCODE_PURE ? [] : pluginOrigins

View file

@ -1,4 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { makeGlobalNode } from "@opencode-ai/core/effect/app-node"
import { Plugin } from "../plugin"
import { Format } from "../format"
import { LSP } from "@/lsp/lsp"
@ -14,7 +14,7 @@ import { Service } from "./bootstrap-service"
export { Service } from "./bootstrap-service"
export type { Interface } from "./bootstrap-service"
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
// Yield each bootstrap dep at layer init so `run` itself has R = never.
@ -49,20 +49,7 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer: Layer.Layer<Service> = layer.pipe(
Layer.provide([
Config.defaultLayer,
Format.defaultLayer,
LSP.defaultLayer,
Plugin.defaultLayer,
Project.defaultLayer,
ShareNext.defaultLayer,
Snapshot.defaultLayer,
Vcs.defaultLayer,
]),
)
export const node = LayerNode.make({
export const node = makeGlobalNode({
service: Service,
layer: layer,
deps: [Config.node, Format.node, LSP.node, Plugin.node, Project.node, ShareNext.node, Snapshot.node, Vcs.node],

View file

@ -1,11 +0,0 @@
import { Effect, Layer } from "effect"
import { InstanceStore } from "./instance-store"
export const layer = Layer.unwrap(
Effect.promise(async () => {
const { InstanceBootstrap } = await import("./bootstrap")
return InstanceStore.defaultLayer.pipe(Layer.provide(InstanceBootstrap.defaultLayer))
}),
)
export * as InstanceLayer from "./instance-layer"

View file

@ -1,4 +1,5 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { makeGlobalNode, Node } from "@opencode-ai/core/effect/app-node"
import { GlobalBus } from "@/bus/global"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { WorkspaceContext } from "@/control-plane/workspace-context"
@ -8,7 +9,6 @@ import { FSUtil } from "@opencode-ai/core/fs-util"
import { Context, Deferred, Duration, Effect, Exit, Layer, Scope } from "effect"
import { type InstanceContext } from "./instance-context"
import { InstanceBootstrap } from "./bootstrap-service"
import { InstanceBootstrap as InstanceBootstrapGraph } from "./bootstrap"
import * as Project from "./project"
export interface LoadInput {
@ -34,7 +34,7 @@ interface Entry {
readonly deferred: Deferred.Deferred<InstanceContext>
}
export const layer: Layer.Layer<Service, never, Project.Service | InstanceBootstrap.Service> = Layer.effect(
const layer: Layer.Layer<Service, never, Project.Service | InstanceBootstrap.Service> = Layer.effect(
Service,
Effect.gen(function* () {
const project = yield* Project.Service
@ -202,12 +202,12 @@ export const layer: Layer.Layer<Service, never, Project.Service | InstanceBootst
}),
)
export const defaultLayer = layer.pipe(Layer.provide(Project.defaultLayer))
export const bootstrapNode = LayerNode.unbound(InstanceBootstrap.Service, Node.tags.values.global)
export const node = LayerNode.make({
export const node = makeGlobalNode({
service: Service,
layer: layer,
deps: [Project.node, InstanceBootstrapGraph.node],
deps: [Project.node, bootstrapNode],
})
export * as InstanceStore from "./instance-store"

View file

@ -103,7 +103,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Pr
type GitResult = { code: number; text: string; stderr: string }
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const fs = yield* FSUtil.Service
@ -463,17 +463,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(ProjectV2.defaultLayer),
Layer.provide(ProjectDirectories.defaultLayer),
Layer.provide(AppProcess.defaultLayer),
Layer.provide(CrossSpawnSpawner.defaultLayer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Database.defaultLayer),
Layer.provide(RuntimeFlags.defaultLayer),
)
export const use = serviceUse(Service)
export const node = LayerNode.make({

View file

@ -295,7 +295,7 @@ interface State {
export class Service extends Context.Service<Service, Interface>()("@opencode/Vcs") {}
export const layer: Layer.Layer<Service, never, Git.Service | EventV2Bridge.Service> = Layer.effect(
const layer: Layer.Layer<Service, never, Git.Service | EventV2Bridge.Service> = Layer.effect(
Service,
Effect.gen(function* () {
const git = yield* Git.Service
@ -418,8 +418,6 @@ export const layer: Layer.Layer<Service, never, Git.Service | EventV2Bridge.Serv
}),
)
export const defaultLayer = layer.pipe(Layer.provide(Git.defaultLayer), Layer.provide(EventV2Bridge.defaultLayer))
export const node = LayerNode.make({ service: Service, layer: layer, deps: [Git.node, EventV2Bridge.node] })
export * as Vcs from "./vcs"

View file

@ -106,7 +106,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Pr
export const use = serviceUse(Service)
export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> = Layer.effect(
const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> = Layer.effect(
Service,
Effect.gen(function* () {
const auth = yield* Auth.Service
@ -224,10 +224,6 @@ export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> =
}),
)
export const defaultLayer = Layer.suspend(() =>
layer.pipe(Layer.provide(Auth.defaultLayer), Layer.provide(Plugin.defaultLayer)),
)
export const node = LayerNode.make({ service: Service, layer: layer, deps: [Auth.node, Plugin.node] })
export * as ProviderAuth from "./auth"

View file

@ -1299,7 +1299,7 @@ function modelSuggestions(provider: Info | undefined, modelID: ModelV2.ID, enabl
.map((item) => item.id)
}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const fs = yield* FSUtil.Service
@ -1949,18 +1949,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = Layer.suspend(() =>
layer.pipe(
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Env.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(Auth.defaultLayer),
Layer.provide(Plugin.defaultLayer),
Layer.provide(ModelsDev.defaultLayer),
Layer.provide(RuntimeFlags.defaultLayer),
),
)
const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"]
const smallModelFamilyPriority = ["gemini-flash", "gpt-nano", "claude-haiku"]
export function sort<T extends { id: string }>(models: T[]) {

View file

@ -463,7 +463,9 @@ export function message(msgs: ModelMessage[], model: Provider.Model, options: Re
if (
options.store !== true &&
key &&
["@ai-sdk/openai", "@ai-sdk/azure", "@ai-sdk/amazon-bedrock/mantle"].includes(model.api.npm)
["@ai-sdk/openai", "@ai-sdk/azure", "@ai-sdk/amazon-bedrock/mantle", "@ai-sdk/github-copilot"].includes(
model.api.npm,
)
) {
msgs = mapProviderOptions(msgs, (options) => {
if (!options?.[key] || !("itemId" in options[key])) return options
@ -605,8 +607,14 @@ function anthropicOpus47OrLater(apiId: string) {
return major > 4 || (major === 4 && minor >= 7)
}
function anthropicSonnet5OrLater(apiId: string) {
const version = /sonnet-(\d+)(?:[.@-]|$)|claude-(\d+)-sonnet(?:[.@-]|$)/i.exec(apiId)
if (!version) return false
return Number(version[1] ?? version[2]) >= 5
}
function anthropicAdaptiveEfforts(apiId: string): string[] | null {
if (anthropicOpus47OrLater(apiId) || apiId.includes("fable-5")) {
if (anthropicOpus47OrLater(apiId) || anthropicSonnet5OrLater(apiId) || apiId.includes("fable-5")) {
return ["low", "medium", "high", "xhigh", "max"]
}
if (
@ -620,7 +628,7 @@ function anthropicAdaptiveEfforts(apiId: string): string[] | null {
}
function anthropicOmitsThinking(apiId: string) {
return anthropicOpus47OrLater(apiId) || apiId.includes("fable-5")
return anthropicOpus47OrLater(apiId) || anthropicSonnet5OrLater(apiId) || apiId.includes("fable-5")
}
function googleThinkingLevelEfforts(apiId: string) {
@ -1255,6 +1263,16 @@ const SLUG_OVERRIDES: Record<string, string> = {
}
export function providerOptions(model: Provider.Model, options: { [x: string]: any }) {
const usesOpenAIReasoningGate =
model.api.npm === "@ai-sdk/openai" ||
model.api.npm === "@ai-sdk/azure" ||
model.api.npm === "@ai-sdk/amazon-bedrock/mantle"
const normalized =
usesOpenAIReasoningGate &&
(model.capabilities.reasoning || options.reasoningEffort !== undefined || options.reasoningSummary !== undefined)
? { ...options, forceReasoning: true }
: options
if (model.api.npm === "@ai-sdk/gateway") {
// Gateway providerOptions are split across two namespaces:
// - `gateway`: gateway-native routing/caching controls (order, only, byok, etc.)
@ -1264,8 +1282,8 @@ export function providerOptions(model: Provider.Model, options: { [x: string]: a
const i = model.api.id.indexOf("/")
const rawSlug = i > 0 ? model.api.id.slice(0, i) : undefined
const slug = rawSlug ? (SLUG_OVERRIDES[rawSlug] ?? rawSlug) : undefined
const gateway = options.gateway
const rest = Object.fromEntries(Object.entries(options).filter(([k]) => k !== "gateway"))
const gateway = normalized.gateway
const rest = Object.fromEntries(Object.entries(normalized).filter(([k]) => k !== "gateway"))
const has = Object.keys(rest).length > 0
const result: Record<string, any> = {}
@ -1299,9 +1317,9 @@ export function providerOptions(model: Provider.Model, options: { [x: string]: a
// providerOptions["openai"], but OpenAIResponsesLanguageModel checks
// "azure" first. Pass both so model options work on either code path.
if (model.api.npm === "@ai-sdk/azure") {
return { openai: options, azure: options }
return { openai: normalized, azure: normalized }
}
return { [key]: options }
return { [key]: normalized }
}
export function maxOutputTokens(model: Provider.Model, outputTokenMax = OUTPUT_TOKEN_MAX): number {

View file

@ -61,7 +61,7 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/Question") {}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2Bridge.Service
@ -156,8 +156,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(Layer.provide(EventV2Bridge.defaultLayer))
export const node = LayerNode.make({ service: Service, layer: layer, deps: [EventV2Bridge.node] })
export * as Question from "."

View file

@ -51,6 +51,7 @@ import { Worktree } from "@/worktree"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { MoveSession } from "@opencode-ai/core/control-plane/move-session"
import { Database } from "@opencode-ai/core/database/database"
import { AppNodeBuilderV1 } from "@/effect/app-node-builder-v1"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
import { EventV2 } from "@opencode-ai/core/event"
@ -63,6 +64,7 @@ import { PtyTicket } from "@opencode-ai/core/pty/ticket"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { SessionV2 } from "@opencode-ai/core/session"
import { SessionExecution } from "@opencode-ai/core/session/execution"
import * as SessionExecutionLocal from "@opencode-ai/core/session/execution/local"
import { lazy } from "@/util/lazy"
import { CorsConfig, isAllowedCorsOrigin, type CorsOptions } from "@opencode-ai/server/cors"
@ -98,7 +100,7 @@ import { sessionHandlers } from "./handlers/session"
import { syncHandlers } from "./handlers/sync"
import { tuiHandlers } from "./handlers/tui"
import { handlers } from "@opencode-ai/server/handlers"
import { locationServiceMapLayer } from "@opencode-ai/core/location-services"
import { buildLocationServiceMap, LocationServiceMap } from "@opencode-ai/core/location-services"
import { layer as locationLayer } from "@opencode-ai/server/location"
import { sessionLocationLayer } from "@opencode-ai/server/middleware/session-location"
import { PtyEnvironment } from "@opencode-ai/server/pty-environment"
@ -131,10 +133,10 @@ const cors = (corsOptions?: CorsOptions) =>
// - ptyConnectApiRoutes: typed WebSocket upgrade route with ticket-aware auth.
// - instanceApiRoutes: remaining typed instance routes.
// - uiRoute: raw catch-all fallback; auth is router middleware so public static assets can bypass it.
const authOnlyRouterLayer = authorizationRouterMiddleware.layer.pipe(Layer.provide(ServerAuth.Config.defaultLayer))
const httpApiAuthLayer = authorizationLayer.pipe(Layer.provide(ServerAuth.Config.defaultLayer))
const ptyConnectHttpApiAuthLayer = ptyConnectAuthorizationLayer.pipe(Layer.provide(ServerAuth.Config.defaultLayer))
const serverHttpApiAuthLayer = serverAuthorizationLayer.pipe(Layer.provide(ServerAuth.Config.defaultLayer))
const authOnlyRouterLayer = authorizationRouterMiddleware.layer.pipe(Layer.provide(ServerAuth.Config.layer))
const httpApiAuthLayer = authorizationLayer.pipe(Layer.provide(ServerAuth.Config.layer))
const ptyConnectHttpApiAuthLayer = ptyConnectAuthorizationLayer.pipe(Layer.provide(ServerAuth.Config.layer))
const serverHttpApiAuthLayer = serverAuthorizationLayer.pipe(Layer.provide(ServerAuth.Config.layer))
const workspaceRoutingLive = workspaceRoutingLayer.pipe(Layer.provide(Socket.layerWebSocketConstructorGlobal))
const rootApiRoutes = HttpApiBuilder.layer(RootHttpApi).pipe(
Layer.provide([controlHandlers, controlPlaneHandlers, globalHandlers]),
@ -269,6 +271,8 @@ const app = LayerNode.group([
export function createRoutes(
corsOptions?: CorsOptions,
): Layer.Layer<never, EffectConfig.ConfigError, RouteRequirements> {
const locationServiceMapV2 = buildLocationServiceMap()
return Layer.mergeAll(
rootApiRoutes,
eventApiRoutes,
@ -284,7 +288,7 @@ export function createRoutes(
corsVaryFix,
fenceLayer,
cors(corsOptions),
MoveSession.defaultLayer,
AppNodeBuilderV1.build(MoveSession.node, [[LocationServiceMap.node, locationServiceMapV2]]),
HttpServer.layerServices,
]),
Layer.provide(Layer.succeed(CorsConfig)(corsOptions)),
@ -294,14 +298,14 @@ export function createRoutes(
Layer.provide(locationLayer),
Layer.provide(PtyEnvironment.layer),
Layer.provide(
SessionV2.defaultLayer.pipe(
Layer.provide(SessionExecutionLocal.defaultLayer),
Layer.provide(locationServiceMapLayer),
),
AppNodeBuilderV1.build(SessionV2.node, [
[LocationServiceMap.node, locationServiceMapV2],
[SessionExecution.node, SessionExecutionLocal.node],
]),
),
Layer.provide(locationServiceMapLayer),
Layer.provide(locationServiceMapV2),
Layer.provide(LayerNode.compile(app)),
Layer.provide(AppNodeBuilderV1.build(app)),
)
}

View file

@ -1,4 +1,5 @@
import { Context, Effect, Layer, Option } from "effect"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import * as Socket from "effect/unstable/socket/Socket"
export const SERVER_CLOSING_EVENT = () => new Socket.CloseEvent(1001, "server closing")
@ -13,7 +14,7 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/HttpApiWebSocketTracker") {}
export const layer = Layer.sync(Service)(() => {
const layer = Layer.sync(Service)(() => {
const sockets = new Set<Close>()
let closing = false
return Service.of({
@ -44,6 +45,8 @@ export const layer = Layer.sync(Service)(() => {
})
})
export const node = LayerNode.make({ service: Service, layer, deps: [] })
export const register = (close: Close) =>
Effect.gen(function* () {
const tracker = yield* Effect.serviceOption(Service)

View file

@ -1,6 +1,7 @@
import "./init-projectors"
import { NodeHttpServer } from "@effect/platform-node"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { ConfigProvider, Context, Effect, Exit, Layer, Scope } from "effect"
import { HttpRouter, HttpServer } from "effect/unstable/http"
import { OpenApi } from "effect/unstable/httpapi"
@ -102,7 +103,7 @@ function listenerLayer(opts: ListenOptions, port: number) {
disableLogger: true,
disableListenLog: true,
}).pipe(
Layer.provideMerge(WebSocketTracker.layer),
Layer.provideMerge(AppNodeBuilder.build(WebSocketTracker.node)),
Layer.provideMerge(serverLayer({ port, hostname: opts.hostname })),
// Install a fresh `ConfigProvider` per listener so `Config.string(...)`
// reads reflect the current `process.env`. Effect's default

View file

@ -153,7 +153,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Se
export const use = serviceUse(Service)
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const config = yield* Config.Service
@ -544,19 +544,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = Layer.suspend(() =>
layer.pipe(
Layer.provide(Provider.defaultLayer),
Layer.provide(Session.defaultLayer),
Layer.provide(SessionProcessor.defaultLayer),
Layer.provide(Agent.defaultLayer),
Layer.provide(Plugin.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(RuntimeFlags.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer),
),
)
export const node = LayerNode.make({
service: Service,
layer: layer,

View file

@ -45,7 +45,7 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/Instruction") {}
export const layer: Layer.Layer<
const layer: Layer.Layer<
Service,
never,
FSUtil.Service | Config.Service | Global.Service | HttpClient.HttpClient | RuntimeFlags.Service
@ -224,14 +224,6 @@ export const layer: Layer.Layer<
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(Config.defaultLayer),
Layer.provide(Global.layer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(FetchHttpClient.layer),
Layer.provide(RuntimeFlags.defaultLayer),
)
export function loaded(messages: SessionV1.WithParts[]) {
return extract(messages)
}

View file

@ -8,7 +8,7 @@ import { Context, Effect, Layer } from "effect"
import * as Stream from "effect/Stream"
import { streamText, wrapLanguageModel, type ModelMessage, type Tool } from "ai"
import type { LLMEvent } from "@opencode-ai/llm"
import { LLMClient, RequestExecutor, WebSocketExecutor } from "@opencode-ai/llm/route"
import { LLMClient } from "@opencode-ai/llm/route"
import type { LLMClientService } from "@opencode-ai/llm/route"
import { GitLabWorkflowLanguageModel } from "gitlab-ai-provider"
import { ProviderTransform } from "@/provider/transform"
@ -384,26 +384,11 @@ const live: Layer.Layer<
}),
)
export const layer = live.pipe(Layer.provide(Permission.defaultLayer), Layer.provide(EventV2Bridge.defaultLayer))
export const defaultLayer = Layer.suspend(() =>
layer.pipe(
Layer.provide(Auth.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(Provider.defaultLayer),
Layer.provide(Plugin.defaultLayer),
Layer.provide(
LLMClient.layer.pipe(Layer.provide(Layer.mergeAll(RequestExecutor.defaultLayer, WebSocketExecutor.layer))),
),
Layer.provide(RuntimeFlags.defaultLayer),
),
)
export const hasToolCalls = LLMRequestPrep.hasToolCalls
export const node = LayerNode.make({
service: Service,
layer: layer,
layer: live,
deps: [
Auth.node,
Config.node,

View file

@ -78,7 +78,7 @@ type StreamEvent = LLMEvent
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionProcessor") {}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const session = yield* Session.Service
@ -694,23 +694,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = Layer.suspend(() =>
layer.pipe(
Layer.provide(Session.defaultLayer),
Layer.provide(Snapshot.defaultLayer),
Layer.provide(Agent.defaultLayer),
Layer.provide(LLM.defaultLayer),
Layer.provide(Permission.defaultLayer),
Layer.provide(Plugin.defaultLayer),
Layer.provide(SessionSummary.defaultLayer),
Layer.provide(SessionStatus.defaultLayer),
Layer.provide(Image.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(Database.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer),
),
)
export const node = LayerNode.make({
service: Service,
layer: layer,

View file

@ -110,7 +110,7 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionPrompt") {}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const status = yield* SessionStatus.Service
@ -1490,40 +1490,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = Layer.suspend(() =>
layer.pipe(
Layer.provide(SessionRunState.defaultLayer),
Layer.provide(SessionStatus.defaultLayer),
Layer.provide(SessionCompaction.defaultLayer),
Layer.provide(SessionProcessor.defaultLayer),
Layer.provide(Command.defaultLayer),
Layer.provide(Permission.defaultLayer),
Layer.provide(MCP.defaultLayer),
Layer.provide(LSP.defaultLayer),
Layer.provide(ToolRegistry.defaultLayer),
Layer.provide(Truncate.defaultLayer),
Layer.provide(Provider.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(Instruction.defaultLayer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Plugin.defaultLayer),
Layer.provide(Session.defaultLayer),
Layer.provide(SessionRevert.defaultLayer),
Layer.provide(SessionSummary.defaultLayer),
Layer.provide(Image.defaultLayer),
Layer.provide(
Layer.mergeAll(
Agent.defaultLayer,
Database.defaultLayer,
SystemPrompt.defaultLayer,
LLM.defaultLayer,
CrossSpawnSpawner.defaultLayer,
RuntimeFlags.defaultLayer,
EventV2Bridge.defaultLayer,
),
),
),
)
const ModelRef = Schema.Struct({
providerID: ProviderV2.ID,
modelID: ModelV2.ID,

View file

@ -25,7 +25,7 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionRevert") {}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const sessions = yield* Session.Service
@ -137,17 +137,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = Layer.suspend(() =>
layer.pipe(
Layer.provide(SessionRunState.defaultLayer),
Layer.provide(Session.defaultLayer),
Layer.provide(Snapshot.defaultLayer),
Layer.provide(Storage.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(SessionSummary.defaultLayer),
),
)
export const node = LayerNode.make({
service: Service,
layer: layer,

View file

@ -26,7 +26,7 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionRunState") {}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const jobs = yield* Job.Service
@ -108,8 +108,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(Layer.provide(Job.defaultLayer), Layer.provide(SessionStatus.defaultLayer))
const cancelJobs = Effect.fn("SessionRunState.cancelJobs")(function* (jobs: Job.Interface, sessionID: SessionID) {
const running = yield* jobs.list()
const pending = new Set<string>([sessionID])

View file

@ -9,11 +9,8 @@ import { Decimal } from "decimal.js"
import type { ProviderMetadata, Usage } from "@opencode-ai/llm"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { Database } from "@opencode-ai/core/database/database"
import { makeRuntime } from "@opencode-ai/core/effect/runtime"
import { EventV2Bridge } from "@/event-v2-bridge"
import { SessionV2 } from "@opencode-ai/core/session"
import * as SessionExecutionLocal from "@opencode-ai/core/session/execution/local"
import { locationServiceMapLayer } from "@opencode-ai/core/location-services"
import { NotFoundError } from "@/storage/storage"
import { eq } from "drizzle-orm"
@ -46,8 +43,6 @@ import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { SessionMessage } from "@opencode-ai/schema/session-message"
const runtime = makeRuntime(Database.Service, Database.defaultLayer)
const parentTitlePrefix = "New session - "
const childTitlePrefix = "Child session - "
@ -488,7 +483,7 @@ export type Patch = Omit<Partial<Info>, "time" | "share" | "summary" | "revert"
permission?: Info["permission"] | null
}
export const layer: Layer.Layer<
const layer: Layer.Layer<
Service,
never,
Job.Service | RuntimeFlags.Service | Database.Service | EventV2Bridge.Service
@ -940,19 +935,6 @@ export const layer: Layer.Layer<
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(Job.defaultLayer),
Layer.provide(Database.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(
SessionV2.defaultLayer.pipe(
Layer.provide(SessionExecutionLocal.defaultLayer),
Layer.provide(locationServiceMapLayer),
),
),
Layer.provide(RuntimeFlags.defaultLayer),
)
const cancelJobs = Effect.fn("Session.cancelJobs")(function* (jobs: Job.Interface, sessionID: SessionID) {
const running = yield* jobs.list()
yield* Effect.forEach(
@ -1022,76 +1004,6 @@ function listByProject(
)
}
export function* listGlobal(input?: {
directory?: string
roots?: boolean
start?: number
cursor?: number
search?: string
limit?: number
archived?: boolean
}) {
const conditions: SQL[] = []
if (input?.directory) {
conditions.push(eq(SessionTable.directory, input.directory))
}
if (input?.roots) {
conditions.push(isNull(SessionTable.parent_id))
}
if (input?.start) {
conditions.push(gte(SessionTable.time_updated, input.start))
}
if (input?.cursor) {
conditions.push(lt(SessionTable.time_updated, input.cursor))
}
if (input?.search) {
conditions.push(like(SessionTable.title, `%${input.search}%`))
}
if (!input?.archived) {
conditions.push(isNull(SessionTable.time_archived))
}
const limit = input?.limit ?? 100
const rows = runtime.runSync(({ db }) => {
const query =
conditions.length > 0
? db
.select()
.from(SessionTable)
.where(and(...conditions))
: db.select().from(SessionTable)
return query.orderBy(desc(SessionTable.time_updated), desc(SessionTable.id)).limit(limit).all().pipe(Effect.orDie)
})
const ids = [...new Set(rows.map((row) => row.project_id))]
const projects = new Map<string, ProjectInfo>()
if (ids.length > 0) {
const items = runtime.runSync(({ db }) =>
db
.select({ id: ProjectTable.id, name: ProjectTable.name, worktree: ProjectTable.worktree })
.from(ProjectTable)
.where(inArray(ProjectTable.id, ids))
.all()
.pipe(Effect.orDie),
)
for (const item of items) {
projects.set(item.id, {
id: item.id,
name: item.name ?? undefined,
worktree: item.worktree,
})
}
}
for (const row of rows) {
const project = projects.get(row.project_id) ?? null
yield { ...fromRow(row), project }
}
}
export const node = LayerNode.make({
service: Service,
layer: layer,

View file

@ -18,7 +18,7 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionStatus") {}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2Bridge.Service
@ -51,8 +51,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(Layer.provide(EventV2Bridge.defaultLayer))
export const node = LayerNode.make({ service: Service, layer: layer, deps: [EventV2Bridge.node] })
export * as SessionStatus from "./status"

View file

@ -71,7 +71,7 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionSummary") {}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const sessions = yield* Session.Service
@ -145,15 +145,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = Layer.suspend(() =>
layer.pipe(
Layer.provide(Session.defaultLayer),
Layer.provide(Snapshot.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(Config.defaultLayer),
),
)
export const DiffInput = Schema.Struct({
sessionID: SessionID,
messageID: Schema.optional(MessageID),

View file

@ -47,7 +47,7 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/SystemPrompt") {}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const skill = yield* Skill.Service
@ -128,12 +128,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(Skill.defaultLayer),
Layer.provide(MCP.defaultLayer),
Layer.provide(locationServiceMapLayer),
)
const locationServiceMapNode = LayerNode.make({
service: LocationServiceMap.Service,
layer: locationServiceMapLayer,

View file

@ -20,7 +20,7 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionTodo") {}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2Bridge.Service
@ -69,8 +69,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(Layer.provide(EventV2Bridge.defaultLayer), Layer.provide(Database.defaultLayer))
export const node = LayerNode.make({ service: Service, layer: layer, deps: [EventV2Bridge.node, Database.node] })
export * as Todo from "./todo"

View file

@ -14,7 +14,7 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionShare") {}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const cfg = yield* Config.Service
@ -49,13 +49,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(ShareNext.defaultLayer),
Layer.provide(Session.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(RuntimeFlags.defaultLayer),
)
export const node = LayerNode.make({
service: Service,
layer: layer,

View file

@ -109,7 +109,7 @@ function key(item: Data) {
}
}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const account = yield* Account.Service
@ -362,16 +362,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(Account.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(Database.defaultLayer),
Layer.provide(FetchHttpClient.layer),
Layer.provide(Provider.defaultLayer),
Layer.provide(Session.defaultLayer),
)
export const node = LayerNode.make({
service: Service,
layer: layer,

View file

@ -26,7 +26,7 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/SkillDiscovery") {}
export const layer: Layer.Layer<Service, never, FSUtil.Service | Path.Path | HttpClient.HttpClient> = Layer.effect(
const layer: Layer.Layer<Service, never, FSUtil.Service | Path.Path | HttpClient.HttpClient> = Layer.effect(
Service,
Effect.gen(function* () {
const fs = yield* FSUtil.Service
@ -135,12 +135,6 @@ export const layer: Layer.Layer<Service, never, FSUtil.Service | Path.Path | Htt
}),
)
export const defaultLayer: Layer.Layer<Service> = layer.pipe(
Layer.provide(FetchHttpClient.layer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(NodePath.layer),
)
export const node = LayerNode.make({ service: Service, layer: layer, deps: [FSUtil.node, path, httpClient] })
export * as Discovery from "./discovery"

View file

@ -247,7 +247,7 @@ const loadSkills = Effect.fnUntraced(function* (
export class Service extends Context.Service<Service, Interface>()("@opencode/Skill") {}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const discovery = yield* Discovery.Service
@ -318,15 +318,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(Discovery.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Global.layer),
Layer.provide(RuntimeFlags.defaultLayer),
)
export function fmt(list: Info[], opts: { verbose: boolean }) {
const described = list.filter((skill) => skill.description !== undefined)
if (described.length === 0) return "No skills are currently available."

View file

@ -46,7 +46,7 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/Snapshot") {}
export const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | Config.Service> = Layer.effect(
const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | Config.Service> = Layer.effect(
Service,
Effect.gen(function* () {
const fs = yield* FSUtil.Service
@ -798,12 +798,6 @@ export const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Serv
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(AppProcess.defaultLayer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Config.defaultLayer),
)
export const node = LayerNode.make({
service: Service,
layer: layer,

View file

@ -210,7 +210,7 @@ const MIGRATIONS: Migration[] = [
}),
]
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const fs = yield* FSUtil.Service
@ -322,8 +322,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(Layer.provide(FSUtil.defaultLayer), Layer.provide(Git.defaultLayer))
export const node = LayerNode.make({ service: Service, layer: layer, deps: [FSUtil.node, Git.node] })
export * as Storage from "./storage"

View file

@ -33,7 +33,6 @@ import { Glob } from "@opencode-ai/core/util/glob"
import path from "path"
import { pathToFileURL } from "url"
import { Effect, Layer, Context } from "effect"
import { FetchHttpClient, HttpClient } from "effect/unstable/http"
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Format } from "../format"
@ -80,7 +79,7 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/ToolRegistry") {}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const config = yield* Config.Service
@ -315,30 +314,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = Layer.suspend(() =>
layer
.pipe(
Layer.provide(Config.defaultLayer),
Layer.provide(Plugin.defaultLayer),
Layer.provide(Question.defaultLayer),
Layer.provide(Todo.defaultLayer),
Layer.provide(Skill.defaultLayer),
Layer.provide(Agent.defaultLayer),
Layer.provide(Session.defaultLayer),
Layer.provide(Job.defaultLayer),
Layer.provide(Provider.defaultLayer),
Layer.provide(LSP.defaultLayer),
Layer.provide(Instruction.defaultLayer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(FetchHttpClient.layer),
Layer.provide(Format.defaultLayer),
Layer.provide(CrossSpawnSpawner.defaultLayer),
Layer.provide(Truncate.defaultLayer),
)
.pipe(Layer.provide(Database.defaultLayer), Layer.provide(RuntimeFlags.defaultLayer)),
)
function isZodType(value: unknown): value is z.ZodType {
return typeof value === "object" && value !== null && "_zod" in value
}
@ -417,7 +392,7 @@ function isJsonSchemaObject(value: unknown): value is Record<string, unknown> {
export const node = LayerNode.make({
service: Service,
layer: layer.pipe(Layer.provide(Ripgrep.defaultLayer)),
layer,
deps: [
Config.node,
Plugin.node,
@ -438,6 +413,7 @@ export const node = LayerNode.make({
Truncate.node,
RuntimeFlags.node,
Database.node,
Ripgrep.node,
],
})

View file

@ -46,7 +46,7 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/Truncate") {}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const fs = yield* FSUtil.Service
@ -151,8 +151,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(Layer.provide(FSUtil.defaultLayer), Layer.provide(NodePath.layer))
export const node = LayerNode.make({ service: Service, layer: layer, deps: [FSUtil.node] })
export * as Truncate from "./truncate"

View file

@ -1,7 +1,6 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { path } from "@opencode-ai/core/effect/app-node-platform"
import { Global } from "@opencode-ai/core/global"
import { InstanceLayer } from "@/project/instance-layer"
import { InstanceStore } from "@/project/instance-store"
import { Project } from "@/project/project"
import { Database } from "@opencode-ai/core/database/database"
@ -14,7 +13,6 @@ import { GlobalBus } from "@/bus/global"
import { Git } from "@/git"
import { Effect, Layer, Path, Schema, Scope, Context } from "effect"
import { ChildProcess } from "effect/unstable/process"
import { NodePath } from "@effect/platform-node"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { AppProcess } from "@opencode-ai/core/process"
import { InstanceState } from "@/effect/instance-state"
@ -131,7 +129,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Wo
type GitResult = { code: number; text: string; stderr: string }
export const layer: Layer.Layer<
const layer: Layer.Layer<
Service,
never,
| FSUtil.Service
@ -616,17 +614,6 @@ export const layer: Layer.Layer<
}),
)
export const appLayer = layer.pipe(
Layer.provide(Git.defaultLayer),
Layer.provide(AppProcess.defaultLayer),
Layer.provide(Project.defaultLayer),
Layer.provide(Database.defaultLayer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(NodePath.layer),
)
export const defaultLayer = appLayer.pipe(Layer.provide(InstanceLayer.layer))
export const node = LayerNode.make({
service: Service,
layer: layer,

View file

@ -1,4 +1,5 @@
import { expect } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect, Layer, Option } from "effect"
import { sql } from "drizzle-orm"
@ -13,9 +14,10 @@ const truncate = Layer.effectDiscard(
yield* db.run(sql`DELETE FROM account_state`)
yield* db.run(sql`DELETE FROM account`)
}),
).pipe(Layer.provide(Database.defaultLayer))
)
const truncateNode = LayerNode.make({ name: "truncate-account", layer: truncate, deps: [Database.node] })
const it = testEffect(Layer.merge(AccountRepo.defaultLayer, truncate))
const it = testEffect(LayerNode.compile(LayerNode.group([AccountRepo.node, truncateNode])))
it.live("list returns empty when no accounts exist", () =>
Effect.gen(function* () {

View file

@ -1,4 +1,6 @@
import { expect } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
import { Duration, Effect, Layer, Option, Schema } from "effect"
import { sql } from "drizzle-orm"
import { HttpClient, HttpClientError, HttpClientResponse } from "effect/unstable/http"
@ -25,15 +27,16 @@ const truncate = Layer.effectDiscard(
yield* db.run(sql`DELETE FROM account_state`)
yield* db.run(sql`DELETE FROM account`)
}),
).pipe(Layer.provide(Database.defaultLayer))
)
const truncateNode = LayerNode.make({ name: "truncate-account", layer: truncate, deps: [Database.node] })
const it = testEffect(Layer.merge(AccountRepo.defaultLayer, truncate))
const it = testEffect(LayerNode.compile(LayerNode.group([AccountRepo.node, truncateNode])))
const insideEagerRefreshWindow = Duration.toMillis(Duration.minutes(1))
const outsideEagerRefreshWindow = Duration.toMillis(Duration.minutes(10))
const live = (client: HttpClient.HttpClient) =>
Account.layer.pipe(Layer.provide(Layer.succeed(HttpClient.HttpClient, client)))
LayerNode.compile(Account.node, [[httpClient, Layer.succeed(HttpClient.HttpClient, client)]])
const json = (req: Parameters<typeof HttpClientResponse.fromWeb>[0], body: unknown, status = 200) =>
HttpClientResponse.fromWeb(

View file

@ -1,6 +1,7 @@
import { describe, expect } from "bun:test"
import { Directory } from "@/acp/directory"
import { Command } from "@/command"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { Provider } from "@/provider/provider"
@ -83,8 +84,9 @@ const snapshot = (directory: string) => {
}
const fakeLayer = (calls: string[]) =>
Directory.layer.pipe(
Layer.provide(
LayerNode.compile(Directory.node, [
[
Directory.loaderNode,
Layer.succeed(
Directory.Loader,
Directory.Loader.of({
@ -95,8 +97,8 @@ const fakeLayer = (calls: string[]) =>
}),
}),
),
),
)
],
])
describe("ACP directory snapshot", () => {
it.effect("two concurrent callers share one load", () => {

View file

@ -1,5 +1,6 @@
import { describe, expect, it } from "bun:test"
import type { AgentSideConnection } from "@agentclientprotocol/sdk"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import type { Event, Message, OpencodeClient, Part, SessionMessageResponse, ToolPart } from "@opencode-ai/sdk/v2"
import { Effect, ManagedRuntime } from "effect"
import { ACPEvent } from "@/acp/event"
@ -30,7 +31,7 @@ const pollUntil = async (
}
function makeSessionService() {
return ManagedRuntime.make(ACPSession.defaultLayer).runSync(
return ManagedRuntime.make(LayerNode.compile(ACPSession.node)).runSync(
ACPSession.Service.use((service) => Effect.succeed(service)),
)
}

View file

@ -6,6 +6,7 @@ import type {
SessionUpdate,
} from "@agentclientprotocol/sdk"
import type { Event, OpencodeClient } from "@opencode-ai/sdk/v2"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { createTwoFilesPatch } from "diff"
import { Effect, ManagedRuntime } from "effect"
import { mkdtemp, rm } from "node:fs/promises"
@ -37,7 +38,7 @@ const pollUntil = async (
}
function makeSessionService() {
return ManagedRuntime.make(ACPSession.defaultLayer).runSync(
return ManagedRuntime.make(LayerNode.compile(ACPSession.node)).runSync(
ACPSession.Service.use((service) => Effect.succeed(service)),
)
}

View file

@ -1,5 +1,6 @@
import { describe, expect } from "bun:test"
import type { McpServer } from "@agentclientprotocol/sdk"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect } from "effect"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
@ -7,7 +8,7 @@ import * as ACPError from "@/acp/error"
import * as ACPSession from "@/acp/session"
import { testEffect } from "../lib/effect"
const sessionTest = testEffect(ACPSession.defaultLayer)
const sessionTest = testEffect(LayerNode.compile(ACPSession.node))
const model = (providerID: string, modelID: string): ACPSession.SelectedModel => ({
providerID: ProviderV2.ID.make(providerID),

View file

@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test"
import type { SessionNotification } from "@agentclientprotocol/sdk"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { UsageService } from "@/acp/usage"
@ -97,24 +98,26 @@ const fakeLayer = (input: {
readonly messages?: Effect.Effect<readonly UsageService.SessionMessage[], unknown>
readonly providers?: (directory: string) => Effect.Effect<Record<ProviderV2.ID, Provider.Info>, unknown>
}) =>
UsageService.layer.pipe(
Layer.provide(
Layer.mergeAll(
Layer.succeed(
UsageService.MessageLoader,
UsageService.MessageLoader.of({
messages: () => input.messages ?? Effect.succeed([]),
}),
),
Layer.succeed(
UsageService.ContextLimitLoader,
UsageService.ContextLimitLoader.of({
providers: input.providers ?? (() => Effect.succeed(providers())),
}),
),
LayerNode.compile(UsageService.node, [
[
UsageService.messageLoaderNode,
Layer.succeed(
UsageService.MessageLoader,
UsageService.MessageLoader.of({
messages: () => input.messages ?? Effect.succeed([]),
}),
),
),
)
],
[
UsageService.contextLimitLoaderNode,
Layer.succeed(
UsageService.ContextLimitLoader,
UsageService.ContextLimitLoader.of({
providers: input.providers ?? (() => Effect.succeed(providers())),
}),
),
],
])
const connection = (updates: SessionNotification[]) => ({
sessionUpdate(params: SessionNotification) {

View file

@ -1,4 +1,5 @@
import { afterEach, expect } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Cause, Effect, Exit, Layer } from "effect"
import path from "path"
import { disposeAllInstances, TestInstance } from "../fixture/fixture"
@ -14,17 +15,11 @@ import { Plugin } from "../../src/plugin"
import { Provider } from "../../src/provider/provider"
import { Skill } from "../../src/skill"
import { Truncate } from "../../src/tool/truncate"
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
const agentLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
Agent.layer.pipe(
Layer.provide(Plugin.defaultLayer),
Layer.provide(Provider.defaultLayer),
Layer.provide(Auth.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(Skill.defaultLayer),
Layer.provide(locationServiceMapLayer),
Layer.provide(RuntimeFlags.layer(flags)),
LayerNode.compile(
LayerNode.group([Agent.node, Plugin.node, Provider.node, Auth.node, Config.node, Skill.node, RuntimeFlags.node]),
[[RuntimeFlags.node, RuntimeFlags.layer(flags)]],
)
const it = testEffect(agentLayer())

View file

@ -1,4 +1,5 @@
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { expect } from "bun:test"
import { Effect } from "effect"
import { Agent } from "../../src/agent/agent"
@ -6,7 +7,7 @@ import { deriveSubagentSessionPermission } from "../../src/agent/subagent-permis
import { Permission } from "../../src/permission"
import { testEffect } from "../lib/effect"
const it = testEffect(Agent.defaultLayer)
const it = testEffect(LayerNode.compile(Agent.node))
function testAgent(input: {
name: string

View file

@ -1,16 +1,15 @@
import { expect } from "bun:test"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { LocationServiceMap, locationServiceMapLayer } from "@opencode-ai/core/location-services"
import { Effect, Layer } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import { Npm } from "@opencode-ai/core/npm"
import { Effect } from "effect"
import path from "path"
import { pathToFileURL } from "url"
import { Agent } from "../../src/agent/agent"
import { EventV2Bridge } from "../../src/event-v2-bridge"
import { Config } from "../../src/config/config"
import { Env } from "../../src/env"
import { Account } from "../../src/account/account"
import { Auth } from "../../src/auth"
import { RuntimeFlags } from "../../src/effect/runtime-flags"
import { Plugin } from "../../src/plugin"
import { Provider } from "../../src/provider/provider"
import { Skill } from "../../src/skill"
import { AccountTest } from "../fake/account"
import { AuthTest } from "../fake/auth"
import { NpmTest } from "../fake/npm"
@ -18,6 +17,8 @@ import { ProviderTest } from "../fake/provider"
import { SkillTest } from "../fake/skill"
import { testEffect } from "../lib/effect"
import { PLUGIN_AGENT } from "../fixture/agent-plugin.constants"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
// `it.instance` skips InstanceBootstrap so LSP / MCP don't spin up — those
// services hang during scope teardown on Windows and aren't needed
@ -25,30 +26,16 @@ import { PLUGIN_AGENT } from "../fixture/agent-plugin.constants"
const pluginUrl = pathToFileURL(path.join(import.meta.dir, "..", "fixture", "agent-plugin.ts")).href
const provider = ProviderTest.fake()
const configLayer = Config.layer.pipe(
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Env.defaultLayer),
Layer.provide(AuthTest.empty),
Layer.provide(AccountTest.empty),
Layer.provide(NpmTest.noop),
Layer.provide(FetchHttpClient.layer),
const it = testEffect(
AppNodeBuilder.build(LayerNode.group([Agent.node, Plugin.node]), [
[Auth.node, AuthTest.empty],
[Account.node, AccountTest.empty],
[Npm.node, NpmTest.noop],
[Provider.node, provider.layer],
[Skill.node, SkillTest.empty],
[RuntimeFlags.node, RuntimeFlags.layer({ disableDefaultPlugins: true })],
]),
)
const pluginLayer = Plugin.layer.pipe(
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(configLayer),
Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })),
)
const agentLayer = Agent.layer.pipe(
Layer.provide(configLayer),
Layer.provide(AuthTest.empty),
Layer.provide(SkillTest.empty),
Layer.provide(provider.layer),
Layer.provide(pluginLayer),
Layer.provide(locationServiceMapLayer),
Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })),
)
const it = testEffect(Layer.mergeAll(agentLayer, pluginLayer))
it.instance(
"plugin-registered agents appear in Agent.list",

View file

@ -1,12 +1,10 @@
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect } from "effect"
import { Auth } from "../../src/auth"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { testEffect } from "../lib/effect"
const node = CrossSpawnSpawner.defaultLayer
const it = testEffect(Layer.mergeAll(Auth.defaultLayer, node))
const it = testEffect(LayerNode.compile(Auth.node))
describe("Auth", () => {
it.instance("set normalizes trailing slashes in keys", () =>

View file

@ -1,4 +1,5 @@
import { afterEach, expect } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Effect } from "effect"
import { fileURLToPath } from "url"
@ -6,7 +7,7 @@ import { InstanceRef } from "../../src/effect/instance-ref"
import { disposeAllInstances, TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const it = testEffect(FSUtil.defaultLayer)
const it = testEffect(LayerNode.compile(FSUtil.node))
afterEach(async () => {
await disposeAllInstances()

View file

@ -76,39 +76,35 @@ Positionals:
message message to send [array] [default: []]
Options:
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]
--command the command to run, use message for args [string]
-c, --continue continue the last session [boolean]
-s, --session session id to continue [string]
--fork fork the session before continuing (requires --continue or
--session) [boolean]
--share share the session [boolean]
-m, --model model to use in the format of provider/model [string]
--agent agent to use [string]
--format format: default (formatted) or json (raw JSON events)
-h, --help show help [boolean]
-v, --version show version number [boolean]
--print-logs print logs to stderr [boolean]
--log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"]
--pure run without external plugins [boolean]
--command the command to run, use message for args [string]
-c, --continue continue the last session [boolean]
-s, --session session id to continue [string]
--fork fork the session before continuing (requires --continue or --session) [boolean]
--share share the session [boolean]
-m, --model model to use in the format of provider/model [string]
--agent agent to use [string]
--format format: default (formatted) or json (raw JSON events)
[string] [choices: "default", "json"] [default: "default"]
-f, --file file(s) to attach to message [array]
--title title for the session (uses truncated prompt if no value
provided) [string]
--attach attach to a running opencode server (e.g.,
http://localhost:4096) [string]
-p, --password basic auth password (defaults to OPENCODE_SERVER_PASSWORD)
-f, --file file(s) to attach to message [array]
--title title for the session (uses truncated prompt if no value provided) [string]
--attach attach to a running opencode server (e.g., http://localhost:4096) [string]
-p, --password basic auth password (defaults to OPENCODE_SERVER_PASSWORD) [string]
-u, --username basic auth username (defaults to OPENCODE_SERVER_USERNAME or 'opencode')
[string]
-u, --username basic auth username (defaults to OPENCODE_SERVER_USERNAME or
'opencode') [string]
--dir directory to run in, path on remote server if attaching
--dir directory to run in, path on remote server if attaching [string]
--port port for the local server (defaults to random port if no value provided)
[number]
--variant model variant (provider-specific reasoning effort, e.g., high, max, minimal)
[string]
--port port for the local server (defaults to random port if no value
provided) [number]
--variant model variant (provider-specific reasoning effort, e.g., high,
max, minimal) [string]
--thinking show thinking blocks [boolean]
--dangerously-skip-permissions auto-approve permissions that are not explicitly denied
(dangerous!) [boolean] [default: false]"
--thinking show thinking blocks [boolean]
-i, --interactive run in direct interactive split-footer mode [boolean] [default: false]
--auto auto-approve permissions that are not explicitly denied (dangerous!)
[boolean] [default: false]"
`;
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode debug --help 1`] = `

View file

@ -1,5 +1,6 @@
import path from "path"
import { NodeFileSystem } from "@effect/platform-node"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { describe, expect, test } from "bun:test"
import { Effect, FileSystem, Layer } from "effect"
@ -98,7 +99,7 @@ function userMessage(
}
}
const it = testEffect(Layer.mergeAll(FSUtil.defaultLayer, NodeFileSystem.layer))
const it = testEffect(Layer.mergeAll(LayerNode.compile(FSUtil.node), NodeFileSystem.layer))
function remap(root: string, file: string) {
if (file === Global.Path.state) {
@ -123,7 +124,7 @@ function remappedFs(root: string) {
writeJson: (file, data, mode) => fs.writeJson(remap(root, file), data, mode),
})
}),
).pipe(Layer.provide(FSUtil.defaultLayer))
).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
}
describe("run variant shared", () => {

View file

@ -1,11 +1,11 @@
import { expect } from "bun:test"
import { Effect, Layer } from "effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect } from "effect"
import { Config } from "@/config/config"
import { Agent as AgentSvc } from "../../src/agent/agent"
import { testEffect } from "../lib/effect"
const it = testEffect(Layer.mergeAll(Config.defaultLayer, AgentSvc.defaultLayer, CrossSpawnSpawner.defaultLayer))
const it = testEffect(LayerNode.compile(LayerNode.group([Config.node, AgentSvc.node])))
it.instance(
"agent color parsed from project config",

View file

@ -1,13 +1,14 @@
import { test, expect, describe, afterEach, beforeEach, spyOn } from "bun:test"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
import { Cause, Effect, Exit, Layer, Option } from "effect"
import { NamedError } from "@opencode-ai/core/util/error"
import { FetchHttpClient, HttpClient, HttpClientResponse } from "effect/unstable/http"
import { NodeFileSystem, NodePath } from "@effect/platform-node"
import { Config } from "@/config/config"
import { ConfigManaged } from "@/config/managed"
import { ConfigParse } from "../../src/config/parse"
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
import { Npm } from "@opencode-ai/core/npm"
import { InstanceRef } from "../../src/effect/instance-ref"
import type { InstanceContext } from "../../src/project/instance-context"
@ -41,13 +42,6 @@ import { AccountTest } from "../fake/account"
import { AuthTest } from "../fake/auth"
import { NpmTest } from "../fake/npm"
/** Infra layer that provides FileSystem, Path, ChildProcessSpawner for test fixtures */
const infra = CrossSpawnSpawner.defaultLayer.pipe(
Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)),
)
const testFlock = EffectFlock.defaultLayer
const unexpectedHttp = HttpClient.make((request) =>
Effect.die(`unexpected http request: ${request.method} ${request.url}`),
)
@ -104,16 +98,12 @@ const configLayer = (
client?: HttpClient.HttpClient
} = {},
) =>
Config.layer.pipe(
Layer.provide(testFlock),
Layer.provide(Env.defaultLayer),
Layer.provide(options.auth ?? AuthTest.empty),
Layer.provide(options.account ?? AccountTest.empty),
Layer.provideMerge(infra),
Layer.provide(NpmTest.noop),
Layer.provide(Layer.succeed(HttpClient.HttpClient, options.client ?? unexpectedHttp)),
Layer.provideMerge(FSUtil.defaultLayer),
)
LayerNode.compile(LayerNode.group([Config.node, FSUtil.node, Env.node, CrossSpawnSpawner.node]), [
[Auth.node, options.auth ?? AuthTest.empty],
[Account.node, options.account ?? AccountTest.empty],
[Npm.node, NpmTest.noop],
[httpClient, Layer.succeed(HttpClient.HttpClient, options.client ?? unexpectedHttp)],
])
const layer = configLayer()
@ -171,7 +161,7 @@ const withInstanceDir = <A, E, R>(dir: string, effect: Effect.Effect<A, E, R>) =
Effect.provideService(TestInstance, { directory: dir }),
provideInstanceEffect(dir),
Effect.provide(testInstanceStoreLayer),
Effect.provide(CrossSpawnSpawner.defaultLayer),
Effect.provide(LayerNode.compile(CrossSpawnSpawner.node)),
)
const withGlobalConfigDir = <A, E, R>(dir: string, effect: Effect.Effect<A, E, R>) =>
@ -325,7 +315,7 @@ it.effect("creates global jsonc config with schema when no global configs exist"
const content = yield* FSUtil.use.readFileString(path.join(dir, "opencode.jsonc"))
expect(content).toContain('"$schema": "https://opencode.ai/config.json"')
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer)),
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))),
),
)
@ -340,7 +330,7 @@ it.effect("does not create global config when OPENCODE_CONFIG_DIR is set", () =>
yield* Config.use.get().pipe(provideInstanceEffect(dir))
expect(yield* FSUtil.use.existsSafe(path.join(dir, "opencode.jsonc"))).toBe(false)
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer)),
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))),
),
)
}),
@ -930,7 +920,7 @@ it.effect("does not try to install dependencies in read-only OPENCODE_CONFIG_DIR
yield* Effect.addFinalizer(() => FSUtil.use.chmod(readonly, 0o755).pipe(Effect.ignore))
yield* withProcessEnv("OPENCODE_CONFIG_DIR", readonly, Config.use.get().pipe(provideInstanceEffect(dir)))
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer)),
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))),
)
it.effect("installs dependencies in writable OPENCODE_CONFIG_DIR", () =>
@ -948,7 +938,7 @@ it.effect("installs dependencies in writable OPENCODE_CONFIG_DIR", () =>
)
expect(yield* FSUtil.use.readFileString(path.join(configDir, ".gitignore"))).toContain("package-lock.json")
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer)),
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))),
)
// Note: deduplication and serialization of npm installs is now handled by the
@ -1533,16 +1523,12 @@ test("remote well-known config can use FetchHttpClient layer", async () => {
Effect.scoped,
Effect.provide(
Layer.mergeAll(
Config.layer.pipe(
Layer.provide(testFlock),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Env.defaultLayer),
Layer.provide(wellKnownAuth(server.url.origin)),
Layer.provide(AccountTest.empty),
Layer.provideMerge(infra),
Layer.provide(NpmTest.noop),
Layer.provide(FetchHttpClient.layer),
),
LayerNode.compile(LayerNode.group([Config.node, FSUtil.node, Env.node, CrossSpawnSpawner.node]), [
[Auth.node, wellKnownAuth(server.url.origin)],
[Account.node, AccountTest.empty],
[Npm.node, NpmTest.noop],
[httpClient, FetchHttpClient.layer],
]),
testInstanceStoreLayer,
),
),

View file

@ -1,6 +1,8 @@
import { expect } from "bun:test"
import path from "path"
import { pathToFileURL } from "url"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect, Layer } from "effect"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Global } from "@opencode-ai/core/global"
@ -11,7 +13,7 @@ import { TuiConfig } from "../../src/config/tui"
import { TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const it = testEffect(Layer.mergeAll(Config.defaultLayer, FSUtil.defaultLayer))
const it = testEffect(LayerNode.compile(LayerNode.group([Config.node, FSUtil.node])))
const winIt = process.platform === "win32" ? it.instance : it.instance.skip
const globalConfigFiles = ["opencode.json", "opencode.jsonc", "tui.json", "tui.jsonc"].map((file) =>
@ -69,12 +71,16 @@ const withPlatform = <A, E, R>(platform: typeof process.platform, self: Effect.E
const getTuiConfig = (directory: string) =>
TuiConfig.Service.use((svc) => svc.get()).pipe(
Effect.provide(TuiConfig.defaultLayer.pipe(Layer.provide(Layer.succeed(CurrentWorkingDirectory, directory)))),
Effect.provide(
AppNodeBuilder.build(TuiConfig.node).pipe(Layer.provide(Layer.succeed(CurrentWorkingDirectory, directory))),
),
)
const getTuiPluginOrigins = (directory: string) =>
TuiConfig.Service.use((svc) => svc.pluginOrigins()).pipe(
Effect.provide(TuiConfig.defaultLayer.pipe(Layer.provide(Layer.succeed(CurrentWorkingDirectory, directory)))),
Effect.provide(
AppNodeBuilder.build(TuiConfig.node).pipe(Layer.provide(Layer.succeed(CurrentWorkingDirectory, directory))),
),
)
it.instance("keeps server and tui plugin merge semantics aligned", () =>

View file

@ -5,9 +5,8 @@ import Http from "node:http"
import path from "node:path"
import { NodeHttpServer } from "@effect/platform-node"
import { Effect, Exit, Fiber, Layer, Schema } from "effect"
import { FetchHttpClient, HttpServer, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
import { HttpServer, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
import { eq } from "drizzle-orm"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { GlobalBus, type GlobalEvent } from "@/bus/global"
import { Database } from "@opencode-ai/core/database/database"
import { ProjectV2 } from "@opencode-ai/core/project"
@ -16,6 +15,7 @@ import { AbsolutePath } from "@opencode-ai/core/schema"
import { Session as SessionNs } from "@/session/session"
import { SessionID } from "@/session/schema"
import { SessionTable } from "@opencode-ai/core/session/sql"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { EventSequenceTable } from "@opencode-ai/core/event/sql"
import { resetDatabase } from "../fixture/db"
import { disposeAllInstances, provideTmpdirInstance, requireInstance, TestInstance } from "../fixture/fixture"
@ -27,13 +27,10 @@ import type { Target, WorkspaceAdapter, WorkspaceInfo } from "../../src/control-
import * as Workspace from "../../src/control-plane/workspace"
import { InstanceStore } from "@/project/instance-store"
import { InstanceBootstrap } from "@/project/bootstrap"
import { Auth } from "@/auth"
import { SessionPrompt } from "@/session/prompt"
import { Project } from "@/project/project"
import { Vcs } from "@/project/vcs"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { EventV2Bridge } from "@/event-v2-bridge"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
const originalEnv = {
OPENCODE_AUTH_CONTENT: process.env.OPENCODE_AUTH_CONTENT,
@ -44,26 +41,27 @@ const originalEnv = {
}
const workspaceLayer = (experimentalWorkspaces: boolean) =>
Workspace.layer.pipe(
Layer.provide(Auth.defaultLayer),
Layer.provide(SessionNs.defaultLayer),
Layer.provide(SessionPrompt.defaultLayer),
Layer.provide(Project.defaultLayer),
Layer.provide(Vcs.defaultLayer),
Layer.provide(Database.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(FetchHttpClient.layer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(RuntimeFlags.layer({ experimentalWorkspaces })),
Layer.provide(Ripgrep.defaultLayer),
Layer.provide(InstanceStore.defaultLayer.pipe(Layer.provide(InstanceBootstrap.defaultLayer))),
AppNodeBuilder.build(
LayerNode.group([
Workspace.node,
SessionNs.node,
SessionProjector.node,
Database.node,
InstanceStore.node,
Ripgrep.node,
]),
[
[RuntimeFlags.node, RuntimeFlags.layer({ experimentalWorkspaces })],
[
InstanceStore.bootstrapNode,
Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void })),
],
],
)
const testServerLayer = Layer.mergeAll(
NodeHttpServer.layer(Http.createServer, { host: "127.0.0.1", port: 0 }),
workspaceLayer(true),
SessionNs.defaultLayer,
Database.defaultLayer,
)
const it = testEffect(testServerLayer)

View file

@ -1,4 +1,5 @@
import { expect } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Context, Deferred, Effect, Fiber, Layer, Logger } from "effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { AppLayer } from "../../src/effect/app-runtime"
@ -9,7 +10,8 @@ import { attach } from "../../src/effect/run-service"
import { TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const it = testEffect(CrossSpawnSpawner.defaultLayer)
const observabilityLayer = LayerNode.compile(Observability.node)
const it = testEffect(LayerNode.compile(CrossSpawnSpawner.node))
function check(loggers: ReadonlySet<Logger.Logger<unknown, any>>) {
return {
@ -34,7 +36,7 @@ it.live("makeRuntime installs the observability logger", () =>
)
const current = yield* Dummy.use((svc) => svc.current()).pipe(
Effect.provide(Layer.provideMerge(layer, Observability.layer)),
Effect.provide(Layer.provideMerge(layer, observabilityLayer)),
)
expect(current.size).toBeGreaterThan(0)
@ -94,6 +96,6 @@ it.instance(
expect(result.directory).toBe(test.directory)
expect(result.size).toBeGreaterThan(0)
}).pipe(Effect.provide(Observability.layer)),
}).pipe(Effect.provide(observabilityLayer)),
{ git: true },
)

View file

@ -10,12 +10,12 @@ class TestConfig extends ConfigService.Service<TestConfig>()("@test/ConfigServic
}) {}
const fromConfig = (input: Record<string, unknown>) =>
TestConfig.defaultLayer.pipe(Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown(input))))
TestConfig.layer.pipe(Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown(input))))
const readConfig = TestConfig.useSync((config) => config)
describe("ConfigService", () => {
it.effect("defaultLayer parses values from the active ConfigProvider", () =>
it.effect("layer parses values from the active ConfigProvider", () =>
Effect.gen(function* () {
const config = yield* readConfig.pipe(
Effect.provide(
@ -33,7 +33,7 @@ describe("ConfigService", () => {
}),
)
it.effect("defaultLayer applies Effect Config defaults", () =>
it.effect("layer applies Effect Config defaults", () =>
Effect.gen(function* () {
const config = yield* readConfig.pipe(Effect.provide(fromConfig({ NAME: "kit" })))
@ -47,7 +47,7 @@ describe("ConfigService", () => {
Effect.gen(function* () {
const config = yield* readConfig.pipe(
Effect.provide(
TestConfig.layer({
TestConfig.configLayer({
name: "direct",
token: Option.some("parsed"),
port: 9000,

View file

@ -1,5 +1,6 @@
import { expect } from "bun:test"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { $ } from "bun"
import { Context, Deferred, Duration, Effect, Exit, Fiber, Layer } from "effect"
import { InstanceState } from "@/effect/instance-state"
@ -12,7 +13,7 @@ import {
} from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const it = testEffect(Layer.mergeAll(CrossSpawnSpawner.defaultLayer, testInstanceStoreLayer))
const it = testEffect(Layer.mergeAll(LayerNode.compile(CrossSpawnSpawner.node), testInstanceStoreLayer))
const access = <A, E>(state: InstanceState.InstanceState<A, E>, dir: string) =>
InstanceState.get(state).pipe(provideInstanceEffect(dir))

View file

@ -1,15 +1,16 @@
import { describe, expect } from "bun:test"
import { ConfigProvider, Effect, Layer } from "effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { RuntimeFlags } from "../../src/effect/runtime-flags"
import { it } from "../lib/effect"
const fromConfig = (input: Record<string, unknown>) =>
RuntimeFlags.defaultLayer.pipe(Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown(input))))
AppNodeBuilder.build(RuntimeFlags.node).pipe(Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown(input))))
const readFlags = RuntimeFlags.Service.useSync((flags) => flags)
describe("RuntimeFlags", () => {
it.effect("defaultLayer defaults autoShare to false", () =>
it.effect("layer defaults autoShare to false", () =>
Effect.gen(function* () {
const flags = yield* readFlags.pipe(Effect.provide(fromConfig({})))
@ -17,7 +18,7 @@ describe("RuntimeFlags", () => {
}),
)
it.effect("defaultLayer parses plugin flags from the active ConfigProvider", () =>
it.effect("layer parses plugin flags from the active ConfigProvider", () =>
Effect.gen(function* () {
const flags = yield* readFlags.pipe(
Effect.provide(
@ -64,7 +65,7 @@ describe("RuntimeFlags", () => {
}),
)
it.effect("defaultLayer parses OPENCODE_EXPERIMENTAL_LSP_TY", () =>
it.effect("layer parses OPENCODE_EXPERIMENTAL_LSP_TY", () =>
Effect.gen(function* () {
const flags = yield* readFlags.pipe(
Effect.provide(

View file

@ -1,12 +1,11 @@
import { describe, test, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { NodeFileSystem } from "@effect/platform-node"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect } from "effect"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { testEffect } from "../lib/effect"
import path from "path"
const live = FSUtil.layer.pipe(Layer.provide(NodeFileSystem.layer))
const { effect: it } = testEffect(live)
const { effect: it } = testEffect(LayerNode.compile(FSUtil.node))
describe("FSUtil", () => {
describe("isDir", () => {

View file

@ -7,8 +7,10 @@ import { Effect, Context, Layer } from "effect"
import type * as PlatformError from "effect/PlatformError"
import type * as Scope from "effect/Scope"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import type { Config } from "@/config/config"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { InstanceRef } from "../../src/effect/instance-ref"
import { InstanceBootstrap } from "../../src/project/bootstrap-service"
import type { InstanceContext } from "../../src/project/instance-context"
@ -17,7 +19,9 @@ import { InstanceStore } from "../../src/project/instance-store"
import { TestLLMServer } from "../lib/llm-server"
const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
export const testInstanceStoreLayer = InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrap))
export const testInstanceStoreLayer = LayerNode.compile(InstanceStore.node, [
[InstanceStore.bootstrapNode, noopBootstrap],
])
export async function provideTestInstance<R>(input: {
directory: string
@ -204,7 +208,7 @@ export const withTmpdirInstance =
Effect.gen(function* () {
const directory = yield* tmpdirScoped(options)
return yield* self.pipe(Effect.provideService(TestInstance, { directory }), provideInstanceEffect(directory))
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(CrossSpawnSpawner.defaultLayer))
}).pipe(Effect.provide(testInstanceStoreLayer), Effect.provide(AppNodeBuilder.build(CrossSpawnSpawner.node)))
export function provideTmpdirServer<A, E, R>(
self: (input: { dir: string; llm: TestLLMServer["Service"] }) => Effect.Effect<A, E, R>,

View file

@ -1,5 +1,5 @@
import { FetchHttpClient } from "effect/unstable/http"
import { Layer } from "effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Database } from "@opencode-ai/core/database/database"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Auth } from "../../src/auth"
@ -14,17 +14,21 @@ import { SessionPrompt } from "../../src/session/prompt"
import { EventV2Bridge } from "../../src/event-v2-bridge"
export const workspaceLayerWithRuntimeFlags = (overrides: Partial<RuntimeFlags.Info>) =>
Workspace.layer.pipe(
Layer.provide(Auth.defaultLayer),
Layer.provide(Session.defaultLayer),
Layer.provide(SessionPrompt.defaultLayer),
Layer.provide(Project.defaultLayer),
Layer.provide(Vcs.defaultLayer),
Layer.provide(Database.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(FetchHttpClient.layer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(RuntimeFlags.layer(overrides)),
Layer.provide(InstanceStore.defaultLayer),
Layer.provide(InstanceBootstrap.defaultLayer),
AppNodeBuilder.build(
LayerNode.group([
Workspace.node,
Auth.node,
Session.node,
SessionPrompt.node,
Project.node,
Vcs.node,
Database.node,
EventV2Bridge.node,
FSUtil.node,
InstanceStore.node,
]),
[
[InstanceStore.bootstrapNode, InstanceBootstrap.node],
[RuntimeFlags.node, RuntimeFlags.layer(overrides)],
],
)

View file

@ -1,5 +1,6 @@
import { NodeFileSystem } from "@effect/platform-node"
import { describe, expect } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Effect, Layer } from "effect"
import { provideTmpdirInstance, testInstanceStoreLayer, TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
@ -7,7 +8,9 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Format } from "../../src/format"
import * as Formatter from "../../src/format/formatter"
const it = testEffect(Layer.mergeAll(Format.defaultLayer, CrossSpawnSpawner.defaultLayer, NodeFileSystem.layer))
const it = testEffect(
Layer.mergeAll(LayerNode.compile(LayerNode.group([Format.node, CrossSpawnSpawner.node])), NodeFileSystem.layer),
)
describe("Format", () => {
it.instance("status() returns empty list when no formatters are configured", () =>
@ -106,7 +109,11 @@ describe("Format", () => {
)
testEffect(
Layer.mergeAll(Format.defaultLayer, CrossSpawnSpawner.defaultLayer, NodeFileSystem.layer, testInstanceStoreLayer),
Layer.mergeAll(
LayerNode.compile(LayerNode.group([Format.node, CrossSpawnSpawner.node])),
NodeFileSystem.layer,
testInstanceStoreLayer,
),
).live("status() initializes formatter state per directory", () =>
Effect.gen(function* () {
const a = yield* provideTmpdirInstance(() => Format.use.status(), {

View file

@ -1,20 +1,18 @@
import { describe, expect } from "bun:test"
import { Cause, Effect, Exit, Layer } from "effect"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Cause, Effect, Exit } from "effect"
import { Image } from "@/image/image"
import { Config } from "@/config/config"
import { MessageID, PartID, SessionID } from "@/session/schema"
import path from "node:path"
import { TestConfig } from "../fixture/config"
import { testEffect } from "../lib/effect"
const it = testEffect(Layer.mergeAll(Image.layer.pipe(Layer.provide(TestConfig.layer()))))
const it = testEffect(LayerNode.compile(Image.node, [[Config.node, TestConfig.layer()]]))
const tiny = testEffect(
Layer.mergeAll(
Image.layer.pipe(
Layer.provide(
TestConfig.layer({ get: () => Effect.succeed({ attachment: { image: { max_base64_bytes: 1 } } }) }),
),
),
),
LayerNode.compile(Image.node, [
[Config.node, TestConfig.layer({ get: () => Effect.succeed({ attachment: { image: { max_base64_bytes: 1 } } }) })],
]),
)
function part(mime: string, data: string) {

View file

@ -1,10 +1,13 @@
import { describe, expect } from "bun:test"
import { makeGlobalNode } from "@opencode-ai/core/effect/app-node"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
import { Effect, Layer, Stream } from "effect"
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import { Installation } from "../../src/installation"
import { InstallationChannel } from "@opencode-ai/core/installation/version"
import { AppProcess } from "@opencode-ai/core/process"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { testEffect } from "../lib/effect"
const encoder = new TextEncoder()
@ -52,8 +55,15 @@ function testLayer(
httpHandler: (request: HttpClientRequest.HttpClientRequest) => Response,
spawnHandler?: (cmd: string, args: readonly string[]) => string | { code: number; stdout?: string; stderr?: string },
) {
const appProcess = AppProcess.layer.pipe(Layer.provide(mockSpawner(spawnHandler)))
return Installation.layer.pipe(Layer.provide(mockHttpClient(httpHandler)), Layer.provide(appProcess))
const spawnerNode = makeGlobalNode({
service: ChildProcessSpawner.ChildProcessSpawner,
layer: mockSpawner(spawnHandler),
deps: [],
})
return LayerNode.compile(Installation.node, [
[httpClient, mockHttpClient(httpHandler)],
[CrossSpawnSpawner.node, spawnerNode],
])
}
describe("installation", () => {

Some files were not shown because too many files have changed in this diff Show more