Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions packages/opencode/src/bus/bus-event.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import z from "zod"
import type { ZodType } from "zod"
import { Schema } from "effect"
import { zodObject } from "@/util/effect-zod"

export type Definition = ReturnType<typeof define>
export type Definition<Type extends string = string, Properties extends Schema.Top = Schema.Top> = {
type: Type
properties: Properties
}

const registry = new Map<string, Definition>()

export function define<Type extends string, Properties extends ZodType>(type: Type, properties: Properties) {
const result = {
type,
properties,
}
export function define<Type extends string, Properties extends Schema.Top>(
type: Type,
properties: Properties,
): Definition<Type, Properties> {
const result = { type, properties }
registry.set(type, result)
return result
}
Expand All @@ -21,7 +25,7 @@ export function payloads() {
return z
.object({
type: z.literal(type),
properties: def.properties,
properties: zodObject(def.properties),
})
.meta({
ref: `Event.${def.type}`,
Expand Down
13 changes: 4 additions & 9 deletions packages/opencode/src/bus/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import z from "zod"
import { Effect, Exit, Layer, PubSub, Scope, Context, Stream, Schema as EffectSchema, Types } from "effect"
import { Effect, Exit, Layer, PubSub, Scope, Context, Stream, Schema } from "effect"
import { EffectBridge } from "@/effect"
import { Log } from "../util"
import { BusEvent } from "./bus-event"
Expand All @@ -9,16 +8,12 @@ import { makeRuntime } from "@/effect/run-service"

const log = Log.create({ service: "bus" })

type BusProperties<D extends BusEvent.Definition = BusEvent.Definition> = D extends {
effectProperties: infer Properties extends EffectSchema.Top
}
? Types.DeepMutable<EffectSchema.Schema.Type<Properties>>
: z.infer<D["properties"]>
type BusProperties<D extends BusEvent.Definition<string, Schema.Top>> = Schema.Schema.Type<D["properties"]>

export const InstanceDisposed = BusEvent.define(
"server.instance.disposed",
z.object({
directory: z.string(),
Schema.Struct({
directory: Schema.String,
}),
)

Expand Down
26 changes: 13 additions & 13 deletions packages/opencode/src/cli/cmd/tui/event.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { BusEvent } from "@/bus/bus-event"
import { SessionID } from "@/session/schema"
import z from "zod"
import { Schema } from "effect"

export const TuiEvent = {
PromptAppend: BusEvent.define("tui.prompt.append", z.object({ text: z.string() })),
PromptAppend: BusEvent.define("tui.prompt.append", Schema.Struct({ text: Schema.String })),
CommandExecute: BusEvent.define(
"tui.command.execute",
z.object({
command: z.union([
z.enum([
Schema.Struct({
command: Schema.Union([
Schema.Literals([
"session.list",
"session.new",
"session.share",
Expand All @@ -26,23 +26,23 @@ export const TuiEvent = {
"prompt.submit",
"agent.cycle",
]),
z.string(),
Schema.String,
]),
}),
),
ToastShow: BusEvent.define(
"tui.toast.show",
z.object({
title: z.string().optional(),
message: z.string(),
variant: z.enum(["info", "success", "warning", "error"]),
duration: z.number().default(5000).optional().describe("Duration in milliseconds"),
Schema.Struct({
title: Schema.optional(Schema.String),
message: Schema.String,
variant: Schema.Literals(["info", "success", "warning", "error"]),
duration: Schema.optional(Schema.Number).annotate({ description: "Duration in milliseconds" }),
}),
),
SessionSelect: BusEvent.define(
"tui.session.select",
z.object({
sessionID: SessionID.zod.describe("Session ID to navigate to"),
Schema.Struct({
sessionID: SessionID.annotate({ description: "Session ID to navigate to" }),
}),
),
}
4 changes: 2 additions & 2 deletions packages/opencode/src/cli/cmd/tui/ui/toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { useTheme } from "@tui/context/theme"
import { useTerminalDimensions } from "@opentui/solid"
import { SplitBorder } from "../component/border"
import { TextAttributes } from "@opentui/core"
import z from "zod"
import { Schema } from "effect"
import { type TuiEvent } from "../event"

export type ToastOptions = z.infer<typeof TuiEvent.ToastShow.properties>
export type ToastOptions = Schema.Schema.Type<typeof TuiEvent.ToastShow.properties>

export function Toast() {
const toast = useToast()
Expand Down
12 changes: 6 additions & 6 deletions packages/opencode/src/command/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { InstanceState } from "@/effect"
import { EffectBridge } from "@/effect"
import type { InstanceContext } from "@/project/instance"
import { SessionID, MessageID } from "@/session/schema"
import { Effect, Layer, Context } from "effect"
import { Effect, Layer, Context, Schema } from "effect"
import z from "zod"
import { Config } from "../config"
import { MCP } from "../mcp"
Expand All @@ -18,11 +18,11 @@ type State = {
export const Event = {
Executed: BusEvent.define(
"command.executed",
z.object({
name: z.string(),
sessionID: SessionID.zod,
arguments: z.string(),
messageID: MessageID.zod,
Schema.Struct({
name: Schema.String,
sessionID: SessionID,
arguments: Schema.String,
messageID: MessageID,
}),
),
}
Expand Down
25 changes: 4 additions & 21 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "e
import { EffectFlock } from "@opencode-ai/shared/util/effect-flock"
import { InstanceRef } from "@/effect/instance-ref"
import { zod, ZodOverride } from "@/util/effect-zod"
import { NonNegativeInt, PositiveInt, withStatics } from "@/util/schema"
import { NonNegativeInt, PositiveInt, withStatics, type DeepMutable } from "@/util/schema"
import { ConfigAgent } from "./agent"
import { ConfigCommand } from "./command"
import { ConfigFormatter } from "./formatter"
Expand Down Expand Up @@ -249,26 +249,9 @@ export const Info = Schema.Struct({
})),
)

// Schema.Struct produces readonly types by default, but the service code
// below mutates Info objects directly (e.g. `config.mode = ...`). Strip the
// readonly recursively so callers get the same mutable shape zod inferred.
//
// `Types.DeepMutable` from effect-smol would be a drop-in, but its fallback
// branch `{ -readonly [K in keyof T]: ... }` collapses `unknown` to `{}`
// (since `keyof unknown = never`), which widens `Record<string, unknown>`
// fields like `ConfigPlugin.Options`. The local version gates on
// `extends object` so `unknown` passes through.
//
// Tuple branch preserves `ConfigPlugin.Spec`'s `readonly [string, Options]`
// shape (otherwise the general array branch widens it to an array).
type DeepMutable<T> = T extends readonly [unknown, ...unknown[]]
? { -readonly [K in keyof T]: DeepMutable<T[K]> }
: T extends readonly (infer U)[]
? DeepMutable<U>[]
: T extends object
? { -readonly [K in keyof T]: DeepMutable<T[K]> }
: T

// Uses the shared `DeepMutable` from `@/util/schema`. See the definition
// there for why the local variant is needed over `Types.DeepMutable` from
// effect-smol (the upstream version collapses `unknown` to `{}`).
export type Info = DeepMutable<Schema.Schema.Type<typeof Info>> & {
// plugin_origins is derived state, not a persisted config field. It keeps each winning plugin spec together
// with the file and scope it came from so later runtime code can make location-sensitive decisions.
Expand Down
28 changes: 15 additions & 13 deletions packages/opencode/src/control-plane/workspace.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import z from "zod"
import { Schema } from "effect"
import { setTimeout as sleep } from "node:timers/promises"
import { fn } from "@/util/fn"
import { Database, asc, eq, inArray } from "@/storage"
Expand All @@ -25,36 +26,37 @@ import { errorData } from "@/util/error"
import { AppRuntime } from "@/effect/app-runtime"
import { waitEvent } from "./util"
import { WorkspaceContext } from "./workspace-context"
import { NonNegativeInt } from "@/util/schema"

export const Info = WorkspaceInfo.meta({
ref: "Workspace",
})
export type Info = z.infer<typeof Info>

export const ConnectionStatus = z.object({
workspaceID: WorkspaceID.zod,
status: z.enum(["connected", "connecting", "disconnected", "error"]),
export const ConnectionStatus = Schema.Struct({
workspaceID: WorkspaceID,
status: Schema.Literals(["connected", "connecting", "disconnected", "error"]),
})
export type ConnectionStatus = z.infer<typeof ConnectionStatus>
export type ConnectionStatus = Schema.Schema.Type<typeof ConnectionStatus>

const Restore = z.object({
workspaceID: WorkspaceID.zod,
sessionID: SessionID.zod,
total: z.number().int().min(0),
step: z.number().int().min(0),
const Restore = Schema.Struct({
workspaceID: WorkspaceID,
sessionID: SessionID,
total: NonNegativeInt,
step: NonNegativeInt,
})

export const Event = {
Ready: BusEvent.define(
"workspace.ready",
z.object({
name: z.string(),
Schema.Struct({
name: Schema.String,
}),
),
Failed: BusEvent.define(
"workspace.failed",
z.object({
message: z.string(),
Schema.Struct({
message: Schema.String,
}),
),
Restore: BusEvent.define("workspace.restore", Restore),
Expand Down
6 changes: 3 additions & 3 deletions packages/opencode/src/file/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { InstanceState } from "@/effect"

import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Git } from "@/git"
import { Effect, Layer, Context, Scope } from "effect"
import { Effect, Layer, Context, Schema, Scope } from "effect"
import * as Stream from "effect/Stream"
import { formatPatch, structuredPatch } from "diff"
import fuzzysort from "fuzzysort"
Expand Down Expand Up @@ -76,8 +76,8 @@ export type Content = z.infer<typeof Content>
export const Event = {
Edited: BusEvent.define(
"file.edited",
z.object({
file: z.string(),
Schema.Struct({
file: Schema.String,
}),
),
}
Expand Down
8 changes: 4 additions & 4 deletions packages/opencode/src/file/watcher.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Cause, Effect, Layer, Context } from "effect"
import { Cause, Effect, Layer, Context, Schema } from "effect"
// @ts-ignore
import { createWrapper } from "@parcel/watcher/wrapper"
import type ParcelWatcher from "@parcel/watcher"
Expand All @@ -25,9 +25,9 @@ const SUBSCRIBE_TIMEOUT_MS = 10_000
export const Event = {
Updated: BusEvent.define(
"file.watcher.updated",
z.object({
file: z.string(),
event: z.union([z.literal("add"), z.literal("change"), z.literal("unlink")]),
Schema.Struct({
file: Schema.String,
event: Schema.Literals(["add", "change", "unlink"]),
}),
),
}
Expand Down
5 changes: 3 additions & 2 deletions packages/opencode/src/ide/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { BusEvent } from "@/bus/bus-event"
import z from "zod"
import { Schema } from "effect"
import { NamedError } from "@opencode-ai/shared/util/error"
import { Log } from "../util"
import { Process } from "@/util"
Expand All @@ -17,8 +18,8 @@ const log = Log.create({ service: "ide" })
export const Event = {
Installed: BusEvent.define(
"ide.installed",
z.object({
ide: z.string(),
Schema.Struct({
ide: Schema.String,
}),
),
}
Expand Down
8 changes: 4 additions & 4 deletions packages/opencode/src/installation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ export type ReleaseType = "patch" | "minor" | "major"
export const Event = {
Updated: BusEvent.define(
"installation.updated",
z.object({
version: z.string(),
Schema.Struct({
version: Schema.String,
}),
),
UpdateAvailable: BusEvent.define(
"installation.update-available",
z.object({
version: z.string(),
Schema.Struct({
version: Schema.String,
}),
),
}
Expand Down
7 changes: 4 additions & 3 deletions packages/opencode/src/lsp/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Log } from "../util"
import { Process } from "../util"
import { LANGUAGE_EXTENSIONS } from "./language"
import z from "zod"
import { Schema } from "effect"
import type * as LSPServer from "./server"
import { NamedError } from "@opencode-ai/shared/util/error"
import { withTimeout } from "../util/timeout"
Expand Down Expand Up @@ -41,9 +42,9 @@ export const InitializeError = NamedError.create(
export const Event = {
Diagnostics: BusEvent.define(
"lsp.client.diagnostics",
z.object({
serverID: z.string(),
path: z.string(),
Schema.Struct({
serverID: Schema.String,
path: Schema.String,
}),
),
}
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/lsp/lsp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { zod, ZodOverride } from "@/util/effect-zod"
const log = Log.create({ service: "lsp" })

export const Event = {
Updated: BusEvent.define("lsp.updated", z.object({})),
Updated: BusEvent.define("lsp.updated", Schema.Struct({})),
}

const Position = Schema.Struct({
Expand Down
12 changes: 6 additions & 6 deletions packages/opencode/src/mcp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { BusEvent } from "../bus/bus-event"
import { Bus } from "@/bus"
import { TuiEvent } from "@/cli/cmd/tui/event"
import open from "open"
import { Effect, Exit, Layer, Option, Context, Stream } from "effect"
import { Effect, Exit, Layer, Option, Context, Schema, Stream } from "effect"
import { EffectBridge } from "@/effect"
import { InstanceState } from "@/effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
Expand All @@ -47,16 +47,16 @@ export type Resource = z.infer<typeof Resource>

export const ToolsChanged = BusEvent.define(
"mcp.tools.changed",
z.object({
server: z.string(),
Schema.Struct({
server: Schema.String,
}),
)

export const BrowserOpenFailed = BusEvent.define(
"mcp.browser.open.failed",
z.object({
mcpName: z.string(),
url: z.string(),
Schema.Struct({
mcpName: Schema.String,
url: Schema.String,
}),
)

Expand Down
Loading
Loading