mirror of
https://fastgit.cc/https://github.com/anomalyco/opencode
synced 2026-05-02 06:54:35 +08:00
Compare commits
17 Commits
docs-sdk
...
kit/effect
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38a44903c5 | ||
|
|
aaeff9b3ef | ||
|
|
34e332c139 | ||
|
|
478156456e | ||
|
|
6252412d94 | ||
|
|
c2609cbf04 | ||
|
|
2115df57bf | ||
|
|
29ec07700c | ||
|
|
bcae852d28 | ||
|
|
16ddf5f559 | ||
|
|
8c79c58c4d | ||
|
|
97ed9ba624 | ||
|
|
a6b6395c8a | ||
|
|
21f8027ef7 | ||
|
|
a5aa72bd7d | ||
|
|
563177c6ac | ||
|
|
4eae8ec037 |
1
.github/VOUCHED.td
vendored
1
.github/VOUCHED.td
vendored
@@ -32,6 +32,7 @@ rekram1-node
|
||||
-ricardo-m-l
|
||||
-robinmordasiewicz
|
||||
rubdos
|
||||
-saisharan0103 spamming ai prs
|
||||
shantur
|
||||
simonklee
|
||||
-spider-yamet clawdbot/llm psychosis, spam pinging the team
|
||||
|
||||
32
bun.lock
32
bun.lock
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -85,7 +85,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -119,7 +119,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -146,7 +146,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "3.0.64",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
@@ -170,7 +170,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -194,7 +194,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -228,7 +228,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -263,7 +263,7 @@
|
||||
},
|
||||
"packages/desktop-electron": {
|
||||
"name": "@opencode-ai/desktop-electron",
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -309,7 +309,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -338,7 +338,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -354,7 +354,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -496,7 +496,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
@@ -531,7 +531,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -546,7 +546,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -581,7 +581,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -630,7 +630,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -204,6 +204,9 @@ function createGlobalSync() {
|
||||
},
|
||||
translate: language.t,
|
||||
getSdk: sdkFor,
|
||||
global: {
|
||||
provider: globalStore.provider,
|
||||
},
|
||||
})
|
||||
|
||||
async function loadSessions(directory: string) {
|
||||
|
||||
@@ -260,9 +260,6 @@ export async function bootstrapDirectory(input: {
|
||||
const seededPath = input.global.path.directory === input.directory ? input.global.path : undefined
|
||||
if (seededProject) input.setStore("project", seededProject)
|
||||
if (seededPath) input.setStore("path", seededPath)
|
||||
if (input.store.provider.all.length === 0 && input.global.provider.all.length > 0) {
|
||||
input.setStore("provider", input.global.provider)
|
||||
}
|
||||
if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) {
|
||||
input.setStore("config", reconcile(input.global.config, { merge: false }))
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ describe("createChildStoreManager", () => {
|
||||
onDispose() {},
|
||||
translate: (key) => key,
|
||||
getSdk: () => null!,
|
||||
global: { provider: null! },
|
||||
})
|
||||
|
||||
Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createRoot, getOwner, onCleanup, runWithOwner, type Owner } from "solid-js"
|
||||
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import type { OpencodeClient, VcsInfo } from "@opencode-ai/sdk/v2/client"
|
||||
import type { OpencodeClient, ProviderListResponse, VcsInfo } from "@opencode-ai/sdk/v2/client"
|
||||
import {
|
||||
DIR_IDLE_TTL_MS,
|
||||
MAX_DIR_STORES,
|
||||
@@ -27,6 +27,9 @@ export function createChildStoreManager(input: {
|
||||
onDispose: (directory: string) => void
|
||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||
getSdk: (directory: string) => OpencodeClient
|
||||
global: {
|
||||
provider: ProviderListResponse
|
||||
}
|
||||
}) {
|
||||
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
|
||||
const vcsCache = new Map<string, VcsCache>()
|
||||
@@ -189,7 +192,13 @@ export function createChildStoreManager(input: {
|
||||
get provider_ready() {
|
||||
return !providerQuery.isLoading
|
||||
},
|
||||
provider: { all: [], connected: [], default: {} },
|
||||
get provider() {
|
||||
const EMPTY = { all: [], connected: [], default: {} }
|
||||
if (providerQuery.isLoading) return EMPTY
|
||||
if (providerQuery.data?.all.length === 0 && input.global.provider.all.length > 0)
|
||||
return input.global.provider
|
||||
return providerQuery.data ?? EMPTY
|
||||
},
|
||||
config: {},
|
||||
get path() {
|
||||
if (pathQuery.isLoading || !pathQuery.data)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -141,7 +141,10 @@ export async function handler(
|
||||
)
|
||||
validateModelSettings(billingSource, authInfo)
|
||||
updateProviderKey(authInfo, providerInfo)
|
||||
logger.metric({ provider: providerInfo.id })
|
||||
logger.metric({
|
||||
provider: providerInfo.id,
|
||||
"provider.model": providerInfo.model,
|
||||
})
|
||||
|
||||
const startTimestamp = Date.now()
|
||||
const reqUrl = providerInfo.modifyUrl(providerInfo.api, isStream)
|
||||
@@ -149,12 +152,23 @@ export async function handler(
|
||||
providerInfo.modifyBody({
|
||||
...createBodyConverter(opts.format, providerInfo.format)(body),
|
||||
model: providerInfo.model,
|
||||
...providerInfo.payloadModifier,
|
||||
...Object.fromEntries(
|
||||
Object.entries(providerInfo.payloadMappings ?? {})
|
||||
.map(([k, v]) => [k, input.request.headers.get(v)])
|
||||
.filter(([_k, v]) => !!v),
|
||||
),
|
||||
...(() => {
|
||||
const replacer = (obj: Record<string, any>): Record<string, any> =>
|
||||
Object.fromEntries(
|
||||
Object.entries(obj).flatMap(([k, v]) => {
|
||||
if (Array.isArray(v)) return [[k, v]]
|
||||
if (typeof v === "object") return [[k, replacer(v)]]
|
||||
if (v === "$ip") return [[k, ip]]
|
||||
if (v === "$workspace") return authInfo?.workspaceID ? [[k, authInfo?.workspaceID]] : []
|
||||
if (v.startsWith("$header.")) {
|
||||
const headerValue = input.request.headers.get(v.slice(8))
|
||||
return headerValue ? [[k, headerValue]] : []
|
||||
}
|
||||
return [[k, v]]
|
||||
}),
|
||||
)
|
||||
return replacer(providerInfo.payloadModifier ?? {})
|
||||
})(),
|
||||
}),
|
||||
)
|
||||
logger.debug("REQUEST URL: " + reqUrl)
|
||||
@@ -514,7 +528,6 @@ export async function handler(
|
||||
reqModel,
|
||||
providerModel: modelProvider.model,
|
||||
adjustCacheUsage: providerProps.adjustCacheUsage,
|
||||
safetyIdentifier: modelProvider.safetyIdentifier ? ip : undefined,
|
||||
workspaceID: authInfo?.workspaceID,
|
||||
}
|
||||
if (format === "anthropic") return anthropicHelper(opts)
|
||||
|
||||
@@ -23,7 +23,7 @@ type Usage = {
|
||||
}
|
||||
}
|
||||
|
||||
export const oaCompatHelper: ProviderHelper = ({ adjustCacheUsage, safetyIdentifier }) => ({
|
||||
export const oaCompatHelper: ProviderHelper = ({ adjustCacheUsage }) => ({
|
||||
format: "oa-compat",
|
||||
modifyUrl: (providerApi: string) => providerApi + "/chat/completions",
|
||||
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
|
||||
@@ -34,7 +34,6 @@ export const oaCompatHelper: ProviderHelper = ({ adjustCacheUsage, safetyIdentif
|
||||
return {
|
||||
...body,
|
||||
...(body.stream ? { stream_options: { include_usage: true } } : {}),
|
||||
...(safetyIdentifier ? { safety_identifier: safetyIdentifier } : {}),
|
||||
}
|
||||
},
|
||||
createBinaryStreamDecoder: () => undefined,
|
||||
|
||||
@@ -18,10 +18,7 @@ export const openaiHelper: ProviderHelper = ({ workspaceID }) => ({
|
||||
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
|
||||
headers.set("authorization", `Bearer ${apiKey}`)
|
||||
},
|
||||
modifyBody: (body: Record<string, any>) => ({
|
||||
...body,
|
||||
...(workspaceID ? { safety_identifier: workspaceID } : {}),
|
||||
}),
|
||||
modifyBody: (body: Record<string, any>) => body,
|
||||
createBinaryStreamDecoder: () => undefined,
|
||||
streamSeparator: "\n\n",
|
||||
createUsageParser: () => {
|
||||
|
||||
@@ -37,7 +37,6 @@ export type ProviderHelper = (input: {
|
||||
reqModel: string
|
||||
providerModel: string
|
||||
adjustCacheUsage?: boolean
|
||||
safetyIdentifier?: string
|
||||
workspaceID?: string
|
||||
}) => {
|
||||
format: ZenData.Format
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -40,7 +40,6 @@ export namespace ZenData {
|
||||
disabled: z.boolean().optional(),
|
||||
storeModel: z.string().optional(),
|
||||
payloadModifier: z.record(z.string(), z.any()).optional(),
|
||||
safetyIdentifier: z.boolean().optional(),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"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.30",
|
||||
"version": "1.14.31",
|
||||
"name": "@opencode-ai/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -120,13 +120,17 @@ export const layer = Layer.effect(
|
||||
}
|
||||
})()
|
||||
|
||||
if (yield* afs.existsSafe(dir)) {
|
||||
if (yield* afs.existsSafe(path.join(dir, "node_modules", name))) {
|
||||
return resolveEntryPoint(name, path.join(dir, "node_modules", name))
|
||||
}
|
||||
|
||||
const tree = yield* reify({ dir, add: [pkg] })
|
||||
const first = tree.edgesOut.values().next().value?.to
|
||||
if (!first) return yield* new InstallFailedError({ add: [pkg], dir })
|
||||
if (!first) {
|
||||
const result = resolveEntryPoint(name, path.join(dir, "node_modules", name))
|
||||
if (Option.isSome(result.entrypoint)) return result
|
||||
return yield* new InstallFailedError({ add: [pkg], dir })
|
||||
}
|
||||
return resolveEntryPoint(first.name, first.path)
|
||||
}, Effect.scoped)
|
||||
|
||||
|
||||
16
packages/core/test/global.test.ts
Normal file
16
packages/core/test/global.test.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import fs from "fs/promises"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
|
||||
describe("global paths", () => {
|
||||
test("tmp path is under the system temp directory", () => {
|
||||
expect(Global.Path.tmp).toBe(path.join(os.tmpdir(), "opencode"))
|
||||
expect(Global.make().tmp).toBe(Global.Path.tmp)
|
||||
})
|
||||
|
||||
test("tmp path is created on module load", async () => {
|
||||
expect((await fs.stat(Global.Path.tmp)).isDirectory()).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,12 @@
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { Effect, Layer, Option } from "effect"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { Npm } from "@opencode-ai/core/npm"
|
||||
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
|
||||
import { tmpdir } from "./fixture/tmpdir"
|
||||
|
||||
const win = process.platform === "win32"
|
||||
@@ -15,6 +20,14 @@ const writePackage = (dir: string, pkg: Record<string, unknown>) =>
|
||||
}),
|
||||
)
|
||||
|
||||
const npmLayer = (cache: string) =>
|
||||
Npm.layer.pipe(
|
||||
Layer.provide(EffectFlock.layer),
|
||||
Layer.provide(AppFileSystem.layer),
|
||||
Layer.provide(Global.layerWith({ cache, state: path.join(cache, "state") })),
|
||||
Layer.provide(NodeFileSystem.layer),
|
||||
)
|
||||
|
||||
describe("Npm.sanitize", () => {
|
||||
test("keeps normal scoped package specs unchanged", () => {
|
||||
expect(Npm.sanitize("@opencode/acme")).toBe("@opencode/acme")
|
||||
@@ -29,6 +42,28 @@ describe("Npm.sanitize", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("Npm.add", () => {
|
||||
test("reifies when package cache directory exists without the package installed", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.mkdir(path.join(tmp.path, "fixture-provider"))
|
||||
await writePackage(path.join(tmp.path, "fixture-provider"), {
|
||||
name: "fixture-provider",
|
||||
main: "index.js",
|
||||
})
|
||||
await Bun.write(path.join(tmp.path, "fixture-provider", "index.js"), "export const fixture = true\n")
|
||||
|
||||
const spec = `fixture-provider@file:${path.join(tmp.path, "fixture-provider")}`
|
||||
await fs.mkdir(path.join(tmp.path, "cache", "packages", Npm.sanitize(spec)), { recursive: true })
|
||||
|
||||
const entry = await Effect.gen(function* () {
|
||||
const npm = yield* Npm.Service
|
||||
return yield* npm.add(spec)
|
||||
}).pipe(Effect.scoped, Effect.provide(npmLayer(path.join(tmp.path, "cache"))), Effect.runPromise)
|
||||
|
||||
expect(Option.isSome(entry.entrypoint)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Npm.install", () => {
|
||||
test("respects omit from project .npmrc", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop-electron",
|
||||
"private": true,
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The open source coding agent."
|
||||
version = "1.14.30"
|
||||
version = "1.14.31"
|
||||
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.30/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.31/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.30/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.31/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.30/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.31/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.30/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.31/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.30/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.31/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"$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.30",
|
||||
"version": "1.14.31",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -198,7 +198,7 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho
|
||||
| `project` | `bridged` | list, current, git init, update |
|
||||
| `file` | `bridged` partial | find text/file/symbol, list/content/status |
|
||||
| `mcp` | `bridged` | status, add, OAuth, connect/disconnect |
|
||||
| `workspace` | `bridged` | adaptor/list/status/create/remove/session-restore |
|
||||
| `workspace` | `bridged` | adapter/list/status/create/remove/session-restore |
|
||||
| top-level instance routes | `bridged` | path, vcs, command, agent, skill, lsp, formatter, dispose |
|
||||
| experimental JSON routes | `bridged` | console, tool, worktree list/mutations, global session list, resource list |
|
||||
| `session` | `bridged` | read, lifecycle, prompt, message/part mutations, revert, permission reply |
|
||||
@@ -290,7 +290,7 @@ This checklist tracks bridge parity only. Checked routes are available through t
|
||||
|
||||
### Workspace Routes
|
||||
|
||||
- [x] `GET /experimental/workspace/adaptor` - list workspace adaptors.
|
||||
- [x] `GET /experimental/workspace/adapter` - list workspace adapters.
|
||||
- [x] `POST /experimental/workspace` - create workspace.
|
||||
- [x] `GET /experimental/workspace` - list workspaces.
|
||||
- [x] `GET /experimental/workspace/status` - workspace status.
|
||||
|
||||
@@ -353,7 +353,7 @@ piecewise.
|
||||
- [ ] `src/cli/cmd/tui/event.ts`
|
||||
- [ ] `src/cli/ui.ts`
|
||||
- [ ] `src/command/index.ts`
|
||||
- [x] `src/control-plane/adaptors/worktree.ts`
|
||||
- [x] `src/control-plane/adapters/worktree.ts`
|
||||
- [x] `src/control-plane/types.ts`
|
||||
- [x] `src/control-plane/workspace.ts`
|
||||
- [ ] `src/file/index.ts`
|
||||
|
||||
@@ -10,7 +10,7 @@ import { errorMessage } from "@/util/error"
|
||||
import { useSDK } from "../context/sdk"
|
||||
import { useToast } from "../ui/toast"
|
||||
|
||||
type Adaptor = {
|
||||
type Adapter = {
|
||||
type: string
|
||||
name: string
|
||||
description: string
|
||||
@@ -108,26 +108,26 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) =
|
||||
const sdk = useSDK()
|
||||
const toast = useToast()
|
||||
const [creating, setCreating] = createSignal<string>()
|
||||
const [adaptors, setAdaptors] = createSignal<Adaptor[]>()
|
||||
const [adapters, setAdapters] = createSignal<Adapter[]>()
|
||||
|
||||
onMount(() => {
|
||||
dialog.setSize("medium")
|
||||
void (async () => {
|
||||
const dir = sync.path.directory || sdk.directory
|
||||
const url = new URL("/experimental/workspace/adaptor", sdk.url)
|
||||
const url = new URL("/experimental/workspace/adapter", sdk.url)
|
||||
if (dir) url.searchParams.set("directory", dir)
|
||||
const res = await sdk
|
||||
.fetch(url)
|
||||
.then((x) => x.json() as Promise<Adaptor[]>)
|
||||
.then((x) => x.json() as Promise<Adapter[]>)
|
||||
.catch(() => undefined)
|
||||
if (!res) {
|
||||
toast.show({
|
||||
message: "Failed to load workspace adaptors",
|
||||
message: "Failed to load workspace adapters",
|
||||
variant: "error",
|
||||
})
|
||||
return
|
||||
}
|
||||
setAdaptors(res)
|
||||
setAdapters(res)
|
||||
})()
|
||||
})
|
||||
|
||||
@@ -142,13 +142,13 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) =
|
||||
},
|
||||
]
|
||||
}
|
||||
const list = adaptors()
|
||||
const list = adapters()
|
||||
if (!list) {
|
||||
return [
|
||||
{
|
||||
title: "Loading workspaces...",
|
||||
value: "loading" as const,
|
||||
description: "Fetching available workspace adaptors",
|
||||
description: "Fetching available workspace adapters",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BoxRenderable, MouseButton, MouseEvent, RGBA, TextAttributes } from "@opentui/core"
|
||||
import { useRenderer } from "@opentui/solid"
|
||||
import { For, createMemo, createSignal, onCleanup, onMount, type JSX } from "solid-js"
|
||||
import { useTheme, tint } from "@tui/context/theme"
|
||||
import * as Sound from "@tui/util/sound"
|
||||
@@ -554,6 +555,7 @@ function buildIdleState(t: number, ctx: LogoContext): IdleState {
|
||||
export function Logo(props: { shape?: LogoShape; ink?: RGBA; idle?: boolean } = {}) {
|
||||
const ctx = props.shape ? build(props.shape) : DEFAULT
|
||||
const { theme } = useTheme()
|
||||
const renderer = useRenderer()
|
||||
const [rings, setRings] = createSignal<Ring[]>([])
|
||||
const [hold, setHold] = createSignal<Hold>()
|
||||
const [release, setRelease] = createSignal<Release>()
|
||||
@@ -684,6 +686,7 @@ export function Logo(props: { shape?: LogoShape; ink?: RGBA; idle?: boolean } =
|
||||
})
|
||||
|
||||
const idleState = createMemo(() => (props.idle ? buildIdleState(frame().t, ctx) : undefined))
|
||||
const useSubpixelBlocks = () => renderer.capabilities?.rgb === true
|
||||
|
||||
const renderLine = (
|
||||
line: string,
|
||||
@@ -789,7 +792,7 @@ export function Logo(props: { shape?: LogoShape; ink?: RGBA; idle?: boolean } =
|
||||
}
|
||||
|
||||
// Solid █: render as ▀ so the top pixel (fg) and bottom pixel (bg) can carry independent shimmer values
|
||||
if (char === "█") {
|
||||
if (char === "█" && useSubpixelBlocks()) {
|
||||
return (
|
||||
<text
|
||||
fg={shade(inkTop, theme, n + p + e + b)}
|
||||
|
||||
72
packages/opencode/src/control-plane/adapters/index.ts
Normal file
72
packages/opencode/src/control-plane/adapters/index.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { ProjectID } from "@/project/schema"
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { WorkspaceAdapter as PluginWorkspaceAdapter, WorkspaceInfo as PluginWorkspaceInfo } from "@opencode-ai/plugin"
|
||||
import { EffectBridge } from "@/effect/bridge"
|
||||
import { errorMessage } from "@/util/error"
|
||||
import { type InternalWorkspaceAdapter, WorkspaceAdapterError, type WorkspaceAdapterEntry, WorkspaceInfo } from "../types"
|
||||
import type { Interface as WorktreeService } from "@/worktree"
|
||||
import { WorktreeAdapterEntry, worktreeAdapter } from "./worktree"
|
||||
|
||||
const BUILTIN: WorkspaceAdapterEntry[] = [{ type: "worktree", ...WorktreeAdapterEntry }]
|
||||
|
||||
export const makeBuiltinAdapters = (worktree: WorktreeService) =>
|
||||
new Map<string, InternalWorkspaceAdapter>([["worktree", worktreeAdapter(worktree)]])
|
||||
|
||||
const plugins = new Map<ProjectID, Map<string, InternalWorkspaceAdapter>>()
|
||||
const emptyBuiltinAdapters = new Map<string, InternalWorkspaceAdapter>()
|
||||
|
||||
export function getAdapter(
|
||||
projectID: ProjectID,
|
||||
type: string,
|
||||
builtin: ReadonlyMap<string, InternalWorkspaceAdapter> = emptyBuiltinAdapters,
|
||||
): InternalWorkspaceAdapter {
|
||||
const custom = plugins.get(projectID)?.get(type)
|
||||
if (custom) return custom
|
||||
|
||||
const adapter = builtin.get(type)
|
||||
if (adapter) return adapter
|
||||
|
||||
throw new Error(`Unknown workspace adapter: ${type}`)
|
||||
}
|
||||
|
||||
export async function listAdapters(projectID: ProjectID): Promise<WorkspaceAdapterEntry[]> {
|
||||
const custom = [...(plugins.get(projectID)?.entries() ?? [])].map(([type, adapter]) => ({
|
||||
type,
|
||||
name: adapter.name,
|
||||
description: adapter.description,
|
||||
}))
|
||||
return [...BUILTIN, ...custom]
|
||||
}
|
||||
|
||||
const adapterError = (cause: unknown) => new WorkspaceAdapterError({ message: errorMessage(cause), cause })
|
||||
const decodeInfo = (value: PluginWorkspaceInfo) =>
|
||||
Schema.decodeEffect(WorkspaceInfo)(value).pipe(Effect.mapError(adapterError))
|
||||
|
||||
function runPromiseAdapter<A>(fn: () => A | Promise<A>) {
|
||||
return Effect.gen(function* () {
|
||||
const bridge = yield* EffectBridge.make()
|
||||
return yield* bridge.run(Effect.tryPromise({
|
||||
try: () => Promise.resolve().then(fn),
|
||||
catch: adapterError,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
function fromPromiseAdapter(adapter: PluginWorkspaceAdapter): InternalWorkspaceAdapter {
|
||||
return {
|
||||
name: adapter.name,
|
||||
description: adapter.description,
|
||||
configure: (info) => runPromiseAdapter(() => adapter.configure(info)).pipe(Effect.flatMap(decodeInfo)),
|
||||
create: (info, env, from) => runPromiseAdapter(() => adapter.create(info, env, from)),
|
||||
remove: (info) => runPromiseAdapter(() => adapter.remove(info)),
|
||||
target: (info) => runPromiseAdapter(() => adapter.target(info)),
|
||||
}
|
||||
}
|
||||
|
||||
export function registerAdapter(projectID: ProjectID, type: string, adapter: PluginWorkspaceAdapter) {
|
||||
// Plugins can be loaded per-project so we need to scope them. If you
|
||||
// want to install a global one pass `ProjectID.global`.
|
||||
const adapters = plugins.get(projectID) ?? new Map<string, InternalWorkspaceAdapter>()
|
||||
adapters.set(type, fromPromiseAdapter(adapter))
|
||||
plugins.set(projectID, adapters)
|
||||
}
|
||||
76
packages/opencode/src/control-plane/adapters/worktree.ts
Normal file
76
packages/opencode/src/control-plane/adapters/worktree.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Cause, Effect, Schema } from "effect"
|
||||
import type { Interface as WorktreeService } from "@/worktree"
|
||||
import { type InternalWorkspaceAdapter, WorkspaceAdapterError, WorkspaceInfo } from "../types"
|
||||
|
||||
const WorktreeConfig = Schema.Struct({
|
||||
name: WorkspaceInfo.fields.name,
|
||||
branch: Schema.String,
|
||||
directory: Schema.String,
|
||||
})
|
||||
const decodeWorktreeConfig = Schema.decodeUnknownSync(WorktreeConfig)
|
||||
|
||||
export const WorktreeAdapterEntry = {
|
||||
name: "Worktree",
|
||||
description: "Create a git worktree",
|
||||
}
|
||||
|
||||
const adapterError = (message: string, cause: unknown) => new WorkspaceAdapterError({ message, cause })
|
||||
|
||||
const catchWorktreeError = <A, R>(effect: Effect.Effect<A, never, R>) =>
|
||||
effect.pipe(
|
||||
Effect.catchCause((cause) =>
|
||||
Cause.hasInterruptsOnly(cause) ? Effect.failCause(cause) : Effect.fail(adapterError(Cause.pretty(cause), cause)),
|
||||
),
|
||||
)
|
||||
|
||||
const decodeConfig = (info: WorkspaceInfo) =>
|
||||
Effect.try({
|
||||
try: () => decodeWorktreeConfig(info),
|
||||
catch: (cause) => adapterError(cause instanceof Error ? cause.message : String(cause), cause),
|
||||
})
|
||||
|
||||
export function worktreeAdapter(worktree: WorktreeService): InternalWorkspaceAdapter {
|
||||
return {
|
||||
...WorktreeAdapterEntry,
|
||||
configure(info) {
|
||||
return catchWorktreeError(
|
||||
Effect.gen(function* () {
|
||||
const next = yield* worktree.makeWorktreeInfo()
|
||||
return {
|
||||
...info,
|
||||
name: next.name,
|
||||
branch: next.branch,
|
||||
directory: next.directory,
|
||||
}
|
||||
}),
|
||||
)
|
||||
},
|
||||
create(info) {
|
||||
return Effect.gen(function* () {
|
||||
const config = yield* decodeConfig(info)
|
||||
yield* catchWorktreeError(
|
||||
worktree.createFromInfo({
|
||||
name: config.name,
|
||||
directory: config.directory,
|
||||
branch: config.branch,
|
||||
}),
|
||||
)
|
||||
})
|
||||
},
|
||||
remove(info) {
|
||||
return Effect.gen(function* () {
|
||||
const config = yield* decodeConfig(info)
|
||||
yield* catchWorktreeError(worktree.remove({ directory: config.directory }))
|
||||
})
|
||||
},
|
||||
target(info) {
|
||||
return Effect.gen(function* () {
|
||||
const config = yield* decodeConfig(info)
|
||||
return {
|
||||
type: "local" as const,
|
||||
directory: config.directory,
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import type { ProjectID } from "@/project/schema"
|
||||
import type { WorkspaceAdaptor, WorkspaceAdaptorEntry } from "../types"
|
||||
import { WorktreeAdaptor } from "./worktree"
|
||||
|
||||
const BUILTIN: Record<string, WorkspaceAdaptor> = {
|
||||
worktree: WorktreeAdaptor,
|
||||
}
|
||||
|
||||
const state = new Map<ProjectID, Map<string, WorkspaceAdaptor>>()
|
||||
|
||||
export function getAdaptor(projectID: ProjectID, type: string): WorkspaceAdaptor {
|
||||
const custom = state.get(projectID)?.get(type)
|
||||
if (custom) return custom
|
||||
|
||||
const builtin = BUILTIN[type]
|
||||
if (builtin) return builtin
|
||||
|
||||
throw new Error(`Unknown workspace adaptor: ${type}`)
|
||||
}
|
||||
|
||||
export async function listAdaptors(projectID: ProjectID): Promise<WorkspaceAdaptorEntry[]> {
|
||||
const builtin = await Promise.all(
|
||||
Object.entries(BUILTIN).map(async ([type, adaptor]) => {
|
||||
return {
|
||||
type,
|
||||
name: adaptor.name,
|
||||
description: adaptor.description,
|
||||
}
|
||||
}),
|
||||
)
|
||||
const custom = [...(state.get(projectID)?.entries() ?? [])].map(([type, adaptor]) => ({
|
||||
type,
|
||||
name: adaptor.name,
|
||||
description: adaptor.description,
|
||||
}))
|
||||
return [...builtin, ...custom]
|
||||
}
|
||||
|
||||
// Plugins can be loaded per-project so we need to scope them. If you
|
||||
// want to install a global one pass `ProjectID.global`
|
||||
export function registerAdaptor(projectID: ProjectID, type: string, adaptor: WorkspaceAdaptor) {
|
||||
const adaptors = state.get(projectID) ?? new Map<string, WorkspaceAdaptor>()
|
||||
adaptors.set(type, adaptor)
|
||||
state.set(projectID, adaptors)
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import { Schema } from "effect"
|
||||
import { type WorkspaceAdaptor, WorkspaceInfo } from "../types"
|
||||
|
||||
const WorktreeConfig = Schema.Struct({
|
||||
name: WorkspaceInfo.fields.name,
|
||||
branch: Schema.String,
|
||||
directory: Schema.String,
|
||||
})
|
||||
const decodeWorktreeConfig = Schema.decodeUnknownSync(WorktreeConfig)
|
||||
|
||||
async function loadWorktree() {
|
||||
const [{ AppRuntime }, { Worktree }] = await Promise.all([import("@/effect/app-runtime"), import("@/worktree")])
|
||||
return { AppRuntime, Worktree }
|
||||
}
|
||||
|
||||
export const WorktreeAdaptor: WorkspaceAdaptor = {
|
||||
name: "Worktree",
|
||||
description: "Create a git worktree",
|
||||
async configure(info) {
|
||||
const { AppRuntime, Worktree } = await loadWorktree()
|
||||
const next = await AppRuntime.runPromise(Worktree.Service.use((svc) => svc.makeWorktreeInfo()))
|
||||
return {
|
||||
...info,
|
||||
name: next.name,
|
||||
branch: next.branch,
|
||||
directory: next.directory,
|
||||
}
|
||||
},
|
||||
async create(info) {
|
||||
const { AppRuntime, Worktree } = await loadWorktree()
|
||||
const config = decodeWorktreeConfig(info)
|
||||
await AppRuntime.runPromise(
|
||||
Worktree.Service.use((svc) =>
|
||||
svc.createFromInfo({
|
||||
name: config.name,
|
||||
directory: config.directory,
|
||||
branch: config.branch,
|
||||
}),
|
||||
),
|
||||
)
|
||||
},
|
||||
async remove(info) {
|
||||
const { AppRuntime, Worktree } = await loadWorktree()
|
||||
const config = decodeWorktreeConfig(info)
|
||||
await AppRuntime.runPromise(Worktree.Service.use((svc) => svc.remove({ directory: config.directory })))
|
||||
},
|
||||
target(info) {
|
||||
const config = decodeWorktreeConfig(info)
|
||||
return {
|
||||
type: "local",
|
||||
directory: config.directory,
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Schema } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { ProjectID } from "@/project/schema"
|
||||
import { WorkspaceID } from "./schema"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
@@ -17,12 +17,12 @@ export const WorkspaceInfo = Schema.Struct({
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type WorkspaceInfo = DeepMutable<Schema.Schema.Type<typeof WorkspaceInfo>>
|
||||
|
||||
export const WorkspaceAdaptorEntry = Schema.Struct({
|
||||
export const WorkspaceAdapterEntry = Schema.Struct({
|
||||
type: Schema.String,
|
||||
name: Schema.String,
|
||||
description: Schema.String,
|
||||
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type WorkspaceAdaptorEntry = Schema.Schema.Type<typeof WorkspaceAdaptorEntry>
|
||||
export type WorkspaceAdapterEntry = Schema.Schema.Type<typeof WorkspaceAdapterEntry>
|
||||
|
||||
export type Target =
|
||||
| {
|
||||
@@ -35,11 +35,20 @@ export type Target =
|
||||
headers?: HeadersInit
|
||||
}
|
||||
|
||||
export type WorkspaceAdaptor = {
|
||||
export class WorkspaceAdapterError extends Schema.TaggedErrorClass<WorkspaceAdapterError>()("WorkspaceAdapterError", {
|
||||
message: Schema.String,
|
||||
cause: Schema.optional(Schema.Defect),
|
||||
}) {}
|
||||
|
||||
export type InternalWorkspaceAdapter = {
|
||||
name: string
|
||||
description: string
|
||||
configure(info: WorkspaceInfo): WorkspaceInfo | Promise<WorkspaceInfo>
|
||||
create(info: WorkspaceInfo, env: Record<string, string | undefined>, from?: WorkspaceInfo): Promise<void>
|
||||
remove(info: WorkspaceInfo): Promise<void>
|
||||
target(info: WorkspaceInfo): Target | Promise<Target>
|
||||
configure(info: WorkspaceInfo): Effect.Effect<WorkspaceInfo, WorkspaceAdapterError>
|
||||
create(
|
||||
info: WorkspaceInfo,
|
||||
env: Record<string, string | undefined>,
|
||||
from?: WorkspaceInfo,
|
||||
): Effect.Effect<void, WorkspaceAdapterError>
|
||||
remove(info: WorkspaceInfo): Effect.Effect<void, WorkspaceAdapterError>
|
||||
target(info: WorkspaceInfo): Effect.Effect<Target, WorkspaceAdapterError>
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ import { Filesystem } from "@/util/filesystem"
|
||||
import { ProjectID } from "@/project/schema"
|
||||
import { Slug } from "@opencode-ai/core/util/slug"
|
||||
import { WorkspaceTable } from "./workspace.sql"
|
||||
import { getAdaptor } from "./adaptors"
|
||||
import { type WorkspaceInfo, WorkspaceInfo as WorkspaceInfoSchema } from "./types"
|
||||
import { getAdapter, makeBuiltinAdapters } from "./adapters"
|
||||
import { type Target, type WorkspaceInfo, WorkspaceAdapterError, WorkspaceInfo as WorkspaceInfoSchema } from "./types"
|
||||
import { WorkspaceID } from "./schema"
|
||||
import { Session } from "@/session/session"
|
||||
import { SessionTable } from "@/session/session.sql"
|
||||
@@ -27,6 +27,7 @@ import { waitEvent } from "./util"
|
||||
import { WorkspaceContext } from "./workspace-context"
|
||||
import { NonNegativeInt, withStatics } from "@/util/schema"
|
||||
import { zod as effectZod, zodObject } from "@/util/effect-zod"
|
||||
import { Worktree } from "@/worktree"
|
||||
|
||||
export const Info = WorkspaceInfoSchema
|
||||
export type Info = WorkspaceInfo
|
||||
@@ -136,14 +137,15 @@ export class SyncAbortedError extends Schema.TaggedErrorClass<SyncAbortedError>(
|
||||
cause: Schema.optional(Schema.Defect),
|
||||
}) {}
|
||||
|
||||
type CreateError = Auth.AuthError
|
||||
type CreateError = Auth.AuthError | WorkspaceAdapterError | unknown
|
||||
type SessionRestoreError =
|
||||
| WorkspaceNotFoundError
|
||||
| SessionEventsNotFoundError
|
||||
| SessionRestoreHttpError
|
||||
| WorkspaceAdapterError
|
||||
| HttpClientError.HttpClientError
|
||||
type WaitForSyncError = SyncTimeoutError | SyncAbortedError
|
||||
type SyncLoopError = SyncHttpError | HttpClientError.HttpClientError
|
||||
type SyncLoopError = SyncHttpError | WorkspaceAdapterError | HttpClientError.HttpClientError
|
||||
|
||||
export interface Interface {
|
||||
readonly create: (input: CreateInput) => Effect.Effect<Info, CreateError>
|
||||
@@ -152,6 +154,7 @@ export interface Interface {
|
||||
readonly get: (id: WorkspaceID) => Effect.Effect<Info | undefined>
|
||||
readonly remove: (id: WorkspaceID) => Effect.Effect<Info | undefined>
|
||||
readonly status: () => Effect.Effect<ConnectionStatus[]>
|
||||
readonly target: (workspace: Info) => Effect.Effect<Target, WorkspaceAdapterError>
|
||||
readonly isSyncing: (workspaceID: WorkspaceID) => Effect.Effect<boolean>
|
||||
readonly waitForSync: (
|
||||
workspaceID: WorkspaceID,
|
||||
@@ -170,8 +173,16 @@ export const layer = Layer.effect(
|
||||
const session = yield* Session.Service
|
||||
const http = yield* HttpClient.HttpClient
|
||||
const sync = yield* SyncEvent.Service
|
||||
const worktree = yield* Worktree.Service
|
||||
const connections = new Map<WorkspaceID, ConnectionStatus>()
|
||||
const syncFibers = yield* FiberMap.make<WorkspaceID, void, SyncLoopError>()
|
||||
const builtinAdapters = makeBuiltinAdapters(worktree)
|
||||
const adapterFor = (space: { projectID: ProjectID; type: string }) =>
|
||||
getAdapter(space.projectID, space.type, builtinAdapters)
|
||||
|
||||
const target = Effect.fn("Workspace.target")(function* (space: Info) {
|
||||
return yield* adapterFor(space).target(space)
|
||||
})
|
||||
|
||||
const setStatus = (id: WorkspaceID, status: ConnectionStatus["status"]) => {
|
||||
const prev = connections.get(id)
|
||||
@@ -335,10 +346,9 @@ export const layer = Layer.effect(
|
||||
})
|
||||
|
||||
const syncWorkspaceLoop = Effect.fn("Workspace.syncWorkspaceLoop")(function* (space: Info) {
|
||||
const adaptor = getAdaptor(space.projectID, space.type)
|
||||
const target = yield* Effect.promise(() => Promise.resolve(adaptor.target(space)))
|
||||
const nextTarget = yield* target(space)
|
||||
|
||||
if (target.type === "local") return
|
||||
if (nextTarget.type === "local") return
|
||||
|
||||
let attempt = 0
|
||||
|
||||
@@ -346,8 +356,8 @@ export const layer = Layer.effect(
|
||||
log.info("connecting to global sync", { workspace: space.name })
|
||||
setStatus(space.id, "connecting")
|
||||
|
||||
const stream = yield* connectSSE(target.url, target.headers).pipe(
|
||||
Effect.tap(() => syncHistory(space, target.url, target.headers)),
|
||||
const stream = yield* connectSSE(nextTarget.url, nextTarget.headers).pipe(
|
||||
Effect.tap(() => syncHistory(space, nextTarget.url, nextTarget.headers)),
|
||||
Effect.catch((err) =>
|
||||
Effect.sync(() => {
|
||||
setStatus(space.id, "error")
|
||||
@@ -419,11 +429,10 @@ export const layer = Layer.effect(
|
||||
const startSync = Effect.fn("Workspace.startSync")(function* (space: Info) {
|
||||
if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) return
|
||||
|
||||
const adaptor = getAdaptor(space.projectID, space.type)
|
||||
const target = yield* Effect.promise(() => Promise.resolve(adaptor.target(space)))
|
||||
const nextTarget = yield* target(space)
|
||||
|
||||
if (target.type === "local") {
|
||||
setStatus(space.id, (yield* Effect.promise(() => Filesystem.exists(target.directory))) ? "connected" : "error")
|
||||
if (nextTarget.type === "local") {
|
||||
setStatus(space.id, (yield* Effect.promise(() => Filesystem.exists(nextTarget.directory))) ? "connected" : "error")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -458,10 +467,8 @@ export const layer = Layer.effect(
|
||||
|
||||
const create = Effect.fn("Workspace.create")(function* (input: CreateInput) {
|
||||
const id = WorkspaceID.ascending(input.id)
|
||||
const adaptor = getAdaptor(input.projectID, input.type)
|
||||
const config = yield* Effect.promise(() =>
|
||||
Promise.resolve(adaptor.configure({ ...input, id, name: Slug.create(), directory: null })),
|
||||
)
|
||||
const adapter = adapterFor(input)
|
||||
const config = yield* adapter.configure({ ...input, id, name: Slug.create(), directory: null })
|
||||
|
||||
const info: Info = {
|
||||
id,
|
||||
@@ -496,7 +503,7 @@ export const layer = Layer.effect(
|
||||
OTEL_RESOURCE_ATTRIBUTES: process.env.OTEL_RESOURCE_ATTRIBUTES,
|
||||
}
|
||||
|
||||
yield* Effect.promise(() => adaptor.create(config, env))
|
||||
yield* adapter.create(config, env)
|
||||
yield* Effect.all(
|
||||
[
|
||||
waitEvent({
|
||||
@@ -531,8 +538,7 @@ export const layer = Layer.effect(
|
||||
workspaceID: input.workspaceID,
|
||||
})
|
||||
|
||||
const adaptor = getAdaptor(space.projectID, space.type)
|
||||
const target = yield* Effect.promise(() => Promise.resolve(adaptor.target(space)))
|
||||
const nextTarget = yield* target(space)
|
||||
|
||||
yield* sync.run(Session.Event.Updated, {
|
||||
sessionID: input.sessionID,
|
||||
@@ -573,7 +579,7 @@ export const layer = Layer.effect(
|
||||
sessionID: input.sessionID,
|
||||
workspaceType: space.type,
|
||||
directory: space.directory,
|
||||
target: target.type === "remote" ? String(route(target.url, "/sync/replay")) : target.directory,
|
||||
target: nextTarget.type === "remote" ? String(route(nextTarget.url, "/sync/replay")) : nextTarget.directory,
|
||||
events: rows.length,
|
||||
batches: total,
|
||||
first: rows[0]?.seq,
|
||||
@@ -605,10 +611,10 @@ export const layer = Layer.effect(
|
||||
events: events.length,
|
||||
first: events[0]?.seq,
|
||||
last: events.at(-1)?.seq,
|
||||
target: target.type === "remote" ? String(route(target.url, "/sync/replay")) : target.directory,
|
||||
target: nextTarget.type === "remote" ? String(route(nextTarget.url, "/sync/replay")) : nextTarget.directory,
|
||||
})
|
||||
|
||||
if (target.type === "local") {
|
||||
if (nextTarget.type === "local") {
|
||||
yield* sync.replayAll(events)
|
||||
log.info("session restore batch replayed locally", {
|
||||
workspaceID: input.workspaceID,
|
||||
@@ -618,10 +624,10 @@ export const layer = Layer.effect(
|
||||
events: events.length,
|
||||
})
|
||||
} else {
|
||||
const url = route(target.url, "/sync/replay")
|
||||
const url = route(nextTarget.url, "/sync/replay")
|
||||
const res = yield* http.execute(
|
||||
HttpClientRequest.post(url, {
|
||||
headers: new Headers(target.headers),
|
||||
headers: new Headers(nextTarget.headers),
|
||||
body: HttpBody.jsonUnsafe({
|
||||
directory: space.directory ?? "",
|
||||
events,
|
||||
@@ -726,12 +732,11 @@ export const layer = Layer.effect(
|
||||
const info = fromRow(row)
|
||||
yield* Effect.catch(
|
||||
Effect.gen(function* () {
|
||||
const adaptor = getAdaptor(info.projectID, row.type)
|
||||
yield* Effect.tryPromise(() => Promise.resolve(adaptor.remove(info)))
|
||||
yield* adapterFor(info).remove(info)
|
||||
}),
|
||||
() =>
|
||||
Effect.sync(() => {
|
||||
log.error("adaptor not available when removing workspace", { type: row.type })
|
||||
log.error("adapter not available when removing workspace", { type: row.type })
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -818,6 +823,7 @@ export const layer = Layer.effect(
|
||||
get,
|
||||
remove,
|
||||
status,
|
||||
target,
|
||||
isSyncing,
|
||||
waitForSync,
|
||||
startWorkspaceSyncing,
|
||||
@@ -829,6 +835,7 @@ export const defaultLayer = layer.pipe(
|
||||
Layer.provide(Auth.defaultLayer),
|
||||
Layer.provide(Session.defaultLayer),
|
||||
Layer.provide(SyncEvent.defaultLayer),
|
||||
Layer.provide(Worktree.defaultLayer),
|
||||
Layer.provide(FetchHttpClient.layer),
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import type {
|
||||
PluginInput,
|
||||
Plugin as PluginInstance,
|
||||
PluginModule,
|
||||
WorkspaceAdaptor as PluginWorkspaceAdaptor,
|
||||
WorkspaceAdapter as PluginWorkspaceAdapter,
|
||||
} from "@opencode-ai/plugin"
|
||||
import { Config } from "@/config/config"
|
||||
import { Bus } from "../bus"
|
||||
@@ -24,8 +24,7 @@ import { InstanceState } from "@/effect/instance-state"
|
||||
import { errorMessage } from "@/util/error"
|
||||
import { PluginLoader } from "./loader"
|
||||
import { parsePluginSpecifier, readPluginId, readV1Plugin, resolvePluginId } from "./shared"
|
||||
import { registerAdaptor } from "@/control-plane/adaptors"
|
||||
import type { WorkspaceAdaptor } from "@/control-plane/types"
|
||||
import { registerAdapter } from "@/control-plane/adapters"
|
||||
|
||||
const log = Log.create({ service: "plugin" })
|
||||
|
||||
@@ -138,8 +137,8 @@ export const layer = Layer.effect(
|
||||
worktree: ctx.worktree,
|
||||
directory: ctx.directory,
|
||||
experimental_workspace: {
|
||||
register(type: string, adaptor: PluginWorkspaceAdaptor) {
|
||||
registerAdaptor(ctx.project.id, type, adaptor as WorkspaceAdaptor)
|
||||
register(type: string, adapter: PluginWorkspaceAdapter) {
|
||||
registerAdapter(ctx.project.id, type, adapter)
|
||||
},
|
||||
},
|
||||
get serverUrl(): URL {
|
||||
|
||||
@@ -2,10 +2,10 @@ import { Hono } from "hono"
|
||||
import { describeRoute, resolver, validator } from "hono-openapi"
|
||||
import z from "zod"
|
||||
import { Effect } from "effect"
|
||||
import { listAdaptors } from "@/control-plane/adaptors"
|
||||
import { listAdapters } from "@/control-plane/adapters"
|
||||
import { Workspace } from "@/control-plane/workspace"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { WorkspaceAdaptorEntry } from "@/control-plane/types"
|
||||
import { WorkspaceAdapterEntry } from "@/control-plane/types"
|
||||
import { zodObject } from "@/util/effect-zod"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { errors } from "../../error"
|
||||
@@ -18,24 +18,24 @@ const log = Log.create({ service: "server.workspace" })
|
||||
export const WorkspaceRoutes = lazy(() =>
|
||||
new Hono()
|
||||
.get(
|
||||
"/adaptor",
|
||||
"/adapter",
|
||||
describeRoute({
|
||||
summary: "List workspace adaptors",
|
||||
description: "List all available workspace adaptors for the current project.",
|
||||
operationId: "experimental.workspace.adaptor.list",
|
||||
summary: "List workspace adapters",
|
||||
description: "List all available workspace adapters for the current project.",
|
||||
operationId: "experimental.workspace.adapter.list",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Workspace adaptors",
|
||||
description: "Workspace adapters",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.array(zodObject(WorkspaceAdaptorEntry))),
|
||||
schema: resolver(z.array(zodObject(WorkspaceAdapterEntry))),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
return c.json(await listAdaptors(Instance.project.id))
|
||||
return c.json(await listAdapters(Instance.project.id))
|
||||
},
|
||||
)
|
||||
.post(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Workspace } from "@/control-plane/workspace"
|
||||
import { WorkspaceAdaptorEntry } from "@/control-plane/types"
|
||||
import { WorkspaceAdapterEntry } from "@/control-plane/types"
|
||||
import { NonNegativeInt } from "@/util/schema"
|
||||
import { Schema, Struct } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
@@ -16,7 +16,7 @@ export const SessionRestoreResponse = Schema.Struct({
|
||||
})
|
||||
|
||||
export const WorkspacePaths = {
|
||||
adaptors: `${root}/adaptor`,
|
||||
adapters: `${root}/adapter`,
|
||||
list: root,
|
||||
status: `${root}/status`,
|
||||
remove: `${root}/:id`,
|
||||
@@ -27,13 +27,13 @@ export const WorkspaceApi = HttpApi.make("workspace")
|
||||
.add(
|
||||
HttpApiGroup.make("workspace")
|
||||
.add(
|
||||
HttpApiEndpoint.get("adaptors", WorkspacePaths.adaptors, {
|
||||
success: described(Schema.Array(WorkspaceAdaptorEntry), "Workspace adaptors"),
|
||||
HttpApiEndpoint.get("adapters", WorkspacePaths.adapters, {
|
||||
success: described(Schema.Array(WorkspaceAdapterEntry), "Workspace adapters"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "experimental.workspace.adaptor.list",
|
||||
summary: "List workspace adaptors",
|
||||
description: "List all available workspace adaptors for the current project.",
|
||||
identifier: "experimental.workspace.adapter.list",
|
||||
summary: "List workspace adapters",
|
||||
description: "List all available workspace adapters for the current project.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("list", WorkspacePaths.list, {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { listAdaptors } from "@/control-plane/adaptors"
|
||||
import { listAdapters } from "@/control-plane/adapters"
|
||||
import { Workspace } from "@/control-plane/workspace"
|
||||
import * as InstanceState from "@/effect/instance-state"
|
||||
import { Effect } from "effect"
|
||||
@@ -10,9 +10,9 @@ export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspac
|
||||
Effect.gen(function* () {
|
||||
const workspace = yield* Workspace.Service
|
||||
|
||||
const adaptors = Effect.fn("WorkspaceHttpApi.adaptors")(function* () {
|
||||
const adapters = Effect.fn("WorkspaceHttpApi.adapters")(function* () {
|
||||
const instance = yield* InstanceState.context
|
||||
return yield* Effect.promise(() => listAdaptors(instance.project.id))
|
||||
return yield* Effect.promise(() => listAdapters(instance.project.id))
|
||||
})
|
||||
|
||||
const list = Effect.fn("WorkspaceHttpApi.list")(function* () {
|
||||
@@ -51,7 +51,7 @@ export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspac
|
||||
})
|
||||
|
||||
return handlers
|
||||
.handle("adaptors", adaptors)
|
||||
.handle("adapters", adapters)
|
||||
.handle("list", list)
|
||||
.handle("create", create)
|
||||
.handle("status", status)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { getAdaptor } from "@/control-plane/adaptors"
|
||||
import { WorkspaceID } from "@/control-plane/schema"
|
||||
import type { Target } from "@/control-plane/types"
|
||||
import type { Target, WorkspaceAdapterError } from "@/control-plane/types"
|
||||
import { Workspace } from "@/control-plane/workspace"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { Session } from "@/session/session"
|
||||
@@ -8,7 +7,7 @@ import { HttpApiProxy } from "./proxy"
|
||||
import * as Fence from "@/server/fence"
|
||||
import { getWorkspaceRouteSessionID, isLocalWorkspaceRoute, workspaceProxyURL } from "@/server/workspace"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Context, Data, Effect, Layer } from "effect"
|
||||
import { Cause, Context, Data, Effect, Exit, Layer } from "effect"
|
||||
import { HttpClient, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
|
||||
import { HttpApiMiddleware } from "effect/unstable/httpapi"
|
||||
import * as Socket from "effect/unstable/socket/Socket"
|
||||
@@ -17,6 +16,7 @@ type RemoteTarget = Extract<Target, { type: "remote" }>
|
||||
|
||||
type RequestPlan = Data.TaggedEnum<{
|
||||
MissingWorkspace: { readonly workspaceID: WorkspaceID }
|
||||
TargetError: { readonly workspaceID: WorkspaceID; readonly message: string }
|
||||
Local: { readonly directory: string; readonly workspaceID?: WorkspaceID }
|
||||
Remote: {
|
||||
readonly request: HttpServerRequest.HttpServerRequest
|
||||
@@ -87,13 +87,17 @@ function missingWorkspaceResponse(id: WorkspaceID): HttpServerResponse.HttpServe
|
||||
})
|
||||
}
|
||||
|
||||
function resolveTarget(workspace: Workspace.Info): Effect.Effect<Target> {
|
||||
return Effect.gen(function* () {
|
||||
const adaptor = yield* Effect.sync(() => getAdaptor(workspace.projectID, workspace.type))
|
||||
return yield* Effect.promise(() => Promise.resolve(adaptor.target(workspace)))
|
||||
function targetErrorResponse(id: WorkspaceID, message: string): HttpServerResponse.HttpServerResponse {
|
||||
return HttpServerResponse.text(`Workspace target unavailable: ${id}\n${message}`, {
|
||||
status: 503,
|
||||
contentType: "text/plain; charset=utf-8",
|
||||
})
|
||||
}
|
||||
|
||||
function resolveTarget(workspace: Workspace.Info): Effect.Effect<Target, WorkspaceAdapterError, Workspace.Service> {
|
||||
return Workspace.Service.use((svc) => svc.target(workspace))
|
||||
}
|
||||
|
||||
function proxyRemote(
|
||||
client: HttpClient.HttpClient,
|
||||
request: HttpServerRequest.HttpServerRequest,
|
||||
@@ -135,7 +139,11 @@ function planWorkspaceRequest(
|
||||
workspace: Workspace.Info,
|
||||
): Effect.Effect<RequestPlan, never, Workspace.Service> {
|
||||
return Effect.gen(function* () {
|
||||
const target = yield* resolveTarget(workspace)
|
||||
const exit = yield* resolveTarget(workspace).pipe(Effect.exit)
|
||||
if (Exit.isFailure(exit)) {
|
||||
return RequestPlan.TargetError({ workspaceID: workspace.id, message: Cause.pretty(exit.cause) })
|
||||
}
|
||||
const target = exit.value
|
||||
if (target.type === "remote") return RequestPlan.Remote({ request, workspace, target, url })
|
||||
return RequestPlan.Local({ directory: target.directory, workspaceID: workspace.id })
|
||||
})
|
||||
@@ -170,6 +178,7 @@ function routeWorkspace<E>(
|
||||
): Effect.Effect<HttpServerResponse.HttpServerResponse, E, Socket.WebSocketConstructor | Workspace.Service> {
|
||||
return RequestPlan.$match(plan, {
|
||||
MissingWorkspace: ({ workspaceID }) => Effect.succeed(missingWorkspaceResponse(workspaceID)),
|
||||
TargetError: ({ workspaceID, message }) => Effect.succeed(targetErrorResponse(workspaceID, message)),
|
||||
Remote: ({ request, workspace, target, url }) => proxyRemote(client, request, workspace, target, url),
|
||||
Local: ({ directory, workspaceID }) =>
|
||||
effect.pipe(Effect.provideService(WorkspaceRouteContext, WorkspaceRouteContext.of({ directory, workspaceID }))),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { MiddlewareHandler } from "hono"
|
||||
import type { UpgradeWebSocket } from "hono/ws"
|
||||
import { getAdaptor } from "@/control-plane/adaptors"
|
||||
import { WorkspaceID } from "@/control-plane/schema"
|
||||
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
||||
import { Workspace } from "@/control-plane/workspace"
|
||||
@@ -10,7 +9,7 @@ import { Instance } from "@/project/instance"
|
||||
import { Session } from "@/session/session"
|
||||
import { SessionID } from "@/session/schema"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { Effect } from "effect"
|
||||
import { Cause, Effect, Exit } from "effect"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { ServerProxy } from "./proxy"
|
||||
|
||||
@@ -91,8 +90,16 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
|
||||
return next()
|
||||
}
|
||||
|
||||
const adaptor = getAdaptor(workspace.projectID, workspace.type)
|
||||
const target = await adaptor.target(workspace)
|
||||
const targetExit = await AppRuntime.runPromiseExit(Workspace.Service.use((svc) => svc.target(workspace)))
|
||||
if (Exit.isFailure(targetExit)) {
|
||||
return new Response(`Workspace target unavailable: ${workspace.id}\n${Cause.pretty(targetExit.cause)}`, {
|
||||
status: 503,
|
||||
headers: {
|
||||
"content-type": "text/plain; charset=utf-8",
|
||||
},
|
||||
})
|
||||
}
|
||||
const target = targetExit.value
|
||||
|
||||
if (target.type === "local") {
|
||||
return WorkspaceContext.provide({
|
||||
|
||||
@@ -772,7 +772,7 @@ export const toModelMessagesEffect = Effect.fnUntraced(function* (
|
||||
return {
|
||||
type: "content",
|
||||
value: [
|
||||
{ type: "text", text: outputObject.text },
|
||||
...(outputObject.text ? [{ type: "text", text: outputObject.text }] : []),
|
||||
...attachments.map((attachment) => ({
|
||||
type: "media",
|
||||
mediaType: attachment.mime,
|
||||
@@ -938,10 +938,18 @@ export const toModelMessagesEffect = Effect.fnUntraced(function* (
|
||||
})
|
||||
}
|
||||
if (part.type === "reasoning") {
|
||||
if (differentModel) {
|
||||
if (part.text.trim().length > 0)
|
||||
assistantMessage.parts.push({
|
||||
type: "text",
|
||||
text: part.text,
|
||||
})
|
||||
continue
|
||||
}
|
||||
assistantMessage.parts.push({
|
||||
type: "reasoning",
|
||||
text: part.text,
|
||||
...(differentModel ? {} : { providerMetadata: part.metadata }),
|
||||
providerMetadata: part.metadata,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,9 +142,9 @@ const Share = Schema.Struct({
|
||||
url: Schema.String,
|
||||
})
|
||||
|
||||
// Legacy HTTP accepted any number here, and persisted data may already contain
|
||||
// negative values. Keep archive timestamps permissive while other clocks stay non-negative.
|
||||
export const ArchivedTimestamp = Schema.Number
|
||||
// Legacy HTTP accepted negative values here. Keep archive timestamps permissive
|
||||
// while excluding non-finite values that cannot round-trip through JSON.
|
||||
export const ArchivedTimestamp = Schema.Finite
|
||||
|
||||
const Time = Schema.Struct({
|
||||
created: NonNegativeInt,
|
||||
|
||||
@@ -94,7 +94,7 @@ Importantly, **sync events automatically re-publish as bus events**. This makes
|
||||
|
||||
### Event shape
|
||||
|
||||
- The shape of the events are slightly different. A sync event has the `type`, `id`, `seq`, `aggregateID`, and `data` fields. A bus event has the `type` and `properties` fields. `data` and `properties` are largely the same thing. This conversion is automatically handled when the sync system re-published the event throught the bus.
|
||||
- The shape of the events are slightly different. A sync event has the `type`, `id`, `seq`, `aggregateID`, and `data` fields. A bus event has the `type` and `properties` fields. `data` and `properties` are largely the same thing. This conversion is automatically handled when the sync system re-published the event through the bus.
|
||||
|
||||
The reason for this is because sync events need to track more information. I chose not to copy the `properties` naming to more clearly disambiguate the event types.
|
||||
|
||||
@@ -112,9 +112,9 @@ The system install projectors in `server/projectors.js`. It calls `SyncEvent.ini
|
||||
|
||||
This allows you to "reshape" an event from the sync system before it's published to the bus. This should be avoided, but might be necessary for temporary backwards compat.
|
||||
|
||||
The only time we use this is the `session.updated` event. Previously this event contained the entire session object. The sync even only contains the fields updated. We convert the event to contain to full object for backwards compatibility (but ideally we'd remove this).
|
||||
The only time we use this is the `session.updated` event. Previously this event contained the entire session object. The sync event only contains the fields updated. We convert the event to contain the full object for backwards compatibility (but ideally we'd remove this).
|
||||
|
||||
It's very important that types are correct when working with events. Event definitions have a `schema` which carries the defintiion of the event shape (provided by a zod schema, inferred into a TypeScript type). Examples:
|
||||
It's very important that types are correct when working with events. Event definitions have a `schema` which carries the definition of the event shape (provided by a zod schema, inferred into a TypeScript type). Examples:
|
||||
|
||||
```ts
|
||||
// The schema from `Updated` typechecks the object correctly
|
||||
|
||||
@@ -5,6 +5,7 @@ import { provideInstance, tmpdir } from "../fixture/fixture"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Agent } from "../../src/agent/agent"
|
||||
import { Permission } from "../../src/permission"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
|
||||
// Helper to evaluate permission for a tool with wildcard pattern
|
||||
function evalPerm(agent: Agent.Info | undefined, permission: string): Permission.Action | undefined {
|
||||
@@ -83,7 +84,7 @@ test("explore agent denies edit and write", async () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("explore agent asks for external directories and allows Truncate.GLOB", async () => {
|
||||
test("explore agent asks for external directories and allows whitelisted external paths", async () => {
|
||||
const { Truncate } = await import("../../src/tool/truncate")
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
@@ -93,6 +94,9 @@ test("explore agent asks for external directories and allows Truncate.GLOB", asy
|
||||
expect(explore).toBeDefined()
|
||||
expect(Permission.evaluate("external_directory", "/some/other/path", explore!.permission).action).toBe("ask")
|
||||
expect(Permission.evaluate("external_directory", Truncate.GLOB, explore!.permission).action).toBe("allow")
|
||||
expect(
|
||||
Permission.evaluate("external_directory", path.join(Global.Path.tmp, "agent-work"), explore!.permission).action,
|
||||
).toBe("allow")
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -515,6 +519,20 @@ test("Truncate.GLOB is allowed even when user denies external_directory globally
|
||||
})
|
||||
})
|
||||
|
||||
test("global tmp directory children are allowed for external_directory", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const build = await load(tmp.path, (svc) => svc.get("build"))
|
||||
expect(
|
||||
Permission.evaluate("external_directory", path.join(Global.Path.tmp, "scratch"), build!.permission).action,
|
||||
).toBe("allow")
|
||||
expect(Permission.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("ask")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("Truncate.GLOB is allowed even when user denies external_directory per-agent", async () => {
|
||||
const { Truncate } = await import("../../src/tool/truncate")
|
||||
await using tmp = await tmpdir({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { getAdaptor, registerAdaptor } from "../../src/control-plane/adaptors"
|
||||
import { Effect } from "effect"
|
||||
import { getAdapter, registerAdapter } from "../../src/control-plane/adapters"
|
||||
import { ProjectID } from "../../src/project/schema"
|
||||
import type { WorkspaceInfo } from "../../src/control-plane/types"
|
||||
|
||||
@@ -15,7 +16,7 @@ function info(projectID: WorkspaceInfo["projectID"], type: string): WorkspaceInf
|
||||
}
|
||||
}
|
||||
|
||||
function adaptor(dir: string) {
|
||||
function adapter(dir: string) {
|
||||
return {
|
||||
name: dir,
|
||||
description: dir,
|
||||
@@ -33,19 +34,19 @@ function adaptor(dir: string) {
|
||||
}
|
||||
}
|
||||
|
||||
describe("control-plane/adaptors", () => {
|
||||
test("isolates custom adaptors by project", async () => {
|
||||
describe("control-plane/adapters", () => {
|
||||
test("isolates custom adapters by project", async () => {
|
||||
const type = `demo-${Math.random().toString(36).slice(2)}`
|
||||
const one = ProjectID.make(`project-${Math.random().toString(36).slice(2)}`)
|
||||
const two = ProjectID.make(`project-${Math.random().toString(36).slice(2)}`)
|
||||
registerAdaptor(one, type, adaptor("/one"))
|
||||
registerAdaptor(two, type, adaptor("/two"))
|
||||
registerAdapter(one, type, adapter("/one"))
|
||||
registerAdapter(two, type, adapter("/two"))
|
||||
|
||||
expect(await (await getAdaptor(one, type)).target(info(one, type))).toEqual({
|
||||
expect(await Effect.runPromise(getAdapter(one, type).target(info(one, type)))).toEqual({
|
||||
type: "local",
|
||||
directory: "/one",
|
||||
})
|
||||
expect(await (await getAdaptor(two, type)).target(info(two, type))).toEqual({
|
||||
expect(await Effect.runPromise(getAdapter(two, type).target(info(two, type)))).toEqual({
|
||||
type: "local",
|
||||
directory: "/two",
|
||||
})
|
||||
@@ -54,16 +55,16 @@ describe("control-plane/adaptors", () => {
|
||||
test("latest install wins within a project", async () => {
|
||||
const type = `demo-${Math.random().toString(36).slice(2)}`
|
||||
const id = ProjectID.make(`project-${Math.random().toString(36).slice(2)}`)
|
||||
registerAdaptor(id, type, adaptor("/one"))
|
||||
registerAdapter(id, type, adapter("/one"))
|
||||
|
||||
expect(await (await getAdaptor(id, type)).target(info(id, type))).toEqual({
|
||||
expect(await Effect.runPromise(getAdapter(id, type).target(info(id, type)))).toEqual({
|
||||
type: "local",
|
||||
directory: "/one",
|
||||
})
|
||||
|
||||
registerAdaptor(id, type, adaptor("/two"))
|
||||
registerAdapter(id, type, adapter("/two"))
|
||||
|
||||
expect(await (await getAdaptor(id, type)).target(info(id, type))).toEqual({
|
||||
expect(await Effect.runPromise(getAdapter(id, type).target(info(id, type)))).toEqual({
|
||||
type: "local",
|
||||
directory: "/two",
|
||||
})
|
||||
@@ -4,6 +4,7 @@ import Http from "node:http"
|
||||
import path from "node:path"
|
||||
import { setTimeout as delay } from "node:timers/promises"
|
||||
import { NodeHttpServer } from "@effect/platform-node"
|
||||
import type { WorkspaceAdapter, WorkspaceInfo as PluginWorkspaceInfo } from "@opencode-ai/plugin"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { HttpServer, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
|
||||
import { asc, eq } from "drizzle-orm"
|
||||
@@ -23,10 +24,10 @@ import { EventSequenceTable, EventTable } from "@/sync/event.sql"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { provideTmpdirInstance, tmpdir } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { registerAdaptor } from "../../src/control-plane/adaptors"
|
||||
import { registerAdapter } from "../../src/control-plane/adapters"
|
||||
import { WorkspaceID } from "../../src/control-plane/schema"
|
||||
import { WorkspaceTable } from "../../src/control-plane/workspace.sql"
|
||||
import type { Target, WorkspaceAdaptor, WorkspaceInfo } from "../../src/control-plane/types"
|
||||
import type { Target, WorkspaceInfo } from "../../src/control-plane/types"
|
||||
import * as WorkspaceOld from "../../src/control-plane/workspace"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
|
||||
@@ -48,18 +49,18 @@ const originalEnv = {
|
||||
}
|
||||
|
||||
type RecordedCreate = {
|
||||
info: WorkspaceInfo
|
||||
info: PluginWorkspaceInfo
|
||||
env: Record<string, string | undefined>
|
||||
from?: WorkspaceInfo
|
||||
from?: PluginWorkspaceInfo
|
||||
}
|
||||
|
||||
type RecordedAdaptor = {
|
||||
adaptor: WorkspaceAdaptor
|
||||
type RecordedAdapter = {
|
||||
adapter: WorkspaceAdapter
|
||||
calls: {
|
||||
configure: WorkspaceInfo[]
|
||||
configure: PluginWorkspaceInfo[]
|
||||
create: RecordedCreate[]
|
||||
remove: WorkspaceInfo[]
|
||||
target: WorkspaceInfo[]
|
||||
remove: PluginWorkspaceInfo[]
|
||||
target: PluginWorkspaceInfo[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,13 +166,17 @@ function eventuallyEffect(effect: Effect.Effect<void>, timeout = 1500) {
|
||||
})
|
||||
}
|
||||
|
||||
function recordedAdaptor(input: {
|
||||
target: (info: WorkspaceInfo) => Target | Promise<Target>
|
||||
configure?: (info: WorkspaceInfo) => WorkspaceInfo | Promise<WorkspaceInfo>
|
||||
create?: (info: WorkspaceInfo, env: Record<string, string | undefined>, from?: WorkspaceInfo) => Promise<void>
|
||||
remove?: (info: WorkspaceInfo) => Promise<void>
|
||||
}): RecordedAdaptor {
|
||||
const calls: RecordedAdaptor["calls"] = {
|
||||
function recordedAdapter(input: {
|
||||
target: (info: PluginWorkspaceInfo) => Target | Promise<Target>
|
||||
configure?: (info: PluginWorkspaceInfo) => PluginWorkspaceInfo | Promise<PluginWorkspaceInfo>
|
||||
create?: (
|
||||
info: PluginWorkspaceInfo,
|
||||
env: Record<string, string | undefined>,
|
||||
from?: PluginWorkspaceInfo,
|
||||
) => Promise<void>
|
||||
remove?: (info: PluginWorkspaceInfo) => Promise<void>
|
||||
}): RecordedAdapter {
|
||||
const calls: RecordedAdapter["calls"] = {
|
||||
configure: [],
|
||||
create: [],
|
||||
remove: [],
|
||||
@@ -180,7 +185,7 @@ function recordedAdaptor(input: {
|
||||
|
||||
return {
|
||||
calls,
|
||||
adaptor: {
|
||||
adapter: {
|
||||
name: "recorded",
|
||||
description: "recorded",
|
||||
configure(info) {
|
||||
@@ -207,8 +212,8 @@ function recordedAdaptor(input: {
|
||||
}
|
||||
}
|
||||
|
||||
function localAdaptor(dir: string, input?: { createDir?: boolean; remove?: (info: WorkspaceInfo) => Promise<void> }) {
|
||||
return recordedAdaptor({
|
||||
function localAdapter(dir: string, input?: { createDir?: boolean; remove?: (info: PluginWorkspaceInfo) => Promise<void> }) {
|
||||
return recordedAdapter({
|
||||
configure(info) {
|
||||
return { ...info, directory: dir }
|
||||
},
|
||||
@@ -223,8 +228,8 @@ function localAdaptor(dir: string, input?: { createDir?: boolean; remove?: (info
|
||||
})
|
||||
}
|
||||
|
||||
function remoteAdaptor(url: string, input?: { directory?: string | null; headers?: HeadersInit }) {
|
||||
return recordedAdaptor({
|
||||
function remoteAdapter(url: string, input?: { directory?: string | null; headers?: HeadersInit }) {
|
||||
return recordedAdapter({
|
||||
configure(info) {
|
||||
return { ...info, directory: input?.directory ?? info.directory }
|
||||
},
|
||||
@@ -429,7 +434,7 @@ describe("workspace-old CRUD", () => {
|
||||
const workspaceID = WorkspaceID.ascending("wrk_create_local")
|
||||
const type = unique("create-local")
|
||||
const targetDir = path.join(dir, "created-local")
|
||||
const recorded = recordedAdaptor({
|
||||
const recorded = recordedAdapter({
|
||||
configure(info) {
|
||||
return {
|
||||
...info,
|
||||
@@ -446,7 +451,7 @@ describe("workspace-old CRUD", () => {
|
||||
return { type: "local", directory: targetDir }
|
||||
},
|
||||
})
|
||||
registerAdaptor(Instance.project.id, type, recorded.adaptor)
|
||||
registerAdapter(Instance.project.id, type, recorded.adapter)
|
||||
|
||||
const info = await createWorkspace({
|
||||
id: workspaceID,
|
||||
@@ -489,17 +494,17 @@ describe("workspace-old CRUD", () => {
|
||||
test("create propagates configure failures and does not insert a workspace", async () => {
|
||||
await withInstance(async () => {
|
||||
const type = unique("configure-failure")
|
||||
registerAdaptor(
|
||||
registerAdapter(
|
||||
Instance.project.id,
|
||||
type,
|
||||
recordedAdaptor({
|
||||
recordedAdapter({
|
||||
configure() {
|
||||
throw new Error("configure exploded")
|
||||
},
|
||||
target() {
|
||||
return { type: "local", directory: "/unused" }
|
||||
},
|
||||
}).adaptor,
|
||||
}).adapter,
|
||||
)
|
||||
|
||||
await expect(
|
||||
@@ -509,10 +514,10 @@ describe("workspace-old CRUD", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("create leaves the inserted row when adaptor create fails", async () => {
|
||||
test("create leaves the inserted row when adapter create fails", async () => {
|
||||
await withInstance(async () => {
|
||||
const type = unique("create-failure")
|
||||
const recorded = recordedAdaptor({
|
||||
const recorded = recordedAdapter({
|
||||
async create() {
|
||||
throw new Error("create exploded")
|
||||
},
|
||||
@@ -520,7 +525,7 @@ describe("workspace-old CRUD", () => {
|
||||
return { type: "local", directory: "/unused" }
|
||||
},
|
||||
})
|
||||
registerAdaptor(Instance.project.id, type, recorded.adaptor)
|
||||
registerAdapter(Instance.project.id, type, recorded.adapter)
|
||||
|
||||
await expect(
|
||||
createWorkspace({ type, branch: "branch", projectID: Instance.project.id, extra: { x: 1 } }),
|
||||
@@ -538,8 +543,8 @@ describe("workspace-old CRUD", () => {
|
||||
await withInstance(async (dir) => {
|
||||
const type = unique("local-error")
|
||||
const missing = path.join(dir, "missing-local-target")
|
||||
const recorded = localAdaptor(missing, { createDir: false })
|
||||
registerAdaptor(Instance.project.id, type, recorded.adaptor)
|
||||
const recorded = localAdapter(missing, { createDir: false })
|
||||
registerAdapter(Instance.project.id, type, recorded.adapter)
|
||||
|
||||
const info = await createWorkspace({ type, branch: null, projectID: Instance.project.id, extra: null })
|
||||
|
||||
@@ -576,8 +581,8 @@ describe("workspace-old CRUD", () => {
|
||||
Effect.gen(function* () {
|
||||
const workspace = yield* WorkspaceOld.Service
|
||||
const type = unique("remote-create")
|
||||
const recorded = remoteAdaptor(`${url}/base/?ignored=1#hash`, { directory: dir })
|
||||
registerAdaptor(Instance.project.id, type, recorded.adaptor)
|
||||
const recorded = remoteAdapter(`${url}/base/?ignored=1#hash`, { directory: dir })
|
||||
registerAdapter(Instance.project.id, type, recorded.adapter)
|
||||
|
||||
const info = yield* workspace.create({ type, branch: null, projectID: Instance.project.id, extra: null })
|
||||
|
||||
@@ -603,11 +608,11 @@ describe("workspace-old CRUD", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("remove deletes the workspace, associated sessions, adaptor resources, and status", async () => {
|
||||
test("remove deletes the workspace, associated sessions, adapter resources, and status", async () => {
|
||||
await withInstance(async (dir) => {
|
||||
const type = unique("remove-local")
|
||||
const recorded = localAdaptor(path.join(dir, "remove-local"))
|
||||
registerAdaptor(Instance.project.id, type, recorded.adaptor)
|
||||
const recorded = localAdapter(path.join(dir, "remove-local"))
|
||||
registerAdapter(Instance.project.id, type, recorded.adapter)
|
||||
const info = await createWorkspace({ type, branch: null, projectID: Instance.project.id, extra: null })
|
||||
const one = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))
|
||||
const two = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))
|
||||
@@ -628,21 +633,21 @@ describe("workspace-old CRUD", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("remove still deletes the row when the adaptor cannot remove resources", async () => {
|
||||
test("remove still deletes the row when the adapter cannot remove resources", async () => {
|
||||
await withInstance(async () => {
|
||||
const type = unique("remove-throws")
|
||||
const info = workspaceInfo(Instance.project.id, type, { id: WorkspaceID.ascending("wrk_remove_throws") })
|
||||
registerAdaptor(
|
||||
registerAdapter(
|
||||
Instance.project.id,
|
||||
type,
|
||||
recordedAdaptor({
|
||||
recordedAdapter({
|
||||
async remove() {
|
||||
throw new Error("remove exploded")
|
||||
},
|
||||
target() {
|
||||
return { type: "local", directory: "/unused" }
|
||||
},
|
||||
}).adaptor,
|
||||
}).adapter,
|
||||
)
|
||||
insertWorkspace(info)
|
||||
|
||||
@@ -661,7 +666,7 @@ describe("workspace-old sync state", () => {
|
||||
const session = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))
|
||||
attachSessionToWorkspace(session.id, info.id)
|
||||
insertWorkspace(info)
|
||||
registerAdaptor(Instance.project.id, type, localAdaptor(path.join(dir, "flag-disabled")).adaptor)
|
||||
registerAdapter(Instance.project.id, type, localAdapter(path.join(dir, "flag-disabled")).adapter)
|
||||
|
||||
startWorkspaceSyncing(Instance.project.id)
|
||||
await delay(25)
|
||||
@@ -682,8 +687,8 @@ describe("workspace-old sync state", () => {
|
||||
await fs.mkdir(withoutSessionDir, { recursive: true })
|
||||
insertWorkspace(withSession)
|
||||
insertWorkspace(withoutSession)
|
||||
registerAdaptor(Instance.project.id, withSessionType, localAdaptor(withSessionDir).adaptor)
|
||||
registerAdaptor(Instance.project.id, withoutSessionType, localAdaptor(withoutSessionDir).adaptor)
|
||||
registerAdapter(Instance.project.id, withSessionType, localAdapter(withSessionDir).adapter)
|
||||
registerAdapter(Instance.project.id, withoutSessionType, localAdapter(withoutSessionDir).adapter)
|
||||
attachSessionToWorkspace(
|
||||
(await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))).id,
|
||||
withSession.id,
|
||||
@@ -707,10 +712,10 @@ describe("workspace-old sync state", () => {
|
||||
const type = unique("missing-local")
|
||||
const info = workspaceInfo(Instance.project.id, type)
|
||||
insertWorkspace(info)
|
||||
registerAdaptor(
|
||||
registerAdapter(
|
||||
Instance.project.id,
|
||||
type,
|
||||
localAdaptor(path.join(dir, "missing-target"), { createDir: false }).adaptor,
|
||||
localAdapter(path.join(dir, "missing-target"), { createDir: false }).adapter,
|
||||
)
|
||||
attachSessionToWorkspace(
|
||||
(await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))).id,
|
||||
@@ -738,7 +743,7 @@ describe("workspace-old sync state", () => {
|
||||
const target = path.join(dir, "dedupe-local")
|
||||
await fs.mkdir(target, { recursive: true })
|
||||
insertWorkspace(info)
|
||||
registerAdaptor(Instance.project.id, type, localAdaptor(target).adaptor)
|
||||
registerAdapter(Instance.project.id, type, localAdapter(target).adapter)
|
||||
attachSessionToWorkspace(
|
||||
(await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))).id,
|
||||
info.id,
|
||||
@@ -795,7 +800,7 @@ describe("workspace-old sync state", () => {
|
||||
const type = unique("remote-start")
|
||||
const info = workspaceInfo(Instance.project.id, type)
|
||||
insertWorkspace(info)
|
||||
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/sync`).adaptor)
|
||||
registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/sync`).adapter)
|
||||
attachSessionToWorkspace((yield* sessionSvc.create({})).id, info.id)
|
||||
|
||||
yield* workspace.startWorkspaceSyncing(Instance.project.id)
|
||||
@@ -850,7 +855,7 @@ describe("workspace-old sync state", () => {
|
||||
const type = unique("remote-connect-fail")
|
||||
const info = workspaceInfo(Instance.project.id, type)
|
||||
insertWorkspace(info)
|
||||
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/failed`).adaptor)
|
||||
registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/failed`).adapter)
|
||||
attachSessionToWorkspace((yield* sessionSvc.create({})).id, info.id)
|
||||
|
||||
yield* workspace.startWorkspaceSyncing(Instance.project.id)
|
||||
@@ -890,7 +895,7 @@ describe("workspace-old sync state", () => {
|
||||
const type = unique("remote-history-fail")
|
||||
const info = workspaceInfo(Instance.project.id, type)
|
||||
insertWorkspace(info)
|
||||
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/history-failed`).adaptor)
|
||||
registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/history-failed`).adapter)
|
||||
attachSessionToWorkspace((yield* sessionSvc.create({})).id, info.id)
|
||||
|
||||
yield* workspace.startWorkspaceSyncing(Instance.project.id)
|
||||
@@ -947,7 +952,7 @@ describe("workspace-old sync state", () => {
|
||||
const type = unique("history-replay")
|
||||
const info = workspaceInfo(Instance.project.id, type)
|
||||
insertWorkspace(info)
|
||||
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/history`).adaptor)
|
||||
registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/history`).adapter)
|
||||
const session = yield* sessionSvc.create({ title: "before history" })
|
||||
attachSessionToWorkspace(session.id, info.id)
|
||||
historySessionID = session.id
|
||||
@@ -1014,7 +1019,7 @@ describe("workspace-old sync state", () => {
|
||||
const type = unique("sse-forward")
|
||||
const info = workspaceInfo(Instance.project.id, type)
|
||||
insertWorkspace(info)
|
||||
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/sse-forward`).adaptor)
|
||||
registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/sse-forward`).adapter)
|
||||
attachSessionToWorkspace((yield* sessionSvc.create({})).id, info.id)
|
||||
|
||||
yield* workspace.startWorkspaceSyncing(Instance.project.id)
|
||||
@@ -1095,7 +1100,7 @@ describe("workspace-old sync state", () => {
|
||||
const type = unique("sse-sync")
|
||||
const info = workspaceInfo(Instance.project.id, type)
|
||||
insertWorkspace(info)
|
||||
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/sse-sync`).adaptor)
|
||||
registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/sse-sync`).adapter)
|
||||
const session = yield* sessionSvc.create({ title: "before sse" })
|
||||
attachSessionToWorkspace(session.id, info.id)
|
||||
sseSessionID = session.id
|
||||
@@ -1232,7 +1237,7 @@ describe("workspace-old sessionRestore", () => {
|
||||
const type = unique("restore-missing-session")
|
||||
const info = workspaceInfo(Instance.project.id, type, { directory: dir })
|
||||
insertWorkspace(info)
|
||||
registerAdaptor(Instance.project.id, type, localAdaptor(dir).adaptor)
|
||||
registerAdapter(Instance.project.id, type, localAdapter(dir).adapter)
|
||||
|
||||
await expect(
|
||||
restoreWorkspaceSession({ workspaceID: info.id, sessionID: SessionID.descending("ses_missing_restore") }),
|
||||
@@ -1273,13 +1278,13 @@ describe("workspace-old sessionRestore", () => {
|
||||
const type = unique("restore-remote")
|
||||
const info = workspaceInfo(Instance.project.id, type, { directory: dir })
|
||||
insertWorkspace(info)
|
||||
registerAdaptor(
|
||||
registerAdapter(
|
||||
Instance.project.id,
|
||||
type,
|
||||
remoteAdaptor(`${url}/restore/?ignored=1#hash`, {
|
||||
remoteAdapter(`${url}/restore/?ignored=1#hash`, {
|
||||
directory: dir,
|
||||
headers: { authorization: "Bearer restore" },
|
||||
}).adaptor,
|
||||
}).adapter,
|
||||
)
|
||||
const session = yield* sessionSvc.create({ title: "restore remote" })
|
||||
replaceSessionEvents(session.id, 24)
|
||||
@@ -1353,7 +1358,7 @@ describe("workspace-old sessionRestore", () => {
|
||||
const type = unique("restore-null-dir")
|
||||
const info = workspaceInfo(Instance.project.id, type, { directory: null })
|
||||
insertWorkspace(info)
|
||||
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/null-dir`, { directory: null }).adaptor)
|
||||
registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/null-dir`, { directory: null }).adapter)
|
||||
const session = yield* sessionSvc.create({ title: "null dir" })
|
||||
replaceSessionEvents(session.id, 0)
|
||||
|
||||
@@ -1397,7 +1402,7 @@ describe("workspace-old sessionRestore", () => {
|
||||
const type = unique("restore-remote-fail")
|
||||
const info = workspaceInfo(Instance.project.id, type, { directory: dir })
|
||||
insertWorkspace(info)
|
||||
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/fail`, { directory: dir }).adaptor)
|
||||
registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/fail`, { directory: dir }).adapter)
|
||||
const session = yield* sessionSvc.create({ title: "restore fail" })
|
||||
replaceSessionEvents(session.id, 11)
|
||||
|
||||
@@ -1437,7 +1442,7 @@ describe("workspace-old sessionRestore", () => {
|
||||
const type = unique("restore-local")
|
||||
const info = workspaceInfo(Instance.project.id, type, { directory: dir })
|
||||
insertWorkspace(info)
|
||||
registerAdaptor(Instance.project.id, type, localAdaptor(dir).adaptor)
|
||||
registerAdapter(Instance.project.id, type, localAdapter(dir).adapter)
|
||||
const session = yield* sessionSvc.create({ title: "restore local" })
|
||||
replaceSessionEvents(session.id, 20)
|
||||
|
||||
@@ -1488,7 +1493,7 @@ describe("workspace-old sessionRestore", () => {
|
||||
const type = unique("restore-real-events")
|
||||
const info = workspaceInfo(Instance.project.id, type, { directory: dir })
|
||||
insertWorkspace(info)
|
||||
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/real`, { directory: dir }).adaptor)
|
||||
registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/real`, { directory: dir }).adapter)
|
||||
const session = yield* sessionSvc.create({ title: "real events" })
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const msg = yield* sessionSvc.updateMessage({
|
||||
|
||||
@@ -34,7 +34,7 @@ afterAll(() => {
|
||||
})
|
||||
|
||||
describe("plugin.workspace", () => {
|
||||
it.live("plugin can install a workspace adaptor", () =>
|
||||
it.live("plugin can install a workspace adapter", () =>
|
||||
provideTmpdirInstance((dir) =>
|
||||
Effect.gen(function* () {
|
||||
const type = `plug-${Math.random().toString(36).slice(2)}`
|
||||
@@ -48,7 +48,7 @@ describe("plugin.workspace", () => {
|
||||
"export default async ({ experimental_workspace }) => {",
|
||||
` experimental_workspace.register(${JSON.stringify(type)}, {`,
|
||||
' name: "plug",',
|
||||
' description: "plugin workspace adaptor",',
|
||||
' description: "plugin workspace adapter",',
|
||||
" configure(input) {",
|
||||
` return { ...input, name: "plug", branch: "plug/main", directory: ${JSON.stringify(space)} }`,
|
||||
" },",
|
||||
@@ -258,6 +258,18 @@ describe("HttpApi server", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("matches SDK-affecting request schema details", () => {
|
||||
const effect = effectOpenApi()
|
||||
const sessionUpdate = effect.paths["/session/{sessionID}"]?.patch?.requestBody
|
||||
const sessionUpdateSchema =
|
||||
typeof sessionUpdate === "object" && sessionUpdate && "content" in sessionUpdate
|
||||
? sessionUpdate.content?.["application/json"]?.schema
|
||||
: undefined
|
||||
const sessionUpdateProperties = sessionUpdateSchema?.properties as Record<string, OpenApiSchema> | undefined
|
||||
const time = sessionUpdateProperties?.time
|
||||
expect(time?.properties?.archived).toEqual({ type: "number" })
|
||||
})
|
||||
|
||||
test("documents event routes as server-sent events", () => {
|
||||
const effect = effectOpenApi()
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NodeHttpServer, NodeServices } from "@effect/platform-node"
|
||||
import type { WorkspaceAdapter } from "@opencode-ai/plugin"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
import { describe, expect } from "bun:test"
|
||||
@@ -7,8 +8,7 @@ import { HttpClient, HttpClientRequest, HttpRouter, HttpServerResponse } from "e
|
||||
import * as Socket from "effect/unstable/socket/Socket"
|
||||
import { mkdir } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { registerAdaptor } from "../../src/control-plane/adaptors"
|
||||
import type { WorkspaceAdaptor } from "../../src/control-plane/types"
|
||||
import { registerAdapter } from "../../src/control-plane/adapters"
|
||||
import { Workspace } from "../../src/control-plane/workspace"
|
||||
import { InstanceRef, WorkspaceRef } from "../../src/effect/instance-ref"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
@@ -49,7 +49,7 @@ const instanceContextTestLayer = instanceRouterMiddleware
|
||||
.combine(workspaceRouterMiddleware)
|
||||
.layer.pipe(Layer.provide(Socket.layerWebSocketConstructorGlobal))
|
||||
|
||||
const localAdaptor = (directory: string): WorkspaceAdaptor => ({
|
||||
const localAdapter = (directory: string): WorkspaceAdapter => ({
|
||||
name: "Local Test",
|
||||
description: "Create a local test workspace",
|
||||
configure: (info) => ({ ...info, name: "local-test", directory }),
|
||||
@@ -63,7 +63,7 @@ const localAdaptor = (directory: string): WorkspaceAdaptor => ({
|
||||
const createLocalWorkspace = (input: { projectID: Project.Info["id"]; type: string; directory: string }) =>
|
||||
Effect.acquireRelease(
|
||||
Effect.gen(function* () {
|
||||
registerAdaptor(input.projectID, input.type, localAdaptor(input.directory))
|
||||
registerAdapter(input.projectID, input.type, localAdapter(input.directory))
|
||||
const workspace = yield* Workspace.Service
|
||||
return yield* workspace.create({
|
||||
type: input.type,
|
||||
|
||||
@@ -2,9 +2,9 @@ import { afterEach, describe, expect } from "bun:test"
|
||||
import { mkdir } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { Effect } from "effect"
|
||||
import type { WorkspaceAdapter } from "@opencode-ai/plugin"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { registerAdaptor } from "../../src/control-plane/adaptors"
|
||||
import type { WorkspaceAdaptor } from "../../src/control-plane/types"
|
||||
import { registerAdapter } from "../../src/control-plane/adapters"
|
||||
import { Workspace } from "../../src/control-plane/workspace"
|
||||
import { PermissionID } from "../../src/permission/schema"
|
||||
import { ModelID, ProviderID } from "../../src/provider/schema"
|
||||
@@ -82,7 +82,7 @@ function createTextMessage(directory: string, sessionID: SessionID, text: string
|
||||
)
|
||||
}
|
||||
|
||||
const localAdaptor = (directory: string): WorkspaceAdaptor => ({
|
||||
const localAdapter = (directory: string): WorkspaceAdapter => ({
|
||||
name: "Local Test",
|
||||
description: "Create a local test workspace",
|
||||
configure: (info) => ({ ...info, name: "local-test", directory }),
|
||||
@@ -95,7 +95,7 @@ const localAdaptor = (directory: string): WorkspaceAdaptor => ({
|
||||
|
||||
const createLocalWorkspace = (input: { projectID: Project.Info["id"]; type: string; directory: string }) =>
|
||||
Effect.gen(function* () {
|
||||
registerAdaptor(input.projectID, input.type, localAdaptor(input.directory))
|
||||
registerAdapter(input.projectID, input.type, localAdapter(input.directory))
|
||||
return yield* Workspace.Service.use((svc) =>
|
||||
svc.create({
|
||||
type: input.type,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NodeHttpServer, NodeServices } from "@effect/platform-node"
|
||||
import type { WorkspaceAdapter } from "@opencode-ai/plugin"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Context, Effect, Layer, Queue } from "effect"
|
||||
@@ -15,9 +16,8 @@ import * as Socket from "effect/unstable/socket/Socket"
|
||||
import Http from "node:http"
|
||||
import { mkdir } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { registerAdaptor } from "../../src/control-plane/adaptors"
|
||||
import { registerAdapter } from "../../src/control-plane/adapters"
|
||||
import { WorkspaceID } from "../../src/control-plane/schema"
|
||||
import type { WorkspaceAdaptor } from "../../src/control-plane/types"
|
||||
import { Workspace } from "../../src/control-plane/workspace"
|
||||
import { WorkspaceTable } from "../../src/control-plane/workspace.sql"
|
||||
import { Project } from "../../src/project/project"
|
||||
@@ -82,7 +82,7 @@ const listenAdditionalServer = <E, R>(handler: TestHandler<E, R>) =>
|
||||
return HttpServer.formatAddress(server.address)
|
||||
})
|
||||
|
||||
const localAdaptor = (directory: string): WorkspaceAdaptor => ({
|
||||
const localAdapter = (directory: string): WorkspaceAdapter => ({
|
||||
name: "Local Test",
|
||||
description: "Create a local test workspace",
|
||||
configure: (info) => ({ ...info, name: "local-test", directory }),
|
||||
@@ -93,7 +93,7 @@ const localAdaptor = (directory: string): WorkspaceAdaptor => ({
|
||||
target: () => ({ type: "local" as const, directory }),
|
||||
})
|
||||
|
||||
const remoteAdaptor = (directory: string, url: string, headers?: HeadersInit): WorkspaceAdaptor => ({
|
||||
const remoteAdapter = (directory: string, url: string, headers?: HeadersInit): WorkspaceAdapter => ({
|
||||
name: "Remote Test",
|
||||
description: "Create a remote test workspace",
|
||||
configure: (info) => ({ ...info, name: "remote-test", directory }),
|
||||
@@ -116,10 +116,10 @@ const syncResponse = (request: HttpServerRequest.HttpServerRequest) => {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const createWorkspace = (input: { projectID: Project.Info["id"]; type: string; adaptor: WorkspaceAdaptor }) =>
|
||||
const createWorkspace = (input: { projectID: Project.Info["id"]; type: string; adapter: WorkspaceAdapter }) =>
|
||||
Effect.acquireRelease(
|
||||
Effect.gen(function* () {
|
||||
registerAdaptor(input.projectID, input.type, input.adaptor)
|
||||
registerAdapter(input.projectID, input.type, input.adapter)
|
||||
const workspace = yield* Workspace.Service
|
||||
return yield* workspace.create({
|
||||
type: input.type,
|
||||
@@ -144,14 +144,14 @@ const createRemoteWorkspace = (input: {
|
||||
createWorkspace({
|
||||
projectID: input.projectID,
|
||||
type: input.type,
|
||||
adaptor: remoteAdaptor(path.join(input.dir, `.${input.type}`), input.url, input.headers),
|
||||
adapter: remoteAdapter(path.join(input.dir, `.${input.type}`), input.url, input.headers),
|
||||
})
|
||||
|
||||
const createLocalWorkspace = (input: { projectID: Project.Info["id"]; type: string; directory: string }) =>
|
||||
createWorkspace({
|
||||
projectID: input.projectID,
|
||||
type: input.type,
|
||||
adaptor: localAdaptor(input.directory),
|
||||
adapter: localAdapter(input.directory),
|
||||
})
|
||||
|
||||
const insertRemoteWorkspaceWithoutSync = (input: {
|
||||
@@ -162,7 +162,7 @@ const insertRemoteWorkspaceWithoutSync = (input: {
|
||||
}) =>
|
||||
Effect.sync(() => {
|
||||
const id = WorkspaceID.ascending()
|
||||
registerAdaptor(input.projectID, input.type, remoteAdaptor(path.join(input.dir, `.${input.type}`), input.url))
|
||||
registerAdapter(input.projectID, input.type, remoteAdapter(path.join(input.dir, `.${input.type}`), input.url))
|
||||
Database.use((db) => db.insert(WorkspaceTable).values({ id, type: input.type, project_id: input.projectID }).run())
|
||||
return id
|
||||
})
|
||||
@@ -237,7 +237,7 @@ describe("HttpApi workspace routing middleware", () => {
|
||||
{ status: 201, headers: { "x-remote": "yes" } },
|
||||
)
|
||||
})
|
||||
// The adaptor target tells the middleware where to proxy selected remote
|
||||
// The adapter target tells the middleware where to proxy selected remote
|
||||
// workspace requests. Appending /probe to this base should produce
|
||||
// `${remoteUrl}/base/probe` on the fake remote server above.
|
||||
const workspace = yield* createRemoteWorkspace({
|
||||
|
||||
@@ -4,8 +4,8 @@ import { mkdir } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { registerAdaptor } from "../../src/control-plane/adaptors"
|
||||
import type { WorkspaceAdaptor } from "../../src/control-plane/types"
|
||||
import { registerAdapter } from "../../src/control-plane/adapters"
|
||||
import type { WorkspaceAdapter } from "@opencode-ai/plugin"
|
||||
import { Workspace } from "../../src/control-plane/workspace"
|
||||
import { WorkspacePaths } from "../../src/server/routes/instance/httpapi/groups/workspace"
|
||||
import { Session } from "@/session/session"
|
||||
@@ -36,7 +36,7 @@ function request(path: string, directory: string, init: RequestInit = {}) {
|
||||
})
|
||||
}
|
||||
|
||||
function localAdaptor(directory: string): WorkspaceAdaptor {
|
||||
function localAdapter(directory: string): WorkspaceAdapter {
|
||||
return {
|
||||
name: "Local Test",
|
||||
description: "Create a local test workspace",
|
||||
@@ -60,7 +60,7 @@ function localAdaptor(directory: string): WorkspaceAdaptor {
|
||||
}
|
||||
}
|
||||
|
||||
function remoteAdaptor(directory: string, url: string, headers?: HeadersInit): WorkspaceAdaptor {
|
||||
function remoteAdapter(directory: string, url: string, headers?: HeadersInit): WorkspaceAdapter {
|
||||
return {
|
||||
name: "Remote Test",
|
||||
description: "Create a remote test workspace",
|
||||
@@ -137,14 +137,14 @@ describe("workspace HttpApi", () => {
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped({ git: true })
|
||||
|
||||
const [adaptors, workspaces, status] = yield* Effect.all([
|
||||
request(WorkspacePaths.adaptors, dir),
|
||||
const [adapters, workspaces, status] = yield* Effect.all([
|
||||
request(WorkspacePaths.adapters, dir),
|
||||
request(WorkspacePaths.list, dir),
|
||||
request(WorkspacePaths.status, dir),
|
||||
])
|
||||
|
||||
expect(adaptors.status).toBe(200)
|
||||
expect(yield* Effect.promise(() => adaptors.json())).toContainEqual({
|
||||
expect(adapters.status).toBe(200)
|
||||
expect(yield* Effect.promise(() => adapters.json())).toContainEqual({
|
||||
type: "worktree",
|
||||
name: "Worktree",
|
||||
description: "Create a git worktree",
|
||||
@@ -163,7 +163,7 @@ describe("workspace HttpApi", () => {
|
||||
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true
|
||||
const dir = yield* tmpdirScoped({ git: true })
|
||||
const project = yield* Project.use.fromDirectory(dir)
|
||||
registerAdaptor(project.project.id, "local-test", localAdaptor(path.join(dir, ".workspace")))
|
||||
registerAdapter(project.project.id, "local-test", localAdapter(path.join(dir, ".workspace")))
|
||||
|
||||
const created = yield* request(WorkspacePaths.list, dir, {
|
||||
method: "POST",
|
||||
@@ -195,13 +195,60 @@ describe("workspace HttpApi", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("creates built-in worktree workspace through HttpApi instance context", () =>
|
||||
Effect.gen(function* () {
|
||||
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true
|
||||
const dir = yield* tmpdirScoped({ git: true })
|
||||
|
||||
const created = yield* request(WorkspacePaths.list, dir, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ type: "worktree", branch: null, extra: null }),
|
||||
})
|
||||
const createdBody = yield* Effect.promise(() => created.clone().text())
|
||||
|
||||
expect({ status: created.status, body: createdBody }).toMatchObject({ status: 200 })
|
||||
const workspace = (yield* Effect.promise(() => created.json())) as Workspace.Info
|
||||
expect(workspace.type).toBe("worktree")
|
||||
expect(workspace.directory).toBeString()
|
||||
|
||||
yield* request(WorkspacePaths.remove.replace(":id", workspace.id), dir, { method: "DELETE" })
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("runs promise workspace adapters with legacy instance context", () =>
|
||||
Effect.gen(function* () {
|
||||
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true
|
||||
const dir = yield* tmpdirScoped({ git: true })
|
||||
const workspaceDir = path.join(dir, ".workspace-context")
|
||||
const project = yield* Project.use.fromDirectory(dir)
|
||||
let adapterDirectory: string | undefined
|
||||
registerAdapter(project.project.id, "promise-context", {
|
||||
...localAdapter(workspaceDir),
|
||||
async create() {
|
||||
adapterDirectory = Instance.directory
|
||||
await mkdir(workspaceDir, { recursive: true })
|
||||
},
|
||||
})
|
||||
|
||||
const created = yield* request(WorkspacePaths.list, dir, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ type: "promise-context", branch: null, extra: null }),
|
||||
})
|
||||
|
||||
expect(created.status).toBe(200)
|
||||
expect(adapterDirectory).toBe(dir)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("routes local workspace requests through the workspace target directory", () =>
|
||||
Effect.gen(function* () {
|
||||
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true
|
||||
const dir = yield* tmpdirScoped({ git: true })
|
||||
const workspaceDir = path.join(dir, ".workspace-local")
|
||||
const project = yield* Project.use.fromDirectory(dir)
|
||||
registerAdaptor(project.project.id, "local-target", localAdaptor(workspaceDir))
|
||||
registerAdapter(project.project.id, "local-target", localAdapter(workspaceDir))
|
||||
const created = yield* request(WorkspacePaths.list, dir, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
@@ -250,10 +297,10 @@ describe("workspace HttpApi", () => {
|
||||
})
|
||||
|
||||
const project = yield* Project.use.fromDirectory(dir)
|
||||
registerAdaptor(
|
||||
registerAdapter(
|
||||
project.project.id,
|
||||
"remote-target",
|
||||
remoteAdaptor(path.join(dir, ".remote"), `http://127.0.0.1:${remote.port}/base`, {
|
||||
remoteAdapter(path.join(dir, ".remote"), `http://127.0.0.1:${remote.port}/base`, {
|
||||
"x-target-auth": "secret",
|
||||
}),
|
||||
)
|
||||
@@ -319,10 +366,10 @@ describe("workspace HttpApi", () => {
|
||||
})
|
||||
|
||||
const project = yield* Project.use.fromDirectory(dir)
|
||||
registerAdaptor(
|
||||
registerAdapter(
|
||||
project.project.id,
|
||||
"remote-session-target",
|
||||
remoteAdaptor(path.join(dir, ".remote-session"), `http://127.0.0.1:${remote.port}/base`),
|
||||
remoteAdapter(path.join(dir, ".remote-session"), `http://127.0.0.1:${remote.port}/base`),
|
||||
)
|
||||
const created = yield* request(WorkspacePaths.list, dir, {
|
||||
method: "POST",
|
||||
|
||||
@@ -469,6 +469,13 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
},
|
||||
{
|
||||
...basePart(assistantID, "a2"),
|
||||
type: "reasoning",
|
||||
text: "thinking",
|
||||
metadata: { openai: { reasoning: "meta" } },
|
||||
time: { start: 0 },
|
||||
},
|
||||
{
|
||||
...basePart(assistantID, "a3"),
|
||||
type: "tool",
|
||||
callID: "call-1",
|
||||
tool: "bash",
|
||||
@@ -495,6 +502,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "text", text: "done" },
|
||||
{ type: "text", text: "thinking" },
|
||||
{
|
||||
type: "tool-call",
|
||||
toolCallId: "call-1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -45,7 +45,7 @@ export type WorkspaceTarget =
|
||||
headers?: HeadersInit
|
||||
}
|
||||
|
||||
export type WorkspaceAdaptor = {
|
||||
export type WorkspaceAdapter = {
|
||||
name: string
|
||||
description: string
|
||||
configure(config: WorkspaceInfo): WorkspaceInfo | Promise<WorkspaceInfo>
|
||||
@@ -60,7 +60,7 @@ export type PluginInput = {
|
||||
directory: string
|
||||
worktree: string
|
||||
experimental_workspace: {
|
||||
register(type: string, adaptor: WorkspaceAdaptor): void
|
||||
register(type: string, adapter: WorkspaceAdapter): void
|
||||
}
|
||||
serverUrl: URL
|
||||
$: BunShell
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -29,7 +29,7 @@ import type {
|
||||
ExperimentalConsoleSwitchOrgResponses,
|
||||
ExperimentalResourceListResponses,
|
||||
ExperimentalSessionListResponses,
|
||||
ExperimentalWorkspaceAdaptorListResponses,
|
||||
ExperimentalWorkspaceAdapterListResponses,
|
||||
ExperimentalWorkspaceCreateErrors,
|
||||
ExperimentalWorkspaceCreateResponses,
|
||||
ExperimentalWorkspaceListResponses,
|
||||
@@ -512,11 +512,11 @@ export class App extends HeyApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
export class Adaptor extends HeyApiClient {
|
||||
export class Adapter extends HeyApiClient {
|
||||
/**
|
||||
* List workspace adaptors
|
||||
* List workspace adapters
|
||||
*
|
||||
* List all available workspace adaptors for the current project.
|
||||
* List all available workspace adapters for the current project.
|
||||
*/
|
||||
public list<ThrowOnError extends boolean = false>(
|
||||
parameters?: {
|
||||
@@ -536,8 +536,8 @@ export class Adaptor extends HeyApiClient {
|
||||
},
|
||||
],
|
||||
)
|
||||
return (options?.client ?? this.client).get<ExperimentalWorkspaceAdaptorListResponses, unknown, ThrowOnError>({
|
||||
url: "/experimental/workspace/adaptor",
|
||||
return (options?.client ?? this.client).get<ExperimentalWorkspaceAdapterListResponses, unknown, ThrowOnError>({
|
||||
url: "/experimental/workspace/adapter",
|
||||
...options,
|
||||
...params,
|
||||
})
|
||||
@@ -731,9 +731,9 @@ export class Workspace extends HeyApiClient {
|
||||
})
|
||||
}
|
||||
|
||||
private _adaptor?: Adaptor
|
||||
get adaptor(): Adaptor {
|
||||
return (this._adaptor ??= new Adaptor({ client: this.client }))
|
||||
private _adapter?: Adapter
|
||||
get adapter(): Adapter {
|
||||
return (this._adapter ??= new Adapter({ client: this.client }))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2430,19 +2430,19 @@ export type AppLogResponses = {
|
||||
|
||||
export type AppLogResponse = AppLogResponses[keyof AppLogResponses]
|
||||
|
||||
export type ExperimentalWorkspaceAdaptorListData = {
|
||||
export type ExperimentalWorkspaceAdapterListData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/experimental/workspace/adaptor"
|
||||
url: "/experimental/workspace/adapter"
|
||||
}
|
||||
|
||||
export type ExperimentalWorkspaceAdaptorListResponses = {
|
||||
export type ExperimentalWorkspaceAdapterListResponses = {
|
||||
/**
|
||||
* Workspace adaptors
|
||||
* Workspace adapters
|
||||
*/
|
||||
200: Array<{
|
||||
type: string
|
||||
@@ -2451,8 +2451,8 @@ export type ExperimentalWorkspaceAdaptorListResponses = {
|
||||
}>
|
||||
}
|
||||
|
||||
export type ExperimentalWorkspaceAdaptorListResponse =
|
||||
ExperimentalWorkspaceAdaptorListResponses[keyof ExperimentalWorkspaceAdaptorListResponses]
|
||||
export type ExperimentalWorkspaceAdapterListResponse =
|
||||
ExperimentalWorkspaceAdapterListResponses[keyof ExperimentalWorkspaceAdapterListResponses]
|
||||
|
||||
export type ExperimentalWorkspaceListData = {
|
||||
body?: never
|
||||
|
||||
@@ -415,9 +415,9 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/experimental/workspace/adaptor": {
|
||||
"/experimental/workspace/adapter": {
|
||||
"get": {
|
||||
"operationId": "experimental.workspace.adaptor.list",
|
||||
"operationId": "experimental.workspace.adapter.list",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
@@ -434,11 +434,11 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"summary": "List workspace adaptors",
|
||||
"description": "List all available workspace adaptors for the current project.",
|
||||
"summary": "List workspace adapters",
|
||||
"description": "List all available workspace adapters for the current project.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Workspace adaptors",
|
||||
"description": "Workspace adapters",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@@ -466,7 +466,7 @@
|
||||
"x-codeSamples": [
|
||||
{
|
||||
"lang": "js",
|
||||
"source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.adaptor.list({\n ...\n})"
|
||||
"source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.adapter.list({\n ...\n})"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@opencode-ai/web",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
||||
@@ -110,7 +110,7 @@ On Windows, the defaults for `input_undo` and `terminal_suspend` are different:
|
||||
|
||||
- `input_undo` defaults to `ctrl+z,ctrl+-,super+z` (the `ctrl+z` binding is added because Windows terminals do not support POSIX suspend).
|
||||
- `terminal_suspend` is forced to `none` because native Windows terminals do not support POSIX suspend.
|
||||
:::
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1767,7 +1767,7 @@ SAP AI Core provides access to 40+ models from OpenAI, Anthropic, Google, Amazon
|
||||
|
||||
### STACKIT
|
||||
|
||||
STACKIT AI Model Serving provides fully managed soverign hosting environment for AI models, focusing on LLMs like Llama, Mistral, and Qwen, with maximum data sovereignty on European infrastructure.
|
||||
STACKIT AI Model Serving provides fully managed sovereign hosting environment for AI models, focusing on LLMs like Llama, Mistral, and Qwen, with maximum data sovereignty on European infrastructure.
|
||||
|
||||
1. Head over to [STACKIT Portal](https://portal.stackit.cloud), navigate to **AI Model Serving**, and create an auth token for your project.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "opencode",
|
||||
"displayName": "opencode",
|
||||
"description": "opencode for VS Code",
|
||||
"version": "1.14.30",
|
||||
"version": "1.14.31",
|
||||
"publisher": "sst-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Reference in New Issue
Block a user