feat(client): expose fs read in promise client (#34504)

This commit is contained in:
Kit Langton 2026-06-29 19:21:18 -04:00 committed by GitHub
commit ecfa918760
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 227 additions and 63 deletions

View file

@ -1,16 +1,17 @@
import { NodeFileSystem } from "@effect/platform-node"
import { compile, emitEffectImported, emitPromise, write } from "@opencode-ai/httpapi-codegen"
import { ClientApi, endpointNames, groupNames, omitEndpoints } from "../src/contract"
import { ClientApi, effectOmitEndpoints, endpointNames, groupNames, promiseOmitEndpoints } from "../src/contract"
import { Effect } from "effect"
import { fileURLToPath } from "url"
const contract = compile(ClientApi, { groupNames, endpointNames, omitEndpoints })
const promiseContract = compile(ClientApi, { groupNames, endpointNames, omitEndpoints: promiseOmitEndpoints })
const effectContract = compile(ClientApi, { groupNames, endpointNames, omitEndpoints: effectOmitEndpoints })
await Effect.runPromise(
Effect.all(
[
write(
emitPromise(contract, {
emitPromise(promiseContract, {
outputTypes: {
"events.subscribe": {
name: "OpenCodeEventEncoded",
@ -21,7 +22,7 @@ await Effect.runPromise(
fileURLToPath(new URL("../src/generated", import.meta.url)),
),
write(
emitEffectImported(contract, { module: "../contract", api: "ClientApi" }),
emitEffectImported(effectContract, { module: "../contract", api: "ClientApi" }),
fileURLToPath(new URL("../src/generated-effect", import.meta.url)),
),
],

View file

@ -52,4 +52,5 @@ export const endpointNames = {
"question.request.list": "listRequests",
} as const
export const omitEndpoints = new Set(["fs.read", "pty.connect", "pty.connectToken"])
export const promiseOmitEndpoints = new Set(["pty.connect", "pty.connectToken"])
export const effectOmitEndpoints = new Set(["fs.read", "pty.connect", "pty.connectToken"])

View file

@ -87,6 +87,8 @@ import type {
PermissionsGetOutput,
PermissionsReplyInput,
PermissionsReplyOutput,
FilesReadInput,
FilesReadOutput,
FilesListInput,
FilesListOutput,
FilesFindInput,
@ -155,6 +157,7 @@ interface RequestDescriptor {
readonly successStatus: number
readonly declaredStatuses: ReadonlyArray<number>
readonly empty: boolean
readonly binary?: true
}
export function make(options: ClientOptions) {
@ -200,6 +203,7 @@ export function make(options: ClientOptions) {
const request = async <A>(descriptor: RequestDescriptor, requestOptions?: RequestOptions): Promise<A> => {
const response = await execute(descriptor, requestOptions)
if (response.status !== descriptor.successStatus) return responseError(response, descriptor)
if (descriptor.binary) return new Uint8Array(await response.arrayBuffer()) as A
if (descriptor.empty) {
try {
await response.body?.cancel()
@ -840,6 +844,19 @@ export function make(options: ClientOptions) {
),
},
files: {
read: (input: FilesReadInput, requestOptions?: RequestOptions) =>
request<FilesReadOutput>(
{
method: "GET",
path: `/api/fs/read/${encodePath(input.path)}`,
query: { location: input["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
binary: true,
},
requestOptions,
),
list: (input?: FilesListInput, requestOptions?: RequestOptions) =>
request<FilesListOutput>(
{
@ -1143,6 +1160,10 @@ export function make(options: ClientOptions) {
}
}
function encodePath(value: string): string {
return value.split("/").map(encodeURIComponent).join("/")
}
function appendQuery(params: URLSearchParams, key: string, value: unknown): void {
if (value === undefined || value === null) return
if (Array.isArray(value)) {

View file

@ -2572,6 +2572,15 @@ export type PermissionsReplyInput = {
export type PermissionsReplyOutput = void
export type FilesReadInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly path: string
}
export type FilesReadOutput = globalThis.Uint8Array
export type FilesListInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined

View file

@ -37,11 +37,32 @@ test("exposes every standard HTTP API group", () => {
"attemptComplete",
"attemptCancel",
])
expect(Object.keys(client.files)).toEqual(["list", "find"])
expect(Object.keys(client.files)).toEqual(["read", "list", "find"])
expect(Object.keys(client.ptys)).toEqual(["list", "create", "get", "update", "remove"])
expect(Object.keys(client.project)).toEqual(["current", "directories"])
})
test("files.read returns binary content from the public HTTP contract", async () => {
let request: Request | undefined
const client = OpenCode.make({
baseUrl: "http://localhost:3000",
fetch: async (input) => {
request = input instanceof Request ? input : new Request(input)
return new Response(new Uint8Array([104, 105]))
},
})
const content = await client.files.read({
path: "src/a b#c.ts",
location: { directory: "/tmp/project" },
})
expect(Array.from(content)).toEqual([104, 105])
expect(request?.url).toBe(
"http://localhost:3000/api/fs/read/src/a%20b%23c.ts?location%5Bdirectory%5D=%2Ftmp%2Fproject",
)
})
test("project methods use the public HTTP contract", async () => {
const requests: string[] = []
const client = OpenCode.make({