mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-03 16:56:33 -04:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e84efb181c | |||
| 2e8d690ab1 | |||
| 1ff8d289af | |||
| d54ffbda1c |
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -83,7 +83,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -117,7 +117,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -144,7 +144,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "3.0.64",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
@@ -168,7 +168,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -192,7 +192,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -226,7 +226,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -259,7 +259,7 @@
|
||||
},
|
||||
"packages/desktop-electron": {
|
||||
"name": "@opencode-ai/desktop-electron",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -303,7 +303,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -332,7 +332,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -348,7 +348,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -491,7 +491,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
@@ -526,7 +526,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -541,7 +541,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -576,7 +576,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -625,7 +625,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"name": "@opencode-ai/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop-electron",
|
||||
"private": true,
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The open source coding agent."
|
||||
version = "1.14.29"
|
||||
version = "1.14.28"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/anomalyco/opencode"
|
||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.29/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.28/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.29/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.28/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.29/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.28/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.29/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.28/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.29/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.28/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -20,6 +20,8 @@ const ZedEditorContentsSchema = z.object({
|
||||
contents: z.string().nullable(),
|
||||
})
|
||||
|
||||
const utf8 = new TextEncoder()
|
||||
|
||||
type ZedEditorRow = z.infer<typeof ZedEditorRowSchema>
|
||||
type ZedActiveEditorRow = ZedEditorRow & { item_kind: "Editor"; editor_id: number }
|
||||
|
||||
@@ -45,8 +47,8 @@ export async function resolveZedSelection(dbPath: string, cwd = process.cwd()):
|
||||
.catch(() => undefined)
|
||||
if (text == null) return { type: "unavailable" }
|
||||
|
||||
const startOffset = Math.min(row.selection_start, row.selection_end)
|
||||
const endOffset = Math.max(row.selection_start, row.selection_end)
|
||||
const startOffset = utf8ByteOffsetToStringIndex(text, Math.min(row.selection_start, row.selection_end))
|
||||
const endOffset = utf8ByteOffsetToStringIndex(text, Math.max(row.selection_start, row.selection_end))
|
||||
|
||||
return {
|
||||
type: "selection",
|
||||
@@ -158,7 +160,25 @@ function zedWorkspacePaths(value: string | null) {
|
||||
}
|
||||
|
||||
export function offsetToPosition(text: string, offset: number) {
|
||||
return offsetsToSelection(text, offset, offset).start
|
||||
const stringOffset = utf8ByteOffsetToStringIndex(text, offset)
|
||||
return offsetsToSelection(text, stringOffset, stringOffset).start
|
||||
}
|
||||
|
||||
function utf8ByteOffsetToStringIndex(text: string, byteOffset: number) {
|
||||
if (byteOffset <= 0) return 0
|
||||
|
||||
let bytes = 0
|
||||
for (let index = 0; index < text.length; ) {
|
||||
const codePoint = text.codePointAt(index)
|
||||
if (codePoint === undefined) return text.length
|
||||
|
||||
const nextIndex = index + (codePoint > 0xffff ? 2 : 1)
|
||||
bytes += utf8.encode(text.slice(index, nextIndex)).length
|
||||
if (bytes >= byteOffset) return nextIndex
|
||||
index = nextIndex
|
||||
}
|
||||
|
||||
return text.length
|
||||
}
|
||||
|
||||
function offsetsToSelection(text: string, startOffset: number, endOffset: number) {
|
||||
|
||||
@@ -500,7 +500,8 @@ async function getCustomThemes() {
|
||||
symlink: true,
|
||||
})) {
|
||||
const name = path.basename(item, ".json")
|
||||
result[name] = await Filesystem.readJson(item)
|
||||
const theme = await Filesystem.readJson(item)
|
||||
if (isTheme(theme)) result[name] = theme
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -21,11 +21,12 @@ type OpenApiParameter = {
|
||||
name: string
|
||||
in: string
|
||||
required?: boolean
|
||||
schema?: unknown
|
||||
schema?: OpenApiSchema
|
||||
}
|
||||
|
||||
type OpenApiOperation = {
|
||||
parameters?: OpenApiParameter[]
|
||||
responses?: Record<string, unknown>
|
||||
requestBody?: {
|
||||
required?: boolean
|
||||
content?: Record<string, { schema?: OpenApiSchema }>
|
||||
@@ -46,8 +47,12 @@ type OpenApiSchema = {
|
||||
additionalProperties?: OpenApiSchema | boolean
|
||||
allOf?: OpenApiSchema[]
|
||||
anyOf?: OpenApiSchema[]
|
||||
enum?: string[]
|
||||
items?: OpenApiSchema
|
||||
maximum?: number
|
||||
minimum?: number
|
||||
oneOf?: OpenApiSchema[]
|
||||
prefixItems?: OpenApiSchema[]
|
||||
properties?: Record<string, OpenApiSchema>
|
||||
type?: string
|
||||
}
|
||||
@@ -68,6 +73,13 @@ const InstanceQueryParameters = [
|
||||
] satisfies OpenApiParameter[]
|
||||
|
||||
const LegacyBodyRefParameters = new Set(["Auth", "Config", "Part", "WorktreeRemoveInput", "WorktreeResetInput"])
|
||||
const FiniteNumberValues = new Set(["Infinity", "-Infinity", "NaN"])
|
||||
const QueryNumberParameters = new Set(["start", "cursor", "limit", "method"])
|
||||
const QueryBooleanParameters = new Set(["roots", "archived"])
|
||||
const QueryParameterSchemas = {
|
||||
"GET /find/file limit": { type: "integer", minimum: 1, maximum: 200 },
|
||||
"GET /session/{sessionID}/message limit": { type: "integer", minimum: 0, maximum: Number.MAX_SAFE_INTEGER },
|
||||
} satisfies Record<string, OpenApiSchema>
|
||||
|
||||
function matchLegacyOpenApi(input: Record<string, unknown>) {
|
||||
const spec = input as OpenApiSpec
|
||||
@@ -87,6 +99,45 @@ function matchLegacyOpenApi(input: Record<string, unknown>) {
|
||||
}
|
||||
if (media.schema) media.schema = normalizeRequestSchema(media.schema)
|
||||
}
|
||||
if (path === "/experimental/workspace" && method === "post") {
|
||||
const properties = operation.requestBody.content?.["application/json"]?.schema?.properties
|
||||
if (properties?.branch) properties.branch = { anyOf: [properties.branch, { type: "null" }] }
|
||||
if (properties?.extra) properties.extra = { anyOf: [properties.extra, { type: "null" }] }
|
||||
}
|
||||
if (path === "/tui/publish" && method === "post" && spec.components?.schemas) {
|
||||
const schema = operation.requestBody.content?.["application/json"]?.schema
|
||||
const anyOf = schema?.anyOf
|
||||
if (anyOf?.length === 4) {
|
||||
spec.components.schemas.EventTuiPromptAppend = anyOf[0]
|
||||
spec.components.schemas.EventTuiCommandExecute = anyOf[1]
|
||||
spec.components.schemas.EventTuiToastShow = anyOf[2]
|
||||
spec.components.schemas.EventTuiSessionSelect = anyOf[3]
|
||||
operation.requestBody.content!["application/json"]!.schema = {
|
||||
anyOf: [
|
||||
{ $ref: "#/components/schemas/EventTuiPromptAppend" },
|
||||
{ $ref: "#/components/schemas/EventTuiCommandExecute" },
|
||||
{ $ref: "#/components/schemas/EventTuiToastShow" },
|
||||
{ $ref: "#/components/schemas/EventTuiSessionSelect" },
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
if (path === "/sync/replay" && method === "post" && spec.components?.schemas?.SyncReplayEvent) {
|
||||
const events = operation.requestBody.content?.["application/json"]?.schema?.properties?.events
|
||||
if (events?.items?.$ref === "#/components/schemas/SyncReplayEvent") {
|
||||
events.items = normalizeRequestSchema(structuredClone(spec.components.schemas.SyncReplayEvent))
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((path === "/event" || path === "/global/event") && method === "get") {
|
||||
operation.responses!["200"] = {
|
||||
description: "Event stream",
|
||||
content: {
|
||||
"text/event-stream": {
|
||||
schema: path === "/event" ? {} : { $ref: "#/components/schemas/GlobalEvent" },
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
if (!isInstanceRoute) continue
|
||||
operation.parameters = [
|
||||
@@ -95,22 +146,27 @@ function matchLegacyOpenApi(input: Record<string, unknown>) {
|
||||
(param) => param.in !== "query" || (param.name !== "directory" && param.name !== "workspace"),
|
||||
),
|
||||
]
|
||||
for (const param of operation.parameters) normalizeParameter(param, `${method.toUpperCase()} ${path}`)
|
||||
}
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
function normalizeRequestSchema(schema: OpenApiSchema): OpenApiSchema {
|
||||
const options = schema.anyOf ?? schema.oneOf
|
||||
const options = flattenOptions(schema.anyOf ?? schema.oneOf)
|
||||
if (options) {
|
||||
const withoutNull = options.filter((item) => item.type !== "null")
|
||||
const finite = withoutNull.find((item) => item.type === "number")
|
||||
if (finite && withoutNull.every((item) => item.type === "number" || item.type === "string")) return finite
|
||||
if (finite && withoutNull.every(isFiniteNumberOption)) return { type: "number" }
|
||||
if (withoutNull.length === 1) return normalizeRequestSchema(withoutNull[0])
|
||||
if (schema.anyOf) schema.anyOf = withoutNull.map(normalizeRequestSchema)
|
||||
if (schema.oneOf) schema.oneOf = withoutNull.map(normalizeRequestSchema)
|
||||
}
|
||||
if (schema.allOf) schema.allOf = schema.allOf.map(normalizeRequestSchema)
|
||||
if (schema.allOf) {
|
||||
if (schema.type) delete schema.allOf
|
||||
else schema.allOf = schema.allOf.map(normalizeRequestSchema)
|
||||
}
|
||||
if (schema.prefixItems && schema.items) delete schema.prefixItems
|
||||
if (schema.items) schema.items = normalizeRequestSchema(schema.items)
|
||||
if (schema.properties) {
|
||||
for (const [key, value] of Object.entries(schema.properties)) {
|
||||
@@ -123,6 +179,35 @@ function normalizeRequestSchema(schema: OpenApiSchema): OpenApiSchema {
|
||||
return schema
|
||||
}
|
||||
|
||||
function flattenOptions(options: OpenApiSchema[] | undefined): OpenApiSchema[] | undefined {
|
||||
return options?.flatMap((item) => flattenOptions(item.anyOf ?? item.oneOf) ?? [item])
|
||||
}
|
||||
|
||||
function isFiniteNumberOption(schema: OpenApiSchema) {
|
||||
if (schema.type === "number") return true
|
||||
return schema.type === "string" && schema.enum?.every((value) => FiniteNumberValues.has(value)) === true
|
||||
}
|
||||
|
||||
function normalizeParameter(param: OpenApiParameter, route: string) {
|
||||
if (param.in !== "query" || !param.schema || typeof param.schema !== "object") return
|
||||
const override = QueryParameterSchemas[`${route} ${param.name}` as keyof typeof QueryParameterSchemas]
|
||||
if (override) {
|
||||
param.schema = override
|
||||
return
|
||||
}
|
||||
if (QueryNumberParameters.has(param.name)) {
|
||||
param.schema = { type: "number" }
|
||||
return
|
||||
}
|
||||
if (QueryBooleanParameters.has(param.name)) {
|
||||
param.schema = {
|
||||
anyOf: [{ type: "boolean" }, { type: "string", enum: ["true", "false"] }],
|
||||
}
|
||||
return
|
||||
}
|
||||
param.schema = normalizeRequestSchema(param.schema)
|
||||
}
|
||||
|
||||
export const PublicApi = HttpApi.make("opencode")
|
||||
.addHttpApi(ControlApi)
|
||||
.addHttpApi(GlobalApi)
|
||||
|
||||
@@ -10,12 +10,14 @@ type ZedFixtureOptions = {
|
||||
editor?: boolean
|
||||
selectionStart?: number | null
|
||||
selectionEnd?: number | null
|
||||
contents?: string
|
||||
}
|
||||
|
||||
async function writeZedFixture(dir: string, options: ZedFixtureOptions = {}) {
|
||||
const dbPath = path.join(dir, "zed.sqlite")
|
||||
const filePath = path.join(dir, "file.ts")
|
||||
await Bun.write(filePath, "one\ntwo\nthree")
|
||||
const contents = options.contents ?? "one\ntwo\nthree"
|
||||
await Bun.write(filePath, contents)
|
||||
|
||||
const db = new Database(dbPath)
|
||||
db.run("create table workspaces (workspace_id integer, paths text, timestamp text)")
|
||||
@@ -27,7 +29,7 @@ async function writeZedFixture(dir: string, options: ZedFixtureOptions = {}) {
|
||||
db.run("insert into panes values (1, 1, 1)")
|
||||
db.run("insert into items values (1, 1, 1, 1, ?)", [options.itemKind ?? "Editor"])
|
||||
if (options.editor !== false) {
|
||||
db.run("insert into editors values (1, 1, ?, ?)", [filePath, "one\ntwo\nthree"])
|
||||
db.run("insert into editors values (1, 1, ?, ?)", [filePath, contents])
|
||||
db.run("insert into editor_selections values (1, 1, ?, ?)", [
|
||||
options.selectionStart === undefined ? 4 : options.selectionStart,
|
||||
options.selectionEnd === undefined ? 7 : options.selectionEnd,
|
||||
@@ -38,11 +40,23 @@ async function writeZedFixture(dir: string, options: ZedFixtureOptions = {}) {
|
||||
return { dbPath, filePath }
|
||||
}
|
||||
|
||||
function utf8ByteOffset(text: string, offset: number) {
|
||||
return new TextEncoder().encode(text.slice(0, offset)).length
|
||||
}
|
||||
|
||||
test("offsetToPosition converts Zed offsets to 1-based editor positions", () => {
|
||||
expect(offsetToPosition("one\ntwo\nthree", 0)).toEqual({ line: 1, character: 1 })
|
||||
expect(offsetToPosition("one\ntwo\nthree", 4)).toEqual({ line: 2, character: 1 })
|
||||
expect(offsetToPosition("one\ntwo\nthree", 6)).toEqual({ line: 2, character: 3 })
|
||||
expect(offsetToPosition("one\ntwo\nthree", 100)).toEqual({ line: 3, character: 6 })
|
||||
expect(offsetToPosition("Ж\nabc", utf8ByteOffset("Ж\nabc", "Ж\nabc".indexOf("a")))).toEqual({
|
||||
line: 2,
|
||||
character: 1,
|
||||
})
|
||||
expect(offsetToPosition("😀\nabc", utf8ByteOffset("😀\nabc", "😀\nabc".indexOf("a")))).toEqual({
|
||||
line: 2,
|
||||
character: 1,
|
||||
})
|
||||
})
|
||||
|
||||
test("resolveZedSelection returns active editor selection", async () => {
|
||||
@@ -63,6 +77,102 @@ test("resolveZedSelection returns active editor selection", async () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("resolveZedSelection converts Zed UTF-8 byte offsets to string offsets", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const contents = "a\nЖЖЖЖЖЖЖЖЖЖ\nb\nTARGET\nz"
|
||||
const start = contents.indexOf("TARGET")
|
||||
const fixture = await writeZedFixture(tmp.path, {
|
||||
contents,
|
||||
selectionStart: utf8ByteOffset(contents, start),
|
||||
selectionEnd: utf8ByteOffset(contents, start + "TARGET".length),
|
||||
})
|
||||
|
||||
expect(await resolveZedSelection(fixture.dbPath, tmp.path)).toEqual({
|
||||
type: "selection",
|
||||
selection: {
|
||||
text: "TARGET",
|
||||
filePath: fixture.filePath,
|
||||
source: "zed",
|
||||
selection: {
|
||||
start: { line: 4, character: 1 },
|
||||
end: { line: 4, character: 7 },
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("resolveZedSelection handles non-ASCII text inside the selected range", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const contents = "a\npre\nвыбор\nz"
|
||||
const start = contents.indexOf("выбор")
|
||||
const fixture = await writeZedFixture(tmp.path, {
|
||||
contents,
|
||||
selectionStart: utf8ByteOffset(contents, start),
|
||||
selectionEnd: utf8ByteOffset(contents, start + "выбор".length),
|
||||
})
|
||||
|
||||
expect(await resolveZedSelection(fixture.dbPath, tmp.path)).toEqual({
|
||||
type: "selection",
|
||||
selection: {
|
||||
text: "выбор",
|
||||
filePath: fixture.filePath,
|
||||
source: "zed",
|
||||
selection: {
|
||||
start: { line: 3, character: 1 },
|
||||
end: { line: 3, character: 6 },
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("resolveZedSelection handles emoji before the selected range", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const contents = "😀\nTARGET\nz"
|
||||
const start = contents.indexOf("TARGET")
|
||||
const fixture = await writeZedFixture(tmp.path, {
|
||||
contents,
|
||||
selectionStart: utf8ByteOffset(contents, start),
|
||||
selectionEnd: utf8ByteOffset(contents, start + "TARGET".length),
|
||||
})
|
||||
|
||||
expect(await resolveZedSelection(fixture.dbPath, tmp.path)).toEqual({
|
||||
type: "selection",
|
||||
selection: {
|
||||
text: "TARGET",
|
||||
filePath: fixture.filePath,
|
||||
source: "zed",
|
||||
selection: {
|
||||
start: { line: 2, character: 1 },
|
||||
end: { line: 2, character: 7 },
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("resolveZedSelection handles reversed Zed byte offsets", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const contents = "a\nЖЖЖ\nTARGET\nz"
|
||||
const start = contents.indexOf("TARGET")
|
||||
const fixture = await writeZedFixture(tmp.path, {
|
||||
contents,
|
||||
selectionStart: utf8ByteOffset(contents, start + "TARGET".length),
|
||||
selectionEnd: utf8ByteOffset(contents, start),
|
||||
})
|
||||
|
||||
expect(await resolveZedSelection(fixture.dbPath, tmp.path)).toEqual({
|
||||
type: "selection",
|
||||
selection: {
|
||||
text: "TARGET",
|
||||
filePath: fixture.filePath,
|
||||
source: "zed",
|
||||
selection: {
|
||||
start: { line: 3, character: 1 },
|
||||
end: { line: 3, character: 7 },
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("resolveZedSelection returns empty when no workspace matches", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const fixture = await writeZedFixture(tmp.path, {
|
||||
|
||||
@@ -18,6 +18,11 @@ const original = {
|
||||
}
|
||||
|
||||
const methods = ["get", "post", "put", "delete", "patch"] as const
|
||||
let effectSpec: ReturnType<typeof OpenApi.fromApi> | undefined
|
||||
|
||||
function effectOpenApi() {
|
||||
return (effectSpec ??= OpenApi.fromApi(PublicApi))
|
||||
}
|
||||
|
||||
function app(input?: { password?: string; username?: string }) {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
|
||||
@@ -62,6 +67,7 @@ function openApiRequestBodies(spec: { paths: Record<string, Partial<Record<(type
|
||||
|
||||
type Operation = {
|
||||
parameters?: unknown[]
|
||||
responses?: unknown
|
||||
requestBody?: unknown
|
||||
}
|
||||
|
||||
@@ -76,6 +82,19 @@ function parameterKey(param: unknown) {
|
||||
return `${param.in}:${param.name}:${"required" in param && param.required === true}`
|
||||
}
|
||||
|
||||
function parameterSchema(input: {
|
||||
spec: { paths: Record<string, Partial<Record<(typeof methods)[number], Operation>>> }
|
||||
path: string
|
||||
method: (typeof methods)[number]
|
||||
name: string
|
||||
}) {
|
||||
const param = input.spec.paths[input.path]?.[input.method]?.parameters?.find(
|
||||
(param) => !!param && typeof param === "object" && "name" in param && param.name === input.name,
|
||||
)
|
||||
if (!param || typeof param !== "object" || !("schema" in param)) return
|
||||
return param.schema
|
||||
}
|
||||
|
||||
function requestBodyKey(body: unknown) {
|
||||
if (!body || typeof body !== "object" || !("content" in body)) return ""
|
||||
const requestBody = body as RequestBody
|
||||
@@ -87,6 +106,23 @@ function requestBodyKey(body: unknown) {
|
||||
})
|
||||
}
|
||||
|
||||
function responseContentTypes(input: {
|
||||
spec: { paths: Record<string, Partial<Record<(typeof methods)[number], Operation>>> }
|
||||
path: string
|
||||
method: (typeof methods)[number]
|
||||
status: string
|
||||
}) {
|
||||
const responses = input.spec.paths[input.path]?.[input.method]?.responses
|
||||
if (!responses || typeof responses !== "object" || !(input.status in responses)) return []
|
||||
const response = (responses as Record<string, unknown>)[input.status]
|
||||
if (!response || typeof response !== "object" || !("content" in response)) return []
|
||||
const content = (response as { content?: unknown }).content
|
||||
if (!content || typeof content !== "object") {
|
||||
return []
|
||||
}
|
||||
return Object.keys(content).sort()
|
||||
}
|
||||
|
||||
function authorization(username: string, password: string) {
|
||||
return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
|
||||
}
|
||||
@@ -110,7 +146,7 @@ afterEach(async () => {
|
||||
describe("HttpApi server", () => {
|
||||
test("covers every generated OpenAPI route with Effect HttpApi contracts", async () => {
|
||||
const honoRoutes = openApiRouteKeys(await Server.openapi())
|
||||
const effectRoutes = openApiRouteKeys(OpenApi.fromApi(PublicApi))
|
||||
const effectRoutes = openApiRouteKeys(effectOpenApi())
|
||||
|
||||
expect(honoRoutes.filter((route) => !effectRoutes.includes(route))).toEqual([])
|
||||
expect(effectRoutes.filter((route) => !honoRoutes.includes(route))).toEqual([])
|
||||
@@ -118,7 +154,7 @@ describe("HttpApi server", () => {
|
||||
|
||||
test("matches generated OpenAPI route parameters", async () => {
|
||||
const hono = openApiParameters(await Server.openapi())
|
||||
const effect = openApiParameters(OpenApi.fromApi(PublicApi))
|
||||
const effect = openApiParameters(effectOpenApi())
|
||||
|
||||
expect(
|
||||
Object.keys(hono)
|
||||
@@ -129,7 +165,7 @@ describe("HttpApi server", () => {
|
||||
|
||||
test("matches generated OpenAPI request body shape", async () => {
|
||||
const hono = openApiRequestBodies(await Server.openapi())
|
||||
const effect = openApiRequestBodies(OpenApi.fromApi(PublicApi))
|
||||
const effect = openApiRequestBodies(effectOpenApi())
|
||||
|
||||
expect(
|
||||
Object.keys(hono)
|
||||
@@ -138,6 +174,38 @@ describe("HttpApi server", () => {
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
test("matches SDK-affecting query parameter schemas", async () => {
|
||||
const effect = effectOpenApi()
|
||||
|
||||
expect(parameterSchema({ spec: effect, path: "/session", method: "get", name: "roots" })).toEqual({
|
||||
anyOf: [{ type: "boolean" }, { type: "string", enum: ["true", "false"] }],
|
||||
})
|
||||
expect(parameterSchema({ spec: effect, path: "/session", method: "get", name: "start" })).toEqual({
|
||||
type: "number",
|
||||
})
|
||||
expect(parameterSchema({ spec: effect, path: "/find/file", method: "get", name: "limit" })).toEqual({
|
||||
type: "integer",
|
||||
minimum: 1,
|
||||
maximum: 200,
|
||||
})
|
||||
expect(parameterSchema({ spec: effect, path: "/session/{sessionID}/message", method: "get", name: "limit" })).toEqual({
|
||||
type: "integer",
|
||||
minimum: 0,
|
||||
maximum: Number.MAX_SAFE_INTEGER,
|
||||
})
|
||||
})
|
||||
|
||||
test("documents event routes as server-sent events", () => {
|
||||
const effect = effectOpenApi()
|
||||
|
||||
expect(responseContentTypes({ spec: effect, path: "/event", method: "get", status: "200" })).toEqual([
|
||||
"text/event-stream",
|
||||
])
|
||||
expect(responseContentTypes({ spec: effect, path: "/global/event", method: "get", status: "200" })).toEqual([
|
||||
"text/event-stream",
|
||||
])
|
||||
})
|
||||
|
||||
test("allows requests when auth is disabled", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Bun.write(`${tmp.path}/hello.txt`, "hello")
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
function sdk(directory: string) {
|
||||
const handler = ExperimentalHttpApiServer.webHandler().handler
|
||||
return createOpencodeClient({
|
||||
baseUrl: "http://opencode.test",
|
||||
directory,
|
||||
fetch: ((input, init) => {
|
||||
const request = input instanceof Request ? input : new Request(input, init)
|
||||
return handler(request, ExperimentalHttpApiServer.context)
|
||||
}) as typeof fetch,
|
||||
})
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Instance.disposeAll()
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
describe("HttpApi SDK", () => {
|
||||
test("serves generated SDK requests through the experimental Effect server", async () => {
|
||||
await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
|
||||
await Bun.write(`${tmp.path}/hello.txt`, "hello")
|
||||
|
||||
const client = sdk(tmp.path)
|
||||
const file = await client.file.read({ path: "hello.txt" })
|
||||
expect(file.response.status).toBe(200)
|
||||
expect(file.data?.content).toBe("hello")
|
||||
|
||||
const created = await client.session.create({ title: "sdk session" })
|
||||
if (!created.data) throw new Error("Expected session create response data")
|
||||
expect(created.response.status).toBe(200)
|
||||
expect(created.data.title).toBe("sdk session")
|
||||
|
||||
const listed = await client.session.list({ roots: true, limit: 10 })
|
||||
expect(listed.response.status).toBe(200)
|
||||
expect(listed.data?.map((item) => item.id)).toContain(created.data.id)
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@opencode-ai/web",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "opencode",
|
||||
"displayName": "opencode",
|
||||
"description": "opencode for VS Code",
|
||||
"version": "1.14.29",
|
||||
"version": "1.14.28",
|
||||
"publisher": "sst-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Reference in New Issue
Block a user