refactor(core): consolidate pty service (#30537)

This commit is contained in:
Shoubhit Dash
2026-06-03 15:17:46 +05:30
committed by GitHub
parent c6f684366a
commit 932fb6c9ec
28 changed files with 504 additions and 576 deletions
@@ -1,6 +1,6 @@
import { Pty } from "@/pty"
import { PtyTicket } from "@/pty/ticket"
import { PtyID } from "@/pty/schema"
import { Pty } from "@opencode-ai/core/pty"
import { PtyTicket } from "@opencode-ai/core/pty/ticket"
import { PtyID } from "@opencode-ai/core/pty/schema"
import { PTY_CONNECT_TICKET_QUERY } from "@/server/shared/pty-ticket"
import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
@@ -1,7 +1,13 @@
import { Pty } from "@/pty"
import { PtyID } from "@/pty/schema"
import { PtyTicket } from "@/pty/ticket"
import { handlePtyInput } from "@/pty/input"
import * as InstanceState from "@/effect/instance-state"
import { registerDisposer } from "@/effect/instance-registry"
import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref"
import { PtyPreparation } from "@/pty-preparation"
import { Pty } from "@opencode-ai/core/pty"
import { handlePtyInput } from "@opencode-ai/core/pty/input"
import { PtyID } from "@opencode-ai/core/pty/schema"
import { PtyTicket } from "@opencode-ai/core/pty/ticket"
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { Shell } from "@/shell/shell"
import { EffectBridge } from "@/effect/bridge"
import { CorsConfig, isAllowedRequestOrigin, type CorsOptions } from "@/server/cors"
@@ -10,7 +16,7 @@ import {
PTY_CONNECT_TOKEN_HEADER,
PTY_CONNECT_TOKEN_HEADER_VALUE,
} from "@/server/shared/pty-ticket"
import { Effect, Option, Schema } from "effect"
import { Effect, Layer, Option, Schema } from "effect"
import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import * as Socket from "effect/unstable/socket/Socket"
@@ -23,30 +29,53 @@ function validOrigin(request: HttpServerRequest.HttpServerRequest, opts: CorsOpt
return isAllowedRequestOrigin(request.headers.origin, request.headers.host, opts)
}
const ticketScope = Effect.gen(function* () {
const instance = yield* InstanceRef
const workspaceID = yield* WorkspaceRef
return { directory: instance?.directory, workspaceID }
})
export const ptyHandlers = HttpApiBuilder.group(InstanceHttpApi, "pty", (handlers) =>
Effect.gen(function* () {
const pty = yield* Pty.Service
const tickets = yield* PtyTicket.Service
const cors = yield* CorsConfig
const locations = yield* LocationServiceMap
const unregister = registerDisposer((directory) =>
Effect.runPromise(locations.invalidate({ directory: AbsolutePath.make(directory) })),
)
yield* Effect.addFinalizer(() => Effect.sync(unregister))
const pty = Effect.fnUntraced(function* <A, E, R>(effect: Effect.Effect<A, E, R>) {
return yield* effect.pipe(
Effect.provide(locations.get({ directory: AbsolutePath.make((yield* InstanceState.context).directory) })),
)
})
const shells = Effect.fn("PtyHttpApi.shells")(function* () {
return yield* Effect.promise(() => Shell.list())
})
const list = Effect.fn("PtyHttpApi.list")(function* () {
return yield* pty.list()
return yield* pty(Pty.Service.use((service) => service.list()))
})
const create = Effect.fn("PtyHttpApi.create")(function* (ctx: { payload: typeof Pty.CreateInput.Type }) {
return yield* pty.create({
...ctx.payload,
args: ctx.payload.args ? [...ctx.payload.args] : undefined,
env: ctx.payload.env ? { ...ctx.payload.env } : undefined,
})
return yield* pty(
Pty.Service.use((service) =>
Effect.flatMap(
PtyPreparation.prepareCreate({
...ctx.payload,
args: ctx.payload.args ? [...ctx.payload.args] : undefined,
env: ctx.payload.env ? { ...ctx.payload.env } : undefined,
}),
service.create,
),
),
)
})
const get = Effect.fn("PtyHttpApi.get")(function* (ctx: { params: { ptyID: PtyID } }) {
return yield* pty.get(ctx.params.ptyID).pipe(
return yield* pty(Pty.Service.use((service) => service.get(ctx.params.ptyID))).pipe(
Effect.catchTag("Pty.NotFoundError", (error) =>
Effect.fail(
new ApiError.PtyNotFoundError({
@@ -62,25 +91,27 @@ export const ptyHandlers = HttpApiBuilder.group(InstanceHttpApi, "pty", (handler
params: { ptyID: PtyID }
payload: typeof Pty.UpdateInput.Type
}) {
return yield* pty
.update(ctx.params.ptyID, {
...ctx.payload,
size: ctx.payload.size ? { ...ctx.payload.size } : undefined,
})
.pipe(
Effect.catchTag("Pty.NotFoundError", (error) =>
Effect.fail(
new ApiError.PtyNotFoundError({
ptyID: error.ptyID,
message: `PTY session not found: ${error.ptyID}`,
}),
),
return yield* pty(
Pty.Service.use((service) =>
service.update(ctx.params.ptyID, {
...ctx.payload,
size: ctx.payload.size ? { ...ctx.payload.size } : undefined,
}),
),
).pipe(
Effect.catchTag("Pty.NotFoundError", (error) =>
Effect.fail(
new ApiError.PtyNotFoundError({
ptyID: error.ptyID,
message: `PTY session not found: ${error.ptyID}`,
}),
),
)
),
)
})
const remove = Effect.fn("PtyHttpApi.remove")(function* (ctx: { params: { ptyID: PtyID } }) {
yield* pty.remove(ctx.params.ptyID).pipe(
yield* pty(Pty.Service.use((service) => service.remove(ctx.params.ptyID))).pipe(
Effect.catchTag("Pty.NotFoundError", (error) =>
Effect.fail(
new ApiError.PtyNotFoundError({
@@ -97,7 +128,7 @@ export const ptyHandlers = HttpApiBuilder.group(InstanceHttpApi, "pty", (handler
const request = yield* HttpServerRequest.HttpServerRequest
if (request.headers[PTY_CONNECT_TOKEN_HEADER] !== PTY_CONNECT_TOKEN_HEADER_VALUE || !validOrigin(request, cors))
return yield* new ApiError.PtyForbiddenError({ message: "Invalid PTY connect token request" })
yield* pty.get(ctx.params.ptyID).pipe(
yield* pty(Pty.Service.use((service) => service.get(ctx.params.ptyID))).pipe(
Effect.catchTag("Pty.NotFoundError", (error) =>
Effect.fail(
new ApiError.PtyNotFoundError({
@@ -107,7 +138,7 @@ export const ptyHandlers = HttpApiBuilder.group(InstanceHttpApi, "pty", (handler
),
),
)
return yield* tickets.issue({ ptyID: ctx.params.ptyID, ...(yield* PtyTicket.scope) })
return yield* tickets.issue({ ptyID: ctx.params.ptyID, ...(yield* ticketScope) })
})
return handlers
@@ -119,13 +150,23 @@ export const ptyHandlers = HttpApiBuilder.group(InstanceHttpApi, "pty", (handler
.handle("remove", remove)
.handle("connectToken", connectToken)
}),
)
).pipe(Layer.provide(LocationServiceMap.layer))
export const ptyConnectHandlers = HttpApiBuilder.group(PtyConnectApi, "pty-connect", (handlers) =>
Effect.gen(function* () {
const pty = yield* Pty.Service
const tickets = yield* PtyTicket.Service
const cors = yield* CorsConfig
const locations = yield* LocationServiceMap
const unregister = registerDisposer((directory) =>
Effect.runPromise(locations.invalidate({ directory: AbsolutePath.make(directory) })),
)
yield* Effect.addFinalizer(() => Effect.sync(unregister))
const pty = Effect.fnUntraced(function* <A, E, R>(effect: Effect.Effect<A, E, R>) {
return yield* effect.pipe(
Effect.provide(locations.get({ directory: AbsolutePath.make((yield* InstanceState.context).directory) })),
)
})
return handlers.handleRaw(
"connect",
@@ -133,7 +174,7 @@ export const ptyConnectHandlers = HttpApiBuilder.group(PtyConnectApi, "pty-conne
params: { ptyID: PtyID }
request: HttpServerRequest.HttpServerRequest
}) {
const exists = yield* pty.get(ctx.params.ptyID).pipe(
const exists = yield* pty(Pty.Service.use((service) => service.get(ctx.params.ptyID))).pipe(
Effect.as(true),
Effect.catchTag("Pty.NotFoundError", () => Effect.succeed(false)),
)
@@ -144,7 +185,7 @@ export const ptyConnectHandlers = HttpApiBuilder.group(PtyConnectApi, "pty-conne
const ticket = new URL(ctx.request.url, "http://localhost").searchParams.get(PTY_CONNECT_TICKET_QUERY)
if (ticket) {
const valid = validOrigin(ctx.request, cors)
? yield* tickets.consume({ ticket, ptyID: ctx.params.ptyID, ...(yield* PtyTicket.scope) })
? yield* tickets.consume({ ticket, ptyID: ctx.params.ptyID, ...(yield* ticketScope) })
: false
if (!valid) return HttpServerResponse.empty({ status: 403 })
}
@@ -187,13 +228,13 @@ export const ptyConnectHandlers = HttpApiBuilder.group(PtyConnectApi, "pty-conne
writeScoped(write(new Socket.CloseEvent(code, reason)))
},
}
const handler = yield* pty
.connect(ctx.params.ptyID, adapter, cursor)
.pipe(
Effect.catchTag("Pty.NotFoundError", () =>
closeAccepted(new Socket.CloseEvent(4404, "session not found")).pipe(Effect.as(undefined)),
),
)
const handler = yield* pty(
Pty.Service.use((service) => service.connect(ctx.params.ptyID, adapter, cursor)),
).pipe(
Effect.catchTag("Pty.NotFoundError", () =>
closeAccepted(new Socket.CloseEvent(4404, "session not found")).pipe(Effect.as(undefined)),
),
)
if (!handler) return HttpServerResponse.empty()
// The handshake runs inside `socket.runRaw`, after the input callback is
@@ -214,4 +255,4 @@ export const ptyConnectHandlers = HttpApiBuilder.group(PtyConnectApi, "pty-conne
}),
)
}),
)
).pipe(Layer.provide(LocationServiceMap.layer))
@@ -31,8 +31,7 @@ import { ProjectCopy } from "@opencode-ai/core/project/copy"
import { ProviderAuth } from "@/provider/auth"
import { ModelsDev } from "@opencode-ai/core/models-dev"
import { Provider } from "@/provider/provider"
import { Pty } from "@/pty"
import { PtyTicket } from "@/pty/ticket"
import { PtyTicket } from "@opencode-ai/core/pty/ticket"
import { Question } from "@/question"
import { Session } from "@/session/session"
import { SessionCompaction } from "@/session/compaction"
@@ -212,7 +211,6 @@ export function createRoutes(
ProjectCopy.defaultLayer,
ProviderAuth.defaultLayer,
Provider.defaultLayer,
Pty.defaultLayer,
PtyTicket.defaultLayer,
Question.defaultLayer,
Ripgrep.defaultLayer,