Compare commits

..
Author SHA1 Message Date
opencode-agent[bot] b3526f6ce9 chore: generate 2026-05-10 03:03:37 +00:00
Kit Langton f220f02a2f Source workspace path pattern (#26632) 2026-05-09 23:02:31 -04:00
opencode-agent[bot] 235a86fb60 chore: generate 2026-05-10 02:59:46 +00:00
Kit Langton 67b9c9c027 Source HTTP API ID path patterns (#26623) 2026-05-09 22:58:47 -04:00
opencode 2f11c9f7ed sync release versions for v1.14.46 2026-05-10 02:34:36 +00:00
opencode-agent[bot] e1c1193f3e chore: generate 2026-05-10 02:11:45 +00:00
Kit Langton 29250a0efb fix(session): loosen remaining stored numeric schemas to tolerate legacy data (#26622) 2026-05-09 22:10:48 -04:00
Kit Langton c6e6bdf59f fix(session): tolerate negative token counts in stored parts (#26620) 2026-05-09 22:10:44 -04:00
opencode-agent[bot] d80e1199ca chore: generate 2026-05-10 02:06:39 +00:00
Kit Langton 10ea59066f feat(skill): built-in opencode-meta skill (#26617) 2026-05-09 22:05:37 -04:00
Kit Langton 79d6b10d7c fix(mcp): tolerate output schema ref failures (#26614) 2026-05-09 22:03:59 -04:00
Kit Langton 6e78f36a0f Narrow HTTP API numeric query overrides (#26618) 2026-05-09 22:02:51 -04:00
Kit Langton 16866e1180 Share HTTP API boolean query schema (#26615) 2026-05-09 21:41:15 -04:00
opencode-agent[bot] 6d130e5deb chore: generate 2026-05-10 01:13:41 +00:00
Kit Langton e30d8173c1 Fix OpenAPI workspace query drift (#26609) 2026-05-09 21:12:34 -04:00
opencode 7a79f3a5ea sync release versions for v1.14.45 2026-05-10 00:07:24 +00:00
Kit LangtonandDeveloper b8ca71d309 fix(task): subagent inherits parent agent's deny rules (Plan Mode security bypass) (#26597)
Co-authored-by: Developer <temp@example.com>
2026-05-09 23:51:55 +00:00
Kit LangtonandDeveloper 6849f96825 refactor(provider): share model status schema (#26595)
Co-authored-by: Developer <temp@example.com>
2026-05-09 19:20:31 -04:00
Kit LangtonandDeveloper 00c3248295 fix(config): allow active provider model status (#26592)
Co-authored-by: Developer <temp@example.com>
2026-05-09 18:59:05 -04:00
opencode-agent[bot] 818b56dbd0 chore: generate 2026-05-09 22:47:54 +00:00
Kit Langton 29b5b24787 fix(tui): aggregate bootstrap request failures
TUI bootstrap now reports all parallel fetch failures together instead of losing sibling failures after the first rejection.
2026-05-09 18:46:52 -04:00
Kit Langton 11363170ca fix(sdk): wrap thrown error bodies in Error
SDK throwOnError paths now convert structured response bodies into real Error instances while preserving the original body and status in cause.
2026-05-09 18:46:43 -04:00
Kit Langton ba9e4b67ed fix(tool/read): match permission patterns against worktree-relative path
Read permission checks now use the same worktree-relative path basis as edit/write/apply_patch, so configured patterns apply consistently.
2026-05-09 18:46:29 -04:00
Kit Langton bd1029b19f test(server): cover HttpApi context inheritance
Adds regression coverage for request context inheritance in promptAsync and explicit context provisioning in stream bodies.
2026-05-09 18:46:10 -04:00
Kit LangtonandDeveloper 43b51f09d0 fix(httpapi): align runtime query schemas with workspace routing params (#26581)
Co-authored-by: Developer <temp@example.com>
2026-05-09 16:50:30 -04:00
opencode-agent[bot] c61ab51886 chore: generate 2026-05-09 20:45:29 +00:00
Kit LangtonandDeveloper d373c562f2 fix(session): accept legacy summary diffs (#26579)
Co-authored-by: Developer <temp@example.com>
2026-05-09 16:44:24 -04:00
opencode-agent[bot] 5fa5d876fc chore: generate 2026-05-09 20:31:32 +00:00
Kit LangtonandDeveloper 805af011c9 test(session): regression test for #26574 + mirror loosening on Vcs.FileDiff (#26578)
Co-authored-by: Developer <temp@example.com>
2026-05-09 16:30:31 -04:00
opencode-agent[bot] 480aa8b23b chore: generate 2026-05-09 20:24:28 +00:00
OpeOginni d62442bb5d fix(sessions): allow optional patch field in diff for migrated sessions (#26574) 2026-05-09 16:23:28 -04:00
Kit Langton 8602937a37 test(session): cover workspace-routed messages (#26576) 2026-05-09 16:19:06 -04:00
Kit LangtonandDeveloper 77da433e0a fix(session): accept routing params in message list (#26569)
Co-authored-by: Developer <temp@example.com>
2026-05-09 16:02:35 -04:00
opencode-agent[bot] ad79f3e0cf chore: generate 2026-05-09 19:59:00 +00:00
Kit LangtonandDeveloper 6c2dfd2f52 fix(tui): guard messages.data in session.sync against undefined (#26566)
Co-authored-by: Developer <temp@example.com>
2026-05-09 19:58:03 +00:00
Sebastian 9a8b54fe62 Plugin command API shim (#26564) 2026-05-09 21:56:49 +02:00
Dax dcdbdb218f Move schema utilities into core (#26565) 2026-05-09 19:51:09 +00:00
Kit LangtonandDeveloper 5e49029e70 fix(provider): isolate plugin model mutations (#26561)
Co-authored-by: Developer <temp@example.com>
2026-05-09 15:47:07 -04:00
opencode 19abadaf27 sync release versions for v1.14.44 2026-05-09 19:33:26 +00:00
Kit Langton 6fea0178eb fix(server): defer validation error body change 2026-05-09 14:50:45 -04:00
Kit Langton ebe9dcf27e fix(server): return validation error bodies 2026-05-09 14:39:25 -04:00
Kit Langton 57efec4429 fix(storage): default workspace time migration (#26556) 2026-05-09 14:18:55 -04:00
opencode e22144806f sync release versions for v1.14.43 2026-05-09 18:06:16 +00:00
188 changed files with 4968 additions and 2409 deletions
+17 -17
View File
@@ -29,7 +29,7 @@
}, },
"packages/app": { "packages/app": {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.14.42", "version": "1.14.46",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
@@ -85,7 +85,7 @@
}, },
"packages/console/app": { "packages/console/app": {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.14.42", "version": "1.14.46",
"dependencies": { "dependencies": {
"@cloudflare/vite-plugin": "1.15.2", "@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1", "@ibm/plex": "6.4.1",
@@ -120,7 +120,7 @@
}, },
"packages/console/core": { "packages/console/core": {
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.14.42", "version": "1.14.46",
"dependencies": { "dependencies": {
"@aws-sdk/client-sts": "3.782.0", "@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1", "@jsx-email/render": "1.1.1",
@@ -147,7 +147,7 @@
}, },
"packages/console/function": { "packages/console/function": {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.14.42", "version": "1.14.46",
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "3.0.64", "@ai-sdk/anthropic": "3.0.64",
"@ai-sdk/openai": "3.0.48", "@ai-sdk/openai": "3.0.48",
@@ -171,7 +171,7 @@
}, },
"packages/console/mail": { "packages/console/mail": {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.14.42", "version": "1.14.46",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",
@@ -195,7 +195,7 @@
}, },
"packages/core": { "packages/core": {
"name": "@opencode-ai/core", "name": "@opencode-ai/core",
"version": "1.14.42", "version": "1.14.46",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@@ -229,7 +229,7 @@
}, },
"packages/desktop": { "packages/desktop": {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"version": "1.14.42", "version": "1.14.46",
"dependencies": { "dependencies": {
"drizzle-orm": "catalog:", "drizzle-orm": "catalog:",
"effect": "catalog:", "effect": "catalog:",
@@ -283,7 +283,7 @@
}, },
"packages/enterprise": { "packages/enterprise": {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.14.42", "version": "1.14.46",
"dependencies": { "dependencies": {
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
@@ -313,7 +313,7 @@
}, },
"packages/function": { "packages/function": {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.14.42", "version": "1.14.46",
"dependencies": { "dependencies": {
"@octokit/auth-app": "8.0.1", "@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:", "@octokit/rest": "catalog:",
@@ -329,7 +329,7 @@
}, },
"packages/http-recorder": { "packages/http-recorder": {
"name": "@opencode-ai/http-recorder", "name": "@opencode-ai/http-recorder",
"version": "1.14.42", "version": "1.14.46",
"dependencies": { "dependencies": {
"@effect/platform-node": "catalog:", "@effect/platform-node": "catalog:",
"effect": "catalog:", "effect": "catalog:",
@@ -342,7 +342,7 @@
}, },
"packages/llm": { "packages/llm": {
"name": "@opencode-ai/llm", "name": "@opencode-ai/llm",
"version": "1.14.42", "version": "1.14.46",
"dependencies": { "dependencies": {
"@smithy/eventstream-codec": "4.2.14", "@smithy/eventstream-codec": "4.2.14",
"@smithy/util-utf8": "4.2.2", "@smithy/util-utf8": "4.2.2",
@@ -360,7 +360,7 @@
}, },
"packages/opencode": { "packages/opencode": {
"name": "opencode", "name": "opencode",
"version": "1.14.42", "version": "1.14.46",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@@ -495,7 +495,7 @@
}, },
"packages/plugin": { "packages/plugin": {
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "1.14.42", "version": "1.14.46",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"effect": "catalog:", "effect": "catalog:",
@@ -533,7 +533,7 @@
}, },
"packages/sdk/js": { "packages/sdk/js": {
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "1.14.42", "version": "1.14.46",
"dependencies": { "dependencies": {
"cross-spawn": "catalog:", "cross-spawn": "catalog:",
}, },
@@ -548,7 +548,7 @@
}, },
"packages/slack": { "packages/slack": {
"name": "@opencode-ai/slack", "name": "@opencode-ai/slack",
"version": "1.14.42", "version": "1.14.46",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1", "@slack/bolt": "^3.17.1",
@@ -583,7 +583,7 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@opencode-ai/ui", "name": "@opencode-ai/ui",
"version": "1.14.42", "version": "1.14.46",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
@@ -632,7 +632,7 @@
}, },
"packages/web": { "packages/web": {
"name": "@opencode-ai/web", "name": "@opencode-ai/web",
"version": "1.14.42", "version": "1.14.46",
"dependencies": { "dependencies": {
"@astrojs/cloudflare": "12.6.3", "@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1", "@astrojs/markdown-remark": "6.3.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.14.42", "version": "1.14.46",
"description": "", "description": "",
"type": "module", "type": "module",
"exports": { "exports": {
@@ -28,6 +28,12 @@ import { createOpenSessionFileTab, createSessionTabs, getTabReorderIndex, type S
import { setSessionHandoff } from "@/pages/session/handoff" import { setSessionHandoff } from "@/pages/session/handoff"
import { useSessionLayout } from "@/pages/session/session-layout" import { useSessionLayout } from "@/pages/session/session-layout"
type RenderDiff = (SnapshotFileDiff & { file: string }) | VcsFileDiff
function renderDiff(value: SnapshotFileDiff | VcsFileDiff): value is RenderDiff {
return typeof value.file === "string"
}
export function SessionSidePanel(props: { export function SessionSidePanel(props: {
canReview: () => boolean canReview: () => boolean
diffs: () => (SnapshotFileDiff | VcsFileDiff)[] diffs: () => (SnapshotFileDiff | VcsFileDiff)[]
@@ -70,7 +76,8 @@ export function SessionSidePanel(props: {
}) })
const treeWidth = createMemo(() => (fileOpen() ? `${layout.fileTree.width()}px` : "0px")) const treeWidth = createMemo(() => (fileOpen() ? `${layout.fileTree.width()}px` : "0px"))
const diffFiles = createMemo(() => props.diffs().map((d) => d.file)) const diffs = createMemo(() => props.diffs().filter(renderDiff))
const diffFiles = createMemo(() => diffs().map((d) => d.file))
const kinds = createMemo(() => { const kinds = createMemo(() => {
const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => { const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => {
if (!a) return b if (!a) return b
@@ -81,7 +88,7 @@ export function SessionSidePanel(props: {
const normalize = (p: string) => p.replaceAll("\\\\", "/").replace(/\/+$/, "") const normalize = (p: string) => p.replaceAll("\\\\", "/").replace(/\/+$/, "")
const out = new Map<string, "add" | "del" | "mix">() const out = new Map<string, "add" | "del" | "mix">()
for (const diff of props.diffs()) { for (const diff of diffs()) {
const file = normalize(diff.file) const file = normalize(diff.file)
const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix" const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.14.42", "version": "1.14.46",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.14.42", "version": "1.14.46",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.14.42", "version": "1.14.46",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.14.42", "version": "1.14.46",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "1.14.42", "version": "1.14.46",
"name": "@opencode-ai/core", "name": "@opencode-ai/core",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
+11
View File
@@ -1,4 +1,5 @@
import { Config } from "effect" import { Config } from "effect"
import { InstallationChannel } from "../installation/version"
function truthy(key: string) { function truthy(key: string) {
const value = process.env[key]?.toLowerCase() const value = process.env[key]?.toLowerCase()
@@ -10,6 +11,13 @@ function falsy(key: string) {
return value === "false" || value === "0" return value === "false" || value === "0"
} }
// Channels where new experiments default to ON (unstable / internal users).
// Stable channels (`prod`, `latest`) stay opt-in.
const UNSTABLE_CHANNELS = new Set(["dev", "beta", "local"])
function unstableDefault(key: string) {
return truthy(key) || (!falsy(key) && UNSTABLE_CHANNELS.has(InstallationChannel))
}
function number(key: string) { function number(key: string) {
const value = process.env[key] const value = process.env[key]
if (!value) return undefined if (!value) return undefined
@@ -48,6 +56,9 @@ export const Flag = {
OPENCODE_DISABLE_CLAUDE_CODE_PROMPT: OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_PROMPT"), OPENCODE_DISABLE_CLAUDE_CODE_PROMPT: OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_PROMPT"),
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS, OPENCODE_DISABLE_CLAUDE_CODE_SKILLS,
OPENCODE_DISABLE_EXTERNAL_SKILLS: truthy("OPENCODE_DISABLE_EXTERNAL_SKILLS"), OPENCODE_DISABLE_EXTERNAL_SKILLS: truthy("OPENCODE_DISABLE_EXTERNAL_SKILLS"),
// Default-on for dev/beta/local; opt-in for stable. Set
// OPENCODE_EXPERIMENTAL_CUSTOMIZE_SKILL=false to force off, =true to force on.
OPENCODE_EXPERIMENTAL_CUSTOMIZE_SKILL: unstableDefault("OPENCODE_EXPERIMENTAL_CUSTOMIZE_SKILL"),
OPENCODE_FAKE_VCS: process.env["OPENCODE_FAKE_VCS"], OPENCODE_FAKE_VCS: process.env["OPENCODE_FAKE_VCS"],
OPENCODE_SERVER_PASSWORD: process.env["OPENCODE_SERVER_PASSWORD"], OPENCODE_SERVER_PASSWORD: process.env["OPENCODE_SERVER_PASSWORD"],
OPENCODE_SERVER_USERNAME: process.env["OPENCODE_SERVER_USERNAME"], OPENCODE_SERVER_USERNAME: process.env["OPENCODE_SERVER_USERNAME"],
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"private": true, "private": true,
"version": "1.14.42", "version": "1.14.46",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"homepage": "https://opencode.ai", "homepage": "https://opencode.ai",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.14.42", "version": "1.14.46",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
+6 -6
View File
@@ -1,7 +1,7 @@
id = "opencode" id = "opencode"
name = "OpenCode" name = "OpenCode"
description = "The open source coding agent." description = "The open source coding agent."
version = "1.14.42" version = "1.14.46"
schema_version = 1 schema_version = 1
authors = ["Anomaly"] authors = ["Anomaly"]
repository = "https://github.com/anomalyco/opencode" repository = "https://github.com/anomalyco/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg" icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64] [agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.42/opencode-darwin-arm64.zip" archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.46/opencode-darwin-arm64.zip"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64] [agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.42/opencode-darwin-x64.zip" archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.46/opencode-darwin-x64.zip"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64] [agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.42/opencode-linux-arm64.tar.gz" archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.46/opencode-linux-arm64.tar.gz"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64] [agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.42/opencode-linux-x64.tar.gz" archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.46/opencode-linux-x64.tar.gz"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64] [agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.42/opencode-windows-x64.zip" archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.46/opencode-windows-x64.zip"
cmd = "./opencode.exe" cmd = "./opencode.exe"
args = ["acp"] args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.14.42", "version": "1.14.46",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "1.14.42", "version": "1.14.46",
"name": "@opencode-ai/http-recorder", "name": "@opencode-ai/http-recorder",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "1.14.42", "version": "1.14.46",
"name": "@opencode-ai/llm", "name": "@opencode-ai/llm",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
@@ -1 +1 @@
ALTER TABLE `workspace` ADD `time_used` integer NOT NULL; ALTER TABLE `workspace` ADD `time_used` integer NOT NULL DEFAULT 0;
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "1.14.42", "version": "1.14.46",
"name": "opencode", "name": "opencode",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
+2 -2
View File
@@ -59,10 +59,10 @@ Rules:
## Schema → Zod interop ## Schema → Zod interop
When a service uses Effect Schema internally but needs Zod schemas for the HTTP layer, derive Zod from Schema using the `zod()` helper from `@/util/effect-zod`: When a service uses Effect Schema internally but needs Zod schemas for the HTTP layer, derive Zod from Schema using the `zod()` helper from `@opencode-ai/core/effect-zod`:
```ts ```ts
import { zod } from "@/util/effect-zod" import { zod } from "@opencode-ai/core/effect-zod"
export const ZodInfo = zod(Info) // derives z.ZodType from Schema.Union export const ZodInfo = zod(Info) // derives z.ZodType from Schema.Union
``` ```
+3 -3
View File
@@ -8,7 +8,7 @@ Zod-first definitions to Effect Schema with Zod compatibility shims.
Use Effect Schema as the source of truth for domain models, IDs, inputs, Use Effect Schema as the source of truth for domain models, IDs, inputs,
outputs, and typed errors. Keep Zod available at existing HTTP, tool, and outputs, and typed errors. Keep Zod available at existing HTTP, tool, and
compatibility boundaries by exposing a `.zod` static derived from the Effect compatibility boundaries by exposing a `.zod` static derived from the Effect
schema via `@/util/effect-zod`. schema via `@opencode-ai/core/effect-zod`.
The long-term driver is `specs/effect/http-api.md` — once the HTTP server The long-term driver is `specs/effect/http-api.md` — once the HTTP server
moves to `@effect/platform`, every Schema-first DTO can flow through moves to `@effect/platform`, every Schema-first DTO can flow through
@@ -97,7 +97,7 @@ creating a parallel schema source of truth.
## Escape hatches ## Escape hatches
The walker in `@/util/effect-zod` exposes two explicit escape hatches for The walker in `@opencode-ai/core/effect-zod` exposes two explicit escape hatches for
cases the pure-Schema path cannot express. Each one stays in the codebase cases the pure-Schema path cannot express. Each one stays in the codebase
only as long as its upstream or local dependency requires it — inline only as long as its upstream or local dependency requires it — inline
comments document when each can be deleted. comments document when each can be deleted.
@@ -389,7 +389,7 @@ piecewise.
## Notes ## Notes
- Use `@/util/effect-zod` for all Schema → Zod conversion. - Use `@opencode-ai/core/effect-zod` for all Schema → Zod conversion.
- Prefer one canonical schema definition. Avoid maintaining parallel Zod and - Prefer one canonical schema definition. Avoid maintaining parallel Zod and
Effect definitions for the same domain type. Effect definitions for the same domain type.
- Keep the migration incremental. Converting the domain model first is more - Keep the migration incremental. Converting the domain model first is more
@@ -0,0 +1,204 @@
# OpenAPI Translation Cleanup Plan
## Goal
Trim `packages/opencode/src/server/routes/instance/httpapi/public.ts` until OpenAPI generation is mostly a direct projection of the `HttpApi` route declarations, without breaking the generated SDK surface.
The main failure mode to eliminate is spec-only behavior: anything that appears in `/doc` or the SDK but is not accepted by runtime `HttpApi` validation.
## Current Culprit
`public.ts` exports `PublicApi` with a large `OpenApi.annotations({ transform })` hook. That hook rewrites the generated spec for legacy SDK compatibility.
The highest-risk rewrite is `InstanceQueryParameters`, which injected `directory` and `workspace` into every instance route in OpenAPI even when the runtime query schema did not accept them. This caused the SDK and `/doc` to advertise calls that could fail with `400` at runtime.
## Non-Negotiables
- Do not break the generated JavaScript SDK without an explicit versioned migration plan.
- Runtime route schemas are the source of truth for accepted params, payloads, and responses.
- `/doc`, generated SDK types, and runtime validation must agree for every endpoint.
- Prefer endpoint or schema annotations over post-generation spec surgery.
- Remove one category of rewrite at a time, with focused compatibility checks.
## PR Checklist
Status legend: `[x]` done locally, `[~]` in progress locally, `[ ]` not started.
Current combined PR scope:
- `[x]` PR 1 drift tests: added OpenAPI/runtime query assertions and a negative fixture in `test/server/httpapi-query-schema-drift.test.ts`.
- `[x]` PR 2 injection removal: removed broad `directory` / `workspace` post-generation injection from `public.ts` and replaced it with explicit runtime query schemas on affected routes.
- `[ ]` PR 3+ cleanup: leave query override, path pattern, error shape, auth, and component-shape rewrites for later PRs.
### PR 1: Add OpenAPI/Runtime Query Drift Tests
- `[x]` Add or extend `packages/opencode/test/server/httpapi-query-schema-drift.test.ts`.
- `[x]` Import `OpenApi.fromApi` and `PublicApi`.
- `[x]` Generate the public spec in-process with `OpenApi.fromApi(PublicApi)`.
- `[x]` Add a route inventory for the existing runtime reproducers: `session`, `file`, `experimental`, and `instance` routes.
- `[x]` For each inventory entry, assert every OpenAPI query parameter is declared by the runtime query schema.
- `[x]` Add a negative regression fixture that fails on spec-only `directory` / `workspace` params.
- `[x]` Keep this part test-only.
Verification:
- `[x]` `bun test --timeout 5000 test/server/httpapi-query-schema-drift.test.ts` from `packages/opencode`.
- `[x]` `bun typecheck` from `packages/opencode`.
### PR 2: Delete Spec-Only Workspace Query Injection
- `[x]` Edit `packages/opencode/src/server/routes/instance/httpapi/public.ts`.
- `[x]` Delete `InstanceQueryParameters`.
- `[x]` Delete the `isInstanceRoute` constant.
- `[x]` Delete the branch that prepends `directory` and `workspace` to every instance operation.
- `[x]` Keep `normalizeParameter(param, route)` for parameters that are actually produced by `HttpApi`.
- `[x]` Add `WorkspaceRoutingQuery` / `WorkspaceRoutingQueryFields` to runtime query schemas for affected routes.
- `[x]` Regenerate SDK and inspect diff. Result: no `directory` / `workspace` request-param removals; generated SDK diff is declaration ordering only.
Notes:
- Added `WorkspaceRoutingQuery` in `middleware/workspace-routing.ts` as the canonical runtime schema for middleware-consumed query params.
- Replaced v2 union-query schemas with plain struct query schemas so `OpenApi.fromApi` emits their query params directly. This intentionally exposes the beta `/api/session` pagination/filter params in the SDK; cursor mutual-exclusion rules now live in the handlers, while `directory` / `workspace` remain allowed with cursors for routing.
Expected code shape:
```ts
for (const param of operation.parameters ?? []) normalizeParameter(param, `${method.toUpperCase()} ${path}`)
```
Verification:
- `[x]` `bun test --timeout 5000 test/server/httpapi-query-schema-drift.test.ts` from `packages/opencode`.
- `[x]` `bun dev generate > /tmp/opencode-openapi.json` from `packages/opencode`.
- `[x]` `./packages/sdk/js/script/build.ts` from repo root.
- `[x]` Inspect SDK diff for removed `directory` / `workspace` params. Result: none after explicit runtime schemas; v2 list/message now also expose their existing beta pagination/filter query params in the SDK.
- `[x]` `bun typecheck` from `packages/opencode`.
### PR 3: Replace Broad Query Type Override Sets With Route-Level Helpers
- Edit `packages/opencode/src/server/routes/instance/httpapi/public.ts`.
- Remove broad name-based assumptions from `QueryNumberParameters` and `QueryBooleanParameters` one field at a time.
- Add shared query schema helpers near route group code if needed, for example in `groups/metadata.ts` or a new `groups/query.ts`.
- Prefer route declarations like `Schema.NumberFromString.check(...)` and boolean string decoders like the existing `QueryBoolean` in `groups/session.ts`.
- Keep only route-specific `QueryParameterSchemas` entries when SDK compatibility requires a public encoded type that Effect OpenAPI cannot emit yet.
Concrete first targets:
- `[x]` Consolidate `roots` / `archived` onto an explicit shared route schema helper. Keep `QueryBooleanParameters` until route-level schema metadata can preserve the SDK's `boolean | "true" | "false"` call shape without a global transform.
- `[x]` Replace broad `QueryNumberParameters` reliance for `start` / `cursor` / `limit` with route-specific SDK compatibility schemas. Keep improving route-level constraints where behavior is intentionally stricter.
- Keep `GET /find/file limit`, `GET /session/{sessionID}/diff messageID`, and `GET /session/{sessionID}/message limit` overrides until their route schemas generate identical SDK types directly.
Verification:
- Focused HTTP tests for changed query fields.
- `bun dev generate > /tmp/opencode-openapi.json` from `packages/opencode`.
- `./packages/sdk/js/script/build.ts` from repo root.
- Inspect generated SDK request param types before deleting each override.
- `bun typecheck` from `packages/opencode`.
### PR 4: Move Path Parameter Patterns Into ID Schemas
- Audit `PathParameterSchemas` and `pathParameterSchema()` in `public.ts`.
- Check source schemas in files like `packages/opencode/src/session/schema.ts`, `packages/opencode/src/permission/schema.ts`, and pty schema definitions.
- Add or fix `ZodOverride` / OpenAPI-compatible annotations on branded ID schemas so generated path params include the same patterns without `public.ts` overrides.
- Delete one path override only after generated OpenAPI is unchanged for that param.
Concrete first targets:
- `[x]` `sessionID`
- `[x]` `messageID`
- `[x]` `partID`
- `[x]` `permissionID`
- `[x]` `ptyID`
- `[x]` Remove ambiguous workspace `id` path overrides once the endpoint source schema emits the `wrk` pattern.
Verification:
- `bun dev generate > /tmp/opencode-openapi.json` from `packages/opencode`.
- `./packages/sdk/js/script/build.ts` from repo root.
- Inspect generated path param types and patterns.
- `bun typecheck` from `packages/opencode`.
### PR 5: Replace Built-In Error Rewrites With Declared API Errors
- Edit route group files under `packages/opencode/src/server/routes/instance/httpapi/groups/`.
- Replace SDK-visible `HttpApiError.BadRequest` / `HttpApiError.NotFound` with explicit error schemas from `packages/opencode/src/server/routes/instance/httpapi/errors.ts` or add new ones there.
- Update handlers to fail with the declared API errors at the boundary.
- Remove matching cases from `normalizeLegacyErrorResponses()` only after generated OpenAPI remains SDK-compatible.
- Do this group by group, starting with one small route group.
Concrete first targets:
- `groups/config.ts` `PATCH /config` bad request.
- `groups/session.ts` endpoints that already translate domain not-found errors.
- `groups/file.ts` if any handler currently relies on built-in error shape.
Verification:
- Focused HTTP tests asserting response body shape for changed error paths.
- `bun dev generate > /tmp/opencode-openapi.json` from `packages/opencode`.
- `./packages/sdk/js/script/build.ts` from repo root.
- Inspect SDK error union diff.
- `bun typecheck` from `packages/opencode`.
### PR 6: Remove Auth/Security Spec Rewrites If SDK Can Tolerate It
- Audit `delete operation.security`, `delete operation.responses?.["401"]`, and `delete spec.components?.securitySchemes` in `public.ts`.
- Decide whether SDK should expose auth in generated operation metadata.
- If preserving no-auth SDK surface is required, leave this rewrite and document it as intentional compatibility code.
- If removing it, update SDK generation expectations and docs in the same PR.
Verification:
- `./packages/sdk/js/script/build.ts` from repo root.
- Inspect generated client call signatures and error unions.
- Do not merge if auth churn changes normal SDK call ergonomics unintentionally.
### PR 7: Tackle Component Shape Rewrites One At A Time
- Audit these in `public.ts`: `normalizeComponentNames`, `collapseDuplicateComponents`, `applyLegacySchemaOverrides`, `normalizeComponentDescriptions`, `stripOptionalNull`, `fixSelfReferencingComponents`.
- For each rewrite, make a tiny PR that removes or narrows only that rewrite.
- If generated SDK type names churn broadly, stop and either keep the rewrite or fix `effect-smol` generation first.
Concrete first targets:
- Delete cosmetic `normalizeComponentDescriptions` if SDK output does not change materially.
- Narrow `applyLegacySchemaOverrides` entries that correspond to schemas already fixed at the source.
- Keep `stripOptionalNull` until there is an explicit SDK migration plan, because it likely affects many optional fields.
Verification:
- `bun dev generate > /tmp/opencode-openapi.json` from `packages/opencode`.
- `./packages/sdk/js/script/build.ts` from repo root.
- Inspect generated SDK type-name and optionality diffs.
## Upstream Middleware Query Support
Long-term, `WorkspaceRoutingMiddleware` should declare the query fields it reads once, and `HttpApi` should use that declaration for both runtime validation and OpenAPI generation.
Target in `effect-smol`:
- Extend `HttpApiMiddleware.Service` config with optional query schema support, or add a dedicated middleware query annotation.
- Make runtime request decoding include middleware query schemas.
- Make `OpenApi.fromApi` emit middleware query params for endpoints using that middleware.
Once available, remove `WorkspaceRoutingQueryFields` spreads from route groups and declare `directory` / `workspace` only on `WorkspaceRoutingMiddleware`.
## Suggested PR Order
1. Add drift detection tests only.
2. Remove `InstanceQueryParameters` spec injection; rely on `WorkspaceRoutingQueryFields` already present in runtime schemas.
3. Convert query type overrides into route/schema-level helpers where possible.
4. Convert path parameter overrides into schema annotations or upstream fixes.
5. Replace built-in error response rewrites with explicit declared API errors by route group.
6. Tackle component naming/nullability rewrites only after SDK compatibility snapshots are stable.
## Verification Checklist Per PR
- Focused HTTP tests for changed routes.
- OpenAPI drift tests.
- `bun dev generate > /tmp/opencode-openapi.json` from `packages/opencode`.
- `./packages/sdk/js/script/build.ts` from repo root.
- Inspect generated SDK diff for public API churn.
- `bun typecheck` from `packages/opencode`.
@@ -0,0 +1,67 @@
# TUI Command Shim Removal
Problem:
- v1 keeps a deprecated `api.command` TUI plugin shim so older plugins do not fail during initialization
- v2 should expose only the keymap command API
- tests and fixtures should not encode legacy command behavior as expected behavior
## Remove Public Types
In `packages/plugin/src/tui.ts`, remove:
- `TuiCommand`
- `TuiCommandApi`
- `TuiPluginApi.command`
Keep `api.keymap` as the only TUI command registration and execution surface.
## Remove Runtime Shim
Delete `packages/opencode/src/cli/cmd/tui/plugin/command-shim.ts`.
In `packages/opencode/src/cli/cmd/tui/plugin/api.tsx`, remove:
- the `createCommandShim` import
- the `command: createCommandShim(...)` field from `createTuiApi(...)`
In `packages/opencode/src/cli/cmd/tui/plugin/runtime.ts`, remove:
- the `createCommandShim` import
- the `command: createCommandShim(...)` field from `pluginApi(...)`
## Migration Target
Plugin authors should replace old calls with keymap calls:
```ts
api.keymap.registerLayer({
commands: [
{
name: "plugin.command",
title: "Plugin Command",
namespace: "palette",
slashName: "plugin",
run() {
api.ui.dialog.clear()
},
},
],
bindings: [{ key: "ctrl+shift+p", cmd: "plugin.command" }],
})
```
Direct replacements:
- `api.command.register(cb)` -> `api.keymap.registerLayer({ commands, bindings })`
- `api.command.trigger(name)` -> `api.keymap.dispatchCommand(name)`
- `api.command.show()` -> `api.keymap.dispatchCommand("command.palette.show")`
- `onSelect(dialog)` -> use `api.ui.dialog` from the plugin API closure
## Verification
After removal, run from package directories:
- `bun typecheck` in `packages/plugin`
- `bun typecheck` in `packages/opencode`
- TUI plugin loader tests in `packages/opencode` if runtime plugin API wiring changed
+2 -2
View File
@@ -24,8 +24,8 @@ import { Effect, Context, Layer, Schema } from "effect"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import * as Option from "effect/Option" import * as Option from "effect/Option"
import * as OtelTracer from "@effect/opentelemetry/Tracer" import * as OtelTracer from "@effect/opentelemetry/Tracer"
import { zod } from "@/util/effect-zod" import { zod } from "@opencode-ai/core/effect-zod"
import { withStatics, type DeepMutable } from "@/util/schema" import { withStatics, type DeepMutable } from "@opencode-ai/core/schema"
type ReferenceEntry = NonNullable<Config.Info["reference"]>[string] type ReferenceEntry = NonNullable<Config.Info["reference"]>[string]
type ResolvedReference = { kind: "git"; repository: string; branch?: string } | { kind: "local"; path: string } type ResolvedReference = { kind: "git"; repository: string; branch?: string } | { kind: "local"; path: string }
@@ -0,0 +1,33 @@
import type { Permission } from "../permission"
import type { Agent } from "./agent"
/**
* Build the `permission` ruleset for a subagent's session when it's spawned
* via the task tool. Combines:
*
* 1. The parent **agent's** deny rules — Plan Mode and other agent-level
* restrictions live on the agent ruleset, not on the session, so a
* subagent that only inherited the parent SESSION's permission would
* silently bypass them. (#26514)
* 2. The parent **session's** deny rules and external_directory rules —
* same forwarding the original code already did.
* 3. Default `todowrite` and `task` denies if the subagent's own ruleset
* doesn't already permit them.
*/
export function deriveSubagentSessionPermission(input: {
parentSessionPermission: Permission.Ruleset
parentAgent: Agent.Info | undefined
subagent: Agent.Info
}): Permission.Ruleset {
const canTask = input.subagent.permission.some((rule) => rule.permission === "task")
const canTodo = input.subagent.permission.some((rule) => rule.permission === "todowrite")
const parentAgentDenies = input.parentAgent?.permission.filter((rule) => rule.action === "deny") ?? []
return [
...parentAgentDenies,
...input.parentSessionPermission.filter(
(rule) => rule.permission === "external_directory" || rule.action === "deny",
),
...(canTodo ? [] : [{ permission: "todowrite" as const, pattern: "*" as const, action: "deny" as const }]),
...(canTask ? [] : [{ permission: "task" as const, pattern: "*" as const, action: "deny" as const }]),
]
}
+2 -2
View File
@@ -1,7 +1,7 @@
import path from "path" import path from "path"
import { Effect, Layer, Record, Result, Schema, Context } from "effect" import { Effect, Layer, Record, Result, Schema, Context } from "effect"
import { zod } from "@/util/effect-zod" import { zod } from "@opencode-ai/core/effect-zod"
import { NonNegativeInt } from "@/util/schema" import { NonNegativeInt } from "@opencode-ai/core/schema"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { AppFileSystem } from "@opencode-ai/core/filesystem" import { AppFileSystem } from "@opencode-ai/core/filesystem"
+3 -3
View File
@@ -23,11 +23,11 @@ function span(id: string, value: { value: string; start: number; end: number })
} }
} }
function diff(kind: string, diffs: { file: string; patch: string }[] | undefined) { function diff(kind: string, diffs: { file?: string; patch?: string }[] | undefined) {
return diffs?.map((item, i) => ({ return diffs?.map((item, i) => ({
...item, ...item,
file: redact(`${kind}-file`, String(i), item.file), file: item.file === undefined ? undefined : redact(`${kind}-file`, String(i), item.file),
patch: redact(`${kind}-patch`, String(i), item.patch), patch: item.patch === undefined ? undefined : redact(`${kind}-patch`, String(i), item.patch),
})) }))
} }
@@ -1,14 +1,7 @@
import { createResource, createMemo, For } from "solid-js" import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"
import { createResource, createMemo } from "solid-js"
import { useDialog } from "@tui/ui/dialog" import { useDialog } from "@tui/ui/dialog"
import { useSDK } from "@tui/context/sdk" import { useSDK } from "@tui/context/sdk"
import { useTheme } from "@tui/context/theme"
import { TextAttributes } from "@opentui/core"
import { useKeyboard } from "@opentui/solid"
import { createStore } from "solid-js/store"
import { Locale } from "@/util/locale"
import { getScrollAcceleration } from "../util/scroll"
import { useTuiConfig } from "../context/tui-config"
import path from "path"
export type DialogSkillProps = { export type DialogSkillProps = {
onSelect: (skill: string) => void onSelect: (skill: string) => void
@@ -17,226 +10,27 @@ export type DialogSkillProps = {
export function DialogSkill(props: DialogSkillProps) { export function DialogSkill(props: DialogSkillProps) {
const dialog = useDialog() const dialog = useDialog()
const sdk = useSDK() const sdk = useSDK()
const { theme } = useTheme()
const tuiConfig = useTuiConfig()
const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
const [store, setStore] = createStore({ selected: 0 })
dialog.setSize("large") dialog.setSize("large")
const [skills] = createResource(async () => { const [skills] = createResource(async () => {
const result = await sdk.client.app.skills({ include: "invalid" }) const result = await sdk.client.app.skills()
return Array.isArray(result.data) ? { skills: result.data, invalid: [] } : (result.data ?? { skills: [], invalid: [] }) return result.data ?? []
}) })
const rows = createMemo(() => { const options = createMemo<DialogSelectOption<string>[]>(() => {
const list = skills() ?? { skills: [], invalid: [] } const list = skills() ?? []
const maxWidth = Math.max( const maxWidth = Math.max(0, ...list.map((s) => s.name.length))
16, return list.map((skill) => ({
...list.skills.map((s) => s.name.length), title: skill.name.padEnd(maxWidth),
...list.invalid.map((s) => path.basename(path.dirname(s.path)).length), description: skill.description?.replace(/\s+/g, " ").trim(),
) value: skill.name,
const groups = Map.groupBy( category: "Skills",
list.skills.toSorted((a, b) => sourceSort(a.location).localeCompare(sourceSort(b.location)) || a.name.localeCompare(b.name)), onSelect: () => {
(skill) => sourceRoot(skill.location), props.onSelect(skill.name)
)
return [
...Array.from(groups).flatMap(([root, group]) =>
[
{ type: "header" as const, id: root, root, count: group.length },
...group.map((skill) => ({
type: "skill" as const,
id: skill.name,
name: skill.name.padEnd(maxWidth),
rawName: skill.name,
description: skill.description?.replace(/\s+/g, " ").trim() ?? "No description",
})),
],
),
...(list.invalid.length > 0
? [
{ type: "error-header" as const, id: "errors", count: list.invalid.length },
...list.invalid.map((skill) => ({
type: "error" as const,
id: skill.path,
name: path.basename(path.dirname(skill.path)).padEnd(maxWidth),
reason: skill.reason,
message: skill.message,
location: compactLocation(skill.path),
})),
]
: []),
]
})
const selectable = createMemo(() => rows().filter((row) => row.type === "skill" || row.type === "error"))
const height = createMemo(() =>
Math.min(
18,
rows().reduce((total, row) => total + (row.type === "error" ? 2 : 1), 0),
),
)
function move(offset: number) {
if (selectable().length === 0) return
const next = store.selected + offset
setStore("selected", next < 0 ? selectable().length - 1 : next >= selectable().length ? 0 : next)
}
function select() {
const row = selectable()[store.selected]
if (!row || row.type !== "skill") return
props.onSelect(row.rawName)
dialog.clear() dialog.clear()
} },
}))
useKeyboard((evt) => {
if (evt.name === "up") {
evt.preventDefault()
evt.stopPropagation()
move(-1)
return
}
if (evt.name === "down") {
evt.preventDefault()
evt.stopPropagation()
move(1)
return
}
if (evt.name === "return") {
evt.preventDefault()
evt.stopPropagation()
select()
}
}) })
const title = createMemo(() => { return <DialogSelect title="Skills" placeholder="Search skills..." options={options()} />
const list = skills() ?? { skills: [], invalid: [] }
return list.invalid.length > 0 ? `Skills (${list.skills.length}) ${list.invalid.length} skipped` : `Skills (${list.skills.length})`
})
return (
<box border={true} borderColor={theme.accent} paddingTop={1} paddingBottom={1} gap={1}>
<box flexDirection="row" justifyContent="space-between" paddingLeft={3} paddingRight={3}>
<text fg={theme.accent} attributes={TextAttributes.BOLD}>{title()}</text>
<text>
<span style={{ fg: theme.textMuted }}>(</span>
<span style={{ fg: theme.accent }}>enter</span>
<span style={{ fg: theme.textMuted }}>) invoke Escape to close</span>
</text>
</box>
<scrollbox
height={height()}
paddingLeft={2}
paddingRight={2}
scrollbarOptions={{ visible: false }}
scrollAcceleration={scrollAcceleration()}
>
<For each={rows()}>
{(row) => {
if (row.type === "header") return <SourceRow root={row.root} count={row.count} />
if (row.type === "error-header") return <ErrorHeader count={row.count} />
const selected = createMemo(() => selectable()[store.selected]?.id === row.id)
if (row.type === "error") {
return (
<box
flexDirection="column"
paddingLeft={selected() ? 2 : 3}
paddingRight={2}
backgroundColor={selected() ? theme.backgroundElement : undefined}
onMouseUp={() => setStore("selected", selectable().findIndex((item) => item.id === row.id))}
>
<text>
<span style={{ fg: theme.error }}>! </span>
<span style={{ fg: selected() ? theme.text : theme.accent, attributes: selected() ? TextAttributes.BOLD : undefined }}>
{row.name}
</span>
<span style={{ fg: theme.textMuted }}> {row.reason}: </span>
<span style={{ fg: theme.text }}>{Locale.truncate(row.message, 46)}</span>
</text>
<text fg={theme.textMuted}> {Locale.truncateLeft(row.location, 66)}</text>
</box>
)
}
return (
<box
flexDirection="row"
paddingLeft={3}
paddingRight={2}
backgroundColor={selected() ? theme.backgroundElement : undefined}
onMouseUp={() => {
setStore("selected", selectable().findIndex((item) => item.id === row.id))
props.onSelect(row.rawName)
dialog.clear()
}}
>
<text flexGrow={1} wrapMode="none">
<span style={{ fg: selected() ? theme.selectedListItemText : theme.warning, attributes: TextAttributes.BOLD }}>
{row.name}
</span>
<span style={{ fg: selected() ? theme.text : theme.textMuted }}>
{Locale.truncate(row.description, 46)}
</span>
</text>
</box>
)
}}
</For>
</scrollbox>
</box>
)
}
function compactLocation(location: string) {
const home = process.env.HOME
if (!home) return location
return location.startsWith(home) ? `~${location.slice(home.length)}` : location
}
function sourceSort(location: string) {
const label = sourceLabel(location)
const rank = label === "Global" ? 0 : label === "Project" ? 1 : label === "User" ? 2 : label === "Registry" ? 3 : 4
return `${rank}:${sourceRoot(location)}`
}
function sourceLabel(location: string) {
return sourceRootLabel(sourceRoot(location))
}
function sourceRootLabel(root: string) {
const compact = compactLocation(root)
if (compact.startsWith("~/.agents/") || compact.startsWith("~/.claude/")) return "Global"
if (compact.includes("/.opencode/cache/skills")) return "Registry"
if (root.startsWith(process.cwd())) return "Project"
if (compact.startsWith("~/")) return "User"
return "Project"
}
function sourceRoot(location: string) {
return path.dirname(path.dirname(location))
}
function SourceRow(props: { root: string; count: number }) {
const { theme } = useTheme()
return (
<box paddingLeft={2}>
<text>
<span style={{ fg: theme.accent, attributes: TextAttributes.BOLD }}>{sourceRootLabel(props.root)} </span>
<span style={{ fg: theme.textMuted }}>{compactLocation(props.root)}/</span>
<span style={{ fg: theme.textMuted }}> ({props.count})</span>
</text>
</box>
)
}
function ErrorHeader(props: { count: number }) {
const { theme } = useTheme()
return (
<box paddingLeft={2}>
<text>
<span style={{ fg: theme.error, attributes: TextAttributes.BOLD }}>Skipped skills with errors </span>
<span style={{ fg: theme.textMuted }}>({props.count})</span>
</text>
</box>
)
} }
@@ -0,0 +1,34 @@
/**
* Aggregate Promise.allSettled results into a single Error that names every
* failed endpoint, or return null when all fulfilled. Used at TUI bootstrap
* boundaries so a single 4xx doesn't drown its parallel siblings as
* unhandled rejections — every failure surfaces in one labeled message.
*/
export type LabeledSettled = {
name: string
result: PromiseSettledResult<unknown>
}
export function aggregateFailures(labeled: LabeledSettled[]): Error | null {
const failed = labeled.filter(
(x): x is { name: string; result: PromiseRejectedResult } => x.result.status === "rejected",
)
if (failed.length === 0) return null
const reasons = failed.map((f) => `${f.name}: ${reasonMessage(f.result.reason)}`).join("; ")
const summary = `${failed.length} of ${labeled.length} requests failed: ${reasons}`
const err = new Error(summary)
err.cause = { failures: failed.map((f) => ({ name: f.name, reason: f.result.reason })) }
return err
}
function reasonMessage(reason: unknown): string {
if (reason instanceof Error) return reason.message
if (typeof reason === "string") return reason
if (reason && typeof reason === "object") {
const obj = reason as { message?: unknown; name?: unknown }
if (typeof obj.message === "string") return obj.message
if (typeof obj.name === "string") return obj.name
}
return String(reason)
}
@@ -32,6 +32,7 @@ import * as Log from "@opencode-ai/core/util/log"
import { emptyConsoleState, type ConsoleState } from "@/config/console-state" import { emptyConsoleState, type ConsoleState } from "@/config/console-state"
import path from "path" import path from "path"
import { useKV } from "./kv" import { useKV } from "./kv"
import { aggregateFailures } from "./aggregate-failures"
export const { use: useSync, provider: SyncProvider } = createSimpleContext({ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
name: "Sync", name: "Sync",
@@ -391,16 +392,23 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
.catch(() => emptyConsoleState) .catch(() => emptyConsoleState)
const agentsPromise = sdk.client.app.agents({ workspace }, { throwOnError: true }) const agentsPromise = sdk.client.app.agents({ workspace }, { throwOnError: true })
const configPromise = sdk.client.config.get({ workspace }, { throwOnError: true }) const configPromise = sdk.client.config.get({ workspace }, { throwOnError: true })
const blockingRequests: Promise<unknown>[] = [ const blockingRequests: { name: string; promise: Promise<unknown> }[] = [
providersPromise, { name: "config.providers", promise: providersPromise },
providerListPromise, { name: "provider.list", promise: providerListPromise },
agentsPromise, { name: "app.agents", promise: agentsPromise },
configPromise, { name: "config.get", promise: configPromise },
projectPromise, { name: "project.sync", promise: projectPromise },
...(args.continue ? [sessionListPromise] : []), ...(args.continue ? [{ name: "session.list", promise: sessionListPromise }] : []),
] ]
await Promise.all(blockingRequests) await Promise.allSettled(blockingRequests.map((r) => r.promise))
.then((settled) => {
// Surface every failed endpoint in one labeled message instead of
// letting the first rejection drown its siblings as unhandled
// rejections.
const failure = aggregateFailures(blockingRequests.map((r, i) => ({ name: r.name, result: settled[i] })))
if (failure) throw failure
})
.then(async () => { .then(async () => {
const providersResponse = providersPromise.then((x) => x.data!) const providersResponse = providersPromise.then((x) => x.data!)
const providerListResponse = providerListPromise.then((x) => x.data!) const providerListResponse = providerListPromise.then((x) => x.data!)
@@ -526,10 +534,12 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
if (match.found) draft.session[match.index] = session.data! if (match.found) draft.session[match.index] = session.data!
if (!match.found) draft.session.splice(match.index, 0, session.data!) if (!match.found) draft.session.splice(match.index, 0, session.data!)
draft.todo[sessionID] = todo.data ?? [] draft.todo[sessionID] = todo.data ?? []
draft.message[sessionID] = messages.data!.map((x) => x.info) const infos: (typeof draft.message)[string] = []
for (const message of messages.data!) { for (const message of messages.data ?? []) {
infos.push(message.info)
draft.part[message.info.id] = message.parts draft.part[message.info.id] = message.parts
} }
draft.message[sessionID] = infos
draft.session_diff[sessionID] = diff.data ?? [] draft.session_diff[sessionID] = diff.data ?? []
}), }),
) )
+1 -1
View File
@@ -1,6 +1,6 @@
import { BusEvent } from "@/bus/bus-event" import { BusEvent } from "@/bus/bus-event"
import { SessionID } from "@/session/schema" import { SessionID } from "@/session/schema"
import { PositiveInt } from "@/util/schema" import { PositiveInt } from "@opencode-ai/core/schema"
import { Effect, Schema } from "effect" import { Effect, Schema } from "effect"
const DEFAULT_TOAST_DURATION = 5000 const DEFAULT_TOAST_DURATION = 5000
@@ -17,6 +17,7 @@ import { Slot as HostSlot } from "./slots"
import type { useToast } from "../ui/toast" import type { useToast } from "../ui/toast"
import { InstallationVersion } from "@opencode-ai/core/installation/version" import { InstallationVersion } from "@opencode-ai/core/installation/version"
import * as Keymap from "../keymap" import * as Keymap from "../keymap"
import { createCommandShim } from "./command-shim"
type RouteEntry = { type RouteEntry = {
key: symbol key: symbol
@@ -147,7 +148,9 @@ function stateApi(sync: ReturnType<typeof useSync>): TuiPluginApi["state"] {
return sync.data.session.length return sync.data.session.length
}, },
diff(sessionID) { diff(sessionID) {
return sync.data.session_diff[sessionID] ?? [] return (sync.data.session_diff[sessionID] ?? []).flatMap((item) =>
item.file === undefined ? [] : [{ ...item, file: item.file }],
)
}, },
todo(sessionID) { todo(sessionID) {
return sync.data.todo[sessionID] ?? [] return sync.data.todo[sessionID] ?? []
@@ -200,6 +203,8 @@ export function createTuiApi(input: Input): TuiPluginApi {
} }
return { return {
app: appApi(), app: appApi(),
// Keep deprecated `api.command` working for v1 plugins; remove in v2.
command: createCommandShim(input.keymap, input.dialog, input.tuiConfig.keybinds),
keys: { keys: {
formatSequence(parts) { formatSequence(parts) {
return Keymap.formatKeySequence(parts, input.tuiConfig) return Keymap.formatKeySequence(parts, input.tuiConfig)
@@ -0,0 +1,109 @@
// Legacy `api.command` bridge for v1 plugins; remove in v2.
import type { TuiCommand, TuiPluginApi } from "@opencode-ai/plugin/tui"
import { TuiKeybind } from "../config/keybind"
import type { DialogContext } from "../ui/dialog"
const COMMAND_PALETTE_SHOW = "command.palette.show"
const warned = new Set<string>()
type Warn = (api: string, replacement: string) => void
type LegacyDialog = TuiPluginApi["ui"]["dialog"]
type CommandShimDialog = DialogContext | LegacyDialog
type LegacyKeybinds = TuiPluginApi["tuiConfig"]["keybinds"]
function warnCommandShim(api: string, replacement: string) {
// Warn v1 plugins about deprecated `api.command`; remove this shim path in v2.
console.warn("[tui.plugin] deprecated TUI plugin API", { api, replacement })
}
function createCommandShimDialog(dialog: CommandShimDialog): LegacyDialog {
if (!("stack" in dialog)) return dialog
return {
replace(render, onClose) {
dialog.replace(render, onClose)
},
clear() {
dialog.clear()
},
setSize(size) {
dialog.setSize(size)
},
get size() {
return dialog.size
},
get depth() {
return dialog.stack.length
},
get open() {
return dialog.stack.length > 0
},
}
}
function warnOnce(api: string, replacement: string, warn: Warn) {
if (warned.has(api)) return
warned.add(api)
warn(api, replacement)
}
function toCommand(item: TuiCommand, dialog: LegacyDialog) {
return {
namespace: "palette",
name: item.value,
title: item.title,
desc: item.description,
category: item.category,
suggested: item.suggested,
hidden: item.hidden,
enabled: item.enabled,
slashName: item.slash?.name,
slashAliases: item.slash?.aliases,
run() {
return item.onSelect?.(dialog)
},
}
}
function toBindings(commands: TuiCommand[], keybinds: LegacyKeybinds) {
return commands.flatMap((item) =>
item.keybind
? keybinds.has(TuiKeybind.CommandMap[item.keybind as keyof typeof TuiKeybind.CommandMap] ?? item.keybind)
? keybinds
.get(TuiKeybind.CommandMap[item.keybind as keyof typeof TuiKeybind.CommandMap] ?? item.keybind)
.map((binding) => ({ ...binding, cmd: item.value, desc: binding.desc ?? item.title }))
: [
{
key: item.keybind,
cmd: item.value,
desc: item.title,
},
]
: [],
)
}
export function createCommandShim(
keymap: TuiPluginApi["keymap"],
dialog: CommandShimDialog,
keybinds: LegacyKeybinds,
): TuiPluginApi["command"] {
const shimDialog = createCommandShimDialog(dialog)
return {
register(cb) {
warnOnce("api.command.register", "api.keymap.registerLayer({ commands, bindings })", warnCommandShim)
const commands = cb()
return keymap.registerLayer({
commands: commands.map((item) => toCommand(item, shimDialog)),
bindings: toBindings(commands, keybinds),
})
},
trigger(value) {
warnOnce("api.command.trigger", "api.keymap.dispatchCommand(name)", warnCommandShim)
keymap.dispatchCommand(value)
},
show() {
warnOnce("api.command.show", `api.keymap.dispatchCommand("${COMMAND_PALETTE_SHOW}")`, warnCommandShim)
keymap.dispatchCommand(COMMAND_PALETTE_SHOW)
},
}
}
@@ -39,6 +39,7 @@ import { INTERNAL_TUI_PLUGINS, type InternalTuiPlugin } from "./internal"
import { setupSlots, Slot as View } from "./slots" import { setupSlots, Slot as View } from "./slots"
import type { HostPluginApi, HostSlots } from "./slots" import type { HostPluginApi, HostSlots } from "./slots"
import { ConfigPlugin } from "@/config/plugin" import { ConfigPlugin } from "@/config/plugin"
import { createCommandShim } from "./command-shim"
ensureRuntimePluginSupport({ additional: keymapRuntimeModules }) ensureRuntimePluginSupport({ additional: keymapRuntimeModules })
@@ -576,6 +577,8 @@ function pluginApi(runtime: RuntimeState, plugin: PluginEntry, scope: PluginScop
return { return {
app: api.app, app: api.app,
// Keep deprecated `api.command` working for v1 plugins; remove in v2.
command: createCommandShim(keymap, api.ui.dialog, api.tuiConfig.keybinds),
keys: api.keys, keys: api.keys,
keymap, keymap,
route, route,
+2 -2
View File
@@ -5,8 +5,8 @@ import type { InstanceContext } from "@/project/instance"
import { SessionID, MessageID } from "@/session/schema" import { SessionID, MessageID } from "@/session/schema"
import { Effect, Layer, Context, Schema } from "effect" import { Effect, Layer, Context, Schema } from "effect"
import z from "zod" import z from "zod"
import { zod, ZodOverride } from "@/util/effect-zod" import { zod, ZodOverride } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@/util/schema" import { withStatics } from "@opencode-ai/core/schema"
import { Config } from "@/config/config" import { Config } from "@/config/config"
import { MCP } from "../mcp" import { MCP } from "../mcp"
import { Skill } from "../skill" import { Skill } from "../skill"
+2 -2
View File
@@ -2,8 +2,8 @@ export * as ConfigAgent from "./agent"
import { Exit, Schema, SchemaGetter } from "effect" import { Exit, Schema, SchemaGetter } from "effect"
import { Bus } from "@/bus" import { Bus } from "@/bus"
import { zod } from "@/util/effect-zod" import { zod } from "@opencode-ai/core/effect-zod"
import { PositiveInt, withStatics } from "@/util/schema" import { PositiveInt, withStatics } from "@opencode-ai/core/schema"
import * as Log from "@opencode-ai/core/util/log" import * as Log from "@opencode-ai/core/util/log"
import { NamedError } from "@opencode-ai/core/util/error" import { NamedError } from "@opencode-ai/core/util/error"
import { Glob } from "@opencode-ai/core/util/glob" import { Glob } from "@opencode-ai/core/util/glob"
+2 -2
View File
@@ -5,8 +5,8 @@ import { Schema } from "effect"
import { NamedError } from "@opencode-ai/core/util/error" import { NamedError } from "@opencode-ai/core/util/error"
import { Glob } from "@opencode-ai/core/util/glob" import { Glob } from "@opencode-ai/core/util/glob"
import { Bus } from "@/bus" import { Bus } from "@/bus"
import { zod } from "@/util/effect-zod" import { zod } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@/util/schema" import { withStatics } from "@opencode-ai/core/schema"
import { configEntryNameFromPath } from "./entry-name" import { configEntryNameFromPath } from "./entry-name"
import { InvalidError } from "./error" import { InvalidError } from "./error"
import * as ConfigMarkdown from "./markdown" import * as ConfigMarkdown from "./markdown"
+3 -3
View File
@@ -22,8 +22,8 @@ import { InstanceState } from "@/effect/instance-state"
import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "effect" import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "effect"
import { EffectFlock } from "@opencode-ai/core/util/effect-flock" import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
import { containsPath } from "../project/instance-context" import { containsPath } from "../project/instance-context"
import { zod } from "@/util/effect-zod" import { zod } from "@opencode-ai/core/effect-zod"
import { NonNegativeInt, PositiveInt, withStatics, type DeepMutable } from "@/util/schema" import { NonNegativeInt, PositiveInt, withStatics, type DeepMutable } from "@opencode-ai/core/schema"
import { ConfigAgent } from "./agent" import { ConfigAgent } from "./agent"
import { ConfigCommand } from "./command" import { ConfigCommand } from "./command"
import { ConfigFormatter } from "./formatter" import { ConfigFormatter } from "./formatter"
@@ -307,7 +307,7 @@ export const Info = Schema.Struct({
})), })),
) )
// Uses the shared `DeepMutable` from `@/util/schema`. See the definition // Uses the shared `DeepMutable` from `@opencode-ai/core/schema`. See the definition
// there for why the local variant is needed over `Types.DeepMutable` from // there for why the local variant is needed over `Types.DeepMutable` from
// effect-smol (the upstream version collapses `unknown` to `{}`). // effect-smol (the upstream version collapses `unknown` to `{}`).
export type Info = DeepMutable<Schema.Schema.Type<typeof Info>> & { export type Info = DeepMutable<Schema.Schema.Type<typeof Info>> & {
@@ -1,6 +1,6 @@
import { Schema } from "effect" import { Schema } from "effect"
import { zod } from "@/util/effect-zod" import { zod } from "@opencode-ai/core/effect-zod"
import { NonNegativeInt } from "@/util/schema" import { NonNegativeInt } from "@opencode-ai/core/schema"
export class ConsoleState extends Schema.Class<ConsoleState>("ConsoleState")({ export class ConsoleState extends Schema.Class<ConsoleState>("ConsoleState")({
consoleManagedProviders: Schema.mutable(Schema.Array(Schema.String)), consoleManagedProviders: Schema.mutable(Schema.Array(Schema.String)),
+2 -2
View File
@@ -1,8 +1,8 @@
export * as ConfigFormatter from "./formatter" export * as ConfigFormatter from "./formatter"
import { Schema } from "effect" import { Schema } from "effect"
import { zod } from "@/util/effect-zod" import { zod } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@/util/schema" import { withStatics } from "@opencode-ai/core/schema"
export const Entry = Schema.Struct({ export const Entry = Schema.Struct({
disabled: Schema.optional(Schema.Boolean), disabled: Schema.optional(Schema.Boolean),
+2 -2
View File
@@ -1,6 +1,6 @@
import { Schema } from "effect" import { Schema } from "effect"
import { zod } from "@/util/effect-zod" import { zod } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@/util/schema" import { withStatics } from "@opencode-ai/core/schema"
export const Layout = Schema.Literals(["auto", "stretch"]) export const Layout = Schema.Literals(["auto", "stretch"])
.annotate({ identifier: "LayoutConfig" }) .annotate({ identifier: "LayoutConfig" })
+2 -2
View File
@@ -1,8 +1,8 @@
export * as ConfigLSP from "./lsp" export * as ConfigLSP from "./lsp"
import { Schema } from "effect" import { Schema } from "effect"
import { zod } from "@/util/effect-zod" import { zod } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@/util/schema" import { withStatics } from "@opencode-ai/core/schema"
import * as LSPServer from "../lsp/server" import * as LSPServer from "../lsp/server"
export const Disabled = Schema.Struct({ export const Disabled = Schema.Struct({
+2 -2
View File
@@ -1,6 +1,6 @@
import { Schema } from "effect" import { Schema } from "effect"
import { zod } from "@/util/effect-zod" import { zod } from "@opencode-ai/core/effect-zod"
import { PositiveInt, withStatics } from "@/util/schema" import { PositiveInt, withStatics } from "@opencode-ai/core/schema"
export const Local = Schema.Struct({ export const Local = Schema.Struct({
type: Schema.Literal("local").annotate({ description: "Type of MCP server connection" }), type: Schema.Literal("local").annotate({ description: "Type of MCP server connection" }),
+2 -2
View File
@@ -1,7 +1,7 @@
import { Schema } from "effect" import { Schema } from "effect"
import z from "zod" import z from "zod"
import { zod, ZodOverride } from "@/util/effect-zod" import { zod, ZodOverride } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@/util/schema" import { withStatics } from "@opencode-ai/core/schema"
// The original Zod schema carried an external $ref pointing at the models.dev // The original Zod schema carried an external $ref pointing at the models.dev
// JSON schema. That external reference is not a named SDK component — it is a // JSON schema. That external reference is not a named SDK component — it is a
+1 -1
View File
@@ -3,7 +3,7 @@ export * as ConfigParse from "./parse"
import { type ParseError as JsoncParseError, parse as parseJsoncImpl, printParseErrorCode } from "jsonc-parser" import { type ParseError as JsoncParseError, parse as parseJsoncImpl, printParseErrorCode } from "jsonc-parser"
import { Cause, Exit, Schema as EffectSchema, SchemaIssue } from "effect" import { Cause, Exit, Schema as EffectSchema, SchemaIssue } from "effect"
import z from "zod" import z from "zod"
import type { DeepMutable } from "@/util/schema" import type { DeepMutable } from "@opencode-ai/core/schema"
import { InvalidError, JsonError } from "./error" import { InvalidError, JsonError } from "./error"
type ZodSchema<T> = z.ZodType<T> type ZodSchema<T> = z.ZodType<T>
+2 -2
View File
@@ -1,7 +1,7 @@
export * as ConfigPermission from "./permission" export * as ConfigPermission from "./permission"
import { Schema, SchemaGetter } from "effect" import { Schema, SchemaGetter } from "effect"
import { zod } from "@/util/effect-zod" import { zod } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@/util/schema" import { withStatics } from "@opencode-ai/core/schema"
export const Action = Schema.Literals(["ask", "allow", "deny"]) export const Action = Schema.Literals(["ask", "allow", "deny"])
.annotate({ identifier: "PermissionActionConfig" }) .annotate({ identifier: "PermissionActionConfig" })
+2 -2
View File
@@ -2,8 +2,8 @@ import { Glob } from "@opencode-ai/core/util/glob"
import { Schema } from "effect" import { Schema } from "effect"
import { pathToFileURL } from "url" import { pathToFileURL } from "url"
import { isPathPluginSpec, parsePluginSpecifier, resolvePathPluginTarget } from "@/plugin/shared" import { isPathPluginSpec, parsePluginSpecifier, resolvePathPluginTarget } from "@/plugin/shared"
import { zod } from "@/util/effect-zod" import { zod } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@/util/schema" import { withStatics } from "@opencode-ai/core/schema"
import path from "path" import path from "path"
export const Options = Schema.Record(Schema.String, Schema.Unknown).pipe(withStatics((s) => ({ zod: zod(s) }))) export const Options = Schema.Record(Schema.String, Schema.Unknown).pipe(withStatics((s) => ({ zod: zod(s) })))
+4 -3
View File
@@ -1,6 +1,7 @@
import { Schema } from "effect" import { Schema } from "effect"
import { zod } from "@/util/effect-zod" import { zod } from "@opencode-ai/core/effect-zod"
import { PositiveInt, withStatics } from "@/util/schema" import { PositiveInt, withStatics } from "@opencode-ai/core/schema"
import { ModelStatus } from "@/provider/model-status"
export const Model = Schema.Struct({ export const Model = Schema.Struct({
id: Schema.optional(Schema.String), id: Schema.optional(Schema.String),
@@ -49,7 +50,7 @@ export const Model = Schema.Struct({
}), }),
), ),
experimental: Schema.optional(Schema.Boolean), experimental: Schema.optional(Schema.Boolean),
status: Schema.optional(Schema.Literals(["alpha", "beta", "deprecated"])), status: Schema.optional(ModelStatus),
provider: Schema.optional( provider: Schema.optional(
Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) }), Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) }),
), ),
+2 -2
View File
@@ -1,8 +1,8 @@
export * as ConfigReference from "./reference" export * as ConfigReference from "./reference"
import { Schema } from "effect" import { Schema } from "effect"
import { zod } from "@/util/effect-zod" import { zod } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@/util/schema" import { withStatics } from "@opencode-ai/core/schema"
const Git = Schema.Struct({ const Git = Schema.Struct({
repository: Schema.String.annotate({ repository: Schema.String.annotate({
+2 -2
View File
@@ -1,6 +1,6 @@
import { Schema } from "effect" import { Schema } from "effect"
import { zod } from "@/util/effect-zod" import { zod } from "@opencode-ai/core/effect-zod"
import { PositiveInt, withStatics } from "@/util/schema" import { PositiveInt, withStatics } from "@opencode-ai/core/schema"
export const Server = Schema.Struct({ export const Server = Schema.Struct({
port: Schema.optional(PositiveInt).annotate({ port: Schema.optional(PositiveInt).annotate({
+2 -2
View File
@@ -1,6 +1,6 @@
import { Schema } from "effect" import { Schema } from "effect"
import { zod } from "@/util/effect-zod" import { zod } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@/util/schema" import { withStatics } from "@opencode-ai/core/schema"
export const Info = Schema.Struct({ export const Info = Schema.Struct({
paths: Schema.optional(Schema.Array(Schema.String)).annotate({ paths: Schema.optional(Schema.Array(Schema.String)).annotate({
@@ -1,12 +1,12 @@
import { Schema } from "effect" import { Schema } from "effect"
import { Identifier } from "@/id/id" import { Identifier } from "@/id/id"
import { zod, ZodOverride } from "@/util/effect-zod" import { zod, ZodOverride } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@/util/schema" import { withStatics } from "@opencode-ai/core/schema"
const workspaceIdSchema = Schema.String.annotate({ [ZodOverride]: Identifier.schema("workspace") }).pipe( const workspaceIdSchema = Schema.String.check(Schema.isStartsWith("wrk"))
Schema.brand("WorkspaceID"), .annotate({ [ZodOverride]: Identifier.schema("workspace") })
) .pipe(Schema.brand("WorkspaceID"))
export type WorkspaceID = typeof workspaceIdSchema.Type export type WorkspaceID = typeof workspaceIdSchema.Type
+1 -1
View File
@@ -1,7 +1,7 @@
import { Schema, Struct } from "effect" import { Schema, Struct } from "effect"
import { ProjectID } from "@/project/schema" import { ProjectID } from "@/project/schema"
import { WorkspaceID } from "./schema" import { WorkspaceID } from "./schema"
import type { DeepMutable } from "@/util/schema" import type { DeepMutable } from "@opencode-ai/core/schema"
export const WorkspaceInfo = Schema.Struct({ export const WorkspaceInfo = Schema.Struct({
id: WorkspaceID, id: WorkspaceID,
+2 -2
View File
@@ -14,8 +14,8 @@ import { containsPath } from "../project/instance-context"
import * as Log from "@opencode-ai/core/util/log" import * as Log from "@opencode-ai/core/util/log"
import { Protected } from "./protected" import { Protected } from "./protected"
import { Ripgrep } from "./ripgrep" import { Ripgrep } from "./ripgrep"
import { zod } from "@/util/effect-zod" import { zod } from "@opencode-ai/core/effect-zod"
import { NonNegativeInt, type DeepMutable, withStatics } from "@/util/schema" import { NonNegativeInt, type DeepMutable, withStatics } from "@opencode-ai/core/schema"
export const Info = Schema.Struct({ export const Info = Schema.Struct({
path: Schema.String, path: Schema.String,
+2 -2
View File
@@ -11,8 +11,8 @@ import { Global } from "@opencode-ai/core/global"
import * as Log from "@opencode-ai/core/util/log" import * as Log from "@opencode-ai/core/util/log"
import { sanitizedProcessEnv } from "@opencode-ai/core/util/opencode-process" import { sanitizedProcessEnv } from "@opencode-ai/core/util/opencode-process"
import { which } from "@/util/which" import { which } from "@/util/which"
import { zod } from "@/util/effect-zod" import { zod } from "@opencode-ai/core/effect-zod"
import { NonNegativeInt, withStatics } from "@/util/schema" import { NonNegativeInt, withStatics } from "@opencode-ai/core/schema"
const log = Log.create({ service: "ripgrep" }) const log = Log.create({ service: "ripgrep" })
const VERSION = "15.1.0" const VERSION = "15.1.0"
+2 -2
View File
@@ -7,8 +7,8 @@ import { mergeDeep } from "remeda"
import { Config } from "@/config/config" import { Config } from "@/config/config"
import * as Log from "@opencode-ai/core/util/log" import * as Log from "@opencode-ai/core/util/log"
import * as Formatter from "./formatter" import * as Formatter from "./formatter"
import { zod } from "@/util/effect-zod" import { zod } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@/util/schema" import { withStatics } from "@opencode-ai/core/schema"
const log = Log.create({ service: "format" }) const log = Log.create({ service: "format" })
+2 -2
View File
@@ -13,8 +13,8 @@ import { spawn as lspspawn } from "./launch"
import { Effect, Layer, Context, Schema } from "effect" import { Effect, Layer, Context, Schema } from "effect"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { containsPath } from "@/project/instance-context" import { containsPath } from "@/project/instance-context"
import { NonNegativeInt, withStatics } from "@/util/schema" import { NonNegativeInt, withStatics } from "@opencode-ai/core/schema"
import { zod, ZodOverride } from "@/util/effect-zod" import { zod, ZodOverride } from "@opencode-ai/core/effect-zod"
const log = Log.create({ service: "lsp" }) const log = Log.create({ service: "lsp" })
+4
View File
@@ -0,0 +1,4 @@
declare module "*.md" {
const content: string
export default content
}
+45 -7
View File
@@ -6,6 +6,7 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js" import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"
import { import {
CallToolResultSchema, CallToolResultSchema,
ToolSchema,
type Tool as MCPToolDef, type Tool as MCPToolDef,
ToolListChangedNotificationSchema, ToolListChangedNotificationSchema,
} from "@modelcontextprotocol/sdk/types.js" } from "@modelcontextprotocol/sdk/types.js"
@@ -30,12 +31,21 @@ import { EffectBridge } from "@/effect/bridge"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { zod as effectZod } from "@/util/effect-zod" import { zod as effectZod } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@/util/schema" import { withStatics } from "@opencode-ai/core/schema"
const log = Log.create({ service: "mcp" }) const log = Log.create({ service: "mcp" })
const DEFAULT_TIMEOUT = 30_000 const DEFAULT_TIMEOUT = 30_000
const TolerantToolSchema = ToolSchema.extend({
outputSchema: z.unknown().optional(),
})
const TolerantListToolsResultSchema = z.looseObject({
tools: z.array(TolerantToolSchema),
nextCursor: z.string().optional(),
})
export const Resource = Schema.Struct({ export const Resource = Schema.Struct({
name: Schema.String, name: Schema.String,
uri: Schema.String, uri: Schema.String,
@@ -119,6 +129,38 @@ function remoteURL(key: string, value: string) {
log.warn("invalid remote mcp url", { key }) log.warn("invalid remote mcp url", { key })
} }
function isOutputSchemaValidationError(error: Error) {
return /can't resolve reference|resolves to more than one schema|outputSchema|schema.*reference|reference.*schema/i.test(
error.message,
)
}
function listTools(key: string, client: MCPClient, timeout: number) {
return Effect.tryPromise({
try: () => client.listTools(undefined, { timeout }),
catch: (err) => (err instanceof Error ? err : new Error(String(err))),
}).pipe(
Effect.map((result) => result.tools),
Effect.catch((error) => {
if (!isOutputSchemaValidationError(error)) return Effect.fail(error)
log.warn("failed to validate MCP tool output schemas, retrying without output schema validation", { key, error })
return Effect.tryPromise({
try: () => client.request({ method: "tools/list" }, TolerantListToolsResultSchema, { timeout }),
catch: (err) => (err instanceof Error ? err : new Error(String(err))),
}).pipe(
Effect.map((result) =>
result.tools.map((tool) => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
})),
),
)
}),
)
}
// Convert MCP tool definition to AI SDK Tool type // Convert MCP tool definition to AI SDK Tool type
function convertMcpTool(mcpTool: MCPToolDef, client: MCPClient, timeout?: number): Tool { function convertMcpTool(mcpTool: MCPToolDef, client: MCPClient, timeout?: number): Tool {
const inputSchema = mcpTool.inputSchema const inputSchema = mcpTool.inputSchema
@@ -151,11 +193,7 @@ function convertMcpTool(mcpTool: MCPToolDef, client: MCPClient, timeout?: number
} }
function defs(key: string, client: MCPClient, timeout?: number) { function defs(key: string, client: MCPClient, timeout?: number) {
return Effect.tryPromise({ return listTools(key, client, timeout ?? DEFAULT_TIMEOUT).pipe(
try: () => withTimeout(client.listTools(), timeout ?? DEFAULT_TIMEOUT),
catch: (err) => (err instanceof Error ? err : new Error(String(err))),
}).pipe(
Effect.map((result) => result.tools),
Effect.catch((err) => { Effect.catch((err) => {
log.error("failed to get tools from client", { key, error: err }) log.error("failed to get tools from client", { key, error: err })
return Effect.succeed(undefined) return Effect.succeed(undefined)
+2 -2
View File
@@ -7,9 +7,9 @@ import { MessageID, SessionID } from "@/session/schema"
import { PermissionTable } from "@/session/session.sql" import { PermissionTable } from "@/session/session.sql"
import { Database } from "@/storage/db" import { Database } from "@/storage/db"
import { eq } from "drizzle-orm" import { eq } from "drizzle-orm"
import { zod } from "@/util/effect-zod" import { zod } from "@opencode-ai/core/effect-zod"
import * as Log from "@opencode-ai/core/util/log" import * as Log from "@opencode-ai/core/util/log"
import { withStatics } from "@/util/schema" import { withStatics } from "@opencode-ai/core/schema"
import { Wildcard } from "@/util/wildcard" import { Wildcard } from "@/util/wildcard"
import { Deferred, Effect, Layer, Schema, Context } from "effect" import { Deferred, Effect, Layer, Schema, Context } from "effect"
import os from "os" import os from "os"
+2 -2
View File
@@ -1,8 +1,8 @@
import { Schema } from "effect" import { Schema } from "effect"
import { Identifier } from "@/id/id" import { Identifier } from "@/id/id"
import { zod, ZodOverride } from "@/util/effect-zod" import { zod, ZodOverride } from "@opencode-ai/core/effect-zod"
import { Newtype } from "@/util/schema" import { Newtype } from "@opencode-ai/core/schema"
export class PermissionID extends Newtype<PermissionID>()( export class PermissionID extends Newtype<PermissionID>()(
"PermissionID", "PermissionID",
+2 -2
View File
@@ -18,8 +18,8 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import { NodePath } from "@effect/platform-node" import { NodePath } from "@effect/platform-node"
import { AppFileSystem } from "@opencode-ai/core/filesystem" import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { zod } from "@/util/effect-zod" import { zod } from "@opencode-ai/core/effect-zod"
import { NonNegativeInt, optionalOmitUndefined, withStatics } from "@/util/schema" import { NonNegativeInt, optionalOmitUndefined, withStatics } from "@opencode-ai/core/schema"
import { serviceUse } from "@/effect/service-use" import { serviceUse } from "@/effect/service-use"
const log = Log.create({ service: "project" }) const log = Log.create({ service: "project" })
+2 -2
View File
@@ -1,7 +1,7 @@
import { Schema } from "effect" import { Schema } from "effect"
import { zod } from "@/util/effect-zod" import { zod } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@/util/schema" import { withStatics } from "@opencode-ai/core/schema"
const projectIdSchema = Schema.String.pipe(Schema.brand("ProjectID")) const projectIdSchema = Schema.String.pipe(Schema.brand("ProjectID"))
+10 -7
View File
@@ -6,8 +6,8 @@ import { InstanceState } from "@/effect/instance-state"
import { FileWatcher } from "@/file/watcher" import { FileWatcher } from "@/file/watcher"
import { Git } from "@/git" import { Git } from "@/git"
import * as Log from "@opencode-ai/core/util/log" import * as Log from "@opencode-ai/core/util/log"
import { zod, zodObject } from "@/util/effect-zod" import { zod, zodObject } from "@opencode-ai/core/effect-zod"
import { NonNegativeInt, withStatics } from "@/util/schema" import { NonNegativeInt, withStatics } from "@opencode-ai/core/schema"
const log = Log.create({ service: "vcs" }) const log = Log.create({ service: "vcs" })
const PATCH_CONTEXT_LINES = 2_147_483_647 const PATCH_CONTEXT_LINES = 2_147_483_647
@@ -230,9 +230,12 @@ export type Info = Schema.Schema.Type<typeof Info>
export const FileDiff = Schema.Struct({ export const FileDiff = Schema.Struct({
file: Schema.String, file: Schema.String,
patch: Schema.String, // Mirrors Snapshot.FileDiff (see #26574). The current producer always
additions: NonNegativeInt, // populates patch, but loosening matches the sibling schema so a
deletions: NonNegativeInt, // future code path that omits it can't crash /instance/vcs/diff.
patch: Schema.optional(Schema.String),
additions: Schema.Finite,
deletions: Schema.Finite,
status: Schema.optional(Schema.Literals(["added", "deleted", "modified"])), status: Schema.optional(Schema.Literals(["added", "deleted", "modified"])),
}) })
.annotate({ identifier: "VcsFileDiff" }) .annotate({ identifier: "VcsFileDiff" })
@@ -241,8 +244,8 @@ export type FileDiff = Schema.Schema.Type<typeof FileDiff>
export const FileStatus = Schema.Struct({ export const FileStatus = Schema.Struct({
file: Schema.String, file: Schema.String,
additions: NonNegativeInt, additions: Schema.Finite,
deletions: NonNegativeInt, deletions: Schema.Finite,
status: Schema.Literals(["added", "deleted", "modified"]), status: Schema.Literals(["added", "deleted", "modified"]),
}) })
.annotate({ identifier: "VcsFileStatus" }) .annotate({ identifier: "VcsFileStatus" })
+2 -2
View File
@@ -1,9 +1,9 @@
import type { AuthOAuthResult, Hooks } from "@opencode-ai/plugin" import type { AuthOAuthResult, Hooks } from "@opencode-ai/plugin"
import { Auth } from "@/auth" import { Auth } from "@/auth"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { zod } from "@/util/effect-zod" import { zod } from "@opencode-ai/core/effect-zod"
import { namedSchemaError } from "@/util/named-schema-error" import { namedSchemaError } from "@/util/named-schema-error"
import { optionalOmitUndefined, withStatics } from "@/util/schema" import { optionalOmitUndefined, withStatics } from "@opencode-ai/core/schema"
import { Plugin } from "../plugin" import { Plugin } from "../plugin"
import { ProviderID } from "./schema" import { ProviderID } from "./schema"
import { Array as Arr, Effect, Layer, Record, Result, Context, Schema } from "effect" import { Array as Arr, Effect, Layer, Record, Result, Context, Schema } from "effect"
@@ -0,0 +1,9 @@
import { Schema } from "effect"
export const CatalogModelStatus = Schema.Literals(["alpha", "beta", "deprecated"])
export type CatalogModelStatus = typeof CatalogModelStatus.Type
export const ModelStatus = Schema.Literals(["alpha", "beta", "deprecated", "active"])
export type ModelStatus = typeof ModelStatus.Type
export * as ProviderModelStatus from "./model-status"
+2 -1
View File
@@ -8,6 +8,7 @@ import { Flock } from "@opencode-ai/core/util/flock"
import { Hash } from "@opencode-ai/core/util/hash" import { Hash } from "@opencode-ai/core/util/hash"
import { AppFileSystem } from "@opencode-ai/core/filesystem" import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { withTransientReadRetry } from "@/util/effect-http-client" import { withTransientReadRetry } from "@/util/effect-http-client"
import { CatalogModelStatus } from "./model-status"
const Cost = Schema.Struct({ const Cost = Schema.Struct({
input: Schema.Finite, input: Schema.Finite,
@@ -71,7 +72,7 @@ export const Model = Schema.Struct({
), ),
}), }),
), ),
status: Schema.optional(Schema.Literals(["alpha", "beta", "deprecated"])), status: Schema.optional(CatalogModelStatus),
provider: Schema.optional( provider: Schema.optional(
Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) }), Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) }),
), ),
+5 -4
View File
@@ -13,7 +13,7 @@ import { Auth } from "../auth"
import { Env } from "../env" import { Env } from "../env"
import { InstallationVersion } from "@opencode-ai/core/installation/version" import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { Flag } from "@opencode-ai/core/flag/flag" import { Flag } from "@opencode-ai/core/flag/flag"
import { zod } from "@/util/effect-zod" import { zod } from "@opencode-ai/core/effect-zod"
import { namedSchemaError } from "@/util/named-schema-error" import { namedSchemaError } from "@/util/named-schema-error"
import { iife } from "@/util/iife" import { iife } from "@/util/iife"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
@@ -24,10 +24,11 @@ import { EffectBridge } from "@/effect/bridge"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { AppFileSystem } from "@opencode-ai/core/filesystem" import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { isRecord } from "@/util/record" import { isRecord } from "@/util/record"
import { optionalOmitUndefined, withStatics } from "@/util/schema" import { optionalOmitUndefined, withStatics } from "@opencode-ai/core/schema"
import * as ProviderTransform from "./transform" import * as ProviderTransform from "./transform"
import { ModelID, ProviderID } from "./schema" import { ModelID, ProviderID } from "./schema"
import { ModelStatus } from "./model-status"
const log = Log.create({ service: "provider" }) const log = Log.create({ service: "provider" })
@@ -897,7 +898,7 @@ export const Model = Schema.Struct({
capabilities: ProviderCapabilities, capabilities: ProviderCapabilities,
cost: ProviderCost, cost: ProviderCost,
limit: ProviderLimit, limit: ProviderLimit,
status: Schema.Literals(["alpha", "beta", "deprecated", "active"]), status: ModelStatus,
options: Schema.Record(Schema.String, Schema.Any), options: Schema.Record(Schema.String, Schema.Any),
headers: Schema.Record(Schema.String, Schema.String), headers: Schema.Record(Schema.String, Schema.String),
release_date: Schema.String, release_date: Schema.String,
@@ -1162,7 +1163,7 @@ const layer: Layer.Layer<
const pluginAuth = yield* auth.get(providerID).pipe(Effect.orDie) const pluginAuth = yield* auth.get(providerID).pipe(Effect.orDie)
provider.models = yield* Effect.promise(async () => { provider.models = yield* Effect.promise(async () => {
const next = await models(provider, { auth: pluginAuth }) const next = await models(toPublicInfo(provider), { auth: pluginAuth })
return Object.fromEntries( return Object.fromEntries(
Object.entries(next).map(([id, model]) => [ Object.entries(next).map(([id, model]) => [
id, id,
+2 -2
View File
@@ -1,7 +1,7 @@
import { Schema } from "effect" import { Schema } from "effect"
import { zod } from "@/util/effect-zod" import { zod } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@/util/schema" import { withStatics } from "@opencode-ai/core/schema"
const providerIdSchema = Schema.String.pipe(Schema.brand("ProviderID")) const providerIdSchema = Schema.String.pipe(Schema.brand("ProviderID"))
+2 -2
View File
@@ -10,8 +10,8 @@ import type { Proc } from "#pty"
import * as Log from "@opencode-ai/core/util/log" import * as Log from "@opencode-ai/core/util/log"
import { PtyID } from "./schema" import { PtyID } from "./schema"
import { Effect, Layer, Context, Schema, Types } from "effect" import { Effect, Layer, Context, Schema, Types } from "effect"
import { zod } from "@/util/effect-zod" import { zod } from "@opencode-ai/core/effect-zod"
import { NonNegativeInt, PositiveInt, withStatics } from "@/util/schema" import { NonNegativeInt, PositiveInt, withStatics } from "@opencode-ai/core/schema"
const log = Log.create({ service: "pty" }) const log = Log.create({ service: "pty" })
+5 -3
View File
@@ -1,10 +1,12 @@
import { Schema } from "effect" import { Schema } from "effect"
import { Identifier } from "@/id/id" import { Identifier } from "@/id/id"
import { zod, ZodOverride } from "@/util/effect-zod" import { zod, ZodOverride } from "@opencode-ai/core/effect-zod"
import { withStatics } from "@/util/schema" import { withStatics } from "@opencode-ai/core/schema"
const ptyIdSchema = Schema.String.annotate({ [ZodOverride]: Identifier.schema("pty") }).pipe(Schema.brand("PtyID")) const ptyIdSchema = Schema.String.check(Schema.isStartsWith("pty"))
.annotate({ [ZodOverride]: Identifier.schema("pty") })
.pipe(Schema.brand("PtyID"))
export type PtyID = typeof ptyIdSchema.Type export type PtyID = typeof ptyIdSchema.Type
+1 -1
View File
@@ -3,7 +3,7 @@ export * as PtyTicket from "./ticket"
import { WorkspaceID } from "@/control-plane/schema" import { WorkspaceID } from "@/control-plane/schema"
import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref" import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref"
import { PtyID } from "@/pty/schema" import { PtyID } from "@/pty/schema"
import { PositiveInt } from "@/util/schema" import { PositiveInt } from "@opencode-ai/core/schema"
import { Cache, Context, Duration, Effect, Layer, Schema } from "effect" import { Cache, Context, Duration, Effect, Layer, Schema } from "effect"
const DEFAULT_TTL = Duration.seconds(60) const DEFAULT_TTL = Duration.seconds(60)
+2 -2
View File
@@ -3,9 +3,9 @@ import { Bus } from "@/bus"
import { BusEvent } from "@/bus/bus-event" import { BusEvent } from "@/bus/bus-event"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { SessionID, MessageID } from "@/session/schema" import { SessionID, MessageID } from "@/session/schema"
import { zod } from "@/util/effect-zod" import { zod } from "@opencode-ai/core/effect-zod"
import * as Log from "@opencode-ai/core/util/log" import * as Log from "@opencode-ai/core/util/log"
import { withStatics } from "@/util/schema" import { withStatics } from "@opencode-ai/core/schema"
import { QuestionID } from "./schema" import { QuestionID } from "./schema"
const log = Log.create({ service: "question" }) const log = Log.create({ service: "question" })
+2 -2
View File
@@ -1,8 +1,8 @@
import { Schema } from "effect" import { Schema } from "effect"
import { Identifier } from "@/id/id" import { Identifier } from "@/id/id"
import { zod, ZodOverride } from "@/util/effect-zod" import { zod, ZodOverride } from "@opencode-ai/core/effect-zod"
import { Newtype } from "@/util/schema" import { Newtype } from "@opencode-ai/core/schema"
export class QuestionID extends Newtype<QuestionID>()( export class QuestionID extends Newtype<QuestionID>()(
"QuestionID", "QuestionID",
@@ -5,6 +5,7 @@ import * as Stream from "effect/Stream"
import { HttpServerResponse } from "effect/unstable/http" import { HttpServerResponse } from "effect/unstable/http"
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import * as Sse from "effect/unstable/encoding/Sse" import * as Sse from "effect/unstable/encoding/Sse"
import { WorkspaceRoutingQuery } from "./middleware/workspace-routing"
const log = Log.create({ service: "server" }) const log = Log.create({ service: "server" })
@@ -16,6 +17,7 @@ export const EventApi = HttpApi.make("event").add(
HttpApiGroup.make("event") HttpApiGroup.make("event")
.add( .add(
HttpApiEndpoint.get("subscribe", EventPaths.event, { HttpApiEndpoint.get("subscribe", EventPaths.event, {
query: WorkspaceRoutingQuery,
success: Schema.String.pipe(HttpApiSchema.asText({ contentType: "text/event-stream" })), success: Schema.String.pipe(HttpApiSchema.asText({ contentType: "text/event-stream" })),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -3,7 +3,7 @@ import { Provider } from "@/provider/provider"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization" import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context" import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing" import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
import { described } from "./metadata" import { described } from "./metadata"
const root = "/config" const root = "/config"
@@ -13,6 +13,7 @@ export const ConfigApi = HttpApi.make("config")
HttpApiGroup.make("config") HttpApiGroup.make("config")
.add( .add(
HttpApiEndpoint.get("get", root, { HttpApiEndpoint.get("get", root, {
query: WorkspaceRoutingQuery,
success: described(Config.Info, "Get config info"), success: described(Config.Info, "Get config info"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -22,6 +23,7 @@ export const ConfigApi = HttpApi.make("config")
}), }),
), ),
HttpApiEndpoint.patch("update", root, { HttpApiEndpoint.patch("update", root, {
query: WorkspaceRoutingQuery,
payload: Config.Info, payload: Config.Info,
success: described(Config.Info, "Successfully updated config"), success: described(Config.Info, "Successfully updated config"),
error: HttpApiError.BadRequest, error: HttpApiError.BadRequest,
@@ -33,6 +35,7 @@ export const ConfigApi = HttpApi.make("config")
}), }),
), ),
HttpApiEndpoint.get("providers", `${root}/providers`, { HttpApiEndpoint.get("providers", `${root}/providers`, {
query: WorkspaceRoutingQuery,
success: described(Provider.ConfigProvidersResult, "List of providers"), success: described(Provider.ConfigProvidersResult, "List of providers"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -3,13 +3,18 @@ import { MCP } from "@/mcp"
import { ProviderID, ModelID } from "@/provider/schema" import { ProviderID, ModelID } from "@/provider/schema"
import { Session } from "@/session/session" import { Session } from "@/session/session"
import { Worktree } from "@/worktree" import { Worktree } from "@/worktree"
import { NonNegativeInt } from "@/util/schema" import { NonNegativeInt } from "@opencode-ai/core/schema"
import { Schema, SchemaGetter } from "effect" import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization" import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context" import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing" import {
WorkspaceRoutingMiddleware,
WorkspaceRoutingQuery,
WorkspaceRoutingQueryFields,
} from "../middleware/workspace-routing"
import { described } from "./metadata" import { described } from "./metadata"
import { QueryBoolean } from "./query"
const ConsoleStateResponse = Schema.Struct({ const ConsoleStateResponse = Schema.Struct({
consoleManagedProviders: Schema.mutable(Schema.Array(Schema.String)), consoleManagedProviders: Schema.mutable(Schema.Array(Schema.String)),
@@ -43,19 +48,14 @@ const ToolListItem = Schema.Struct({
}).annotate({ identifier: "ToolListItem" }) }).annotate({ identifier: "ToolListItem" })
const ToolList = Schema.Array(ToolListItem).annotate({ identifier: "ToolList" }) const ToolList = Schema.Array(ToolListItem).annotate({ identifier: "ToolList" })
export const ToolListQuery = Schema.Struct({ export const ToolListQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
provider: ProviderID, provider: ProviderID,
model: ModelID, model: ModelID,
}) })
const QueryBoolean = Schema.Literals(["true", "false"]).pipe(
Schema.decodeTo(Schema.Boolean, {
decode: SchemaGetter.transform((value) => value === "true"),
encode: SchemaGetter.transform((value) => (value ? "true" : "false")),
}),
)
const WorktreeList = Schema.Array(Schema.String) const WorktreeList = Schema.Array(Schema.String)
export const SessionListQuery = Schema.Struct({ export const SessionListQuery = Schema.Struct({
directory: Schema.optional(Schema.String), ...WorkspaceRoutingQueryFields,
roots: Schema.optional(QueryBoolean), roots: Schema.optional(QueryBoolean),
start: Schema.optional(Schema.NumberFromString), start: Schema.optional(Schema.NumberFromString),
cursor: Schema.optional(Schema.NumberFromString), cursor: Schema.optional(Schema.NumberFromString),
@@ -81,6 +81,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
HttpApiGroup.make("experimental") HttpApiGroup.make("experimental")
.add( .add(
HttpApiEndpoint.get("console", ExperimentalPaths.console, { HttpApiEndpoint.get("console", ExperimentalPaths.console, {
query: WorkspaceRoutingQuery,
success: described(ConsoleStateResponse, "Active Console provider metadata"), success: described(ConsoleStateResponse, "Active Console provider metadata"),
error: HttpApiError.InternalServerError, error: HttpApiError.InternalServerError,
}).annotateMerge( }).annotateMerge(
@@ -91,6 +92,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
}), }),
), ),
HttpApiEndpoint.get("consoleOrgs", ExperimentalPaths.consoleOrgs, { HttpApiEndpoint.get("consoleOrgs", ExperimentalPaths.consoleOrgs, {
query: WorkspaceRoutingQuery,
success: described(ConsoleOrgList, "Switchable Console orgs"), success: described(ConsoleOrgList, "Switchable Console orgs"),
error: HttpApiError.InternalServerError, error: HttpApiError.InternalServerError,
}).annotateMerge( }).annotateMerge(
@@ -101,6 +103,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
}), }),
), ),
HttpApiEndpoint.post("consoleSwitch", ExperimentalPaths.consoleSwitch, { HttpApiEndpoint.post("consoleSwitch", ExperimentalPaths.consoleSwitch, {
query: WorkspaceRoutingQuery,
payload: ConsoleSwitchPayload, payload: ConsoleSwitchPayload,
success: described(Schema.Boolean, "Switch success"), success: described(Schema.Boolean, "Switch success"),
error: HttpApiError.BadRequest, error: HttpApiError.BadRequest,
@@ -124,6 +127,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
}), }),
), ),
HttpApiEndpoint.get("toolIDs", ExperimentalPaths.toolIDs, { HttpApiEndpoint.get("toolIDs", ExperimentalPaths.toolIDs, {
query: WorkspaceRoutingQuery,
success: described(ToolIDs, "Tool IDs"), success: described(ToolIDs, "Tool IDs"),
error: HttpApiError.BadRequest, error: HttpApiError.BadRequest,
}).annotateMerge( }).annotateMerge(
@@ -135,6 +139,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
}), }),
), ),
HttpApiEndpoint.get("worktree", ExperimentalPaths.worktree, { HttpApiEndpoint.get("worktree", ExperimentalPaths.worktree, {
query: WorkspaceRoutingQuery,
success: described(WorktreeList, "List of worktree directories"), success: described(WorktreeList, "List of worktree directories"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -144,6 +149,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
}), }),
), ),
HttpApiEndpoint.post("worktreeCreate", ExperimentalPaths.worktree, { HttpApiEndpoint.post("worktreeCreate", ExperimentalPaths.worktree, {
query: WorkspaceRoutingQuery,
payload: Schema.optional(Worktree.CreateInput), payload: Schema.optional(Worktree.CreateInput),
success: described(Worktree.Info, "Worktree created"), success: described(Worktree.Info, "Worktree created"),
error: HttpApiError.BadRequest, error: HttpApiError.BadRequest,
@@ -155,6 +161,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
}), }),
), ),
HttpApiEndpoint.delete("worktreeRemove", ExperimentalPaths.worktree, { HttpApiEndpoint.delete("worktreeRemove", ExperimentalPaths.worktree, {
query: WorkspaceRoutingQuery,
payload: Worktree.RemoveInput, payload: Worktree.RemoveInput,
success: described(Schema.Boolean, "Worktree removed"), success: described(Schema.Boolean, "Worktree removed"),
error: HttpApiError.BadRequest, error: HttpApiError.BadRequest,
@@ -166,6 +173,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
}), }),
), ),
HttpApiEndpoint.post("worktreeReset", ExperimentalPaths.worktreeReset, { HttpApiEndpoint.post("worktreeReset", ExperimentalPaths.worktreeReset, {
query: WorkspaceRoutingQuery,
payload: Worktree.ResetInput, payload: Worktree.ResetInput,
success: described(Schema.Boolean, "Worktree reset"), success: described(Schema.Boolean, "Worktree reset"),
error: HttpApiError.BadRequest, error: HttpApiError.BadRequest,
@@ -188,6 +196,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
}), }),
), ),
HttpApiEndpoint.get("resource", ExperimentalPaths.resource, { HttpApiEndpoint.get("resource", ExperimentalPaths.resource, {
query: WorkspaceRoutingQuery,
success: described(Schema.Record(Schema.String, MCP.Resource), "MCP resources"), success: described(Schema.Record(Schema.String, MCP.Resource), "MCP resources"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -5,18 +5,25 @@ import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization" import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context" import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing" import {
WorkspaceRoutingMiddleware,
WorkspaceRoutingQuery,
WorkspaceRoutingQueryFields,
} from "../middleware/workspace-routing"
import { described } from "./metadata" import { described } from "./metadata"
export const FileQuery = Schema.Struct({ export const FileQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
path: Schema.String, path: Schema.String,
}) })
export const FindTextQuery = Schema.Struct({ export const FindTextQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
pattern: Schema.String, pattern: Schema.String,
}) })
export const FindFileQuery = Schema.Struct({ export const FindFileQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
query: Schema.String, query: Schema.String,
dirs: Schema.optional(Schema.Literals(["true", "false"])), dirs: Schema.optional(Schema.Literals(["true", "false"])),
type: Schema.optional(Schema.Literals(["file", "directory"])), type: Schema.optional(Schema.Literals(["file", "directory"])),
@@ -26,6 +33,7 @@ export const FindFileQuery = Schema.Struct({
}) })
export const FindSymbolQuery = Schema.Struct({ export const FindSymbolQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
query: Schema.String, query: Schema.String,
}) })
@@ -93,6 +101,7 @@ export const FileApi = HttpApi.make("file")
}), }),
), ),
HttpApiEndpoint.get("status", FilePaths.status, { HttpApiEndpoint.get("status", FilePaths.status, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(File.Info), "File status"), success: described(Schema.Array(File.Info), "File status"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -8,7 +8,11 @@ import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization" import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context" import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing" import {
WorkspaceRoutingMiddleware,
WorkspaceRoutingQuery,
WorkspaceRoutingQueryFields,
} from "../middleware/workspace-routing"
import { described } from "./metadata" import { described } from "./metadata"
const PathInfo = Schema.Struct({ const PathInfo = Schema.Struct({
@@ -20,13 +24,10 @@ const PathInfo = Schema.Struct({
}).annotate({ identifier: "Path" }) }).annotate({ identifier: "Path" })
export const VcsDiffQuery = Schema.Struct({ export const VcsDiffQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
mode: Vcs.Mode, mode: Vcs.Mode,
}) })
export const SkillQuery = Schema.Struct({
include: Schema.optional(Schema.Literals(["invalid"])),
})
export class ApiVcsApplyError extends Schema.ErrorClass<ApiVcsApplyError>("VcsApplyError")( export class ApiVcsApplyError extends Schema.ErrorClass<ApiVcsApplyError>("VcsApplyError")(
{ {
name: Schema.Literal("VcsApplyError"), name: Schema.Literal("VcsApplyError"),
@@ -58,6 +59,7 @@ export const InstanceApi = HttpApi.make("instance")
HttpApiGroup.make("instance") HttpApiGroup.make("instance")
.add( .add(
HttpApiEndpoint.post("dispose", InstancePaths.dispose, { HttpApiEndpoint.post("dispose", InstancePaths.dispose, {
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Instance disposed"), success: described(Schema.Boolean, "Instance disposed"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -67,6 +69,7 @@ export const InstanceApi = HttpApi.make("instance")
}), }),
), ),
HttpApiEndpoint.get("path", InstancePaths.path, { HttpApiEndpoint.get("path", InstancePaths.path, {
query: WorkspaceRoutingQuery,
success: PathInfo, success: PathInfo,
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -77,6 +80,7 @@ export const InstanceApi = HttpApi.make("instance")
}), }),
), ),
HttpApiEndpoint.get("vcs", InstancePaths.vcs, { HttpApiEndpoint.get("vcs", InstancePaths.vcs, {
query: WorkspaceRoutingQuery,
success: described(Vcs.Info, "VCS info"), success: described(Vcs.Info, "VCS info"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -87,6 +91,7 @@ export const InstanceApi = HttpApi.make("instance")
}), }),
), ),
HttpApiEndpoint.get("vcsStatus", InstancePaths.vcsStatus, { HttpApiEndpoint.get("vcsStatus", InstancePaths.vcsStatus, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(Vcs.FileStatus), "VCS status"), success: described(Schema.Array(Vcs.FileStatus), "VCS status"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -106,6 +111,7 @@ export const InstanceApi = HttpApi.make("instance")
}), }),
), ),
HttpApiEndpoint.get("vcsDiffRaw", InstancePaths.vcsDiffRaw, { HttpApiEndpoint.get("vcsDiffRaw", InstancePaths.vcsDiffRaw, {
query: WorkspaceRoutingQuery,
success: described( success: described(
Schema.String.pipe(HttpApiSchema.asText({ contentType: "text/x-diff; charset=utf-8" })), Schema.String.pipe(HttpApiSchema.asText({ contentType: "text/x-diff; charset=utf-8" })),
"Raw VCS diff", "Raw VCS diff",
@@ -118,6 +124,7 @@ export const InstanceApi = HttpApi.make("instance")
}), }),
), ),
HttpApiEndpoint.post("vcsApply", InstancePaths.vcsApply, { HttpApiEndpoint.post("vcsApply", InstancePaths.vcsApply, {
query: WorkspaceRoutingQuery,
payload: Vcs.ApplyInput, payload: Vcs.ApplyInput,
success: described(Vcs.ApplyResult, "VCS patch applied"), success: described(Vcs.ApplyResult, "VCS patch applied"),
error: ApiVcsApplyError, error: ApiVcsApplyError,
@@ -129,6 +136,7 @@ export const InstanceApi = HttpApi.make("instance")
}), }),
), ),
HttpApiEndpoint.get("command", InstancePaths.command, { HttpApiEndpoint.get("command", InstancePaths.command, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(Command.Info), "List of commands"), success: described(Schema.Array(Command.Info), "List of commands"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -138,6 +146,7 @@ export const InstanceApi = HttpApi.make("instance")
}), }),
), ),
HttpApiEndpoint.get("agent", InstancePaths.agent, { HttpApiEndpoint.get("agent", InstancePaths.agent, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(Agent.Info), "List of agents"), success: described(Schema.Array(Agent.Info), "List of agents"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -147,8 +156,8 @@ export const InstanceApi = HttpApi.make("instance")
}), }),
), ),
HttpApiEndpoint.get("skill", InstancePaths.skill, { HttpApiEndpoint.get("skill", InstancePaths.skill, {
query: SkillQuery, query: WorkspaceRoutingQuery,
success: described(Schema.Union([Schema.Array(Skill.Info), Skill.ListWithInvalid]), "List of skills"), success: described(Schema.Array(Skill.Info), "List of skills"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
identifier: "app.skills", identifier: "app.skills",
@@ -157,6 +166,7 @@ export const InstanceApi = HttpApi.make("instance")
}), }),
), ),
HttpApiEndpoint.get("lsp", InstancePaths.lsp, { HttpApiEndpoint.get("lsp", InstancePaths.lsp, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(LSP.Status), "LSP server status"), success: described(Schema.Array(LSP.Status), "LSP server status"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -166,6 +176,7 @@ export const InstanceApi = HttpApi.make("instance")
}), }),
), ),
HttpApiEndpoint.get("formatter", InstancePaths.formatter, { HttpApiEndpoint.get("formatter", InstancePaths.formatter, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(Format.Status), "Formatter status"), success: described(Schema.Array(Format.Status), "Formatter status"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -4,7 +4,7 @@ import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization" import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context" import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing" import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
import { described } from "./metadata" import { described } from "./metadata"
export const AddPayload = Schema.Struct({ export const AddPayload = Schema.Struct({
@@ -42,6 +42,7 @@ export const McpApi = HttpApi.make("mcp")
HttpApiGroup.make("mcp") HttpApiGroup.make("mcp")
.add( .add(
HttpApiEndpoint.get("status", McpPaths.status, { HttpApiEndpoint.get("status", McpPaths.status, {
query: WorkspaceRoutingQuery,
success: described(Schema.Record(Schema.String, MCP.Status), "MCP server status"), success: described(Schema.Record(Schema.String, MCP.Status), "MCP server status"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -51,6 +52,7 @@ export const McpApi = HttpApi.make("mcp")
}), }),
), ),
HttpApiEndpoint.post("add", McpPaths.status, { HttpApiEndpoint.post("add", McpPaths.status, {
query: WorkspaceRoutingQuery,
payload: AddPayload, payload: AddPayload,
success: described(StatusMap, "MCP server added successfully"), success: described(StatusMap, "MCP server added successfully"),
error: HttpApiError.BadRequest, error: HttpApiError.BadRequest,
@@ -63,6 +65,7 @@ export const McpApi = HttpApi.make("mcp")
), ),
HttpApiEndpoint.post("authStart", McpPaths.auth, { HttpApiEndpoint.post("authStart", McpPaths.auth, {
params: { name: Schema.String }, params: { name: Schema.String },
query: WorkspaceRoutingQuery,
success: described(AuthStartResponse, "OAuth flow started"), success: described(AuthStartResponse, "OAuth flow started"),
error: [UnsupportedOAuthError, HttpApiError.NotFound], error: [UnsupportedOAuthError, HttpApiError.NotFound],
}).annotateMerge( }).annotateMerge(
@@ -74,6 +77,7 @@ export const McpApi = HttpApi.make("mcp")
), ),
HttpApiEndpoint.post("authCallback", McpPaths.authCallback, { HttpApiEndpoint.post("authCallback", McpPaths.authCallback, {
params: { name: Schema.String }, params: { name: Schema.String },
query: WorkspaceRoutingQuery,
payload: AuthCallbackPayload, payload: AuthCallbackPayload,
success: described(MCP.Status, "OAuth authentication completed"), success: described(MCP.Status, "OAuth authentication completed"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound], error: [HttpApiError.BadRequest, HttpApiError.NotFound],
@@ -87,6 +91,7 @@ export const McpApi = HttpApi.make("mcp")
), ),
HttpApiEndpoint.post("authAuthenticate", McpPaths.authAuthenticate, { HttpApiEndpoint.post("authAuthenticate", McpPaths.authAuthenticate, {
params: { name: Schema.String }, params: { name: Schema.String },
query: WorkspaceRoutingQuery,
success: described(MCP.Status, "OAuth authentication completed"), success: described(MCP.Status, "OAuth authentication completed"),
error: [UnsupportedOAuthError, HttpApiError.NotFound], error: [UnsupportedOAuthError, HttpApiError.NotFound],
}).annotateMerge( }).annotateMerge(
@@ -98,6 +103,7 @@ export const McpApi = HttpApi.make("mcp")
), ),
HttpApiEndpoint.delete("authRemove", McpPaths.auth, { HttpApiEndpoint.delete("authRemove", McpPaths.auth, {
params: { name: Schema.String }, params: { name: Schema.String },
query: WorkspaceRoutingQuery,
success: described(AuthRemoveResponse, "OAuth credentials removed"), success: described(AuthRemoveResponse, "OAuth credentials removed"),
error: HttpApiError.NotFound, error: HttpApiError.NotFound,
}).annotateMerge( }).annotateMerge(
@@ -109,6 +115,7 @@ export const McpApi = HttpApi.make("mcp")
), ),
HttpApiEndpoint.post("connect", McpPaths.connect, { HttpApiEndpoint.post("connect", McpPaths.connect, {
params: { name: Schema.String }, params: { name: Schema.String },
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "MCP server connected successfully"), success: described(Schema.Boolean, "MCP server connected successfully"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -118,6 +125,7 @@ export const McpApi = HttpApi.make("mcp")
), ),
HttpApiEndpoint.post("disconnect", McpPaths.disconnect, { HttpApiEndpoint.post("disconnect", McpPaths.disconnect, {
params: { name: Schema.String }, params: { name: Schema.String },
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "MCP server disconnected successfully"), success: described(Schema.Boolean, "MCP server disconnected successfully"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -4,7 +4,7 @@ import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization" import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context" import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing" import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
import { described } from "./metadata" import { described } from "./metadata"
const root = "/permission" const root = "/permission"
@@ -18,6 +18,7 @@ export const PermissionApi = HttpApi.make("permission")
HttpApiGroup.make("permission") HttpApiGroup.make("permission")
.add( .add(
HttpApiEndpoint.get("list", root, { HttpApiEndpoint.get("list", root, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(Permission.Request), "List of pending permissions"), success: described(Schema.Array(Permission.Request), "List of pending permissions"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -28,6 +29,7 @@ export const PermissionApi = HttpApi.make("permission")
), ),
HttpApiEndpoint.post("reply", `${root}/:requestID/reply`, { HttpApiEndpoint.post("reply", `${root}/:requestID/reply`, {
params: { requestID: PermissionID }, params: { requestID: PermissionID },
query: WorkspaceRoutingQuery,
payload: ReplyPayload, payload: ReplyPayload,
success: described(Schema.Boolean, "Permission processed successfully"), success: described(Schema.Boolean, "Permission processed successfully"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound], error: [HttpApiError.BadRequest, HttpApiError.NotFound],
@@ -4,7 +4,7 @@ import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization" import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context" import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing" import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
import { described } from "./metadata" import { described } from "./metadata"
const root = "/project" const root = "/project"
@@ -19,6 +19,7 @@ export const ProjectApi = HttpApi.make("project")
HttpApiGroup.make("project") HttpApiGroup.make("project")
.add( .add(
HttpApiEndpoint.get("list", root, { HttpApiEndpoint.get("list", root, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(Project.Info), "List of projects"), success: described(Schema.Array(Project.Info), "List of projects"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -28,6 +29,7 @@ export const ProjectApi = HttpApi.make("project")
}), }),
), ),
HttpApiEndpoint.get("current", `${root}/current`, { HttpApiEndpoint.get("current", `${root}/current`, {
query: WorkspaceRoutingQuery,
success: described(Project.Info, "Current project information"), success: described(Project.Info, "Current project information"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -37,6 +39,7 @@ export const ProjectApi = HttpApi.make("project")
}), }),
), ),
HttpApiEndpoint.post("initGit", `${root}/git/init`, { HttpApiEndpoint.post("initGit", `${root}/git/init`, {
query: WorkspaceRoutingQuery,
success: described(Project.Info, "Project information after git initialization"), success: described(Project.Info, "Project information after git initialization"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -47,6 +50,7 @@ export const ProjectApi = HttpApi.make("project")
), ),
HttpApiEndpoint.patch("update", `${root}/:projectID`, { HttpApiEndpoint.patch("update", `${root}/:projectID`, {
params: { projectID: ProjectID }, params: { projectID: ProjectID },
query: WorkspaceRoutingQuery,
payload: UpdatePayload, payload: UpdatePayload,
success: described(Project.Info, "Updated project information"), success: described(Project.Info, "Updated project information"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound], error: [HttpApiError.BadRequest, HttpApiError.NotFound],
@@ -5,7 +5,7 @@ import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization" import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context" import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing" import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
import { described } from "./metadata" import { described } from "./metadata"
const root = "/provider" const root = "/provider"
@@ -15,6 +15,7 @@ export const ProviderApi = HttpApi.make("provider")
HttpApiGroup.make("provider") HttpApiGroup.make("provider")
.add( .add(
HttpApiEndpoint.get("list", root, { HttpApiEndpoint.get("list", root, {
query: WorkspaceRoutingQuery,
success: described(Provider.ListResult, "List of providers"), success: described(Provider.ListResult, "List of providers"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -24,6 +25,7 @@ export const ProviderApi = HttpApi.make("provider")
}), }),
), ),
HttpApiEndpoint.get("auth", `${root}/auth`, { HttpApiEndpoint.get("auth", `${root}/auth`, {
query: WorkspaceRoutingQuery,
success: described(ProviderAuth.Methods, "Provider auth methods"), success: described(ProviderAuth.Methods, "Provider auth methods"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -34,6 +36,7 @@ export const ProviderApi = HttpApi.make("provider")
), ),
HttpApiEndpoint.post("authorize", `${root}/:providerID/oauth/authorize`, { HttpApiEndpoint.post("authorize", `${root}/:providerID/oauth/authorize`, {
params: { providerID: ProviderID }, params: { providerID: ProviderID },
query: WorkspaceRoutingQuery,
payload: ProviderAuth.AuthorizeInput, payload: ProviderAuth.AuthorizeInput,
success: described(Schema.UndefinedOr(ProviderAuth.Authorization), "Authorization URL and method"), success: described(Schema.UndefinedOr(ProviderAuth.Authorization), "Authorization URL and method"),
error: HttpApiError.BadRequest, error: HttpApiError.BadRequest,
@@ -46,6 +49,7 @@ export const ProviderApi = HttpApi.make("provider")
), ),
HttpApiEndpoint.post("callback", `${root}/:providerID/oauth/callback`, { HttpApiEndpoint.post("callback", `${root}/:providerID/oauth/callback`, {
params: { providerID: ProviderID }, params: { providerID: ProviderID },
query: WorkspaceRoutingQuery,
payload: ProviderAuth.CallbackInput, payload: ProviderAuth.CallbackInput,
success: described(Schema.Boolean, "OAuth callback processed successfully"), success: described(Schema.Boolean, "OAuth callback processed successfully"),
error: HttpApiError.BadRequest, error: HttpApiError.BadRequest,
@@ -5,13 +5,20 @@ import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization" import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context" import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing" import {
WorkspaceRoutingMiddleware,
WorkspaceRoutingQuery,
WorkspaceRoutingQueryFields,
} from "../middleware/workspace-routing"
import { ApiNotFoundError } from "../errors" import { ApiNotFoundError } from "../errors"
import { described } from "./metadata" import { described } from "./metadata"
const root = "/pty" const root = "/pty"
export const Params = Schema.Struct({ ptyID: PtyID }) export const Params = Schema.Struct({ ptyID: PtyID })
export const CursorQuery = Schema.Struct({ cursor: Schema.optional(Schema.String) }) export const CursorQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
cursor: Schema.optional(Schema.String),
})
export const ShellItem = Schema.Struct({ export const ShellItem = Schema.Struct({
path: Schema.String, path: Schema.String,
name: Schema.String, name: Schema.String,
@@ -34,6 +41,7 @@ export const PtyApi = HttpApi.make("pty")
HttpApiGroup.make("pty") HttpApiGroup.make("pty")
.add( .add(
HttpApiEndpoint.get("shells", PtyPaths.shells, { HttpApiEndpoint.get("shells", PtyPaths.shells, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(ShellItem), "List of shells"), success: described(Schema.Array(ShellItem), "List of shells"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -43,6 +51,7 @@ export const PtyApi = HttpApi.make("pty")
}), }),
), ),
HttpApiEndpoint.get("list", PtyPaths.list, { HttpApiEndpoint.get("list", PtyPaths.list, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(Pty.Info), "List of sessions"), success: described(Schema.Array(Pty.Info), "List of sessions"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -52,6 +61,7 @@ export const PtyApi = HttpApi.make("pty")
}), }),
), ),
HttpApiEndpoint.post("create", PtyPaths.create, { HttpApiEndpoint.post("create", PtyPaths.create, {
query: WorkspaceRoutingQuery,
payload: Pty.CreateInput, payload: Pty.CreateInput,
success: described(Pty.Info, "Created session"), success: described(Pty.Info, "Created session"),
error: HttpApiError.BadRequest, error: HttpApiError.BadRequest,
@@ -64,6 +74,7 @@ export const PtyApi = HttpApi.make("pty")
), ),
HttpApiEndpoint.get("get", PtyPaths.get, { HttpApiEndpoint.get("get", PtyPaths.get, {
params: { ptyID: PtyID }, params: { ptyID: PtyID },
query: WorkspaceRoutingQuery,
success: described(Pty.Info, "Session info"), success: described(Pty.Info, "Session info"),
error: ApiNotFoundError, error: ApiNotFoundError,
}).annotateMerge( }).annotateMerge(
@@ -75,6 +86,7 @@ export const PtyApi = HttpApi.make("pty")
), ),
HttpApiEndpoint.put("update", PtyPaths.update, { HttpApiEndpoint.put("update", PtyPaths.update, {
params: { ptyID: PtyID }, params: { ptyID: PtyID },
query: WorkspaceRoutingQuery,
payload: Pty.UpdateInput, payload: Pty.UpdateInput,
success: described(Pty.Info, "Updated session"), success: described(Pty.Info, "Updated session"),
error: [HttpApiError.BadRequest, ApiNotFoundError], error: [HttpApiError.BadRequest, ApiNotFoundError],
@@ -87,6 +99,7 @@ export const PtyApi = HttpApi.make("pty")
), ),
HttpApiEndpoint.delete("remove", PtyPaths.remove, { HttpApiEndpoint.delete("remove", PtyPaths.remove, {
params: { ptyID: PtyID }, params: { ptyID: PtyID },
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Session removed"), success: described(Schema.Boolean, "Session removed"),
error: ApiNotFoundError, error: ApiNotFoundError,
}).annotateMerge( }).annotateMerge(
@@ -98,6 +111,7 @@ export const PtyApi = HttpApi.make("pty")
), ),
HttpApiEndpoint.post("connectToken", PtyPaths.connectToken, { HttpApiEndpoint.post("connectToken", PtyPaths.connectToken, {
params: { ptyID: PtyID }, params: { ptyID: PtyID },
query: WorkspaceRoutingQuery,
success: described(PtyTicket.ConnectToken, "WebSocket connect token"), success: described(PtyTicket.ConnectToken, "WebSocket connect token"),
error: [HttpApiError.Forbidden, ApiNotFoundError], error: [HttpApiError.Forbidden, ApiNotFoundError],
}).annotateMerge( }).annotateMerge(
@@ -126,6 +140,7 @@ export const PtyConnectApi = HttpApi.make("pty-connect").add(
.add( .add(
HttpApiEndpoint.get("connect", PtyPaths.connect, { HttpApiEndpoint.get("connect", PtyPaths.connect, {
params: Params, params: Params,
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Connected session"), success: described(Schema.Boolean, "Connected session"),
error: [HttpApiError.Forbidden, HttpApiError.NotFound], error: [HttpApiError.Forbidden, HttpApiError.NotFound],
}).annotateMerge( }).annotateMerge(
@@ -0,0 +1,8 @@
import { Schema, SchemaGetter } from "effect"
export const QueryBoolean = Schema.Literals(["true", "false"]).pipe(
Schema.decodeTo(Schema.Boolean, {
decode: SchemaGetter.transform((value) => value === "true"),
encode: SchemaGetter.transform((value) => (value ? "true" : "false")),
}),
)
@@ -4,7 +4,7 @@ import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization" import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context" import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing" import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
import { described } from "./metadata" import { described } from "./metadata"
const root = "/question" const root = "/question"
@@ -19,6 +19,7 @@ export const QuestionApi = HttpApi.make("question")
HttpApiGroup.make("question") HttpApiGroup.make("question")
.add( .add(
HttpApiEndpoint.get("list", root, { HttpApiEndpoint.get("list", root, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(Question.Request), "List of pending questions"), success: described(Schema.Array(Question.Request), "List of pending questions"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -29,6 +30,7 @@ export const QuestionApi = HttpApi.make("question")
), ),
HttpApiEndpoint.post("reply", `${root}/:requestID/reply`, { HttpApiEndpoint.post("reply", `${root}/:requestID/reply`, {
params: { requestID: QuestionID }, params: { requestID: QuestionID },
query: WorkspaceRoutingQuery,
payload: ReplyPayload, payload: ReplyPayload,
success: described(Schema.Boolean, "Question answered successfully"), success: described(Schema.Boolean, "Question answered successfully"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound], error: [HttpApiError.BadRequest, HttpApiError.NotFound],
@@ -41,6 +43,7 @@ export const QuestionApi = HttpApi.make("question")
), ),
HttpApiEndpoint.post("reject", `${root}/:requestID/reject`, { HttpApiEndpoint.post("reject", `${root}/:requestID/reject`, {
params: { requestID: QuestionID }, params: { requestID: QuestionID },
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Question rejected successfully"), success: described(Schema.Boolean, "Question rejected successfully"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound], error: [HttpApiError.BadRequest, HttpApiError.NotFound],
}).annotateMerge( }).annotateMerge(
@@ -10,23 +10,22 @@ import { SessionSummary } from "@/session/summary"
import { Todo } from "@/session/todo" import { Todo } from "@/session/todo"
import { MessageID, PartID, SessionID } from "@/session/schema" import { MessageID, PartID, SessionID } from "@/session/schema"
import { Snapshot } from "@/snapshot" import { Snapshot } from "@/snapshot"
import { Schema, SchemaGetter, Struct } from "effect" import { Schema, Struct } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization" import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context" import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing" import {
WorkspaceRoutingMiddleware,
WorkspaceRoutingQuery,
WorkspaceRoutingQueryFields,
} from "../middleware/workspace-routing"
import { ApiNotFoundError } from "../errors" import { ApiNotFoundError } from "../errors"
import { described } from "./metadata" import { described } from "./metadata"
import { QueryBoolean } from "./query"
const root = "/session" const root = "/session"
const QueryBoolean = Schema.Literals(["true", "false"]).pipe(
Schema.decodeTo(Schema.Boolean, {
decode: SchemaGetter.transform((value) => value === "true"),
encode: SchemaGetter.transform((value) => (value ? "true" : "false")),
}),
)
export const ListQuery = Schema.Struct({ export const ListQuery = Schema.Struct({
directory: Schema.optional(Schema.String), ...WorkspaceRoutingQueryFields,
scope: Schema.optional(Schema.Literals(["project"])), scope: Schema.optional(Schema.Literals(["project"])),
path: Schema.optional(Schema.String), path: Schema.optional(Schema.String),
roots: Schema.optional(QueryBoolean), roots: Schema.optional(QueryBoolean),
@@ -34,8 +33,12 @@ export const ListQuery = Schema.Struct({
search: Schema.optional(Schema.String), search: Schema.optional(Schema.String),
limit: Schema.optional(Schema.NumberFromString), limit: Schema.optional(Schema.NumberFromString),
}) })
export const DiffQuery = Schema.Struct(Struct.omit(SessionSummary.DiffInput.fields, ["sessionID"])) export const DiffQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
...Struct.omit(SessionSummary.DiffInput.fields, ["sessionID"]),
})
export const MessagesQuery = Schema.Struct({ export const MessagesQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
limit: Schema.optional(Schema.NumberFromString.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0))), limit: Schema.optional(Schema.NumberFromString.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0))),
before: Schema.optional(Schema.String), before: Schema.optional(Schema.String),
}) })
@@ -112,6 +115,7 @@ export const SessionApi = HttpApi.make("session")
}), }),
), ),
HttpApiEndpoint.get("status", SessionPaths.status, { HttpApiEndpoint.get("status", SessionPaths.status, {
query: WorkspaceRoutingQuery,
success: described(StatusMap, "Get session status"), success: described(StatusMap, "Get session status"),
error: HttpApiError.BadRequest, error: HttpApiError.BadRequest,
}).annotateMerge( }).annotateMerge(
@@ -123,6 +127,7 @@ export const SessionApi = HttpApi.make("session")
), ),
HttpApiEndpoint.get("get", SessionPaths.get, { HttpApiEndpoint.get("get", SessionPaths.get, {
params: { sessionID: SessionID }, params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
success: described(Session.Info, "Get session"), success: described(Session.Info, "Get session"),
error: [HttpApiError.BadRequest, ApiNotFoundError], error: [HttpApiError.BadRequest, ApiNotFoundError],
}).annotateMerge( }).annotateMerge(
@@ -134,6 +139,7 @@ export const SessionApi = HttpApi.make("session")
), ),
HttpApiEndpoint.get("children", SessionPaths.children, { HttpApiEndpoint.get("children", SessionPaths.children, {
params: { sessionID: SessionID }, params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
success: described(Schema.Array(Session.Info), "List of children"), success: described(Schema.Array(Session.Info), "List of children"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound], error: [HttpApiError.BadRequest, HttpApiError.NotFound],
}).annotateMerge( }).annotateMerge(
@@ -145,6 +151,7 @@ export const SessionApi = HttpApi.make("session")
), ),
HttpApiEndpoint.get("todo", SessionPaths.todo, { HttpApiEndpoint.get("todo", SessionPaths.todo, {
params: { sessionID: SessionID }, params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
success: described(Schema.Array(Todo.Info), "Todo list"), success: described(Schema.Array(Todo.Info), "Todo list"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound], error: [HttpApiError.BadRequest, HttpApiError.NotFound],
}).annotateMerge( }).annotateMerge(
@@ -179,6 +186,7 @@ export const SessionApi = HttpApi.make("session")
), ),
HttpApiEndpoint.get("message", SessionPaths.message, { HttpApiEndpoint.get("message", SessionPaths.message, {
params: { sessionID: SessionID, messageID: MessageID }, params: { sessionID: SessionID, messageID: MessageID },
query: WorkspaceRoutingQuery,
success: described(MessageV2.WithParts, "Message"), success: described(MessageV2.WithParts, "Message"),
error: [HttpApiError.BadRequest, ApiNotFoundError], error: [HttpApiError.BadRequest, ApiNotFoundError],
}).annotateMerge( }).annotateMerge(
@@ -189,6 +197,7 @@ export const SessionApi = HttpApi.make("session")
}), }),
), ),
HttpApiEndpoint.post("create", SessionPaths.create, { HttpApiEndpoint.post("create", SessionPaths.create, {
query: WorkspaceRoutingQuery,
payload: [HttpApiSchema.NoContent, Session.CreateInput], payload: [HttpApiSchema.NoContent, Session.CreateInput],
success: described(Session.Info, "Successfully created session"), success: described(Session.Info, "Successfully created session"),
error: HttpApiError.BadRequest, error: HttpApiError.BadRequest,
@@ -201,6 +210,7 @@ export const SessionApi = HttpApi.make("session")
), ),
HttpApiEndpoint.delete("remove", SessionPaths.remove, { HttpApiEndpoint.delete("remove", SessionPaths.remove, {
params: { sessionID: SessionID }, params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Successfully deleted session"), success: described(Schema.Boolean, "Successfully deleted session"),
error: [HttpApiError.BadRequest, ApiNotFoundError], error: [HttpApiError.BadRequest, ApiNotFoundError],
}).annotateMerge( }).annotateMerge(
@@ -212,6 +222,7 @@ export const SessionApi = HttpApi.make("session")
), ),
HttpApiEndpoint.patch("update", SessionPaths.update, { HttpApiEndpoint.patch("update", SessionPaths.update, {
params: { sessionID: SessionID }, params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
payload: UpdatePayload, payload: UpdatePayload,
success: described(Session.Info, "Successfully updated session"), success: described(Session.Info, "Successfully updated session"),
error: [HttpApiError.BadRequest, ApiNotFoundError], error: [HttpApiError.BadRequest, ApiNotFoundError],
@@ -224,6 +235,7 @@ export const SessionApi = HttpApi.make("session")
), ),
HttpApiEndpoint.post("fork", SessionPaths.fork, { HttpApiEndpoint.post("fork", SessionPaths.fork, {
params: { sessionID: SessionID }, params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
payload: ForkPayload, payload: ForkPayload,
success: described(Session.Info, "200"), success: described(Session.Info, "200"),
error: ApiNotFoundError, error: ApiNotFoundError,
@@ -236,6 +248,7 @@ export const SessionApi = HttpApi.make("session")
), ),
HttpApiEndpoint.post("abort", SessionPaths.abort, { HttpApiEndpoint.post("abort", SessionPaths.abort, {
params: { sessionID: SessionID }, params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Aborted session"), success: described(Schema.Boolean, "Aborted session"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound], error: [HttpApiError.BadRequest, HttpApiError.NotFound],
}).annotateMerge( }).annotateMerge(
@@ -247,6 +260,7 @@ export const SessionApi = HttpApi.make("session")
), ),
HttpApiEndpoint.post("init", SessionPaths.init, { HttpApiEndpoint.post("init", SessionPaths.init, {
params: { sessionID: SessionID }, params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
payload: InitPayload, payload: InitPayload,
success: described(Schema.Boolean, "200"), success: described(Schema.Boolean, "200"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound], error: [HttpApiError.BadRequest, HttpApiError.NotFound],
@@ -260,6 +274,7 @@ export const SessionApi = HttpApi.make("session")
), ),
HttpApiEndpoint.post("share", SessionPaths.share, { HttpApiEndpoint.post("share", SessionPaths.share, {
params: { sessionID: SessionID }, params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
success: described(Session.Info, "Successfully shared session"), success: described(Session.Info, "Successfully shared session"),
error: [HttpApiError.InternalServerError, ApiNotFoundError], error: [HttpApiError.InternalServerError, ApiNotFoundError],
}).annotateMerge( }).annotateMerge(
@@ -271,6 +286,7 @@ export const SessionApi = HttpApi.make("session")
), ),
HttpApiEndpoint.delete("unshare", SessionPaths.share, { HttpApiEndpoint.delete("unshare", SessionPaths.share, {
params: { sessionID: SessionID }, params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
success: described(Session.Info, "Successfully unshared session"), success: described(Session.Info, "Successfully unshared session"),
error: [HttpApiError.InternalServerError, ApiNotFoundError], error: [HttpApiError.InternalServerError, ApiNotFoundError],
}).annotateMerge( }).annotateMerge(
@@ -282,6 +298,7 @@ export const SessionApi = HttpApi.make("session")
), ),
HttpApiEndpoint.post("summarize", SessionPaths.summarize, { HttpApiEndpoint.post("summarize", SessionPaths.summarize, {
params: { sessionID: SessionID }, params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
payload: SummarizePayload, payload: SummarizePayload,
success: described(Schema.Boolean, "Summarized session"), success: described(Schema.Boolean, "Summarized session"),
error: [HttpApiError.BadRequest, ApiNotFoundError], error: [HttpApiError.BadRequest, ApiNotFoundError],
@@ -294,6 +311,7 @@ export const SessionApi = HttpApi.make("session")
), ),
HttpApiEndpoint.post("prompt", SessionPaths.prompt, { HttpApiEndpoint.post("prompt", SessionPaths.prompt, {
params: { sessionID: SessionID }, params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
payload: PromptPayload, payload: PromptPayload,
success: described(MessageV2.WithParts, "Created message"), success: described(MessageV2.WithParts, "Created message"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound], error: [HttpApiError.BadRequest, HttpApiError.NotFound],
@@ -306,6 +324,7 @@ export const SessionApi = HttpApi.make("session")
), ),
HttpApiEndpoint.post("promptAsync", SessionPaths.promptAsync, { HttpApiEndpoint.post("promptAsync", SessionPaths.promptAsync, {
params: { sessionID: SessionID }, params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
payload: PromptPayload, payload: PromptPayload,
success: described(HttpApiSchema.NoContent, "Prompt accepted"), success: described(HttpApiSchema.NoContent, "Prompt accepted"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound], error: [HttpApiError.BadRequest, HttpApiError.NotFound],
@@ -319,6 +338,7 @@ export const SessionApi = HttpApi.make("session")
), ),
HttpApiEndpoint.post("command", SessionPaths.command, { HttpApiEndpoint.post("command", SessionPaths.command, {
params: { sessionID: SessionID }, params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
payload: CommandPayload, payload: CommandPayload,
success: described(MessageV2.WithParts, "Created message"), success: described(MessageV2.WithParts, "Created message"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound], error: [HttpApiError.BadRequest, HttpApiError.NotFound],
@@ -331,6 +351,7 @@ export const SessionApi = HttpApi.make("session")
), ),
HttpApiEndpoint.post("shell", SessionPaths.shell, { HttpApiEndpoint.post("shell", SessionPaths.shell, {
params: { sessionID: SessionID }, params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
payload: ShellPayload, payload: ShellPayload,
success: described(MessageV2.WithParts, "Created message"), success: described(MessageV2.WithParts, "Created message"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound], error: [HttpApiError.BadRequest, HttpApiError.NotFound],
@@ -343,6 +364,7 @@ export const SessionApi = HttpApi.make("session")
), ),
HttpApiEndpoint.post("revert", SessionPaths.revert, { HttpApiEndpoint.post("revert", SessionPaths.revert, {
params: { sessionID: SessionID }, params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
payload: RevertPayload, payload: RevertPayload,
success: described(Session.Info, "Updated session"), success: described(Session.Info, "Updated session"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound], error: [HttpApiError.BadRequest, HttpApiError.NotFound],
@@ -356,6 +378,7 @@ export const SessionApi = HttpApi.make("session")
), ),
HttpApiEndpoint.post("unrevert", SessionPaths.unrevert, { HttpApiEndpoint.post("unrevert", SessionPaths.unrevert, {
params: { sessionID: SessionID }, params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
success: described(Session.Info, "Updated session"), success: described(Session.Info, "Updated session"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound], error: [HttpApiError.BadRequest, HttpApiError.NotFound],
}).annotateMerge( }).annotateMerge(
@@ -367,6 +390,7 @@ export const SessionApi = HttpApi.make("session")
), ),
HttpApiEndpoint.post("permissionRespond", SessionPaths.permissions, { HttpApiEndpoint.post("permissionRespond", SessionPaths.permissions, {
params: { sessionID: SessionID, permissionID: PermissionID }, params: { sessionID: SessionID, permissionID: PermissionID },
query: WorkspaceRoutingQuery,
payload: PermissionResponsePayload, payload: PermissionResponsePayload,
success: described(Schema.Boolean, "Permission processed successfully"), success: described(Schema.Boolean, "Permission processed successfully"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound], error: [HttpApiError.BadRequest, HttpApiError.NotFound],
@@ -380,6 +404,7 @@ export const SessionApi = HttpApi.make("session")
), ),
HttpApiEndpoint.delete("deleteMessage", SessionPaths.deleteMessage, { HttpApiEndpoint.delete("deleteMessage", SessionPaths.deleteMessage, {
params: { sessionID: SessionID, messageID: MessageID }, params: { sessionID: SessionID, messageID: MessageID },
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Successfully deleted message"), success: described(Schema.Boolean, "Successfully deleted message"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound], error: [HttpApiError.BadRequest, HttpApiError.NotFound],
}).annotateMerge( }).annotateMerge(
@@ -392,6 +417,7 @@ export const SessionApi = HttpApi.make("session")
), ),
HttpApiEndpoint.delete("deletePart", SessionPaths.deletePart, { HttpApiEndpoint.delete("deletePart", SessionPaths.deletePart, {
params: { sessionID: SessionID, messageID: MessageID, partID: PartID }, params: { sessionID: SessionID, messageID: MessageID, partID: PartID },
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Successfully deleted part"), success: described(Schema.Boolean, "Successfully deleted part"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound], error: [HttpApiError.BadRequest, HttpApiError.NotFound],
}).annotateMerge( }).annotateMerge(
@@ -402,6 +428,7 @@ export const SessionApi = HttpApi.make("session")
), ),
HttpApiEndpoint.patch("updatePart", SessionPaths.updatePart, { HttpApiEndpoint.patch("updatePart", SessionPaths.updatePart, {
params: { sessionID: SessionID, messageID: MessageID, partID: PartID }, params: { sessionID: SessionID, messageID: MessageID, partID: PartID },
query: WorkspaceRoutingQuery,
payload: MessageV2.Part, payload: MessageV2.Part,
success: described(MessageV2.Part, "Successfully updated part"), success: described(MessageV2.Part, "Successfully updated part"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound], error: [HttpApiError.BadRequest, HttpApiError.NotFound],
@@ -1,10 +1,10 @@
import { NonNegativeInt } from "@/util/schema" import { NonNegativeInt } from "@opencode-ai/core/schema"
import { SessionID } from "@/session/schema" import { SessionID } from "@/session/schema"
import { Schema } from "effect" import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization" import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context" import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing" import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
import { described } from "./metadata" import { described } from "./metadata"
const root = "/sync" const root = "/sync"
@@ -46,6 +46,7 @@ export const SyncApi = HttpApi.make("sync")
HttpApiGroup.make("sync") HttpApiGroup.make("sync")
.add( .add(
HttpApiEndpoint.post("start", SyncPaths.start, { HttpApiEndpoint.post("start", SyncPaths.start, {
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Workspace sync started"), success: described(Schema.Boolean, "Workspace sync started"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -55,6 +56,7 @@ export const SyncApi = HttpApi.make("sync")
}), }),
), ),
HttpApiEndpoint.post("replay", SyncPaths.replay, { HttpApiEndpoint.post("replay", SyncPaths.replay, {
query: WorkspaceRoutingQuery,
payload: ReplayPayload, payload: ReplayPayload,
success: described(ReplayResponse, "Replayed sync events"), success: described(ReplayResponse, "Replayed sync events"),
error: HttpApiError.BadRequest, error: HttpApiError.BadRequest,
@@ -66,6 +68,7 @@ export const SyncApi = HttpApi.make("sync")
}), }),
), ),
HttpApiEndpoint.post("steal", SyncPaths.steal, { HttpApiEndpoint.post("steal", SyncPaths.steal, {
query: WorkspaceRoutingQuery,
payload: SessionPayload, payload: SessionPayload,
success: described(SessionPayload, "Session stolen into workspace"), success: described(SessionPayload, "Session stolen into workspace"),
error: HttpApiError.BadRequest, error: HttpApiError.BadRequest,
@@ -77,6 +80,7 @@ export const SyncApi = HttpApi.make("sync")
}), }),
), ),
HttpApiEndpoint.post("history", SyncPaths.history, { HttpApiEndpoint.post("history", SyncPaths.history, {
query: WorkspaceRoutingQuery,
payload: HistoryPayload, payload: HistoryPayload,
success: described(Schema.Array(HistoryEvent), "Sync events"), success: described(Schema.Array(HistoryEvent), "Sync events"),
error: HttpApiError.BadRequest, error: HttpApiError.BadRequest,
@@ -4,7 +4,7 @@ import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization" import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context" import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing" import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
import { ApiNotFoundError } from "../errors" import { ApiNotFoundError } from "../errors"
import { described } from "./metadata" import { described } from "./metadata"
@@ -54,6 +54,7 @@ export const TuiApi = HttpApi.make("tui")
HttpApiGroup.make("tui") HttpApiGroup.make("tui")
.add( .add(
HttpApiEndpoint.post("appendPrompt", TuiPaths.appendPrompt, { HttpApiEndpoint.post("appendPrompt", TuiPaths.appendPrompt, {
query: WorkspaceRoutingQuery,
payload: TuiEvent.PromptAppend.properties, payload: TuiEvent.PromptAppend.properties,
success: described(Schema.Boolean, "Prompt processed successfully"), success: described(Schema.Boolean, "Prompt processed successfully"),
error: HttpApiError.BadRequest, error: HttpApiError.BadRequest,
@@ -65,6 +66,7 @@ export const TuiApi = HttpApi.make("tui")
}), }),
), ),
HttpApiEndpoint.post("openHelp", TuiPaths.openHelp, { HttpApiEndpoint.post("openHelp", TuiPaths.openHelp, {
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Help dialog opened successfully"), success: described(Schema.Boolean, "Help dialog opened successfully"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -74,6 +76,7 @@ export const TuiApi = HttpApi.make("tui")
}), }),
), ),
HttpApiEndpoint.post("openSessions", TuiPaths.openSessions, { HttpApiEndpoint.post("openSessions", TuiPaths.openSessions, {
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Session dialog opened successfully"), success: described(Schema.Boolean, "Session dialog opened successfully"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -83,6 +86,7 @@ export const TuiApi = HttpApi.make("tui")
}), }),
), ),
HttpApiEndpoint.post("openThemes", TuiPaths.openThemes, { HttpApiEndpoint.post("openThemes", TuiPaths.openThemes, {
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Theme dialog opened successfully"), success: described(Schema.Boolean, "Theme dialog opened successfully"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -92,6 +96,7 @@ export const TuiApi = HttpApi.make("tui")
}), }),
), ),
HttpApiEndpoint.post("openModels", TuiPaths.openModels, { HttpApiEndpoint.post("openModels", TuiPaths.openModels, {
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Model dialog opened successfully"), success: described(Schema.Boolean, "Model dialog opened successfully"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -101,6 +106,7 @@ export const TuiApi = HttpApi.make("tui")
}), }),
), ),
HttpApiEndpoint.post("submitPrompt", TuiPaths.submitPrompt, { HttpApiEndpoint.post("submitPrompt", TuiPaths.submitPrompt, {
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Prompt submitted successfully"), success: described(Schema.Boolean, "Prompt submitted successfully"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -110,6 +116,7 @@ export const TuiApi = HttpApi.make("tui")
}), }),
), ),
HttpApiEndpoint.post("clearPrompt", TuiPaths.clearPrompt, { HttpApiEndpoint.post("clearPrompt", TuiPaths.clearPrompt, {
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Prompt cleared successfully"), success: described(Schema.Boolean, "Prompt cleared successfully"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -119,6 +126,7 @@ export const TuiApi = HttpApi.make("tui")
}), }),
), ),
HttpApiEndpoint.post("executeCommand", TuiPaths.executeCommand, { HttpApiEndpoint.post("executeCommand", TuiPaths.executeCommand, {
query: WorkspaceRoutingQuery,
payload: CommandPayload, payload: CommandPayload,
success: described(Schema.Boolean, "Command executed successfully"), success: described(Schema.Boolean, "Command executed successfully"),
error: HttpApiError.BadRequest, error: HttpApiError.BadRequest,
@@ -130,6 +138,7 @@ export const TuiApi = HttpApi.make("tui")
}), }),
), ),
HttpApiEndpoint.post("showToast", TuiPaths.showToast, { HttpApiEndpoint.post("showToast", TuiPaths.showToast, {
query: WorkspaceRoutingQuery,
payload: TuiEvent.ToastShow.properties, payload: TuiEvent.ToastShow.properties,
success: described(Schema.Boolean, "Toast notification shown successfully"), success: described(Schema.Boolean, "Toast notification shown successfully"),
}).annotateMerge( }).annotateMerge(
@@ -140,6 +149,7 @@ export const TuiApi = HttpApi.make("tui")
}), }),
), ),
HttpApiEndpoint.post("publish", TuiPaths.publish, { HttpApiEndpoint.post("publish", TuiPaths.publish, {
query: WorkspaceRoutingQuery,
payload: TuiPublishPayload, payload: TuiPublishPayload,
success: described(Schema.Boolean, "Event published successfully"), success: described(Schema.Boolean, "Event published successfully"),
error: HttpApiError.BadRequest, error: HttpApiError.BadRequest,
@@ -151,6 +161,7 @@ export const TuiApi = HttpApi.make("tui")
}), }),
), ),
HttpApiEndpoint.post("selectSession", TuiPaths.selectSession, { HttpApiEndpoint.post("selectSession", TuiPaths.selectSession, {
query: WorkspaceRoutingQuery,
payload: TuiEvent.SessionSelect.properties, payload: TuiEvent.SessionSelect.properties,
success: described(Schema.Boolean, "Session selected successfully"), success: described(Schema.Boolean, "Session selected successfully"),
error: [HttpApiError.BadRequest, ApiNotFoundError], error: [HttpApiError.BadRequest, ApiNotFoundError],
@@ -162,6 +173,7 @@ export const TuiApi = HttpApi.make("tui")
}), }),
), ),
HttpApiEndpoint.get("controlNext", TuiPaths.controlNext, { HttpApiEndpoint.get("controlNext", TuiPaths.controlNext, {
query: WorkspaceRoutingQuery,
success: described(TuiRequestPayload, "Next TUI request"), success: described(TuiRequestPayload, "Next TUI request"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -171,6 +183,7 @@ export const TuiApi = HttpApi.make("tui")
}), }),
), ),
HttpApiEndpoint.post("controlResponse", TuiPaths.controlResponse, { HttpApiEndpoint.post("controlResponse", TuiPaths.controlResponse, {
query: WorkspaceRoutingQuery,
payload: Schema.Unknown, payload: Schema.Unknown,
success: described(Schema.Boolean, "Response submitted successfully"), success: described(Schema.Boolean, "Response submitted successfully"),
}).annotateMerge( }).annotateMerge(
@@ -3,46 +3,31 @@ import { SessionMessage } from "@/v2/session-message"
import { Schema } from "effect" import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../../middleware/authorization" import { Authorization } from "../../middleware/authorization"
import { WorkspaceRoutingQueryFields } from "../../middleware/workspace-routing"
export const MessagesQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
limit: Schema.optional(
Schema.NumberFromString.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(200)),
).annotate({
description: "Maximum number of messages to return. When omitted, the endpoint returns its default page size.",
}),
order: Schema.optional(Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")])).annotate({
description: "Message order for the first page. Use desc for newest first or asc for oldest first.",
}),
cursor: Schema.optional(
Schema.String.annotate({
description:
"Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order.",
}),
),
}).annotate({ identifier: "V2SessionMessagesQuery" })
export const MessageGroup = HttpApiGroup.make("v2.message") export const MessageGroup = HttpApiGroup.make("v2.message")
.add( .add(
HttpApiEndpoint.get("messages", "/api/session/:sessionID/message", { HttpApiEndpoint.get("messages", "/api/session/:sessionID/message", {
params: { sessionID: SessionID }, params: { sessionID: SessionID },
query: Schema.Union([ query: MessagesQuery,
Schema.Struct({
limit: Schema.optional(
Schema.NumberFromString.check(
Schema.isInt(),
Schema.isGreaterThanOrEqualTo(1),
Schema.isLessThanOrEqualTo(200),
),
).annotate({
description:
"Maximum number of messages to return. When omitted, the endpoint returns its default page size.",
}),
order: Schema.optional(Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")])).annotate({
description: "Message order for the first page. Use desc for newest first or asc for oldest first.",
}),
cursor: Schema.optional(Schema.Never),
}),
Schema.Struct({
limit: Schema.optional(
Schema.NumberFromString.check(
Schema.isInt(),
Schema.isGreaterThanOrEqualTo(1),
Schema.isLessThanOrEqualTo(200),
),
).annotate({
description:
"Maximum number of messages to return. When omitted, the endpoint returns its default page size.",
}),
cursor: Schema.String.annotate({
description:
"Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order.",
}),
order: Schema.optional(Schema.Never),
}),
]).annotate({ identifier: "V2SessionMessagesQuery" }),
success: Schema.Struct({ success: Schema.Struct({
items: Schema.Array(SessionMessage.Message), items: Schema.Array(SessionMessage.Message),
cursor: Schema.Struct({ cursor: Schema.Struct({
@@ -1,67 +1,39 @@
import { WorkspaceID } from "@/control-plane/schema"
import { SessionID } from "@/session/schema" import { SessionID } from "@/session/schema"
import { SessionMessage } from "@/v2/session-message" import { SessionMessage } from "@/v2/session-message"
import { Prompt } from "@/v2/session-prompt" import { Prompt } from "@/v2/session-prompt"
import { SessionV2 } from "@/v2/session" import { SessionV2 } from "@/v2/session"
import { Schema, SchemaGetter } from "effect" import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" import { HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../../middleware/authorization" import { Authorization } from "../../middleware/authorization"
import { WorkspaceRoutingQuery, WorkspaceRoutingQueryFields } from "../../middleware/workspace-routing"
import { QueryBoolean } from "../query"
export const SessionGroup = HttpApiGroup.make("v2.session") export const SessionsQuery = Schema.Struct({
.add( ...WorkspaceRoutingQueryFields,
HttpApiEndpoint.get("sessions", "/api/session", {
query: Schema.Union([
Schema.Struct({
limit: Schema.optional( limit: Schema.optional(
Schema.NumberFromString.check( Schema.NumberFromString.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(200)),
Schema.isInt(),
Schema.isGreaterThanOrEqualTo(1),
Schema.isLessThanOrEqualTo(200),
),
).annotate({ ).annotate({
description: "Maximum number of sessions to return. Defaults to the newest 50 sessions.", description: "Maximum number of sessions to return. Defaults to the newest 50 sessions.",
}), }),
order: Schema.optional(Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")])).annotate({ order: Schema.optional(Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")])).annotate({
description: "Session order for the first page. Use desc for newest first or asc for oldest first.", description: "Session order for the first page. Use desc for newest first or asc for oldest first.",
}), }),
directory: Schema.String.pipe(Schema.optional), path: Schema.optional(Schema.String),
path: Schema.String.pipe(Schema.optional), roots: Schema.optional(QueryBoolean),
workspace: WorkspaceID.pipe(Schema.optional), start: Schema.optional(Schema.NumberFromString),
roots: Schema.Literals(["true", "false"]) search: Schema.optional(Schema.String),
.pipe( cursor: Schema.optional(
Schema.decodeTo(Schema.Boolean, { Schema.String.annotate({
decode: SchemaGetter.transform((value) => value === "true"),
encode: SchemaGetter.transform((value) => (value ? "true" : "false")),
}),
)
.pipe(Schema.optional),
start: Schema.NumberFromString.pipe(Schema.optional),
search: Schema.String.pipe(Schema.optional),
cursor: Schema.optional(Schema.Never),
}),
Schema.Struct({
limit: Schema.optional(
Schema.NumberFromString.check(
Schema.isInt(),
Schema.isGreaterThanOrEqualTo(1),
Schema.isLessThanOrEqualTo(200),
),
).annotate({
description: "Maximum number of sessions to return. Defaults to the newest 50 sessions.",
}),
cursor: Schema.String.annotate({
description: description:
"Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order.", "Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order or filters.",
}), }),
order: Schema.optional(Schema.Never), ),
directory: Schema.optional(Schema.Never), }).annotate({ identifier: "V2SessionsQuery" })
path: Schema.optional(Schema.Never),
workspace: Schema.optional(Schema.Never), export const SessionGroup = HttpApiGroup.make("v2.session")
roots: Schema.optional(Schema.Never), .add(
start: Schema.optional(Schema.Never), HttpApiEndpoint.get("sessions", "/api/session", {
search: Schema.optional(Schema.Never), query: SessionsQuery,
}),
]).annotate({ identifier: "V2SessionsQuery" }),
success: Schema.Struct({ success: Schema.Struct({
items: Schema.Array(SessionV2.Info), items: Schema.Array(SessionV2.Info),
cursor: Schema.Struct({ cursor: Schema.Struct({
@@ -82,6 +54,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
.add( .add(
HttpApiEndpoint.post("prompt", "/api/session/:sessionID/prompt", { HttpApiEndpoint.post("prompt", "/api/session/:sessionID/prompt", {
params: { sessionID: SessionID }, params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
payload: Schema.Struct({ payload: Schema.Struct({
prompt: Prompt, prompt: Prompt,
delivery: SessionV2.Delivery.pipe(Schema.optional), delivery: SessionV2.Delivery.pipe(Schema.optional),
@@ -98,6 +71,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
.add( .add(
HttpApiEndpoint.post("compact", "/api/session/:sessionID/compact", { HttpApiEndpoint.post("compact", "/api/session/:sessionID/compact", {
params: { sessionID: SessionID }, params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
success: HttpApiSchema.NoContent, success: HttpApiSchema.NoContent,
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -110,6 +84,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
.add( .add(
HttpApiEndpoint.post("wait", "/api/session/:sessionID/wait", { HttpApiEndpoint.post("wait", "/api/session/:sessionID/wait", {
params: { sessionID: SessionID }, params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
success: HttpApiSchema.NoContent, success: HttpApiSchema.NoContent,
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -122,6 +97,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
.add( .add(
HttpApiEndpoint.get("context", "/api/session/:sessionID/context", { HttpApiEndpoint.get("context", "/api/session/:sessionID/context", {
params: { sessionID: SessionID }, params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
success: Schema.Array(SessionMessage.Message), success: Schema.Array(SessionMessage.Message),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -5,7 +5,7 @@ import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, Op
import { ApiVcsApplyError } from "./instance" import { ApiVcsApplyError } from "./instance"
import { Authorization } from "../middleware/authorization" import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context" import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing" import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
import { described } from "./metadata" import { described } from "./metadata"
const root = "/experimental/workspace" const root = "/experimental/workspace"
@@ -40,6 +40,7 @@ export const WorkspaceApi = HttpApi.make("workspace")
HttpApiGroup.make("workspace") HttpApiGroup.make("workspace")
.add( .add(
HttpApiEndpoint.get("adapters", WorkspacePaths.adapters, { HttpApiEndpoint.get("adapters", WorkspacePaths.adapters, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(WorkspaceAdapterEntry), "Workspace adapters"), success: described(Schema.Array(WorkspaceAdapterEntry), "Workspace adapters"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -49,6 +50,7 @@ export const WorkspaceApi = HttpApi.make("workspace")
}), }),
), ),
HttpApiEndpoint.get("list", WorkspacePaths.list, { HttpApiEndpoint.get("list", WorkspacePaths.list, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(Workspace.Info), "Workspaces"), success: described(Schema.Array(Workspace.Info), "Workspaces"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -58,6 +60,7 @@ export const WorkspaceApi = HttpApi.make("workspace")
}), }),
), ),
HttpApiEndpoint.post("create", WorkspacePaths.list, { HttpApiEndpoint.post("create", WorkspacePaths.list, {
query: WorkspaceRoutingQuery,
payload: CreatePayload, payload: CreatePayload,
success: described(Workspace.Info, "Workspace created"), success: described(Workspace.Info, "Workspace created"),
error: HttpApiError.BadRequest, error: HttpApiError.BadRequest,
@@ -69,6 +72,7 @@ export const WorkspaceApi = HttpApi.make("workspace")
}), }),
), ),
HttpApiEndpoint.post("syncList", WorkspacePaths.syncList, { HttpApiEndpoint.post("syncList", WorkspacePaths.syncList, {
query: WorkspaceRoutingQuery,
success: described(HttpApiSchema.NoContent, "Workspace list synced"), success: described(HttpApiSchema.NoContent, "Workspace list synced"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -78,6 +82,7 @@ export const WorkspaceApi = HttpApi.make("workspace")
}), }),
), ),
HttpApiEndpoint.get("status", WorkspacePaths.status, { HttpApiEndpoint.get("status", WorkspacePaths.status, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(Workspace.ConnectionStatus), "Workspace status"), success: described(Schema.Array(Workspace.ConnectionStatus), "Workspace status"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
@@ -88,6 +93,7 @@ export const WorkspaceApi = HttpApi.make("workspace")
), ),
HttpApiEndpoint.delete("remove", WorkspacePaths.remove, { HttpApiEndpoint.delete("remove", WorkspacePaths.remove, {
params: { id: Workspace.Info.fields.id }, params: { id: Workspace.Info.fields.id },
query: WorkspaceRoutingQuery,
success: described(Schema.UndefinedOr(Workspace.Info), "Workspace removed"), success: described(Schema.UndefinedOr(Workspace.Info), "Workspace removed"),
error: HttpApiError.BadRequest, error: HttpApiError.BadRequest,
}).annotateMerge( }).annotateMerge(
@@ -98,6 +104,7 @@ export const WorkspaceApi = HttpApi.make("workspace")
}), }),
), ),
HttpApiEndpoint.post("warp", WorkspacePaths.warp, { HttpApiEndpoint.post("warp", WorkspacePaths.warp, {
query: WorkspaceRoutingQuery,
payload: WarpPayload, payload: WarpPayload,
success: described(HttpApiSchema.NoContent, "Session warped"), success: described(HttpApiSchema.NoContent, "Session warped"),
error: [ApiWorkspaceWarpError, ApiVcsApplyError], error: [ApiWorkspaceWarpError, ApiVcsApplyError],
@@ -6,7 +6,7 @@ import { MCP } from "@/mcp"
import { Project } from "@/project/project" import { Project } from "@/project/project"
import { Session } from "@/session/session" import { Session } from "@/session/session"
import { ToolRegistry } from "@/tool/registry" import { ToolRegistry } from "@/tool/registry"
import * as EffectZod from "@/util/effect-zod" import * as EffectZod from "@opencode-ai/core/effect-zod"
import { Worktree } from "@/worktree" import { Worktree } from "@/worktree"
import { Effect, Option } from "effect" import { Effect, Option } from "effect"
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse" import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"
@@ -9,7 +9,7 @@ import { Skill } from "@/skill"
import { Effect } from "effect" import { Effect } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi" import { HttpApiBuilder } from "effect/unstable/httpapi"
import { InstanceHttpApi } from "../api" import { InstanceHttpApi } from "../api"
import { ApiVcsApplyError, SkillQuery } from "../groups/instance" import { ApiVcsApplyError } from "../groups/instance"
import { markInstanceForDisposal } from "../lifecycle" import { markInstanceForDisposal } from "../lifecycle"
export const instanceHandlers = HttpApiBuilder.group(InstanceHttpApi, "instance", (handlers) => export const instanceHandlers = HttpApiBuilder.group(InstanceHttpApi, "instance", (handlers) =>
@@ -77,15 +77,8 @@ export const instanceHandlers = HttpApiBuilder.group(InstanceHttpApi, "instance"
return yield* agent.list() return yield* agent.list()
}) })
const getSkill = Effect.fn("InstanceHttpApi.skill")(function* (ctx: { query: typeof SkillQuery.Type }) { const getSkill = Effect.fn("InstanceHttpApi.skill")(function* () {
const skills = yield* skill.all() return yield* skill.all()
if (ctx.query.include === "invalid") {
return {
skills,
invalid: yield* skill.invalid(),
}
}
return skills
}) })
const getLsp = Effect.fn("InstanceHttpApi.lsp")(function* () { const getLsp = Effect.fn("InstanceHttpApi.lsp")(function* () {
@@ -34,6 +34,7 @@ export const messageHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.message
return handlers.handle( return handlers.handle(
"messages", "messages",
Effect.fn(function* (ctx) { Effect.fn(function* (ctx) {
if (ctx.query.cursor && ctx.query.order !== undefined) return yield* new HttpApiError.BadRequest({})
const decoded = yield* Effect.try({ const decoded = yield* Effect.try({
try: () => (ctx.query.cursor ? cursor.decode(ctx.query.cursor) : undefined), try: () => (ctx.query.cursor ? cursor.decode(ctx.query.cursor) : undefined),
catch: () => new HttpApiError.BadRequest({}), catch: () => new HttpApiError.BadRequest({}),
@@ -22,6 +22,31 @@ type SessionCursor = typeof SessionCursor.Type
const decodeCursor = Schema.decodeUnknownSync(SessionCursor) const decodeCursor = Schema.decodeUnknownSync(SessionCursor)
function hasCursorFilter(query: {
readonly order?: unknown
readonly path?: unknown
readonly roots?: unknown
readonly start?: unknown
readonly search?: unknown
}) {
return (
query.order !== undefined ||
query.path !== undefined ||
query.roots !== undefined ||
query.start !== undefined ||
query.search !== undefined
)
}
function hasCursorRoutingMismatch(
query: { readonly directory?: string; readonly workspace?: string },
decoded: SessionCursor | undefined,
) {
if (!decoded) return false
if (query.directory !== undefined && query.directory !== decoded.directory) return true
return query.workspace !== undefined && query.workspace !== decoded.workspaceID
}
const sessionCursor = { const sessionCursor = {
encode( encode(
session: SessionV2.Info, session: SessionV2.Info,
@@ -46,10 +71,12 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session
.handle( .handle(
"sessions", "sessions",
Effect.fn(function* (ctx) { Effect.fn(function* (ctx) {
if (ctx.query.cursor && hasCursorFilter(ctx.query)) return yield* new HttpApiError.BadRequest({})
const decoded = yield* Effect.try({ const decoded = yield* Effect.try({
try: () => (ctx.query.cursor ? sessionCursor.decode(ctx.query.cursor) : undefined), try: () => (ctx.query.cursor ? sessionCursor.decode(ctx.query.cursor) : undefined),
catch: () => new HttpApiError.BadRequest({}), catch: () => new HttpApiError.BadRequest({}),
}) })
if (hasCursorRoutingMismatch(ctx.query, decoded)) return yield* new HttpApiError.BadRequest({})
const order = decoded?.order ?? ctx.query.order ?? "desc" const order = decoded?.order ?? ctx.query.order ?? "desc"
const filters = decoded ?? { const filters = decoded ?? {
directory: ctx.query.directory, directory: ctx.query.directory,
@@ -9,11 +9,23 @@ import * as Fence from "@/server/shared/fence"
import { getWorkspaceRouteSessionID, isLocalWorkspaceRoute, workspaceProxyURL } from "@/server/shared/workspace-routing" import { getWorkspaceRouteSessionID, isLocalWorkspaceRoute, workspaceProxyURL } from "@/server/shared/workspace-routing"
import { NotFoundError } from "@/storage/storage" import { NotFoundError } from "@/storage/storage"
import { Flag } from "@opencode-ai/core/flag/flag" import { Flag } from "@opencode-ai/core/flag/flag"
import { Context, Data, Effect, Layer } from "effect" import { Context, Data, Effect, Layer, Schema } from "effect"
import { HttpClient, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http" import { HttpClient, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
import { HttpApiMiddleware } from "effect/unstable/httpapi" import { HttpApiMiddleware } from "effect/unstable/httpapi"
import * as Socket from "effect/unstable/socket/Socket" import * as Socket from "effect/unstable/socket/Socket"
// Query fields this middleware reads from the URL. Spread into every
// endpoint query schema in groups that apply WorkspaceRoutingMiddleware,
// otherwise HttpApi rejects requests carrying these params with 400.
// HttpApiMiddleware in effect-smol cannot declare query params today —
// remove this once upstream supports middleware-declared query schemas.
export const WorkspaceRoutingQueryFields = {
directory: Schema.optional(Schema.String),
workspace: Schema.optional(Schema.String),
}
export const WorkspaceRoutingQuery = Schema.Struct(WorkspaceRoutingQueryFields)
type RemoteTarget = Extract<Target, { type: "remote" }> type RemoteTarget = Extract<Target, { type: "remote" }>
type RequestPlan = Data.TaggedEnum<{ type RequestPlan = Data.TaggedEnum<{
@@ -51,47 +51,29 @@ type OpenApiResponse = {
content?: Record<string, { schema?: OpenApiSchema }> content?: Record<string, { schema?: OpenApiSchema }>
} }
// Instance routes use middleware for directory/workspace resolution, but HttpApi
// doesn't surface middleware query params in the spec. Inject them explicitly.
const InstanceQueryParameters = [
{
name: "directory",
in: "query",
required: false,
schema: { type: "string" },
},
{
name: "workspace",
in: "query",
required: false,
schema: { type: "string" },
},
] satisfies OpenApiParameter[]
// Query schemas describe decoded Effect values, but the generated SDK needs the // Query schemas describe decoded Effect values, but the generated SDK needs the
// public call shape. These keep SDK callers passing numbers/booleans while the // public call shape. These keep SDK callers passing numbers/booleans while the
// server still decodes string query params at runtime. // server still decodes string query params at runtime.
const QueryNumberParameters = new Set(["start", "cursor", "limit", "method"])
const QueryBooleanParameters = new Set(["roots", "archived"]) const QueryBooleanParameters = new Set(["roots", "archived"])
const QueryParameterSchemas = { const QueryParameterSchemas: Record<string, OpenApiSchema> = {
"GET /experimental/session start": { type: "number" },
"GET /find/file limit": { type: "integer", minimum: 1, maximum: 200 }, "GET /find/file limit": { type: "integer", minimum: 1, maximum: 200 },
"GET /experimental/session cursor": { type: "number" },
"GET /experimental/session limit": { type: "number" },
"GET /session start": { type: "number" },
"GET /session limit": { type: "number" },
"GET /session/{sessionID}/diff messageID": { type: "string", pattern: "^msg.*" }, "GET /session/{sessionID}/diff messageID": { type: "string", pattern: "^msg.*" },
"GET /session/{sessionID}/message limit": { type: "integer", minimum: 0, maximum: Number.MAX_SAFE_INTEGER }, "GET /session/{sessionID}/message limit": { type: "integer", minimum: 0, maximum: Number.MAX_SAFE_INTEGER },
} satisfies Record<string, OpenApiSchema> "GET /api/session limit": { type: "number" },
"GET /api/session start": { type: "number" },
"GET /api/session/{sessionID}/message limit": { type: "number" },
}
const PathParameterSchemas = { const LegacyComponentDescriptions: Record<string, string> = {
sessionID: { type: "string", pattern: "^ses.*" },
messageID: { type: "string", pattern: "^msg.*" },
partID: { type: "string", pattern: "^prt.*" },
permissionID: { type: "string", pattern: "^per.*" },
ptyID: { type: "string", pattern: "^pty.*" },
} satisfies Record<string, OpenApiSchema>
const LegacyComponentDescriptions = {
LogLevel: "Log level", LogLevel: "Log level",
ServerConfig: "Server configuration for opencode serve and web commands", ServerConfig: "Server configuration for opencode serve and web commands",
LayoutConfig: "@deprecated Always uses stretch layout.", LayoutConfig: "@deprecated Always uses stretch layout.",
} satisfies Record<string, string> }
function matchLegacyOpenApi(input: Record<string, unknown>) { function matchLegacyOpenApi(input: Record<string, unknown>) {
const spec = input as OpenApiSpec const spec = input as OpenApiSpec
@@ -122,7 +104,6 @@ function matchLegacyOpenApi(input: Record<string, unknown>) {
delete spec.components?.securitySchemes delete spec.components?.securitySchemes
for (const [path, item] of Object.entries(spec.paths ?? {})) { for (const [path, item] of Object.entries(spec.paths ?? {})) {
const isInstanceRoute = !path.startsWith("/global/") && !path.startsWith("/auth/")
for (const method of ["get", "post", "put", "delete", "patch"] as const) { for (const method of ["get", "post", "put", "delete", "patch"] as const) {
const operation = item[method] const operation = item[method]
if (!operation) continue if (!operation) continue
@@ -183,14 +164,8 @@ function matchLegacyOpenApi(input: Record<string, unknown>) {
}, },
} }
} }
if (!isInstanceRoute) continue const route = `${method.toUpperCase()} ${path}`
operation.parameters = [ for (const param of operation.parameters ?? []) normalizeParameter(param, route)
...InstanceQueryParameters,
...(operation.parameters ?? []).filter(
(param) => param.in !== "query" || (param.name !== "directory" && param.name !== "workspace"),
),
]
for (const param of operation.parameters) normalizeParameter(param, `${method.toUpperCase()} ${path}`)
} }
} }
return input return input
@@ -292,7 +267,7 @@ function applyLegacySchemaOverrides(spec: OpenApiSpec) {
function normalizeComponentDescriptions(spec: OpenApiSpec) { function normalizeComponentDescriptions(spec: OpenApiSpec) {
for (const [name, schema] of Object.entries(spec.components?.schemas ?? {})) { for (const [name, schema] of Object.entries(spec.components?.schemas ?? {})) {
const description = LegacyComponentDescriptions[name as keyof typeof LegacyComponentDescriptions] const description = LegacyComponentDescriptions[name]
if (description) { if (description) {
schema.description = description schema.description = description
continue continue
@@ -438,7 +413,7 @@ function fixSelfReferencingComponents(spec: OpenApiSpec) {
} }
} }
// Simplest fix: generate the raw spec (without transform) to get correct schemas // Simplest fix: generate the raw spec (without transform) to get correct schemas
const raw = OpenApi.fromApi(OpenCodeHttpApi) as unknown as OpenApiSpec const raw: OpenApiSpec = OpenApi.fromApi(OpenCodeHttpApi)
const rawSchemas = raw.components?.schemas const rawSchemas = raw.components?.schemas
if (!rawSchemas) return if (!rawSchemas) return
for (const name of selfRefs) { for (const name of selfRefs) {
@@ -503,19 +478,15 @@ function flattenOptions(options: OpenApiSchema[] | undefined): OpenApiSchema[] |
function normalizeParameter(param: OpenApiParameter, route: string) { function normalizeParameter(param: OpenApiParameter, route: string) {
if (!param.schema || typeof param.schema !== "object") return if (!param.schema || typeof param.schema !== "object") return
if (param.in === "path") { if (param.in === "path") {
param.schema = pathParameterSchema(route, param.name) ?? stripOptionalNull(param.schema) param.schema = stripOptionalNull(param.schema)
return return
} }
if (param.in === "query") { if (param.in === "query") {
const override = QueryParameterSchemas[`${route} ${param.name}` as keyof typeof QueryParameterSchemas] const override = QueryParameterSchemas[`${route} ${param.name}`]
if (override) { if (override) {
param.schema = override param.schema = override
return return
} }
if (QueryNumberParameters.has(param.name)) {
param.schema = { type: "number" }
return
}
if (QueryBooleanParameters.has(param.name)) { if (QueryBooleanParameters.has(param.name)) {
param.schema = { param.schema = {
anyOf: [{ type: "boolean" }, { type: "string", enum: ["true", "false"] }], anyOf: [{ type: "boolean" }, { type: "string", enum: ["true", "false"] }],
@@ -526,15 +497,6 @@ function normalizeParameter(param: OpenApiParameter, route: string) {
param.schema = stripOptionalNull(param.schema) param.schema = stripOptionalNull(param.schema)
} }
function pathParameterSchema(route: string, name: string) {
if (name in PathParameterSchemas) return PathParameterSchemas[name as keyof typeof PathParameterSchemas]
if (name === "id" && route.startsWith("DELETE /experimental/workspace/")) return { type: "string", pattern: "^wrk.*" }
if (name === "id" && route.startsWith("POST /experimental/workspace/")) return { type: "string", pattern: "^wrk.*" }
if (name === "requestID" && route.startsWith("POST /permission/")) return { type: "string", pattern: "^per.*" }
if (name === "requestID" && route.startsWith("POST /question/")) return { type: "string", pattern: "^que.*" }
return undefined
}
export const PublicApi = OpenCodeHttpApi.annotateMerge( export const PublicApi = OpenCodeHttpApi.annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
title: "opencode", title: "opencode",
+16 -16
View File
@@ -23,8 +23,8 @@ import type { SystemError } from "bun"
import type { Provider } from "@/provider/provider" import type { Provider } from "@/provider/provider"
import { ModelID, ProviderID } from "@/provider/schema" import { ModelID, ProviderID } from "@/provider/schema"
import { Effect, Schema, Types } from "effect" import { Effect, Schema, Types } from "effect"
import { zod, ZodOverride } from "@/util/effect-zod" import { zod, ZodOverride } from "@opencode-ai/core/effect-zod"
import { NonNegativeInt, withStatics } from "@/util/schema" import { NonNegativeInt, withStatics } from "@opencode-ai/core/schema"
import { namedSchemaError } from "@/util/named-schema-error" import { namedSchemaError } from "@/util/named-schema-error"
import * as EffectLogger from "@opencode-ai/core/effect/logger" import * as EffectLogger from "@opencode-ai/core/effect/logger"
@@ -143,8 +143,8 @@ export type ReasoningPart = Types.DeepMutable<Schema.Schema.Type<typeof Reasonin
const filePartSourceBase = { const filePartSourceBase = {
text: Schema.Struct({ text: Schema.Struct({
value: Schema.String, value: Schema.String,
start: NonNegativeInt, start: Schema.Finite,
end: NonNegativeInt, end: Schema.Finite,
}).annotate({ identifier: "FilePartSourceText" }), }).annotate({ identifier: "FilePartSourceText" }),
} }
@@ -270,13 +270,13 @@ export const StepFinishPart = Schema.Struct({
snapshot: Schema.optional(Schema.String), snapshot: Schema.optional(Schema.String),
cost: Schema.Finite, cost: Schema.Finite,
tokens: Schema.Struct({ tokens: Schema.Struct({
total: Schema.optional(NonNegativeInt), total: Schema.optional(Schema.Finite),
input: NonNegativeInt, input: Schema.Finite,
output: NonNegativeInt, output: Schema.Finite,
reasoning: NonNegativeInt, reasoning: Schema.Finite,
cache: Schema.Struct({ cache: Schema.Struct({
read: NonNegativeInt, read: Schema.Finite,
write: NonNegativeInt, write: Schema.Finite,
}), }),
}), }),
}) })
@@ -554,13 +554,13 @@ export const Assistant = Schema.Struct({
summary: Schema.optional(Schema.Boolean), summary: Schema.optional(Schema.Boolean),
cost: Schema.Finite, cost: Schema.Finite,
tokens: Schema.Struct({ tokens: Schema.Struct({
total: Schema.optional(NonNegativeInt), total: Schema.optional(Schema.Finite),
input: NonNegativeInt, input: Schema.Finite,
output: NonNegativeInt, output: Schema.Finite,
reasoning: NonNegativeInt, reasoning: Schema.Finite,
cache: Schema.Struct({ cache: Schema.Struct({
read: NonNegativeInt, read: Schema.Finite,
write: NonNegativeInt, write: Schema.Finite,
}), }),
}), }),
structured: Schema.optional(Schema.Any), structured: Schema.optional(Schema.Any),
+7 -7
View File
@@ -1,8 +1,8 @@
import { Schema } from "effect" import { Schema } from "effect"
import { SessionID } from "./schema" import { SessionID } from "./schema"
import { ModelID, ProviderID } from "../provider/schema" import { ModelID, ProviderID } from "../provider/schema"
import { zod } from "@/util/effect-zod" import { zod } from "@opencode-ai/core/effect-zod"
import { NonNegativeInt, withStatics } from "@/util/schema" import { NonNegativeInt, withStatics } from "@opencode-ai/core/schema"
import { namedSchemaError } from "@/util/named-schema-error" import { namedSchemaError } from "@/util/named-schema-error"
export const OutputLengthError = namedSchemaError("MessageOutputLengthError", {}) export const OutputLengthError = namedSchemaError("MessageOutputLengthError", {})
@@ -172,12 +172,12 @@ export const Info = Schema.Struct({
cost: Schema.Finite, cost: Schema.Finite,
summary: Schema.optional(Schema.Boolean), summary: Schema.optional(Schema.Boolean),
tokens: Schema.Struct({ tokens: Schema.Struct({
input: NonNegativeInt, input: Schema.Finite,
output: NonNegativeInt, output: Schema.Finite,
reasoning: NonNegativeInt, reasoning: Schema.Finite,
cache: Schema.Struct({ cache: Schema.Struct({
read: NonNegativeInt, read: Schema.Finite,
write: NonNegativeInt, write: Schema.Finite,
}), }),
}), }),
}), }),

Some files were not shown because too many files have changed in this diff Show More