Compare commits

..

1 Commits

Author SHA1 Message Date
Aiden Cline
020b2240c2 docs: update SDK docs to reference v2 SDK exclusively
- Update all imports to @opencode-ai/sdk/v2
- Fix types URL to point to v2 generated types
- Fix file.read response type: text|binary (not raw|patch)
- Fix permission method name: permission.reply (not postSessionByIdPermissionsByPermissionId)
2026-04-30 23:22:11 -05:00
66 changed files with 317 additions and 666 deletions

1
.github/VOUCHED.td vendored
View File

@@ -32,7 +32,6 @@ rekram1-node
-ricardo-m-l
-robinmordasiewicz
rubdos
-saisharan0103 spamming ai prs
shantur
simonklee
-spider-yamet clawdbot/llm psychosis, spam pinging the team

View File

@@ -29,7 +29,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.14.31",
"version": "1.14.30",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -85,7 +85,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.14.31",
"version": "1.14.30",
"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.31",
"version": "1.14.30",
"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.31",
"version": "1.14.30",
"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.31",
"version": "1.14.30",
"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.31",
"version": "1.14.30",
"bin": {
"opencode": "./bin/opencode",
},
@@ -228,7 +228,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.14.31",
"version": "1.14.30",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -263,7 +263,7 @@
},
"packages/desktop-electron": {
"name": "@opencode-ai/desktop-electron",
"version": "1.14.31",
"version": "1.14.30",
"dependencies": {
"drizzle-orm": "catalog:",
"effect": "catalog:",
@@ -309,7 +309,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.14.31",
"version": "1.14.30",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -338,7 +338,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.14.31",
"version": "1.14.30",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -354,7 +354,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.14.31",
"version": "1.14.30",
"bin": {
"opencode": "./bin/opencode",
},
@@ -496,7 +496,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.14.31",
"version": "1.14.30",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"effect": "catalog:",
@@ -531,7 +531,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.14.31",
"version": "1.14.30",
"dependencies": {
"cross-spawn": "catalog:",
},
@@ -546,7 +546,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.14.31",
"version": "1.14.30",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -581,7 +581,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.14.31",
"version": "1.14.30",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -630,7 +630,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.14.31",
"version": "1.14.30",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",

View File

@@ -30,20 +30,20 @@ export const api = new sst.cloudflare.Worker("Api", {
transform: {
worker: (args) => {
args.logpush = true
// args.bindings = $resolve(args.bindings).apply((bindings) => [
// ...bindings,
// {
// name: "SYNC_SERVER",
// type: "durable_object_namespace",
// className: "SyncServer",
// },
// ])
// args.migrations = {
// // Note: when releasing the next tag, make sure all stages use tag v2
// oldTag: $app.stage === "production" || $app.stage === "thdxr" ? "" : "v1",
// newTag: $app.stage === "production" || $app.stage === "thdxr" ? "" : "v1",
// //newSqliteClasses: ["SyncServer"],
// }
args.bindings = $resolve(args.bindings).apply((bindings) => [
...bindings,
{
name: "SYNC_SERVER",
type: "durable_object_namespace",
className: "SyncServer",
},
])
args.migrations = {
// Note: when releasing the next tag, make sure all stages use tag v2
oldTag: $app.stage === "production" || $app.stage === "thdxr" ? "" : "v1",
newTag: $app.stage === "production" || $app.stage === "thdxr" ? "" : "v1",
//newSqliteClasses: ["SyncServer"],
}
},
},
})

View File

@@ -1,289 +0,0 @@
const displayName = (s: string) =>
s
.split("-")
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join(" ")
const resourceName = (s: string) => displayName(s).replace(/[^a-zA-Z0-9]/g, "")
const varSpec = (label: string, name: string) =>
$jsonStringify({
content: [
{
content: [
{
attrs: {
name,
label,
missing: false,
},
type: "varSpec",
},
],
type: "paragraph",
},
],
type: "doc",
})
const fields = {
model: incident.getAlertAttributeOutput({ name: "Model" }),
product: incident.getAlertAttributeOutput({ name: "Product" }),
}
const alertSource = new incident.AlertSource("HoneycombAlertSource", {
name: $app.stage === "production" ? "Honeycomb" : `Honeycomb (${$app.stage})`,
sourceType: "honeycomb",
template: {
title: {
literal: varSpec("Payload -> Title", "title"),
},
description: {
literal: varSpec("Payload -> Description", "description"),
},
attributes: [
{
alertAttributeId: fields.model.id,
binding: {
value: {
reference: 'expressions["model"]',
},
mergeStrategy: "first_wins",
},
},
{
alertAttributeId: fields.product.id,
binding: {
value: {
reference: 'expressions["product"]',
},
mergeStrategy: "first_wins",
},
},
],
expressions: [
{
label: "Model",
operations: [
{
operationType: "parse",
parse: {
returns: {
array: false,
type: fields.model.type,
},
source: "$['model']",
},
},
],
reference: "model",
rootReference: "payload",
},
{
label: "Product",
operations: [
{
operationType: "parse",
parse: {
returns: {
array: false,
type: fields.product.type,
},
source: "$['product']",
},
},
],
reference: "product",
rootReference: "payload",
},
],
},
})
const webhookRecipient = new honeycomb.WebhookRecipient(`IncidentWebhook`, {
name: "Incident.io Webhook",
url: alertSource.alertEventsUrl,
secret: alertSource.secretToken,
templates: [
{
type: "trigger",
body: $jsonStringify({
title: "{{ .Name }}",
description: "{{ .Description }}",
status: "{{ .Alert.Status }}",
deduplication_key: "{{ .Alert.InstanceID }}",
source_url: "{{ .Result.URL }}",
model: "{{ .Vars.model }}",
product: "{{ .Vars.product }}",
}),
},
],
variables: [
{
name: "model",
},
{
name: "product",
},
],
})
new incident.AlertRoute("HoneycombAlertRoute", {
name: $app.stage === "production" ? "Honeycomb" : `Honeycomb (${$app.stage})`,
enabled: true,
isPrivate: false,
alertSources: [
{
alertSourceId: alertSource.id,
conditionGroups: [
{
conditions: [
{
subject: "alert.title",
operation: "is_set",
paramBindings: [],
},
],
},
],
},
],
conditionGroups: [
{
conditions: [
{
subject: "alert.title",
operation: "is_set",
paramBindings: [],
},
],
},
],
expressions: [],
escalationConfig: {
autoCancelEscalations: true,
escalationTargets: [],
},
incidentConfig: {
autoDeclineEnabled: true,
enabled: true,
conditionGroups: [],
deferTimeSeconds: 0,
groupingKeys: [
{
reference: $interpolate`alert.attributes.${fields.model.id}`,
},
{
reference: $interpolate`alert.attributes.${fields.product.id}`,
},
],
groupingWindowSeconds: 900,
},
incidentTemplate: {
name: {
value: {
literal: varSpec("Alert -> Title", "alert.title"),
},
},
summary: {
value: {
literal: varSpec("Alert -> Description", "alert.description"),
},
},
startInTriage: {
value: {
literal: "true",
},
},
severity: {
mergeStrategy: "first-wins",
},
incidentMode: {
value: {
literal: "standard",
// literal: $app.stage === "production" ? "standard" : "test",
},
},
},
})
type Product = "go" | "zen"
type Trigger = (opts: { model: string; product: Product }) => {
id: string
title: string
description: string
json: honeycomb.GetQuerySpecificationOutputArgs
thresholds: honeycomb.TriggerArgs["thresholds"]
}
type Model = { id: string; products: Product[]; triggers: Trigger[] }
const httpErrors: Trigger = ({ model, product }: { model: string; product: Product }) => ({
id: `IncreasedHttpErrors`,
title: `Increased HTTP Errors for ${displayName(model)} on ${displayName(product)}`,
description: `Detected increased rate of HTTP errors for ${displayName(model)} on OpenCode ${displayName(product)}`,
json: {
calculations: [
{
op: "COUNT",
name: "TOTAL",
filterCombination: "AND",
filters: [
{ column: "model", op: "=", value: model },
{ column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" },
],
},
{
op: "COUNT",
name: "FAILED",
filterCombination: "AND",
filters: [
{ column: "model", op: "=", value: model },
{ column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" },
{ column: "status", op: ">=", value: "400" },
{ column: "status", op: "!=", value: "401" },
],
},
],
formulas: [{ name: "ERROR", expression: "$FAILED / $TOTAL" }],
timeRange: 900,
},
thresholds: [{ op: ">=", value: 50, exceededLimit: 1 }],
})
const models: Model[] = [
{ id: "kimi-k2.6", products: ["go", "zen"], triggers: [httpErrors] },
{ id: "claude-opus-4.7", products: ["zen"], triggers: [httpErrors] },
]
for (const model of models) {
for (const product of model.products) {
for (const trigger of model.triggers) {
const spec = trigger({ model: model.id, product })
new honeycomb.Trigger(`${spec.id}${resourceName(product)}${resourceName(model.id)}`, {
name: spec.title,
description: spec.description,
queryJson: honeycomb.getQuerySpecificationOutput(spec.json).json,
frequency: 900,
alertType: "on_change",
baselineDetails: [{ type: "percentage", offsetMinutes: 60 }],
thresholds: spec.thresholds,
recipients: [
{
id: webhookRecipient.id,
notificationDetails: [
{
variables: [
{ name: "model", value: model.id },
{ name: "product", value: product },
],
},
],
},
],
})
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.14.31",
"version": "1.14.30",
"description": "",
"type": "module",
"exports": {

View File

@@ -204,9 +204,6 @@ function createGlobalSync() {
},
translate: language.t,
getSdk: sdkFor,
global: {
provider: globalStore.provider,
},
})
async function loadSessions(directory: string) {

View File

@@ -260,6 +260,9 @@ 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 }))
}

View File

@@ -23,7 +23,6 @@ describe("createChildStoreManager", () => {
onDispose() {},
translate: (key) => key,
getSdk: () => null!,
global: { provider: null! },
})
Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => {

View File

@@ -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, ProviderListResponse, VcsInfo } from "@opencode-ai/sdk/v2/client"
import type { OpencodeClient, VcsInfo } from "@opencode-ai/sdk/v2/client"
import {
DIR_IDLE_TTL_MS,
MAX_DIR_STORES,
@@ -27,9 +27,6 @@ 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>()
@@ -192,13 +189,7 @@ export function createChildStoreManager(input: {
get provider_ready() {
return !providerQuery.isLoading
},
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
},
provider: { all: [], connected: [], default: {} },
config: {},
get path() {
if (pathQuery.isLoading || !pathQuery.data)

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.14.31",
"version": "1.14.30",
"type": "module",
"license": "MIT",
"scripts": {

View File

@@ -141,10 +141,7 @@ export async function handler(
)
validateModelSettings(billingSource, authInfo)
updateProviderKey(authInfo, providerInfo)
logger.metric({
provider: providerInfo.id,
"provider.model": providerInfo.model,
})
logger.metric({ provider: providerInfo.id })
const startTimestamp = Date.now()
const reqUrl = providerInfo.modifyUrl(providerInfo.api, isStream)
@@ -152,23 +149,12 @@ export async function handler(
providerInfo.modifyBody({
...createBodyConverter(opts.format, providerInfo.format)(body),
model: providerInfo.model,
...(() => {
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 ?? {})
})(),
...providerInfo.payloadModifier,
...Object.fromEntries(
Object.entries(providerInfo.payloadMappings ?? {})
.map(([k, v]) => [k, input.request.headers.get(v)])
.filter(([_k, v]) => !!v),
),
}),
)
logger.debug("REQUEST URL: " + reqUrl)
@@ -528,6 +514,7 @@ 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)

View File

@@ -23,7 +23,7 @@ type Usage = {
}
}
export const oaCompatHelper: ProviderHelper = ({ adjustCacheUsage }) => ({
export const oaCompatHelper: ProviderHelper = ({ adjustCacheUsage, safetyIdentifier }) => ({
format: "oa-compat",
modifyUrl: (providerApi: string) => providerApi + "/chat/completions",
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
@@ -34,6 +34,7 @@ export const oaCompatHelper: ProviderHelper = ({ adjustCacheUsage }) => ({
return {
...body,
...(body.stream ? { stream_options: { include_usage: true } } : {}),
...(safetyIdentifier ? { safety_identifier: safetyIdentifier } : {}),
}
},
createBinaryStreamDecoder: () => undefined,

View File

@@ -18,7 +18,10 @@ 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,
modifyBody: (body: Record<string, any>) => ({
...body,
...(workspaceID ? { safety_identifier: workspaceID } : {}),
}),
createBinaryStreamDecoder: () => undefined,
streamSeparator: "\n\n",
createUsageParser: () => {

View File

@@ -37,6 +37,7 @@ export type ProviderHelper = (input: {
reqModel: string
providerModel: string
adjustCacheUsage?: boolean
safetyIdentifier?: string
workspaceID?: string
}) => {
format: ZenData.Format

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.14.31",
"version": "1.14.30",
"private": true,
"type": "module",
"license": "MIT",

View File

@@ -40,6 +40,7 @@ export namespace ZenData {
disabled: z.boolean().optional(),
storeModel: z.string().optional(),
payloadModifier: z.record(z.string(), z.any()).optional(),
safetyIdentifier: z.boolean().optional(),
}),
),
})

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.14.31",
"version": "1.14.30",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.14.31",
"version": "1.14.30",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.14.31",
"version": "1.14.30",
"name": "@opencode-ai/core",
"type": "module",
"license": "MIT",

View File

@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop-electron",
"private": true,
"version": "1.14.31",
"version": "1.14.30",
"type": "module",
"license": "MIT",
"homepage": "https://opencode.ai",

View File

@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.14.31",
"version": "1.14.30",
"type": "module",
"license": "MIT",
"scripts": {

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.14.31",
"version": "1.14.30",
"private": true,
"type": "module",
"license": "MIT",

View File

@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
version = "1.14.31"
version = "1.14.30"
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.31/opencode-darwin-arm64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.30/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.31/opencode-darwin-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.30/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.31/opencode-linux-arm64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.30/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.31/opencode-linux-x64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.30/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.31/opencode-windows-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.30/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.14.31",
"version": "1.14.30",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.14.31",
"version": "1.14.30",
"name": "opencode",
"type": "module",
"license": "MIT",

View File

@@ -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` | adapter/list/status/create/remove/session-restore |
| `workspace` | `bridged` | adaptor/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/adapter` - list workspace adapters.
- [x] `GET /experimental/workspace/adaptor` - list workspace adaptors.
- [x] `POST /experimental/workspace` - create workspace.
- [x] `GET /experimental/workspace` - list workspaces.
- [x] `GET /experimental/workspace/status` - workspace status.

View File

@@ -353,7 +353,7 @@ piecewise.
- [ ] `src/cli/cmd/tui/event.ts`
- [ ] `src/cli/ui.ts`
- [ ] `src/command/index.ts`
- [x] `src/control-plane/adapters/worktree.ts`
- [x] `src/control-plane/adaptors/worktree.ts`
- [x] `src/control-plane/types.ts`
- [x] `src/control-plane/workspace.ts`
- [ ] `src/file/index.ts`

View File

@@ -10,7 +10,7 @@ import { errorMessage } from "@/util/error"
import { useSDK } from "../context/sdk"
import { useToast } from "../ui/toast"
type Adapter = {
type Adaptor = {
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 [adapters, setAdapters] = createSignal<Adapter[]>()
const [adaptors, setAdaptors] = createSignal<Adaptor[]>()
onMount(() => {
dialog.setSize("medium")
void (async () => {
const dir = sync.path.directory || sdk.directory
const url = new URL("/experimental/workspace/adapter", sdk.url)
const url = new URL("/experimental/workspace/adaptor", sdk.url)
if (dir) url.searchParams.set("directory", dir)
const res = await sdk
.fetch(url)
.then((x) => x.json() as Promise<Adapter[]>)
.then((x) => x.json() as Promise<Adaptor[]>)
.catch(() => undefined)
if (!res) {
toast.show({
message: "Failed to load workspace adapters",
message: "Failed to load workspace adaptors",
variant: "error",
})
return
}
setAdapters(res)
setAdaptors(res)
})()
})
@@ -142,13 +142,13 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) =
},
]
}
const list = adapters()
const list = adaptors()
if (!list) {
return [
{
title: "Loading workspaces...",
value: "loading" as const,
description: "Fetching available workspace adapters",
description: "Fetching available workspace adaptors",
},
]
}

View File

@@ -1,5 +1,4 @@
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"
@@ -555,7 +554,6 @@ 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>()
@@ -686,7 +684,6 @@ 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,
@@ -792,7 +789,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 === "█" && useSubpixelBlocks()) {
if (char === "█") {
return (
<text
fg={shade(inkTop, theme, n + p + e + b)}

View File

@@ -1,45 +0,0 @@
import type { ProjectID } from "@/project/schema"
import type { WorkspaceAdapter, WorkspaceAdapterEntry } from "../types"
import { WorktreeAdapter } from "./worktree"
const BUILTIN: Record<string, WorkspaceAdapter> = {
worktree: WorktreeAdapter,
}
const state = new Map<ProjectID, Map<string, WorkspaceAdapter>>()
export function getAdapter(projectID: ProjectID, type: string): WorkspaceAdapter {
const custom = state.get(projectID)?.get(type)
if (custom) return custom
const builtin = BUILTIN[type]
if (builtin) return builtin
throw new Error(`Unknown workspace adapter: ${type}`)
}
export async function listAdapters(projectID: ProjectID): Promise<WorkspaceAdapterEntry[]> {
const builtin = await Promise.all(
Object.entries(BUILTIN).map(async ([type, adapter]) => {
return {
type,
name: adapter.name,
description: adapter.description,
}
}),
)
const custom = [...(state.get(projectID)?.entries() ?? [])].map(([type, adapter]) => ({
type,
name: adapter.name,
description: adapter.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 registerAdapter(projectID: ProjectID, type: string, adapter: WorkspaceAdapter) {
const adapters = state.get(projectID) ?? new Map<string, WorkspaceAdapter>()
adapters.set(type, adapter)
state.set(projectID, adapters)
}

View File

@@ -0,0 +1,45 @@
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)
}

View File

@@ -1,5 +1,5 @@
import { Schema } from "effect"
import { type WorkspaceAdapter, WorkspaceInfo } from "../types"
import { type WorkspaceAdaptor, WorkspaceInfo } from "../types"
const WorktreeConfig = Schema.Struct({
name: WorkspaceInfo.fields.name,
@@ -13,7 +13,7 @@ async function loadWorktree() {
return { AppRuntime, Worktree }
}
export const WorktreeAdapter: WorkspaceAdapter = {
export const WorktreeAdaptor: WorkspaceAdaptor = {
name: "Worktree",
description: "Create a git worktree",
async configure(info) {

View File

@@ -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 WorkspaceAdapterEntry = Schema.Struct({
export const WorkspaceAdaptorEntry = Schema.Struct({
type: Schema.String,
name: Schema.String,
description: Schema.String,
}).pipe(withStatics((s) => ({ zod: zod(s) })))
export type WorkspaceAdapterEntry = Schema.Schema.Type<typeof WorkspaceAdapterEntry>
export type WorkspaceAdaptorEntry = Schema.Schema.Type<typeof WorkspaceAdaptorEntry>
export type Target =
| {
@@ -35,7 +35,7 @@ export type Target =
headers?: HeadersInit
}
export type WorkspaceAdapter = {
export type WorkspaceAdaptor = {
name: string
description: string
configure(info: WorkspaceInfo): WorkspaceInfo | Promise<WorkspaceInfo>

View File

@@ -16,7 +16,7 @@ import { Filesystem } from "@/util/filesystem"
import { ProjectID } from "@/project/schema"
import { Slug } from "@opencode-ai/core/util/slug"
import { WorkspaceTable } from "./workspace.sql"
import { getAdapter } from "./adapters"
import { getAdaptor } from "./adaptors"
import { type WorkspaceInfo, WorkspaceInfo as WorkspaceInfoSchema } from "./types"
import { WorkspaceID } from "./schema"
import { Session } from "@/session/session"
@@ -335,8 +335,8 @@ export const layer = Layer.effect(
})
const syncWorkspaceLoop = Effect.fn("Workspace.syncWorkspaceLoop")(function* (space: Info) {
const adapter = getAdapter(space.projectID, space.type)
const target = yield* Effect.promise(() => Promise.resolve(adapter.target(space)))
const adaptor = getAdaptor(space.projectID, space.type)
const target = yield* Effect.promise(() => Promise.resolve(adaptor.target(space)))
if (target.type === "local") return
@@ -419,8 +419,8 @@ export const layer = Layer.effect(
const startSync = Effect.fn("Workspace.startSync")(function* (space: Info) {
if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) return
const adapter = getAdapter(space.projectID, space.type)
const target = yield* Effect.promise(() => Promise.resolve(adapter.target(space)))
const adaptor = getAdaptor(space.projectID, space.type)
const target = yield* Effect.promise(() => Promise.resolve(adaptor.target(space)))
if (target.type === "local") {
setStatus(space.id, (yield* Effect.promise(() => Filesystem.exists(target.directory))) ? "connected" : "error")
@@ -458,9 +458,9 @@ export const layer = Layer.effect(
const create = Effect.fn("Workspace.create")(function* (input: CreateInput) {
const id = WorkspaceID.ascending(input.id)
const adapter = getAdapter(input.projectID, input.type)
const adaptor = getAdaptor(input.projectID, input.type)
const config = yield* Effect.promise(() =>
Promise.resolve(adapter.configure({ ...input, id, name: Slug.create(), directory: null })),
Promise.resolve(adaptor.configure({ ...input, id, name: Slug.create(), directory: null })),
)
const info: Info = {
@@ -496,7 +496,7 @@ export const layer = Layer.effect(
OTEL_RESOURCE_ATTRIBUTES: process.env.OTEL_RESOURCE_ATTRIBUTES,
}
yield* Effect.promise(() => adapter.create(config, env))
yield* Effect.promise(() => adaptor.create(config, env))
yield* Effect.all(
[
waitEvent({
@@ -531,8 +531,8 @@ export const layer = Layer.effect(
workspaceID: input.workspaceID,
})
const adapter = getAdapter(space.projectID, space.type)
const target = yield* Effect.promise(() => Promise.resolve(adapter.target(space)))
const adaptor = getAdaptor(space.projectID, space.type)
const target = yield* Effect.promise(() => Promise.resolve(adaptor.target(space)))
yield* sync.run(Session.Event.Updated, {
sessionID: input.sessionID,
@@ -726,12 +726,12 @@ export const layer = Layer.effect(
const info = fromRow(row)
yield* Effect.catch(
Effect.gen(function* () {
const adapter = getAdapter(info.projectID, row.type)
yield* Effect.tryPromise(() => Promise.resolve(adapter.remove(info)))
const adaptor = getAdaptor(info.projectID, row.type)
yield* Effect.tryPromise(() => Promise.resolve(adaptor.remove(info)))
}),
() =>
Effect.sync(() => {
log.error("adapter not available when removing workspace", { type: row.type })
log.error("adaptor not available when removing workspace", { type: row.type })
}),
)

View File

@@ -3,7 +3,7 @@ import type {
PluginInput,
Plugin as PluginInstance,
PluginModule,
WorkspaceAdapter as PluginWorkspaceAdapter,
WorkspaceAdaptor as PluginWorkspaceAdaptor,
} from "@opencode-ai/plugin"
import { Config } from "@/config/config"
import { Bus } from "../bus"
@@ -24,8 +24,8 @@ import { InstanceState } from "@/effect/instance-state"
import { errorMessage } from "@/util/error"
import { PluginLoader } from "./loader"
import { parsePluginSpecifier, readPluginId, readV1Plugin, resolvePluginId } from "./shared"
import { registerAdapter } from "@/control-plane/adapters"
import type { WorkspaceAdapter } from "@/control-plane/types"
import { registerAdaptor } from "@/control-plane/adaptors"
import type { WorkspaceAdaptor } from "@/control-plane/types"
const log = Log.create({ service: "plugin" })
@@ -138,8 +138,8 @@ export const layer = Layer.effect(
worktree: ctx.worktree,
directory: ctx.directory,
experimental_workspace: {
register(type: string, adapter: PluginWorkspaceAdapter) {
registerAdapter(ctx.project.id, type, adapter as WorkspaceAdapter)
register(type: string, adaptor: PluginWorkspaceAdaptor) {
registerAdaptor(ctx.project.id, type, adaptor as WorkspaceAdaptor)
},
},
get serverUrl(): URL {

View File

@@ -2,10 +2,10 @@ import { Hono } from "hono"
import { describeRoute, resolver, validator } from "hono-openapi"
import z from "zod"
import { Effect } from "effect"
import { listAdapters } from "@/control-plane/adapters"
import { listAdaptors } from "@/control-plane/adaptors"
import { Workspace } from "@/control-plane/workspace"
import { AppRuntime } from "@/effect/app-runtime"
import { WorkspaceAdapterEntry } from "@/control-plane/types"
import { WorkspaceAdaptorEntry } 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(
"/adapter",
"/adaptor",
describeRoute({
summary: "List workspace adapters",
description: "List all available workspace adapters for the current project.",
operationId: "experimental.workspace.adapter.list",
summary: "List workspace adaptors",
description: "List all available workspace adaptors for the current project.",
operationId: "experimental.workspace.adaptor.list",
responses: {
200: {
description: "Workspace adapters",
description: "Workspace adaptors",
content: {
"application/json": {
schema: resolver(z.array(zodObject(WorkspaceAdapterEntry))),
schema: resolver(z.array(zodObject(WorkspaceAdaptorEntry))),
},
},
},
},
}),
async (c) => {
return c.json(await listAdapters(Instance.project.id))
return c.json(await listAdaptors(Instance.project.id))
},
)
.post(

View File

@@ -1,5 +1,5 @@
import { Workspace } from "@/control-plane/workspace"
import { WorkspaceAdapterEntry } from "@/control-plane/types"
import { WorkspaceAdaptorEntry } 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 = {
adapters: `${root}/adapter`,
adaptors: `${root}/adaptor`,
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("adapters", WorkspacePaths.adapters, {
success: described(Schema.Array(WorkspaceAdapterEntry), "Workspace adapters"),
HttpApiEndpoint.get("adaptors", WorkspacePaths.adaptors, {
success: described(Schema.Array(WorkspaceAdaptorEntry), "Workspace adaptors"),
}).annotateMerge(
OpenApi.annotations({
identifier: "experimental.workspace.adapter.list",
summary: "List workspace adapters",
description: "List all available workspace adapters for the current project.",
identifier: "experimental.workspace.adaptor.list",
summary: "List workspace adaptors",
description: "List all available workspace adaptors for the current project.",
}),
),
HttpApiEndpoint.get("list", WorkspacePaths.list, {

View File

@@ -1,4 +1,4 @@
import { listAdapters } from "@/control-plane/adapters"
import { listAdaptors } from "@/control-plane/adaptors"
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 adapters = Effect.fn("WorkspaceHttpApi.adapters")(function* () {
const adaptors = Effect.fn("WorkspaceHttpApi.adaptors")(function* () {
const instance = yield* InstanceState.context
return yield* Effect.promise(() => listAdapters(instance.project.id))
return yield* Effect.promise(() => listAdaptors(instance.project.id))
})
const list = Effect.fn("WorkspaceHttpApi.list")(function* () {
@@ -51,7 +51,7 @@ export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspac
})
return handlers
.handle("adapters", adapters)
.handle("adaptors", adaptors)
.handle("list", list)
.handle("create", create)
.handle("status", status)

View File

@@ -1,4 +1,4 @@
import { getAdapter } from "@/control-plane/adapters"
import { getAdaptor } from "@/control-plane/adaptors"
import { WorkspaceID } from "@/control-plane/schema"
import type { Target } from "@/control-plane/types"
import { Workspace } from "@/control-plane/workspace"
@@ -89,8 +89,8 @@ function missingWorkspaceResponse(id: WorkspaceID): HttpServerResponse.HttpServe
function resolveTarget(workspace: Workspace.Info): Effect.Effect<Target> {
return Effect.gen(function* () {
const adapter = yield* Effect.sync(() => getAdapter(workspace.projectID, workspace.type))
return yield* Effect.promise(() => Promise.resolve(adapter.target(workspace)))
const adaptor = yield* Effect.sync(() => getAdaptor(workspace.projectID, workspace.type))
return yield* Effect.promise(() => Promise.resolve(adaptor.target(workspace)))
})
}

View File

@@ -1,6 +1,6 @@
import type { MiddlewareHandler } from "hono"
import type { UpgradeWebSocket } from "hono/ws"
import { getAdapter } from "@/control-plane/adapters"
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"
@@ -91,8 +91,8 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
return next()
}
const adapter = getAdapter(workspace.projectID, workspace.type)
const target = await adapter.target(workspace)
const adaptor = getAdaptor(workspace.projectID, workspace.type)
const target = await adaptor.target(workspace)
if (target.type === "local") {
return WorkspaceContext.provide({

View File

@@ -772,7 +772,7 @@ export const toModelMessagesEffect = Effect.fnUntraced(function* (
return {
type: "content",
value: [
...(outputObject.text ? [{ type: "text", text: outputObject.text }] : []),
{ type: "text", text: outputObject.text },
...attachments.map((attachment) => ({
type: "media",
mediaType: attachment.mime,
@@ -938,18 +938,10 @@ 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,
providerMetadata: part.metadata,
...(differentModel ? {} : { providerMetadata: part.metadata }),
})
}
}

View File

@@ -142,9 +142,9 @@ const Share = Schema.Struct({
url: Schema.String,
})
// 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
// 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
const Time = Schema.Struct({
created: NonNegativeInt,

View File

@@ -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 through 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 throught 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 event only contains the fields updated. We convert the event to contain the 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 even only contains the fields updated. We convert the event to contain to 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 definition 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 defintiion of the event shape (provided by a zod schema, inferred into a TypeScript type). Examples:
```ts
// The schema from `Updated` typechecks the object correctly

View File

@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import { getAdapter, registerAdapter } from "../../src/control-plane/adapters"
import { getAdaptor, registerAdaptor } from "../../src/control-plane/adaptors"
import { ProjectID } from "../../src/project/schema"
import type { WorkspaceInfo } from "../../src/control-plane/types"
@@ -15,7 +15,7 @@ function info(projectID: WorkspaceInfo["projectID"], type: string): WorkspaceInf
}
}
function adapter(dir: string) {
function adaptor(dir: string) {
return {
name: dir,
description: dir,
@@ -33,19 +33,19 @@ function adapter(dir: string) {
}
}
describe("control-plane/adapters", () => {
test("isolates custom adapters by project", async () => {
describe("control-plane/adaptors", () => {
test("isolates custom adaptors 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)}`)
registerAdapter(one, type, adapter("/one"))
registerAdapter(two, type, adapter("/two"))
registerAdaptor(one, type, adaptor("/one"))
registerAdaptor(two, type, adaptor("/two"))
expect(await (await getAdapter(one, type)).target(info(one, type))).toEqual({
expect(await (await getAdaptor(one, type)).target(info(one, type))).toEqual({
type: "local",
directory: "/one",
})
expect(await (await getAdapter(two, type)).target(info(two, type))).toEqual({
expect(await (await getAdaptor(two, type)).target(info(two, type))).toEqual({
type: "local",
directory: "/two",
})
@@ -54,16 +54,16 @@ describe("control-plane/adapters", () => {
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)}`)
registerAdapter(id, type, adapter("/one"))
registerAdaptor(id, type, adaptor("/one"))
expect(await (await getAdapter(id, type)).target(info(id, type))).toEqual({
expect(await (await getAdaptor(id, type)).target(info(id, type))).toEqual({
type: "local",
directory: "/one",
})
registerAdapter(id, type, adapter("/two"))
registerAdaptor(id, type, adaptor("/two"))
expect(await (await getAdapter(id, type)).target(info(id, type))).toEqual({
expect(await (await getAdaptor(id, type)).target(info(id, type))).toEqual({
type: "local",
directory: "/two",
})

View File

@@ -23,10 +23,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 { registerAdapter } from "../../src/control-plane/adapters"
import { registerAdaptor } from "../../src/control-plane/adaptors"
import { WorkspaceID } from "../../src/control-plane/schema"
import { WorkspaceTable } from "../../src/control-plane/workspace.sql"
import type { Target, WorkspaceAdapter, WorkspaceInfo } from "../../src/control-plane/types"
import type { Target, WorkspaceAdaptor, WorkspaceInfo } from "../../src/control-plane/types"
import * as WorkspaceOld from "../../src/control-plane/workspace"
import { AppRuntime } from "@/effect/app-runtime"
@@ -53,8 +53,8 @@ type RecordedCreate = {
from?: WorkspaceInfo
}
type RecordedAdapter = {
adapter: WorkspaceAdapter
type RecordedAdaptor = {
adaptor: WorkspaceAdaptor
calls: {
configure: WorkspaceInfo[]
create: RecordedCreate[]
@@ -165,13 +165,13 @@ function eventuallyEffect(effect: Effect.Effect<void>, timeout = 1500) {
})
}
function recordedAdapter(input: {
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>
}): RecordedAdapter {
const calls: RecordedAdapter["calls"] = {
}): RecordedAdaptor {
const calls: RecordedAdaptor["calls"] = {
configure: [],
create: [],
remove: [],
@@ -180,7 +180,7 @@ function recordedAdapter(input: {
return {
calls,
adapter: {
adaptor: {
name: "recorded",
description: "recorded",
configure(info) {
@@ -207,8 +207,8 @@ function recordedAdapter(input: {
}
}
function localAdapter(dir: string, input?: { createDir?: boolean; remove?: (info: WorkspaceInfo) => Promise<void> }) {
return recordedAdapter({
function localAdaptor(dir: string, input?: { createDir?: boolean; remove?: (info: WorkspaceInfo) => Promise<void> }) {
return recordedAdaptor({
configure(info) {
return { ...info, directory: dir }
},
@@ -223,8 +223,8 @@ function localAdapter(dir: string, input?: { createDir?: boolean; remove?: (info
})
}
function remoteAdapter(url: string, input?: { directory?: string | null; headers?: HeadersInit }) {
return recordedAdapter({
function remoteAdaptor(url: string, input?: { directory?: string | null; headers?: HeadersInit }) {
return recordedAdaptor({
configure(info) {
return { ...info, directory: input?.directory ?? info.directory }
},
@@ -429,7 +429,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 = recordedAdapter({
const recorded = recordedAdaptor({
configure(info) {
return {
...info,
@@ -446,7 +446,7 @@ describe("workspace-old CRUD", () => {
return { type: "local", directory: targetDir }
},
})
registerAdapter(Instance.project.id, type, recorded.adapter)
registerAdaptor(Instance.project.id, type, recorded.adaptor)
const info = await createWorkspace({
id: workspaceID,
@@ -489,17 +489,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")
registerAdapter(
registerAdaptor(
Instance.project.id,
type,
recordedAdapter({
recordedAdaptor({
configure() {
throw new Error("configure exploded")
},
target() {
return { type: "local", directory: "/unused" }
},
}).adapter,
}).adaptor,
)
await expect(
@@ -509,10 +509,10 @@ describe("workspace-old CRUD", () => {
})
})
test("create leaves the inserted row when adapter create fails", async () => {
test("create leaves the inserted row when adaptor create fails", async () => {
await withInstance(async () => {
const type = unique("create-failure")
const recorded = recordedAdapter({
const recorded = recordedAdaptor({
async create() {
throw new Error("create exploded")
},
@@ -520,7 +520,7 @@ describe("workspace-old CRUD", () => {
return { type: "local", directory: "/unused" }
},
})
registerAdapter(Instance.project.id, type, recorded.adapter)
registerAdaptor(Instance.project.id, type, recorded.adaptor)
await expect(
createWorkspace({ type, branch: "branch", projectID: Instance.project.id, extra: { x: 1 } }),
@@ -538,8 +538,8 @@ describe("workspace-old CRUD", () => {
await withInstance(async (dir) => {
const type = unique("local-error")
const missing = path.join(dir, "missing-local-target")
const recorded = localAdapter(missing, { createDir: false })
registerAdapter(Instance.project.id, type, recorded.adapter)
const recorded = localAdaptor(missing, { createDir: false })
registerAdaptor(Instance.project.id, type, recorded.adaptor)
const info = await createWorkspace({ type, branch: null, projectID: Instance.project.id, extra: null })
@@ -576,8 +576,8 @@ describe("workspace-old CRUD", () => {
Effect.gen(function* () {
const workspace = yield* WorkspaceOld.Service
const type = unique("remote-create")
const recorded = remoteAdapter(`${url}/base/?ignored=1#hash`, { directory: dir })
registerAdapter(Instance.project.id, type, recorded.adapter)
const recorded = remoteAdaptor(`${url}/base/?ignored=1#hash`, { directory: dir })
registerAdaptor(Instance.project.id, type, recorded.adaptor)
const info = yield* workspace.create({ type, branch: null, projectID: Instance.project.id, extra: null })
@@ -603,11 +603,11 @@ describe("workspace-old CRUD", () => {
})
})
test("remove deletes the workspace, associated sessions, adapter resources, and status", async () => {
test("remove deletes the workspace, associated sessions, adaptor resources, and status", async () => {
await withInstance(async (dir) => {
const type = unique("remove-local")
const recorded = localAdapter(path.join(dir, "remove-local"))
registerAdapter(Instance.project.id, type, recorded.adapter)
const recorded = localAdaptor(path.join(dir, "remove-local"))
registerAdaptor(Instance.project.id, type, recorded.adaptor)
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 +628,21 @@ describe("workspace-old CRUD", () => {
})
})
test("remove still deletes the row when the adapter cannot remove resources", async () => {
test("remove still deletes the row when the adaptor 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") })
registerAdapter(
registerAdaptor(
Instance.project.id,
type,
recordedAdapter({
recordedAdaptor({
async remove() {
throw new Error("remove exploded")
},
target() {
return { type: "local", directory: "/unused" }
},
}).adapter,
}).adaptor,
)
insertWorkspace(info)
@@ -661,7 +661,7 @@ describe("workspace-old sync state", () => {
const session = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))
attachSessionToWorkspace(session.id, info.id)
insertWorkspace(info)
registerAdapter(Instance.project.id, type, localAdapter(path.join(dir, "flag-disabled")).adapter)
registerAdaptor(Instance.project.id, type, localAdaptor(path.join(dir, "flag-disabled")).adaptor)
startWorkspaceSyncing(Instance.project.id)
await delay(25)
@@ -682,8 +682,8 @@ describe("workspace-old sync state", () => {
await fs.mkdir(withoutSessionDir, { recursive: true })
insertWorkspace(withSession)
insertWorkspace(withoutSession)
registerAdapter(Instance.project.id, withSessionType, localAdapter(withSessionDir).adapter)
registerAdapter(Instance.project.id, withoutSessionType, localAdapter(withoutSessionDir).adapter)
registerAdaptor(Instance.project.id, withSessionType, localAdaptor(withSessionDir).adaptor)
registerAdaptor(Instance.project.id, withoutSessionType, localAdaptor(withoutSessionDir).adaptor)
attachSessionToWorkspace(
(await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))).id,
withSession.id,
@@ -707,10 +707,10 @@ describe("workspace-old sync state", () => {
const type = unique("missing-local")
const info = workspaceInfo(Instance.project.id, type)
insertWorkspace(info)
registerAdapter(
registerAdaptor(
Instance.project.id,
type,
localAdapter(path.join(dir, "missing-target"), { createDir: false }).adapter,
localAdaptor(path.join(dir, "missing-target"), { createDir: false }).adaptor,
)
attachSessionToWorkspace(
(await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))).id,
@@ -738,7 +738,7 @@ describe("workspace-old sync state", () => {
const target = path.join(dir, "dedupe-local")
await fs.mkdir(target, { recursive: true })
insertWorkspace(info)
registerAdapter(Instance.project.id, type, localAdapter(target).adapter)
registerAdaptor(Instance.project.id, type, localAdaptor(target).adaptor)
attachSessionToWorkspace(
(await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))).id,
info.id,
@@ -795,7 +795,7 @@ describe("workspace-old sync state", () => {
const type = unique("remote-start")
const info = workspaceInfo(Instance.project.id, type)
insertWorkspace(info)
registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/sync`).adapter)
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/sync`).adaptor)
attachSessionToWorkspace((yield* sessionSvc.create({})).id, info.id)
yield* workspace.startWorkspaceSyncing(Instance.project.id)
@@ -850,7 +850,7 @@ describe("workspace-old sync state", () => {
const type = unique("remote-connect-fail")
const info = workspaceInfo(Instance.project.id, type)
insertWorkspace(info)
registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/failed`).adapter)
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/failed`).adaptor)
attachSessionToWorkspace((yield* sessionSvc.create({})).id, info.id)
yield* workspace.startWorkspaceSyncing(Instance.project.id)
@@ -890,7 +890,7 @@ describe("workspace-old sync state", () => {
const type = unique("remote-history-fail")
const info = workspaceInfo(Instance.project.id, type)
insertWorkspace(info)
registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/history-failed`).adapter)
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/history-failed`).adaptor)
attachSessionToWorkspace((yield* sessionSvc.create({})).id, info.id)
yield* workspace.startWorkspaceSyncing(Instance.project.id)
@@ -947,7 +947,7 @@ describe("workspace-old sync state", () => {
const type = unique("history-replay")
const info = workspaceInfo(Instance.project.id, type)
insertWorkspace(info)
registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/history`).adapter)
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/history`).adaptor)
const session = yield* sessionSvc.create({ title: "before history" })
attachSessionToWorkspace(session.id, info.id)
historySessionID = session.id
@@ -1014,7 +1014,7 @@ describe("workspace-old sync state", () => {
const type = unique("sse-forward")
const info = workspaceInfo(Instance.project.id, type)
insertWorkspace(info)
registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/sse-forward`).adapter)
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/sse-forward`).adaptor)
attachSessionToWorkspace((yield* sessionSvc.create({})).id, info.id)
yield* workspace.startWorkspaceSyncing(Instance.project.id)
@@ -1095,7 +1095,7 @@ describe("workspace-old sync state", () => {
const type = unique("sse-sync")
const info = workspaceInfo(Instance.project.id, type)
insertWorkspace(info)
registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/sse-sync`).adapter)
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/sse-sync`).adaptor)
const session = yield* sessionSvc.create({ title: "before sse" })
attachSessionToWorkspace(session.id, info.id)
sseSessionID = session.id
@@ -1232,7 +1232,7 @@ describe("workspace-old sessionRestore", () => {
const type = unique("restore-missing-session")
const info = workspaceInfo(Instance.project.id, type, { directory: dir })
insertWorkspace(info)
registerAdapter(Instance.project.id, type, localAdapter(dir).adapter)
registerAdaptor(Instance.project.id, type, localAdaptor(dir).adaptor)
await expect(
restoreWorkspaceSession({ workspaceID: info.id, sessionID: SessionID.descending("ses_missing_restore") }),
@@ -1273,13 +1273,13 @@ describe("workspace-old sessionRestore", () => {
const type = unique("restore-remote")
const info = workspaceInfo(Instance.project.id, type, { directory: dir })
insertWorkspace(info)
registerAdapter(
registerAdaptor(
Instance.project.id,
type,
remoteAdapter(`${url}/restore/?ignored=1#hash`, {
remoteAdaptor(`${url}/restore/?ignored=1#hash`, {
directory: dir,
headers: { authorization: "Bearer restore" },
}).adapter,
}).adaptor,
)
const session = yield* sessionSvc.create({ title: "restore remote" })
replaceSessionEvents(session.id, 24)
@@ -1353,7 +1353,7 @@ describe("workspace-old sessionRestore", () => {
const type = unique("restore-null-dir")
const info = workspaceInfo(Instance.project.id, type, { directory: null })
insertWorkspace(info)
registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/null-dir`, { directory: null }).adapter)
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/null-dir`, { directory: null }).adaptor)
const session = yield* sessionSvc.create({ title: "null dir" })
replaceSessionEvents(session.id, 0)
@@ -1397,7 +1397,7 @@ describe("workspace-old sessionRestore", () => {
const type = unique("restore-remote-fail")
const info = workspaceInfo(Instance.project.id, type, { directory: dir })
insertWorkspace(info)
registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/fail`, { directory: dir }).adapter)
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/fail`, { directory: dir }).adaptor)
const session = yield* sessionSvc.create({ title: "restore fail" })
replaceSessionEvents(session.id, 11)
@@ -1437,7 +1437,7 @@ describe("workspace-old sessionRestore", () => {
const type = unique("restore-local")
const info = workspaceInfo(Instance.project.id, type, { directory: dir })
insertWorkspace(info)
registerAdapter(Instance.project.id, type, localAdapter(dir).adapter)
registerAdaptor(Instance.project.id, type, localAdaptor(dir).adaptor)
const session = yield* sessionSvc.create({ title: "restore local" })
replaceSessionEvents(session.id, 20)
@@ -1488,7 +1488,7 @@ describe("workspace-old sessionRestore", () => {
const type = unique("restore-real-events")
const info = workspaceInfo(Instance.project.id, type, { directory: dir })
insertWorkspace(info)
registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/real`, { directory: dir }).adapter)
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/real`, { directory: dir }).adaptor)
const session = yield* sessionSvc.create({ title: "real events" })
for (let i = 0; i < 3; i++) {
const msg = yield* sessionSvc.updateMessage({

View File

@@ -34,7 +34,7 @@ afterAll(() => {
})
describe("plugin.workspace", () => {
it.live("plugin can install a workspace adapter", () =>
it.live("plugin can install a workspace adaptor", () =>
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 adapter",',
' description: "plugin workspace adaptor",',
" configure(input) {",
` return { ...input, name: "plug", branch: "plug/main", directory: ${JSON.stringify(space)} }`,
" },",

View File

@@ -258,18 +258,6 @@ 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()

View File

@@ -7,8 +7,8 @@ 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 { registerAdapter } from "../../src/control-plane/adapters"
import type { WorkspaceAdapter } from "../../src/control-plane/types"
import { registerAdaptor } from "../../src/control-plane/adaptors"
import type { WorkspaceAdaptor } from "../../src/control-plane/types"
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 localAdapter = (directory: string): WorkspaceAdapter => ({
const localAdaptor = (directory: string): WorkspaceAdaptor => ({
name: "Local Test",
description: "Create a local test workspace",
configure: (info) => ({ ...info, name: "local-test", directory }),
@@ -63,7 +63,7 @@ const localAdapter = (directory: string): WorkspaceAdapter => ({
const createLocalWorkspace = (input: { projectID: Project.Info["id"]; type: string; directory: string }) =>
Effect.acquireRelease(
Effect.gen(function* () {
registerAdapter(input.projectID, input.type, localAdapter(input.directory))
registerAdaptor(input.projectID, input.type, localAdaptor(input.directory))
const workspace = yield* Workspace.Service
return yield* workspace.create({
type: input.type,

View File

@@ -3,8 +3,8 @@ import { mkdir } from "node:fs/promises"
import path from "node:path"
import { Effect } from "effect"
import { Flag } from "@opencode-ai/core/flag/flag"
import { registerAdapter } from "../../src/control-plane/adapters"
import type { WorkspaceAdapter } from "../../src/control-plane/types"
import { registerAdaptor } from "../../src/control-plane/adaptors"
import type { WorkspaceAdaptor } from "../../src/control-plane/types"
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 localAdapter = (directory: string): WorkspaceAdapter => ({
const localAdaptor = (directory: string): WorkspaceAdaptor => ({
name: "Local Test",
description: "Create a local test workspace",
configure: (info) => ({ ...info, name: "local-test", directory }),
@@ -95,7 +95,7 @@ const localAdapter = (directory: string): WorkspaceAdapter => ({
const createLocalWorkspace = (input: { projectID: Project.Info["id"]; type: string; directory: string }) =>
Effect.gen(function* () {
registerAdapter(input.projectID, input.type, localAdapter(input.directory))
registerAdaptor(input.projectID, input.type, localAdaptor(input.directory))
return yield* Workspace.Service.use((svc) =>
svc.create({
type: input.type,

View File

@@ -15,9 +15,9 @@ 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 { registerAdapter } from "../../src/control-plane/adapters"
import { registerAdaptor } from "../../src/control-plane/adaptors"
import { WorkspaceID } from "../../src/control-plane/schema"
import type { WorkspaceAdapter } from "../../src/control-plane/types"
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 localAdapter = (directory: string): WorkspaceAdapter => ({
const localAdaptor = (directory: string): WorkspaceAdaptor => ({
name: "Local Test",
description: "Create a local test workspace",
configure: (info) => ({ ...info, name: "local-test", directory }),
@@ -93,7 +93,7 @@ const localAdapter = (directory: string): WorkspaceAdapter => ({
target: () => ({ type: "local" as const, directory }),
})
const remoteAdapter = (directory: string, url: string, headers?: HeadersInit): WorkspaceAdapter => ({
const remoteAdaptor = (directory: string, url: string, headers?: HeadersInit): WorkspaceAdaptor => ({
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; adapter: WorkspaceAdapter }) =>
const createWorkspace = (input: { projectID: Project.Info["id"]; type: string; adaptor: WorkspaceAdaptor }) =>
Effect.acquireRelease(
Effect.gen(function* () {
registerAdapter(input.projectID, input.type, input.adapter)
registerAdaptor(input.projectID, input.type, input.adaptor)
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,
adapter: remoteAdapter(path.join(input.dir, `.${input.type}`), input.url, input.headers),
adaptor: remoteAdaptor(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,
adapter: localAdapter(input.directory),
adaptor: localAdaptor(input.directory),
})
const insertRemoteWorkspaceWithoutSync = (input: {
@@ -162,7 +162,7 @@ const insertRemoteWorkspaceWithoutSync = (input: {
}) =>
Effect.sync(() => {
const id = WorkspaceID.ascending()
registerAdapter(input.projectID, input.type, remoteAdapter(path.join(input.dir, `.${input.type}`), input.url))
registerAdaptor(input.projectID, input.type, remoteAdaptor(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 adapter target tells the middleware where to proxy selected remote
// The adaptor 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({

View File

@@ -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 { registerAdapter } from "../../src/control-plane/adapters"
import type { WorkspaceAdapter } from "../../src/control-plane/types"
import { registerAdaptor } from "../../src/control-plane/adaptors"
import type { WorkspaceAdaptor } from "../../src/control-plane/types"
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 localAdapter(directory: string): WorkspaceAdapter {
function localAdaptor(directory: string): WorkspaceAdaptor {
return {
name: "Local Test",
description: "Create a local test workspace",
@@ -60,7 +60,7 @@ function localAdapter(directory: string): WorkspaceAdapter {
}
}
function remoteAdapter(directory: string, url: string, headers?: HeadersInit): WorkspaceAdapter {
function remoteAdaptor(directory: string, url: string, headers?: HeadersInit): WorkspaceAdaptor {
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 [adapters, workspaces, status] = yield* Effect.all([
request(WorkspacePaths.adapters, dir),
const [adaptors, workspaces, status] = yield* Effect.all([
request(WorkspacePaths.adaptors, dir),
request(WorkspacePaths.list, dir),
request(WorkspacePaths.status, dir),
])
expect(adapters.status).toBe(200)
expect(yield* Effect.promise(() => adapters.json())).toContainEqual({
expect(adaptors.status).toBe(200)
expect(yield* Effect.promise(() => adaptors.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)
registerAdapter(project.project.id, "local-test", localAdapter(path.join(dir, ".workspace")))
registerAdaptor(project.project.id, "local-test", localAdaptor(path.join(dir, ".workspace")))
const created = yield* request(WorkspacePaths.list, dir, {
method: "POST",
@@ -201,7 +201,7 @@ describe("workspace HttpApi", () => {
const dir = yield* tmpdirScoped({ git: true })
const workspaceDir = path.join(dir, ".workspace-local")
const project = yield* Project.use.fromDirectory(dir)
registerAdapter(project.project.id, "local-target", localAdapter(workspaceDir))
registerAdaptor(project.project.id, "local-target", localAdaptor(workspaceDir))
const created = yield* request(WorkspacePaths.list, dir, {
method: "POST",
headers: { "content-type": "application/json" },
@@ -250,10 +250,10 @@ describe("workspace HttpApi", () => {
})
const project = yield* Project.use.fromDirectory(dir)
registerAdapter(
registerAdaptor(
project.project.id,
"remote-target",
remoteAdapter(path.join(dir, ".remote"), `http://127.0.0.1:${remote.port}/base`, {
remoteAdaptor(path.join(dir, ".remote"), `http://127.0.0.1:${remote.port}/base`, {
"x-target-auth": "secret",
}),
)
@@ -319,10 +319,10 @@ describe("workspace HttpApi", () => {
})
const project = yield* Project.use.fromDirectory(dir)
registerAdapter(
registerAdaptor(
project.project.id,
"remote-session-target",
remoteAdapter(path.join(dir, ".remote-session"), `http://127.0.0.1:${remote.port}/base`),
remoteAdaptor(path.join(dir, ".remote-session"), `http://127.0.0.1:${remote.port}/base`),
)
const created = yield* request(WorkspacePaths.list, dir, {
method: "POST",

View File

@@ -469,13 +469,6 @@ 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",
@@ -502,7 +495,6 @@ describe("session.message-v2.toModelMessage", () => {
role: "assistant",
content: [
{ type: "text", text: "done" },
{ type: "text", text: "thinking" },
{
type: "tool-call",
toolCallId: "call-1",

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.14.31",
"version": "1.14.30",
"type": "module",
"license": "MIT",
"scripts": {

View File

@@ -45,7 +45,7 @@ export type WorkspaceTarget =
headers?: HeadersInit
}
export type WorkspaceAdapter = {
export type WorkspaceAdaptor = {
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, adapter: WorkspaceAdapter): void
register(type: string, adaptor: WorkspaceAdaptor): void
}
serverUrl: URL
$: BunShell

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.14.31",
"version": "1.14.30",
"type": "module",
"license": "MIT",
"scripts": {

View File

@@ -29,7 +29,7 @@ import type {
ExperimentalConsoleSwitchOrgResponses,
ExperimentalResourceListResponses,
ExperimentalSessionListResponses,
ExperimentalWorkspaceAdapterListResponses,
ExperimentalWorkspaceAdaptorListResponses,
ExperimentalWorkspaceCreateErrors,
ExperimentalWorkspaceCreateResponses,
ExperimentalWorkspaceListResponses,
@@ -512,11 +512,11 @@ export class App extends HeyApiClient {
}
}
export class Adapter extends HeyApiClient {
export class Adaptor extends HeyApiClient {
/**
* List workspace adapters
* List workspace adaptors
*
* List all available workspace adapters for the current project.
* List all available workspace adaptors for the current project.
*/
public list<ThrowOnError extends boolean = false>(
parameters?: {
@@ -536,8 +536,8 @@ export class Adapter extends HeyApiClient {
},
],
)
return (options?.client ?? this.client).get<ExperimentalWorkspaceAdapterListResponses, unknown, ThrowOnError>({
url: "/experimental/workspace/adapter",
return (options?.client ?? this.client).get<ExperimentalWorkspaceAdaptorListResponses, unknown, ThrowOnError>({
url: "/experimental/workspace/adaptor",
...options,
...params,
})
@@ -731,9 +731,9 @@ export class Workspace extends HeyApiClient {
})
}
private _adapter?: Adapter
get adapter(): Adapter {
return (this._adapter ??= new Adapter({ client: this.client }))
private _adaptor?: Adaptor
get adaptor(): Adaptor {
return (this._adaptor ??= new Adaptor({ client: this.client }))
}
}

View File

@@ -2430,19 +2430,19 @@ export type AppLogResponses = {
export type AppLogResponse = AppLogResponses[keyof AppLogResponses]
export type ExperimentalWorkspaceAdapterListData = {
export type ExperimentalWorkspaceAdaptorListData = {
body?: never
path?: never
query?: {
directory?: string
workspace?: string
}
url: "/experimental/workspace/adapter"
url: "/experimental/workspace/adaptor"
}
export type ExperimentalWorkspaceAdapterListResponses = {
export type ExperimentalWorkspaceAdaptorListResponses = {
/**
* Workspace adapters
* Workspace adaptors
*/
200: Array<{
type: string
@@ -2451,8 +2451,8 @@ export type ExperimentalWorkspaceAdapterListResponses = {
}>
}
export type ExperimentalWorkspaceAdapterListResponse =
ExperimentalWorkspaceAdapterListResponses[keyof ExperimentalWorkspaceAdapterListResponses]
export type ExperimentalWorkspaceAdaptorListResponse =
ExperimentalWorkspaceAdaptorListResponses[keyof ExperimentalWorkspaceAdaptorListResponses]
export type ExperimentalWorkspaceListData = {
body?: never

View File

@@ -415,9 +415,9 @@
]
}
},
"/experimental/workspace/adapter": {
"/experimental/workspace/adaptor": {
"get": {
"operationId": "experimental.workspace.adapter.list",
"operationId": "experimental.workspace.adaptor.list",
"parameters": [
{
"in": "query",
@@ -434,11 +434,11 @@
}
}
],
"summary": "List workspace adapters",
"description": "List all available workspace adapters for the current project.",
"summary": "List workspace adaptors",
"description": "List all available workspace adaptors for the current project.",
"responses": {
"200": {
"description": "Workspace adapters",
"description": "Workspace adaptors",
"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.adapter.list({\n ...\n})"
"source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.adaptor.list({\n ...\n})"
}
]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.14.31",
"version": "1.14.30",
"type": "module",
"license": "MIT",
"scripts": {

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.14.31",
"version": "1.14.30",
"type": "module",
"license": "MIT",
"exports": {

View File

@@ -2,7 +2,7 @@
"name": "@opencode-ai/web",
"type": "module",
"license": "MIT",
"version": "1.14.31",
"version": "1.14.30",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",

View File

@@ -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.
:::
:::
---

View File

@@ -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 sovereign 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 soverign 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.

View File

@@ -4,7 +4,7 @@ description: Type-safe JS client for opencode server.
---
import config from "../../../config.mjs"
export const typesUrl = `${config.github}/blob/dev/packages/sdk/js/src/gen/types.gen.ts`
export const typesUrl = `${config.github}/blob/dev/packages/sdk/js/src/v2/gen/types.gen.ts`
The opencode JS/TS SDK provides a type-safe client for interacting with the server.
Use it to build integrations and control opencode programmatically.
@@ -28,7 +28,7 @@ npm install @opencode-ai/sdk
Create an instance of opencode:
```javascript
import { createOpencode } from "@opencode-ai/sdk"
import { createOpencode } from "@opencode-ai/sdk/v2"
const { client } = await createOpencode()
```
@@ -52,7 +52,7 @@ This starts both a server and a client
You can pass a configuration object to customize behavior. The instance still picks up your `opencode.json`, but you can override or add configuration inline:
```javascript
import { createOpencode } from "@opencode-ai/sdk"
import { createOpencode } from "@opencode-ai/sdk/v2"
const opencode = await createOpencode({
hostname: "127.0.0.1",
@@ -72,7 +72,7 @@ opencode.server.close()
If you already have a running instance of opencode, you can create a client instance to connect to it:
```javascript
import { createOpencodeClient } from "@opencode-ai/sdk"
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
const client = createOpencodeClient({
baseUrl: "http://localhost:4096",
@@ -96,7 +96,7 @@ const client = createOpencodeClient({
The SDK includes TypeScript definitions for all API types. Import them directly:
```typescript
import type { Session, Message, Part } from "@opencode-ai/sdk"
import type { Session, Message, Part } from "@opencode-ai/sdk/v2"
```
All types are generated from the server's OpenAPI specification and available in the <a href={typesUrl}>types file</a>.
@@ -318,7 +318,7 @@ const { providers, default: defaults } = await client.config.providers()
| `session.shell({ path, body })` | Run a shell command | Returns <a href={typesUrl}><code>AssistantMessage</code></a> |
| `session.revert({ path, body })` | Revert a message | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.unrevert({ path })` | Restore reverted messages | Returns <a href={typesUrl}><code>Session</code></a> |
| `postSessionByIdPermissionsByPermissionId({ path, body })` | Respond to a permission request | Returns `boolean` |
| `permission.reply({ path, body })` | Respond to a permission request | Returns `boolean` |
---
@@ -360,7 +360,7 @@ await client.session.prompt({
| `find.text({ query })` | Search for text in files | Array of match objects with `path`, `lines`, `line_number`, `absolute_offset`, `submatches` |
| `find.files({ query })` | Find files and directories by name | `string[]` (paths) |
| `find.symbols({ query })` | Find workspace symbols | <a href={typesUrl}><code>Symbol[]</code></a> |
| `file.read({ query })` | Read a file | `{ type: "raw" \| "patch", content: string }` |
| `file.read({ query })` | Read a file | `{ type: "text" \| "binary", content: string }` |
| `file.status({ query? })` | Get status for tracked files | <a href={typesUrl}><code>File[]</code></a> |
`find.files` supports a few optional query fields:

View File

@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "1.14.31",
"version": "1.14.30",
"publisher": "sst-dev",
"repository": {
"type": "git",

View File

@@ -12,14 +12,6 @@ export default $config({
apiKey: process.env.STRIPE_SECRET_KEY!,
},
planetscale: "0.4.1",
honeycomb: {
version: "0.49.0",
apiKey: process.env.HONEYCOMB_API_KEY!,
},
incident: {
version: "5.35.0",
apiKey: process.env.INCIDENT_API_KEY!,
},
},
}
},
@@ -27,8 +19,5 @@ export default $config({
await import("./infra/app.js")
await import("./infra/console.js")
await import("./infra/enterprise.js")
if ($app.stage === "production" || $app.stage === "vimtor") {
await import("./infra/monitoring.js")
}
},
})