Procházet zdrojové kódy

refactor: move Pty into pty/index.ts with self-reexport (#22881)

Kit Langton před 1 dnem
rodič
revize
76275fc3ab

+ 49 - 5
packages/opencode/specs/effect/namespace-treeshake.md

@@ -196,15 +196,59 @@ import regressions at runtime — the typechecker does not catch these.
 ## Rules for new code
 
 - No new `export namespace`.
-- Every module file that wants a namespace gets a self-reexport at the
+- Every module directory has a single canonical file — typically
+  `dir/index.ts` — with flat top-level exports and a self-reexport at the
   bottom:
-  `export * as Foo from "./foo"`
-- Consumers import from the file itself:
-  `import { Foo } from "../path/to/foo"`
-- No new barrel `index.ts` files for internal code.
+  `export * as Foo from "."`
+- Consumers import from the directory:
+  `import { Foo } from "@/dir"` or `import { Foo } from "../dir"`.
+- No sibling barrel files. If a directory has multiple independent
+  namespaces, they each get their own file (e.g. `config/config.ts`,
+  `config/plugin.ts`) and their own self-reexport; the `index.ts` in that
+  directory stays minimal or does not exist.
 - If a file needs a sibling, import the sibling file directly:
   `import * as Sibling from "./sibling"`, not `from "."`.
 
+### Why `dir/index.ts` + `"."` is fine for us
+
+A single-file module (e.g. `pty/`) can live entirely in `dir/index.ts`
+with `export * as Foo from "."` at the bottom. Consumers write the
+short form:
+
+```ts
+import { Pty } from "@/pty"
+```
+
+This works in Bun runtime, Bun build, esbuild, and Rollup. It does NOT
+work under Node's `--experimental-strip-types` runner:
+
+```
+node --experimental-strip-types entry.ts
+  ERR_UNSUPPORTED_DIR_IMPORT: Directory import '/.../pty' is not supported
+```
+
+Node requires an explicit file or a `package.json#exports` map for ESM.
+We don't care about that target right now because the opencode CLI is
+built with Bun and the web apps are built with Vite/Rollup. If we ever
+want to run raw `.ts` through Node, we'll need to either use explicit
+`.ts` extensions everywhere or add per-directory `package.json` exports
+maps.
+
+### When NOT to collapse to `index.ts`
+
+Some directories contain multiple independent namespaces where
+`dir/index.ts` would be misleading. Examples:
+
+- `config/` has `Config`, `ConfigPaths`, `ConfigMarkdown`, `ConfigPlugin`,
+  `ConfigKeybinds`. Each lives in its own file with its own self-reexport
+  (`config/config.ts`, `config/plugin.ts`, etc.). Consumers import the
+  specific one: `import { ConfigPlugin } from "@/config/plugin"`.
+- Same shape for `session/`, `server/`, etc.
+
+Collapsing one of those into `index.ts` would mean picking a single
+"canonical" namespace for the directory, which breaks the symmetry and
+hides the other files.
+
 ## Scope
 
 There are still dozens of `export namespace` files left across the codebase.

+ 364 - 1
packages/opencode/src/pty/index.ts

@@ -1 +1,364 @@
-export * as Pty from "./service"
+import { BusEvent } from "@/bus/bus-event"
+import { Bus } from "@/bus"
+import { InstanceState } from "@/effect"
+import { Instance } from "@/project/instance"
+import type { Proc } from "#pty"
+import z from "zod"
+import { Log } from "../util"
+import { lazy } from "@opencode-ai/shared/util/lazy"
+import { Shell } from "@/shell/shell"
+import { Plugin } from "@/plugin"
+import { PtyID } from "./schema"
+import { Effect, Layer, Context } from "effect"
+import { EffectBridge } from "@/effect"
+
+const log = Log.create({ service: "pty" })
+
+const BUFFER_LIMIT = 1024 * 1024 * 2
+const BUFFER_CHUNK = 64 * 1024
+const encoder = new TextEncoder()
+
+type Socket = {
+  readyState: number
+  data?: unknown
+  send: (data: string | Uint8Array | ArrayBuffer) => void
+  close: (code?: number, reason?: string) => void
+}
+
+const sock = (ws: Socket) => (ws.data && typeof ws.data === "object" ? ws.data : ws)
+
+type Active = {
+  info: Info
+  process: Proc
+  buffer: string
+  bufferCursor: number
+  cursor: number
+  subscribers: Map<unknown, Socket>
+}
+
+type State = {
+  dir: string
+  sessions: Map<PtyID, Active>
+}
+
+// WebSocket control frame: 0x00 + UTF-8 JSON.
+const meta = (cursor: number) => {
+  const json = JSON.stringify({ cursor })
+  const bytes = encoder.encode(json)
+  const out = new Uint8Array(bytes.length + 1)
+  out[0] = 0
+  out.set(bytes, 1)
+  return out
+}
+
+const pty = lazy(() => import("#pty"))
+
+export const Info = z
+  .object({
+    id: PtyID.zod,
+    title: z.string(),
+    command: z.string(),
+    args: z.array(z.string()),
+    cwd: z.string(),
+    status: z.enum(["running", "exited"]),
+    pid: z.number(),
+  })
+  .meta({ ref: "Pty" })
+
+export type Info = z.infer<typeof Info>
+
+export const CreateInput = z.object({
+  command: z.string().optional(),
+  args: z.array(z.string()).optional(),
+  cwd: z.string().optional(),
+  title: z.string().optional(),
+  env: z.record(z.string(), z.string()).optional(),
+})
+
+export type CreateInput = z.infer<typeof CreateInput>
+
+export const UpdateInput = z.object({
+  title: z.string().optional(),
+  size: z
+    .object({
+      rows: z.number(),
+      cols: z.number(),
+    })
+    .optional(),
+})
+
+export type UpdateInput = z.infer<typeof UpdateInput>
+
+export const Event = {
+  Created: BusEvent.define("pty.created", z.object({ info: Info })),
+  Updated: BusEvent.define("pty.updated", z.object({ info: Info })),
+  Exited: BusEvent.define("pty.exited", z.object({ id: PtyID.zod, exitCode: z.number() })),
+  Deleted: BusEvent.define("pty.deleted", z.object({ id: PtyID.zod })),
+}
+
+export interface Interface {
+  readonly list: () => Effect.Effect<Info[]>
+  readonly get: (id: PtyID) => Effect.Effect<Info | undefined>
+  readonly create: (input: CreateInput) => Effect.Effect<Info>
+  readonly update: (id: PtyID, input: UpdateInput) => Effect.Effect<Info | undefined>
+  readonly remove: (id: PtyID) => Effect.Effect<void>
+  readonly resize: (id: PtyID, cols: number, rows: number) => Effect.Effect<void>
+  readonly write: (id: PtyID, data: string) => Effect.Effect<void>
+  readonly connect: (
+    id: PtyID,
+    ws: Socket,
+    cursor?: number,
+  ) => Effect.Effect<{ onMessage: (message: string | ArrayBuffer) => void; onClose: () => void } | undefined>
+}
+
+export class Service extends Context.Service<Service, Interface>()("@opencode/Pty") {}
+
+export const layer = Layer.effect(
+  Service,
+  Effect.gen(function* () {
+    const bus = yield* Bus.Service
+    const plugin = yield* Plugin.Service
+    function teardown(session: Active) {
+      try {
+        session.process.kill()
+      } catch {}
+      for (const [sub, ws] of session.subscribers.entries()) {
+        try {
+          if (sock(ws) === sub) ws.close()
+        } catch {}
+      }
+      session.subscribers.clear()
+    }
+
+    const state = yield* InstanceState.make<State>(
+      Effect.fn("Pty.state")(function* (ctx) {
+        const state = {
+          dir: ctx.directory,
+          sessions: new Map<PtyID, Active>(),
+        }
+
+        yield* Effect.addFinalizer(() =>
+          Effect.sync(() => {
+            for (const session of state.sessions.values()) {
+              teardown(session)
+            }
+            state.sessions.clear()
+          }),
+        )
+
+        return state
+      }),
+    )
+
+    const remove = Effect.fn("Pty.remove")(function* (id: PtyID) {
+      const s = yield* InstanceState.get(state)
+      const session = s.sessions.get(id)
+      if (!session) return
+      s.sessions.delete(id)
+      log.info("removing session", { id })
+      teardown(session)
+      yield* bus.publish(Event.Deleted, { id: session.info.id })
+    })
+
+    const list = Effect.fn("Pty.list")(function* () {
+      const s = yield* InstanceState.get(state)
+      return Array.from(s.sessions.values()).map((session) => session.info)
+    })
+
+    const get = Effect.fn("Pty.get")(function* (id: PtyID) {
+      const s = yield* InstanceState.get(state)
+      return s.sessions.get(id)?.info
+    })
+
+    const create = Effect.fn("Pty.create")(function* (input: CreateInput) {
+      const s = yield* InstanceState.get(state)
+      const bridge = yield* EffectBridge.make()
+      const id = PtyID.ascending()
+      const command = input.command || Shell.preferred()
+      const args = input.args || []
+      if (Shell.login(command)) {
+        args.push("-l")
+      }
+
+      const cwd = input.cwd || s.dir
+      const shell = yield* plugin.trigger("shell.env", { cwd }, { env: {} })
+      const env = {
+        ...process.env,
+        ...input.env,
+        ...shell.env,
+        TERM: "xterm-256color",
+        OPENCODE_TERMINAL: "1",
+      } as Record<string, string>
+
+      if (process.platform === "win32") {
+        env.LC_ALL = "C.UTF-8"
+        env.LC_CTYPE = "C.UTF-8"
+        env.LANG = "C.UTF-8"
+      }
+      log.info("creating session", { id, cmd: command, args, cwd })
+
+      const { spawn } = yield* Effect.promise(() => pty())
+      const proc = yield* Effect.sync(() =>
+        spawn(command, args, {
+          name: "xterm-256color",
+          cwd,
+          env,
+        }),
+      )
+
+      const info = {
+        id,
+        title: input.title || `Terminal ${id.slice(-4)}`,
+        command,
+        args,
+        cwd,
+        status: "running",
+        pid: proc.pid,
+      } as const
+      const session: Active = {
+        info,
+        process: proc,
+        buffer: "",
+        bufferCursor: 0,
+        cursor: 0,
+        subscribers: new Map(),
+      }
+      s.sessions.set(id, session)
+      proc.onData(
+        Instance.bind((chunk) => {
+          session.cursor += chunk.length
+
+          for (const [key, ws] of session.subscribers.entries()) {
+            if (ws.readyState !== 1) {
+              session.subscribers.delete(key)
+              continue
+            }
+            if (sock(ws) !== key) {
+              session.subscribers.delete(key)
+              continue
+            }
+            try {
+              ws.send(chunk)
+            } catch {
+              session.subscribers.delete(key)
+            }
+          }
+
+          session.buffer += chunk
+          if (session.buffer.length <= BUFFER_LIMIT) return
+          const excess = session.buffer.length - BUFFER_LIMIT
+          session.buffer = session.buffer.slice(excess)
+          session.bufferCursor += excess
+        }),
+      )
+      proc.onExit(
+        Instance.bind(({ exitCode }) => {
+          if (session.info.status === "exited") return
+          log.info("session exited", { id, exitCode })
+          session.info.status = "exited"
+          bridge.fork(bus.publish(Event.Exited, { id, exitCode }))
+          bridge.fork(remove(id))
+        }),
+      )
+      yield* bus.publish(Event.Created, { info })
+      return info
+    })
+
+    const update = Effect.fn("Pty.update")(function* (id: PtyID, input: UpdateInput) {
+      const s = yield* InstanceState.get(state)
+      const session = s.sessions.get(id)
+      if (!session) return
+      if (input.title) {
+        session.info.title = input.title
+      }
+      if (input.size) {
+        session.process.resize(input.size.cols, input.size.rows)
+      }
+      yield* bus.publish(Event.Updated, { info: session.info })
+      return session.info
+    })
+
+    const resize = Effect.fn("Pty.resize")(function* (id: PtyID, cols: number, rows: number) {
+      const s = yield* InstanceState.get(state)
+      const session = s.sessions.get(id)
+      if (session && session.info.status === "running") {
+        session.process.resize(cols, rows)
+      }
+    })
+
+    const write = Effect.fn("Pty.write")(function* (id: PtyID, data: string) {
+      const s = yield* InstanceState.get(state)
+      const session = s.sessions.get(id)
+      if (session && session.info.status === "running") {
+        session.process.write(data)
+      }
+    })
+
+    const connect = Effect.fn("Pty.connect")(function* (id: PtyID, ws: Socket, cursor?: number) {
+      const s = yield* InstanceState.get(state)
+      const session = s.sessions.get(id)
+      if (!session) {
+        ws.close()
+        return
+      }
+      log.info("client connected to session", { id })
+
+      const sub = sock(ws)
+      session.subscribers.delete(sub)
+      session.subscribers.set(sub, ws)
+
+      const cleanup = () => {
+        session.subscribers.delete(sub)
+      }
+
+      const start = session.bufferCursor
+      const end = session.cursor
+      const from =
+        cursor === -1 ? end : typeof cursor === "number" && Number.isSafeInteger(cursor) ? Math.max(0, cursor) : 0
+
+      const data = (() => {
+        if (!session.buffer) return ""
+        if (from >= end) return ""
+        const offset = Math.max(0, from - start)
+        if (offset >= session.buffer.length) return ""
+        return session.buffer.slice(offset)
+      })()
+
+      if (data) {
+        try {
+          for (let i = 0; i < data.length; i += BUFFER_CHUNK) {
+            ws.send(data.slice(i, i + BUFFER_CHUNK))
+          }
+        } catch {
+          cleanup()
+          ws.close()
+          return
+        }
+      }
+
+      try {
+        ws.send(meta(end))
+      } catch {
+        cleanup()
+        ws.close()
+        return
+      }
+
+      return {
+        onMessage: (message: string | ArrayBuffer) => {
+          session.process.write(typeof message === "string" ? message : new TextDecoder().decode(message))
+        },
+        onClose: () => {
+          log.info("client disconnected from session", { id })
+          cleanup()
+        },
+      }
+    })
+
+    return Service.of({ list, get, create, update, remove, resize, write, connect })
+  }),
+)
+
+export const defaultLayer = layer.pipe(Layer.provide(Bus.layer), Layer.provide(Plugin.defaultLayer))
+
+export * as Pty from "."

+ 0 - 362
packages/opencode/src/pty/service.ts

@@ -1,362 +0,0 @@
-import { BusEvent } from "@/bus/bus-event"
-import { Bus } from "@/bus"
-import { InstanceState } from "@/effect"
-import { Instance } from "@/project/instance"
-import type { Proc } from "#pty"
-import z from "zod"
-import { Log } from "../util"
-import { lazy } from "@opencode-ai/shared/util/lazy"
-import { Shell } from "@/shell/shell"
-import { Plugin } from "@/plugin"
-import { PtyID } from "./schema"
-import { Effect, Layer, Context } from "effect"
-import { EffectBridge } from "@/effect"
-
-const log = Log.create({ service: "pty" })
-
-const BUFFER_LIMIT = 1024 * 1024 * 2
-const BUFFER_CHUNK = 64 * 1024
-const encoder = new TextEncoder()
-
-type Socket = {
-  readyState: number
-  data?: unknown
-  send: (data: string | Uint8Array | ArrayBuffer) => void
-  close: (code?: number, reason?: string) => void
-}
-
-const sock = (ws: Socket) => (ws.data && typeof ws.data === "object" ? ws.data : ws)
-
-type Active = {
-  info: Info
-  process: Proc
-  buffer: string
-  bufferCursor: number
-  cursor: number
-  subscribers: Map<unknown, Socket>
-}
-
-type State = {
-  dir: string
-  sessions: Map<PtyID, Active>
-}
-
-// WebSocket control frame: 0x00 + UTF-8 JSON.
-const meta = (cursor: number) => {
-  const json = JSON.stringify({ cursor })
-  const bytes = encoder.encode(json)
-  const out = new Uint8Array(bytes.length + 1)
-  out[0] = 0
-  out.set(bytes, 1)
-  return out
-}
-
-const pty = lazy(() => import("#pty"))
-
-export const Info = z
-  .object({
-    id: PtyID.zod,
-    title: z.string(),
-    command: z.string(),
-    args: z.array(z.string()),
-    cwd: z.string(),
-    status: z.enum(["running", "exited"]),
-    pid: z.number(),
-  })
-  .meta({ ref: "Pty" })
-
-export type Info = z.infer<typeof Info>
-
-export const CreateInput = z.object({
-  command: z.string().optional(),
-  args: z.array(z.string()).optional(),
-  cwd: z.string().optional(),
-  title: z.string().optional(),
-  env: z.record(z.string(), z.string()).optional(),
-})
-
-export type CreateInput = z.infer<typeof CreateInput>
-
-export const UpdateInput = z.object({
-  title: z.string().optional(),
-  size: z
-    .object({
-      rows: z.number(),
-      cols: z.number(),
-    })
-    .optional(),
-})
-
-export type UpdateInput = z.infer<typeof UpdateInput>
-
-export const Event = {
-  Created: BusEvent.define("pty.created", z.object({ info: Info })),
-  Updated: BusEvent.define("pty.updated", z.object({ info: Info })),
-  Exited: BusEvent.define("pty.exited", z.object({ id: PtyID.zod, exitCode: z.number() })),
-  Deleted: BusEvent.define("pty.deleted", z.object({ id: PtyID.zod })),
-}
-
-export interface Interface {
-  readonly list: () => Effect.Effect<Info[]>
-  readonly get: (id: PtyID) => Effect.Effect<Info | undefined>
-  readonly create: (input: CreateInput) => Effect.Effect<Info>
-  readonly update: (id: PtyID, input: UpdateInput) => Effect.Effect<Info | undefined>
-  readonly remove: (id: PtyID) => Effect.Effect<void>
-  readonly resize: (id: PtyID, cols: number, rows: number) => Effect.Effect<void>
-  readonly write: (id: PtyID, data: string) => Effect.Effect<void>
-  readonly connect: (
-    id: PtyID,
-    ws: Socket,
-    cursor?: number,
-  ) => Effect.Effect<{ onMessage: (message: string | ArrayBuffer) => void; onClose: () => void } | undefined>
-}
-
-export class Service extends Context.Service<Service, Interface>()("@opencode/Pty") {}
-
-export const layer = Layer.effect(
-  Service,
-  Effect.gen(function* () {
-    const bus = yield* Bus.Service
-    const plugin = yield* Plugin.Service
-    function teardown(session: Active) {
-      try {
-        session.process.kill()
-      } catch {}
-      for (const [sub, ws] of session.subscribers.entries()) {
-        try {
-          if (sock(ws) === sub) ws.close()
-        } catch {}
-      }
-      session.subscribers.clear()
-    }
-
-    const state = yield* InstanceState.make<State>(
-      Effect.fn("Pty.state")(function* (ctx) {
-        const state = {
-          dir: ctx.directory,
-          sessions: new Map<PtyID, Active>(),
-        }
-
-        yield* Effect.addFinalizer(() =>
-          Effect.sync(() => {
-            for (const session of state.sessions.values()) {
-              teardown(session)
-            }
-            state.sessions.clear()
-          }),
-        )
-
-        return state
-      }),
-    )
-
-    const remove = Effect.fn("Pty.remove")(function* (id: PtyID) {
-      const s = yield* InstanceState.get(state)
-      const session = s.sessions.get(id)
-      if (!session) return
-      s.sessions.delete(id)
-      log.info("removing session", { id })
-      teardown(session)
-      yield* bus.publish(Event.Deleted, { id: session.info.id })
-    })
-
-    const list = Effect.fn("Pty.list")(function* () {
-      const s = yield* InstanceState.get(state)
-      return Array.from(s.sessions.values()).map((session) => session.info)
-    })
-
-    const get = Effect.fn("Pty.get")(function* (id: PtyID) {
-      const s = yield* InstanceState.get(state)
-      return s.sessions.get(id)?.info
-    })
-
-    const create = Effect.fn("Pty.create")(function* (input: CreateInput) {
-      const s = yield* InstanceState.get(state)
-      const bridge = yield* EffectBridge.make()
-      const id = PtyID.ascending()
-      const command = input.command || Shell.preferred()
-      const args = input.args || []
-      if (Shell.login(command)) {
-        args.push("-l")
-      }
-
-      const cwd = input.cwd || s.dir
-      const shell = yield* plugin.trigger("shell.env", { cwd }, { env: {} })
-      const env = {
-        ...process.env,
-        ...input.env,
-        ...shell.env,
-        TERM: "xterm-256color",
-        OPENCODE_TERMINAL: "1",
-      } as Record<string, string>
-
-      if (process.platform === "win32") {
-        env.LC_ALL = "C.UTF-8"
-        env.LC_CTYPE = "C.UTF-8"
-        env.LANG = "C.UTF-8"
-      }
-      log.info("creating session", { id, cmd: command, args, cwd })
-
-      const { spawn } = yield* Effect.promise(() => pty())
-      const proc = yield* Effect.sync(() =>
-        spawn(command, args, {
-          name: "xterm-256color",
-          cwd,
-          env,
-        }),
-      )
-
-      const info = {
-        id,
-        title: input.title || `Terminal ${id.slice(-4)}`,
-        command,
-        args,
-        cwd,
-        status: "running",
-        pid: proc.pid,
-      } as const
-      const session: Active = {
-        info,
-        process: proc,
-        buffer: "",
-        bufferCursor: 0,
-        cursor: 0,
-        subscribers: new Map(),
-      }
-      s.sessions.set(id, session)
-      proc.onData(
-        Instance.bind((chunk) => {
-          session.cursor += chunk.length
-
-          for (const [key, ws] of session.subscribers.entries()) {
-            if (ws.readyState !== 1) {
-              session.subscribers.delete(key)
-              continue
-            }
-            if (sock(ws) !== key) {
-              session.subscribers.delete(key)
-              continue
-            }
-            try {
-              ws.send(chunk)
-            } catch {
-              session.subscribers.delete(key)
-            }
-          }
-
-          session.buffer += chunk
-          if (session.buffer.length <= BUFFER_LIMIT) return
-          const excess = session.buffer.length - BUFFER_LIMIT
-          session.buffer = session.buffer.slice(excess)
-          session.bufferCursor += excess
-        }),
-      )
-      proc.onExit(
-        Instance.bind(({ exitCode }) => {
-          if (session.info.status === "exited") return
-          log.info("session exited", { id, exitCode })
-          session.info.status = "exited"
-          bridge.fork(bus.publish(Event.Exited, { id, exitCode }))
-          bridge.fork(remove(id))
-        }),
-      )
-      yield* bus.publish(Event.Created, { info })
-      return info
-    })
-
-    const update = Effect.fn("Pty.update")(function* (id: PtyID, input: UpdateInput) {
-      const s = yield* InstanceState.get(state)
-      const session = s.sessions.get(id)
-      if (!session) return
-      if (input.title) {
-        session.info.title = input.title
-      }
-      if (input.size) {
-        session.process.resize(input.size.cols, input.size.rows)
-      }
-      yield* bus.publish(Event.Updated, { info: session.info })
-      return session.info
-    })
-
-    const resize = Effect.fn("Pty.resize")(function* (id: PtyID, cols: number, rows: number) {
-      const s = yield* InstanceState.get(state)
-      const session = s.sessions.get(id)
-      if (session && session.info.status === "running") {
-        session.process.resize(cols, rows)
-      }
-    })
-
-    const write = Effect.fn("Pty.write")(function* (id: PtyID, data: string) {
-      const s = yield* InstanceState.get(state)
-      const session = s.sessions.get(id)
-      if (session && session.info.status === "running") {
-        session.process.write(data)
-      }
-    })
-
-    const connect = Effect.fn("Pty.connect")(function* (id: PtyID, ws: Socket, cursor?: number) {
-      const s = yield* InstanceState.get(state)
-      const session = s.sessions.get(id)
-      if (!session) {
-        ws.close()
-        return
-      }
-      log.info("client connected to session", { id })
-
-      const sub = sock(ws)
-      session.subscribers.delete(sub)
-      session.subscribers.set(sub, ws)
-
-      const cleanup = () => {
-        session.subscribers.delete(sub)
-      }
-
-      const start = session.bufferCursor
-      const end = session.cursor
-      const from =
-        cursor === -1 ? end : typeof cursor === "number" && Number.isSafeInteger(cursor) ? Math.max(0, cursor) : 0
-
-      const data = (() => {
-        if (!session.buffer) return ""
-        if (from >= end) return ""
-        const offset = Math.max(0, from - start)
-        if (offset >= session.buffer.length) return ""
-        return session.buffer.slice(offset)
-      })()
-
-      if (data) {
-        try {
-          for (let i = 0; i < data.length; i += BUFFER_CHUNK) {
-            ws.send(data.slice(i, i + BUFFER_CHUNK))
-          }
-        } catch {
-          cleanup()
-          ws.close()
-          return
-        }
-      }
-
-      try {
-        ws.send(meta(end))
-      } catch {
-        cleanup()
-        ws.close()
-        return
-      }
-
-      return {
-        onMessage: (message: string | ArrayBuffer) => {
-          session.process.write(typeof message === "string" ? message : new TextDecoder().decode(message))
-        },
-        onClose: () => {
-          log.info("client disconnected from session", { id })
-          cleanup()
-        },
-      }
-    })
-
-    return Service.of({ list, get, create, update, remove, resize, write, connect })
-  }),
-)
-
-export const defaultLayer = layer.pipe(Layer.provide(Bus.layer), Layer.provide(Plugin.defaultLayer))