Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8308fbc5a6 | |||
| 075cbb9d94 | |||
| fb4bab8a66 | |||
| b3526f6ce9 | |||
| f220f02a2f | |||
| 235a86fb60 | |||
| 67b9c9c027 | |||
| 2f11c9f7ed | |||
| e1c1193f3e | |||
| 29250a0efb | |||
| c6e6bdf59f | |||
| d80e1199ca | |||
| 10ea59066f | |||
| 79d6b10d7c | |||
| 6e78f36a0f | |||
| 16866e1180 | |||
| 6d130e5deb | |||
| e30d8173c1 | |||
| 7a79f3a5ea | |||
| b8ca71d309 | |||
| 6849f96825 | |||
| 00c3248295 | |||
| 818b56dbd0 | |||
| 29b5b24787 | |||
| 11363170ca | |||
| ba9e4b67ed | |||
| bd1029b19f | |||
| 43b51f09d0 | |||
| c61ab51886 | |||
| d373c562f2 | |||
| 5fa5d876fc | |||
| 805af011c9 | |||
| 480aa8b23b | |||
| d62442bb5d | |||
| 8602937a37 | |||
| 77da433e0a | |||
| ad79f3e0cf | |||
| 6c2dfd2f52 | |||
| 9a8b54fe62 | |||
| dcdbdb218f | |||
| 5e49029e70 | |||
| 19abadaf27 | |||
| 6fea0178eb | |||
| ebe9dcf27e | |||
| 57efec4429 | |||
| e22144806f |
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.14.42",
|
||||
"version": "1.14.46",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -85,7 +85,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.14.42",
|
||||
"version": "1.14.46",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -120,7 +120,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.14.42",
|
||||
"version": "1.14.46",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -147,7 +147,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.14.42",
|
||||
"version": "1.14.46",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "3.0.64",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
@@ -171,7 +171,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.14.42",
|
||||
"version": "1.14.46",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -195,7 +195,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "1.14.42",
|
||||
"version": "1.14.46",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -229,7 +229,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.14.42",
|
||||
"version": "1.14.46",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -283,7 +283,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.14.42",
|
||||
"version": "1.14.46",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -313,7 +313,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.14.42",
|
||||
"version": "1.14.46",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -329,7 +329,7 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"version": "1.14.42",
|
||||
"version": "1.14.46",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -342,7 +342,7 @@
|
||||
},
|
||||
"packages/llm": {
|
||||
"name": "@opencode-ai/llm",
|
||||
"version": "1.14.42",
|
||||
"version": "1.14.46",
|
||||
"dependencies": {
|
||||
"@smithy/eventstream-codec": "4.2.14",
|
||||
"@smithy/util-utf8": "4.2.2",
|
||||
@@ -360,7 +360,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.14.42",
|
||||
"version": "1.14.46",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -495,7 +495,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.14.42",
|
||||
"version": "1.14.46",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
@@ -533,7 +533,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.14.42",
|
||||
"version": "1.14.46",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -548,7 +548,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.14.42",
|
||||
"version": "1.14.46",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -583,7 +583,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.14.42",
|
||||
"version": "1.14.46",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -632,7 +632,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.14.42",
|
||||
"version": "1.14.46",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.14.42",
|
||||
"version": "1.14.46",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { type AgentPartInput, type FilePartInput, type Part, type TextPartInput
|
||||
import type { FileSelection } from "@/context/file"
|
||||
import { encodeFilePath } from "@/context/file/path"
|
||||
import type { AgentPart, FileAttachmentPart, ImageAttachmentPart, Prompt } from "@/context/prompt"
|
||||
import { Identifier } from "@/utils/id"
|
||||
import { PartID } from "@/utils/id"
|
||||
import { createCommentMetadata, formatCommentNote } from "@/utils/comment-note"
|
||||
|
||||
type PromptRequestPart = (TextPartInput | FilePartInput | AgentPartInput) & { id: string }
|
||||
@@ -91,7 +91,7 @@ const toOptimisticPart = (part: PromptRequestPart, sessionID: string, messageID:
|
||||
export function buildRequestParts(input: BuildRequestPartsInput) {
|
||||
const requestParts: PromptRequestPart[] = [
|
||||
{
|
||||
id: Identifier.ascending("part"),
|
||||
id: PartID.ascending(),
|
||||
type: "text",
|
||||
text: input.text,
|
||||
},
|
||||
@@ -100,7 +100,7 @@ export function buildRequestParts(input: BuildRequestPartsInput) {
|
||||
const files = input.prompt.filter(isFileAttachment).map((attachment) => {
|
||||
const path = absolute(input.sessionDirectory, attachment.path)
|
||||
return {
|
||||
id: Identifier.ascending("part"),
|
||||
id: PartID.ascending(),
|
||||
type: "file",
|
||||
mime: "text/plain",
|
||||
url: `file://${encodeFilePath(path)}${fileQuery(attachment.selection)}`,
|
||||
@@ -119,7 +119,7 @@ export function buildRequestParts(input: BuildRequestPartsInput) {
|
||||
|
||||
const agents = input.prompt.filter(isAgentAttachment).map((attachment) => {
|
||||
return {
|
||||
id: Identifier.ascending("part"),
|
||||
id: PartID.ascending(),
|
||||
type: "agent",
|
||||
name: attachment.name,
|
||||
source: {
|
||||
@@ -139,7 +139,7 @@ export function buildRequestParts(input: BuildRequestPartsInput) {
|
||||
used.add(url)
|
||||
|
||||
const filePart = {
|
||||
id: Identifier.ascending("part"),
|
||||
id: PartID.ascending(),
|
||||
type: "file",
|
||||
mime: "text/plain",
|
||||
url,
|
||||
@@ -154,7 +154,7 @@ export function buildRequestParts(input: BuildRequestPartsInput) {
|
||||
used.add(url)
|
||||
return [
|
||||
{
|
||||
id: Identifier.ascending("part"),
|
||||
id: PartID.ascending(),
|
||||
type: "file",
|
||||
mime: "text/plain",
|
||||
url,
|
||||
@@ -165,7 +165,7 @@ export function buildRequestParts(input: BuildRequestPartsInput) {
|
||||
|
||||
return [
|
||||
{
|
||||
id: Identifier.ascending("part"),
|
||||
id: PartID.ascending(),
|
||||
type: "text",
|
||||
text: formatCommentNote({ path: item.path, selection: item.selection, comment }),
|
||||
synthetic: true,
|
||||
@@ -184,7 +184,7 @@ export function buildRequestParts(input: BuildRequestPartsInput) {
|
||||
|
||||
const images = input.images.map((attachment) => {
|
||||
return {
|
||||
id: Identifier.ascending("part"),
|
||||
id: PartID.ascending(),
|
||||
type: "file",
|
||||
mime: attachment.mime,
|
||||
url: attachment.dataUrl,
|
||||
|
||||
@@ -13,7 +13,7 @@ import { usePermission } from "@/context/permission"
|
||||
import { type ContextItem, type ImageAttachmentPart, type Prompt, usePrompt } from "@/context/prompt"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { Identifier } from "@/utils/id"
|
||||
import { MessageID, PartID } from "@/utils/id"
|
||||
import { Worktree as WorktreeState } from "@/utils/worktree"
|
||||
import { buildRequestParts } from "./build-request-parts"
|
||||
import { setCursorPosition } from "./editor-dom"
|
||||
@@ -89,7 +89,7 @@ export async function sendFollowupDraft(input: FollowupSendInput) {
|
||||
model: `${input.draft.model.providerID}/${input.draft.model.modelID}`,
|
||||
variant: input.draft.variant,
|
||||
parts: images.map((attachment) => ({
|
||||
id: Identifier.ascending("part"),
|
||||
id: PartID.ascending(),
|
||||
type: "file" as const,
|
||||
mime: attachment.mime,
|
||||
url: attachment.dataUrl,
|
||||
@@ -103,7 +103,7 @@ export async function sendFollowupDraft(input: FollowupSendInput) {
|
||||
}
|
||||
}
|
||||
|
||||
const messageID = input.messageID ?? Identifier.ascending("message")
|
||||
const messageID = input.messageID ?? MessageID.ascending()
|
||||
const { requestParts, optimisticParts } = buildRequestParts({
|
||||
prompt: input.draft.prompt,
|
||||
context: input.draft.context,
|
||||
@@ -467,7 +467,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
model: `${model.providerID}/${model.modelID}`,
|
||||
variant,
|
||||
parts: images.map((attachment) => ({
|
||||
id: Identifier.ascending("part"),
|
||||
id: PartID.ascending(),
|
||||
type: "file" as const,
|
||||
mime: attachment.mime,
|
||||
url: attachment.dataUrl,
|
||||
@@ -486,7 +486,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
}
|
||||
|
||||
const commentItems = context.filter((item) => item.type === "file" && !!item.comment?.trim())
|
||||
const messageID = Identifier.ascending("message")
|
||||
const messageID = MessageID.ascending()
|
||||
|
||||
const removeOptimisticMessage = () => {
|
||||
sync.session.optimistic.remove({
|
||||
|
||||
@@ -58,7 +58,7 @@ import { SessionSidePanel } from "@/pages/session/session-side-panel"
|
||||
import { TerminalPanel } from "@/pages/session/terminal-panel"
|
||||
import { useSessionCommands } from "@/pages/session/use-session-commands"
|
||||
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
|
||||
import { Identifier } from "@/utils/id"
|
||||
import { MessageID } from "@/utils/id"
|
||||
import { diffs as list } from "@/utils/diffs"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { extractPromptFromParts } from "@/utils/prompt"
|
||||
@@ -1575,10 +1575,7 @@ export default function Page() {
|
||||
}
|
||||
|
||||
const queueFollowup = (draft: FollowupDraft) => {
|
||||
setFollowup("items", draft.sessionID, (items) => [
|
||||
...(items ?? []),
|
||||
{ id: Identifier.ascending("message"), ...draft },
|
||||
])
|
||||
setFollowup("items", draft.sessionID, (items) => [...(items ?? []), { id: MessageID.ascending(), ...draft }])
|
||||
setFollowup("failed", draft.sessionID, undefined)
|
||||
setFollowup("paused", draft.sessionID, undefined)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,12 @@ import { createOpenSessionFileTab, createSessionTabs, getTabReorderIndex, type S
|
||||
import { setSessionHandoff } from "@/pages/session/handoff"
|
||||
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: {
|
||||
canReview: () => boolean
|
||||
diffs: () => (SnapshotFileDiff | VcsFileDiff)[]
|
||||
@@ -70,7 +76,8 @@ export function SessionSidePanel(props: {
|
||||
})
|
||||
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 merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => {
|
||||
if (!a) return b
|
||||
@@ -81,7 +88,7 @@ export function SessionSidePanel(props: {
|
||||
const normalize = (p: string) => p.replaceAll("\\\\", "/").replace(/\/+$/, "")
|
||||
|
||||
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 kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix"
|
||||
|
||||
|
||||
@@ -1,31 +1,15 @@
|
||||
import z from "zod"
|
||||
|
||||
const prefixes = {
|
||||
session: "ses",
|
||||
message: "msg",
|
||||
permission: "per",
|
||||
user: "usr",
|
||||
part: "prt",
|
||||
pty: "pty",
|
||||
} as const
|
||||
|
||||
const LENGTH = 26
|
||||
let lastTimestamp = 0
|
||||
let counter = 0
|
||||
|
||||
type Prefix = keyof typeof prefixes
|
||||
export namespace Identifier {
|
||||
export function schema(prefix: Prefix) {
|
||||
return z.string().startsWith(prefixes[prefix])
|
||||
}
|
||||
type Prefix = "msg" | "prt"
|
||||
|
||||
export function ascending(prefix: Prefix, given?: string) {
|
||||
return generateID(prefix, false, given)
|
||||
}
|
||||
export const MessageID = {
|
||||
ascending: (given?: string) => generateID("msg", false, given),
|
||||
}
|
||||
|
||||
export function descending(prefix: Prefix, given?: string) {
|
||||
return generateID(prefix, true, given)
|
||||
}
|
||||
export const PartID = {
|
||||
ascending: (given?: string) => generateID("prt", false, given),
|
||||
}
|
||||
|
||||
function generateID(prefix: Prefix, descending: boolean, given?: string): string {
|
||||
@@ -33,8 +17,8 @@ function generateID(prefix: Prefix, descending: boolean, given?: string): string
|
||||
return create(prefix, descending)
|
||||
}
|
||||
|
||||
if (!given.startsWith(prefixes[prefix])) {
|
||||
throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`)
|
||||
if (!given.startsWith(prefix)) {
|
||||
throw new Error(`ID ${given} does not start with ${prefix}`)
|
||||
}
|
||||
|
||||
return given
|
||||
@@ -61,7 +45,7 @@ function create(prefix: Prefix, descending: boolean, timestamp?: number): string
|
||||
timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff))
|
||||
}
|
||||
|
||||
return prefixes[prefix] + "_" + bytesToHex(timeBytes) + randomBase62(LENGTH - 12)
|
||||
return prefix + "_" + bytesToHex(timeBytes) + randomBase62(LENGTH - 12)
|
||||
}
|
||||
|
||||
function bytesToHex(bytes: Uint8Array): string {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.14.42",
|
||||
"version": "1.14.46",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.14.42",
|
||||
"version": "1.14.46",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.14.42",
|
||||
"version": "1.14.46",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.14.42",
|
||||
"version": "1.14.46",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.14.42",
|
||||
"version": "1.14.46",
|
||||
"name": "@opencode-ai/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Config } from "effect"
|
||||
import { InstallationChannel } from "../installation/version"
|
||||
|
||||
function truthy(key: string) {
|
||||
const value = process.env[key]?.toLowerCase()
|
||||
@@ -10,6 +11,13 @@ function falsy(key: string) {
|
||||
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) {
|
||||
const value = process.env[key]
|
||||
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_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_SERVER_PASSWORD: process.env["OPENCODE_SERVER_PASSWORD"],
|
||||
OPENCODE_SERVER_USERNAME: process.env["OPENCODE_SERVER_USERNAME"],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.14.42",
|
||||
"version": "1.14.46",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.14.42",
|
||||
"version": "1.14.46",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The open source coding agent."
|
||||
version = "1.14.42"
|
||||
version = "1.14.46"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/anomalyco/opencode"
|
||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.42/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.46/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.42/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.46/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[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"
|
||||
args = ["acp"]
|
||||
|
||||
[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"
|
||||
args = ["acp"]
|
||||
|
||||
[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"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.14.42",
|
||||
"version": "1.14.46",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.14.42",
|
||||
"version": "1.14.46",
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.14.42",
|
||||
"version": "1.14.46",
|
||||
"name": "@opencode-ai/llm",
|
||||
"type": "module",
|
||||
"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,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.14.42",
|
||||
"version": "1.14.46",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -59,10 +59,10 @@ Rules:
|
||||
|
||||
## 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
|
||||
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
|
||||
```
|
||||
|
||||
@@ -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,
|
||||
outputs, and typed errors. Keep Zod available at existing HTTP, tool, and
|
||||
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
|
||||
moves to `@effect/platform`, every Schema-first DTO can flow through
|
||||
@@ -97,7 +97,7 @@ creating a parallel schema source of truth.
|
||||
|
||||
## 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
|
||||
only as long as its upstream or local dependency requires it — inline
|
||||
comments document when each can be deleted.
|
||||
@@ -389,7 +389,7 @@ piecewise.
|
||||
|
||||
## 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
|
||||
Effect definitions for the same domain type.
|
||||
- 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
|
||||
@@ -24,8 +24,8 @@ import { Effect, Context, Layer, Schema } from "effect"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import * as Option from "effect/Option"
|
||||
import * as OtelTracer from "@effect/opentelemetry/Tracer"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { withStatics, type DeepMutable } from "@/util/schema"
|
||||
import { zod } from "@opencode-ai/core/effect-zod"
|
||||
import { withStatics, type DeepMutable } from "@opencode-ai/core/schema"
|
||||
|
||||
type ReferenceEntry = NonNullable<Config.Info["reference"]>[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 }]),
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import path from "path"
|
||||
import { Effect, Layer, Record, Result, Schema, Context } from "effect"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { NonNegativeInt } from "@/util/schema"
|
||||
import { zod } from "@opencode-ai/core/effect-zod"
|
||||
import { NonNegativeInt } from "@opencode-ai/core/schema"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
|
||||
|
||||
@@ -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) => ({
|
||||
...item,
|
||||
file: redact(`${kind}-file`, String(i), item.file),
|
||||
patch: redact(`${kind}-patch`, String(i), item.patch),
|
||||
file: item.file === undefined ? undefined : redact(`${kind}-file`, String(i), item.file),
|
||||
patch: item.patch === undefined ? undefined : redact(`${kind}-patch`, String(i), item.patch),
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -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 path from "path"
|
||||
import { useKV } from "./kv"
|
||||
import { aggregateFailures } from "./aggregate-failures"
|
||||
|
||||
export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
name: "Sync",
|
||||
@@ -391,16 +392,23 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
.catch(() => emptyConsoleState)
|
||||
const agentsPromise = sdk.client.app.agents({ workspace }, { throwOnError: true })
|
||||
const configPromise = sdk.client.config.get({ workspace }, { throwOnError: true })
|
||||
const blockingRequests: Promise<unknown>[] = [
|
||||
providersPromise,
|
||||
providerListPromise,
|
||||
agentsPromise,
|
||||
configPromise,
|
||||
projectPromise,
|
||||
...(args.continue ? [sessionListPromise] : []),
|
||||
const blockingRequests: { name: string; promise: Promise<unknown> }[] = [
|
||||
{ name: "config.providers", promise: providersPromise },
|
||||
{ name: "provider.list", promise: providerListPromise },
|
||||
{ name: "app.agents", promise: agentsPromise },
|
||||
{ name: "config.get", promise: configPromise },
|
||||
{ name: "project.sync", promise: projectPromise },
|
||||
...(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 () => {
|
||||
const providersResponse = providersPromise.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.splice(match.index, 0, session.data!)
|
||||
draft.todo[sessionID] = todo.data ?? []
|
||||
draft.message[sessionID] = messages.data!.map((x) => x.info)
|
||||
for (const message of messages.data!) {
|
||||
const infos: (typeof draft.message)[string] = []
|
||||
for (const message of messages.data ?? []) {
|
||||
infos.push(message.info)
|
||||
draft.part[message.info.id] = message.parts
|
||||
}
|
||||
draft.message[sessionID] = infos
|
||||
draft.session_diff[sessionID] = diff.data ?? []
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { SessionID } from "@/session/schema"
|
||||
import { PositiveInt } from "@/util/schema"
|
||||
import { PositiveInt } from "@opencode-ai/core/schema"
|
||||
import { Effect, Schema } from "effect"
|
||||
|
||||
const DEFAULT_TOAST_DURATION = 5000
|
||||
|
||||
@@ -17,6 +17,7 @@ import { Slot as HostSlot } from "./slots"
|
||||
import type { useToast } from "../ui/toast"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import * as Keymap from "../keymap"
|
||||
import { createCommandShim } from "./command-shim"
|
||||
|
||||
type RouteEntry = {
|
||||
key: symbol
|
||||
@@ -147,7 +148,9 @@ function stateApi(sync: ReturnType<typeof useSync>): TuiPluginApi["state"] {
|
||||
return sync.data.session.length
|
||||
},
|
||||
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) {
|
||||
return sync.data.todo[sessionID] ?? []
|
||||
@@ -200,6 +203,8 @@ export function createTuiApi(input: Input): TuiPluginApi {
|
||||
}
|
||||
return {
|
||||
app: appApi(),
|
||||
// Keep deprecated `api.command` working for v1 plugins; remove in v2.
|
||||
command: createCommandShim(input.keymap, input.dialog, input.tuiConfig.keybinds),
|
||||
keys: {
|
||||
formatSequence(parts) {
|
||||
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 type { HostPluginApi, HostSlots } from "./slots"
|
||||
import { ConfigPlugin } from "@/config/plugin"
|
||||
import { createCommandShim } from "./command-shim"
|
||||
|
||||
ensureRuntimePluginSupport({ additional: keymapRuntimeModules })
|
||||
|
||||
@@ -576,6 +577,8 @@ function pluginApi(runtime: RuntimeState, plugin: PluginEntry, scope: PluginScop
|
||||
|
||||
return {
|
||||
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,
|
||||
keymap,
|
||||
route,
|
||||
|
||||
@@ -5,8 +5,8 @@ import type { InstanceContext } from "@/project/instance"
|
||||
import { SessionID, MessageID } from "@/session/schema"
|
||||
import { Effect, Layer, Context, Schema } from "effect"
|
||||
import z from "zod"
|
||||
import { zod, ZodOverride } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { zod, ZodOverride } from "@opencode-ai/core/effect-zod"
|
||||
import { withStatics } from "@opencode-ai/core/schema"
|
||||
import { Config } from "@/config/config"
|
||||
import { MCP } from "../mcp"
|
||||
import { Skill } from "../skill"
|
||||
|
||||
@@ -2,8 +2,8 @@ export * as ConfigAgent from "./agent"
|
||||
|
||||
import { Exit, Schema, SchemaGetter } from "effect"
|
||||
import { Bus } from "@/bus"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { PositiveInt, withStatics } from "@/util/schema"
|
||||
import { zod } from "@opencode-ai/core/effect-zod"
|
||||
import { PositiveInt, withStatics } from "@opencode-ai/core/schema"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import { Glob } from "@opencode-ai/core/util/glob"
|
||||
|
||||
@@ -5,8 +5,8 @@ import { Schema } from "effect"
|
||||
import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import { Glob } from "@opencode-ai/core/util/glob"
|
||||
import { Bus } from "@/bus"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { zod } from "@opencode-ai/core/effect-zod"
|
||||
import { withStatics } from "@opencode-ai/core/schema"
|
||||
import { configEntryNameFromPath } from "./entry-name"
|
||||
import { InvalidError } from "./error"
|
||||
import * as ConfigMarkdown from "./markdown"
|
||||
|
||||
@@ -22,8 +22,8 @@ import { InstanceState } from "@/effect/instance-state"
|
||||
import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "effect"
|
||||
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
|
||||
import { containsPath } from "../project/instance-context"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { NonNegativeInt, PositiveInt, withStatics, type DeepMutable } from "@/util/schema"
|
||||
import { zod } from "@opencode-ai/core/effect-zod"
|
||||
import { NonNegativeInt, PositiveInt, withStatics, type DeepMutable } from "@opencode-ai/core/schema"
|
||||
import { ConfigAgent } from "./agent"
|
||||
import { ConfigCommand } from "./command"
|
||||
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
|
||||
// effect-smol (the upstream version collapses `unknown` to `{}`).
|
||||
export type Info = DeepMutable<Schema.Schema.Type<typeof Info>> & {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Schema } from "effect"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { NonNegativeInt } from "@/util/schema"
|
||||
import { zod } from "@opencode-ai/core/effect-zod"
|
||||
import { NonNegativeInt } from "@opencode-ai/core/schema"
|
||||
|
||||
export class ConsoleState extends Schema.Class<ConsoleState>("ConsoleState")({
|
||||
consoleManagedProviders: Schema.mutable(Schema.Array(Schema.String)),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export * as ConfigFormatter from "./formatter"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { zod } from "@opencode-ai/core/effect-zod"
|
||||
import { withStatics } from "@opencode-ai/core/schema"
|
||||
|
||||
export const Entry = Schema.Struct({
|
||||
disabled: Schema.optional(Schema.Boolean),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Schema } from "effect"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { zod } from "@opencode-ai/core/effect-zod"
|
||||
import { withStatics } from "@opencode-ai/core/schema"
|
||||
|
||||
export const Layout = Schema.Literals(["auto", "stretch"])
|
||||
.annotate({ identifier: "LayoutConfig" })
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export * as ConfigLSP from "./lsp"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { zod } from "@opencode-ai/core/effect-zod"
|
||||
import { withStatics } from "@opencode-ai/core/schema"
|
||||
import * as LSPServer from "../lsp/server"
|
||||
|
||||
export const Disabled = Schema.Struct({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Schema } from "effect"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { PositiveInt, withStatics } from "@/util/schema"
|
||||
import { zod } from "@opencode-ai/core/effect-zod"
|
||||
import { PositiveInt, withStatics } from "@opencode-ai/core/schema"
|
||||
|
||||
export const Local = Schema.Struct({
|
||||
type: Schema.Literal("local").annotate({ description: "Type of MCP server connection" }),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Schema } from "effect"
|
||||
import z from "zod"
|
||||
import { zod, ZodOverride } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { zod, ZodOverride } from "@opencode-ai/core/effect-zod"
|
||||
import { withStatics } from "@opencode-ai/core/schema"
|
||||
|
||||
// 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
|
||||
|
||||
@@ -3,7 +3,7 @@ export * as ConfigParse from "./parse"
|
||||
import { type ParseError as JsoncParseError, parse as parseJsoncImpl, printParseErrorCode } from "jsonc-parser"
|
||||
import { Cause, Exit, Schema as EffectSchema, SchemaIssue } from "effect"
|
||||
import z from "zod"
|
||||
import type { DeepMutable } from "@/util/schema"
|
||||
import type { DeepMutable } from "@opencode-ai/core/schema"
|
||||
import { InvalidError, JsonError } from "./error"
|
||||
|
||||
type ZodSchema<T> = z.ZodType<T>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export * as ConfigPermission from "./permission"
|
||||
import { Schema, SchemaGetter } from "effect"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { zod } from "@opencode-ai/core/effect-zod"
|
||||
import { withStatics } from "@opencode-ai/core/schema"
|
||||
|
||||
export const Action = Schema.Literals(["ask", "allow", "deny"])
|
||||
.annotate({ identifier: "PermissionActionConfig" })
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Glob } from "@opencode-ai/core/util/glob"
|
||||
import { Schema } from "effect"
|
||||
import { pathToFileURL } from "url"
|
||||
import { isPathPluginSpec, parsePluginSpecifier, resolvePathPluginTarget } from "@/plugin/shared"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { zod } from "@opencode-ai/core/effect-zod"
|
||||
import { withStatics } from "@opencode-ai/core/schema"
|
||||
import path from "path"
|
||||
|
||||
export const Options = Schema.Record(Schema.String, Schema.Unknown).pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Schema } from "effect"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { PositiveInt, withStatics } from "@/util/schema"
|
||||
import { zod } from "@opencode-ai/core/effect-zod"
|
||||
import { PositiveInt, withStatics } from "@opencode-ai/core/schema"
|
||||
import { ModelStatus } from "@/provider/model-status"
|
||||
|
||||
export const Model = Schema.Struct({
|
||||
id: Schema.optional(Schema.String),
|
||||
@@ -49,7 +50,7 @@ export const Model = Schema.Struct({
|
||||
}),
|
||||
),
|
||||
experimental: Schema.optional(Schema.Boolean),
|
||||
status: Schema.optional(Schema.Literals(["alpha", "beta", "deprecated"])),
|
||||
status: Schema.optional(ModelStatus),
|
||||
provider: Schema.optional(
|
||||
Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) }),
|
||||
),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export * as ConfigReference from "./reference"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { zod } from "@opencode-ai/core/effect-zod"
|
||||
import { withStatics } from "@opencode-ai/core/schema"
|
||||
|
||||
const Git = Schema.Struct({
|
||||
repository: Schema.String.annotate({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Schema } from "effect"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { PositiveInt, withStatics } from "@/util/schema"
|
||||
import { zod } from "@opencode-ai/core/effect-zod"
|
||||
import { PositiveInt, withStatics } from "@opencode-ai/core/schema"
|
||||
|
||||
export const Server = Schema.Struct({
|
||||
port: Schema.optional(PositiveInt).annotate({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Schema } from "effect"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { zod } from "@opencode-ai/core/effect-zod"
|
||||
import { withStatics } from "@opencode-ai/core/schema"
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
paths: Schema.optional(Schema.Array(Schema.String)).annotate({
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { Schema } from "effect"
|
||||
|
||||
import { Identifier } from "@/id/id"
|
||||
import { zod, ZodOverride } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { zod } from "@opencode-ai/core/effect-zod"
|
||||
import { withStatics } from "@opencode-ai/core/schema"
|
||||
|
||||
const workspaceIdSchema = Schema.String.annotate({ [ZodOverride]: Identifier.schema("workspace") }).pipe(
|
||||
Schema.brand("WorkspaceID"),
|
||||
)
|
||||
const workspacePrefix = "wrk"
|
||||
const workspaceIdSchema = Schema.String.check(Schema.isStartsWith(workspacePrefix)).pipe(Schema.brand("WorkspaceID"))
|
||||
|
||||
export type WorkspaceID = typeof workspaceIdSchema.Type
|
||||
|
||||
export const WorkspaceID = workspaceIdSchema.pipe(
|
||||
withStatics((schema: typeof workspaceIdSchema) => ({
|
||||
ascending: (id?: string) => schema.make(Identifier.ascending("workspace", id)),
|
||||
ascending: (id?: string) => schema.make(Identifier.ascending(workspacePrefix, id)),
|
||||
zod: zod(schema),
|
||||
})),
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Schema, Struct } from "effect"
|
||||
import { ProjectID } from "@/project/schema"
|
||||
import { WorkspaceID } from "./schema"
|
||||
import type { DeepMutable } from "@/util/schema"
|
||||
import type { DeepMutable } from "@opencode-ai/core/schema"
|
||||
|
||||
export const WorkspaceInfo = Schema.Struct({
|
||||
id: WorkspaceID,
|
||||
|
||||
@@ -14,8 +14,8 @@ import { containsPath } from "../project/instance-context"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Protected } from "./protected"
|
||||
import { Ripgrep } from "./ripgrep"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { NonNegativeInt, type DeepMutable, withStatics } from "@/util/schema"
|
||||
import { zod } from "@opencode-ai/core/effect-zod"
|
||||
import { NonNegativeInt, type DeepMutable, withStatics } from "@opencode-ai/core/schema"
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
path: Schema.String,
|
||||
|
||||
@@ -11,8 +11,8 @@ import { Global } from "@opencode-ai/core/global"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { sanitizedProcessEnv } from "@opencode-ai/core/util/opencode-process"
|
||||
import { which } from "@/util/which"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { NonNegativeInt, withStatics } from "@/util/schema"
|
||||
import { zod } from "@opencode-ai/core/effect-zod"
|
||||
import { NonNegativeInt, withStatics } from "@opencode-ai/core/schema"
|
||||
|
||||
const log = Log.create({ service: "ripgrep" })
|
||||
const VERSION = "15.1.0"
|
||||
|
||||
@@ -7,8 +7,8 @@ import { mergeDeep } from "remeda"
|
||||
import { Config } from "@/config/config"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import * as Formatter from "./formatter"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { zod } from "@opencode-ai/core/effect-zod"
|
||||
import { withStatics } from "@opencode-ai/core/schema"
|
||||
|
||||
const log = Log.create({ service: "format" })
|
||||
|
||||
|
||||
@@ -1,24 +1,6 @@
|
||||
import z from "zod"
|
||||
import { randomBytes } from "crypto"
|
||||
|
||||
const prefixes = {
|
||||
event: "evt",
|
||||
session: "ses",
|
||||
message: "msg",
|
||||
permission: "per",
|
||||
question: "que",
|
||||
user: "usr",
|
||||
part: "prt",
|
||||
pty: "pty",
|
||||
tool: "tool",
|
||||
workspace: "wrk",
|
||||
entry: "ent",
|
||||
account: "act",
|
||||
} as const
|
||||
|
||||
export function schema(prefix: keyof typeof prefixes) {
|
||||
return z.string().startsWith(prefixes[prefix])
|
||||
}
|
||||
export type Prefix = "evt" | "ses" | "msg" | "per" | "que" | "prt" | "pty" | "tool" | "wrk"
|
||||
|
||||
const LENGTH = 26
|
||||
|
||||
@@ -26,21 +8,21 @@ const LENGTH = 26
|
||||
let lastTimestamp = 0
|
||||
let counter = 0
|
||||
|
||||
export function ascending(prefix: keyof typeof prefixes, given?: string) {
|
||||
export function ascending(prefix: Prefix, given?: string) {
|
||||
return generateID(prefix, "ascending", given)
|
||||
}
|
||||
|
||||
export function descending(prefix: keyof typeof prefixes, given?: string) {
|
||||
export function descending(prefix: Prefix, given?: string) {
|
||||
return generateID(prefix, "descending", given)
|
||||
}
|
||||
|
||||
function generateID(prefix: keyof typeof prefixes, direction: "descending" | "ascending", given?: string): string {
|
||||
function generateID(prefix: Prefix, direction: "descending" | "ascending", given?: string): string {
|
||||
if (!given) {
|
||||
return create(prefixes[prefix], direction)
|
||||
return create(prefix, direction)
|
||||
}
|
||||
|
||||
if (!given.startsWith(prefixes[prefix])) {
|
||||
throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`)
|
||||
if (!given.startsWith(prefix)) {
|
||||
throw new Error(`ID ${given} does not start with ${prefix}`)
|
||||
}
|
||||
return given
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ import { spawn as lspspawn } from "./launch"
|
||||
import { Effect, Layer, Context, Schema } from "effect"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { containsPath } from "@/project/instance-context"
|
||||
import { NonNegativeInt, withStatics } from "@/util/schema"
|
||||
import { zod, ZodOverride } from "@/util/effect-zod"
|
||||
import { NonNegativeInt, withStatics } from "@opencode-ai/core/schema"
|
||||
import { zod, ZodOverride } from "@opencode-ai/core/effect-zod"
|
||||
|
||||
const log = Log.create({ service: "lsp" })
|
||||
|
||||
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
declare module "*.md" {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
||||
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"
|
||||
import {
|
||||
CallToolResultSchema,
|
||||
ToolSchema,
|
||||
type Tool as MCPToolDef,
|
||||
ToolListChangedNotificationSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js"
|
||||
@@ -30,12 +31,21 @@ import { EffectBridge } from "@/effect/bridge"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { zod as effectZod } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { zod as effectZod } from "@opencode-ai/core/effect-zod"
|
||||
import { withStatics } from "@opencode-ai/core/schema"
|
||||
|
||||
const log = Log.create({ service: "mcp" })
|
||||
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({
|
||||
name: Schema.String,
|
||||
uri: Schema.String,
|
||||
@@ -119,6 +129,38 @@ function remoteURL(key: string, value: string) {
|
||||
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
|
||||
function convertMcpTool(mcpTool: MCPToolDef, client: MCPClient, timeout?: number): Tool {
|
||||
const inputSchema = mcpTool.inputSchema
|
||||
@@ -151,11 +193,7 @@ function convertMcpTool(mcpTool: MCPToolDef, client: MCPClient, timeout?: number
|
||||
}
|
||||
|
||||
function defs(key: string, client: MCPClient, timeout?: number) {
|
||||
return Effect.tryPromise({
|
||||
try: () => withTimeout(client.listTools(), timeout ?? DEFAULT_TIMEOUT),
|
||||
catch: (err) => (err instanceof Error ? err : new Error(String(err))),
|
||||
}).pipe(
|
||||
Effect.map((result) => result.tools),
|
||||
return listTools(key, client, timeout ?? DEFAULT_TIMEOUT).pipe(
|
||||
Effect.catch((err) => {
|
||||
log.error("failed to get tools from client", { key, error: err })
|
||||
return Effect.succeed(undefined)
|
||||
|
||||
@@ -7,9 +7,9 @@ import { MessageID, SessionID } from "@/session/schema"
|
||||
import { PermissionTable } from "@/session/session.sql"
|
||||
import { Database } from "@/storage/db"
|
||||
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 { withStatics } from "@/util/schema"
|
||||
import { withStatics } from "@opencode-ai/core/schema"
|
||||
import { Wildcard } from "@/util/wildcard"
|
||||
import { Deferred, Effect, Layer, Schema, Context } from "effect"
|
||||
import os from "os"
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { Schema } from "effect"
|
||||
|
||||
import { Identifier } from "@/id/id"
|
||||
import { zod, ZodOverride } from "@/util/effect-zod"
|
||||
import { Newtype } from "@/util/schema"
|
||||
import { zod } from "@opencode-ai/core/effect-zod"
|
||||
import { Newtype } from "@opencode-ai/core/schema"
|
||||
|
||||
const permissionPrefix = "per"
|
||||
|
||||
export class PermissionID extends Newtype<PermissionID>()(
|
||||
"PermissionID",
|
||||
Schema.String.check(Schema.isStartsWith("per")).annotate({ [ZodOverride]: Identifier.schema("permission") }),
|
||||
Schema.String.check(Schema.isStartsWith(permissionPrefix)),
|
||||
) {
|
||||
static ascending(id?: string): PermissionID {
|
||||
return this.make(Identifier.ascending("permission", id))
|
||||
return this.make(Identifier.ascending(permissionPrefix, id))
|
||||
}
|
||||
|
||||
static readonly zod = zod(this)
|
||||
|
||||
@@ -18,8 +18,8 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
import { NodePath } from "@effect/platform-node"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { NonNegativeInt, optionalOmitUndefined, withStatics } from "@/util/schema"
|
||||
import { zod } from "@opencode-ai/core/effect-zod"
|
||||
import { NonNegativeInt, optionalOmitUndefined, withStatics } from "@opencode-ai/core/schema"
|
||||
import { serviceUse } from "@/effect/service-use"
|
||||
|
||||
const log = Log.create({ service: "project" })
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Schema } from "effect"
|
||||
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { zod } from "@opencode-ai/core/effect-zod"
|
||||
import { withStatics } from "@opencode-ai/core/schema"
|
||||
|
||||
const projectIdSchema = Schema.String.pipe(Schema.brand("ProjectID"))
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ import { InstanceState } from "@/effect/instance-state"
|
||||
import { FileWatcher } from "@/file/watcher"
|
||||
import { Git } from "@/git"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { zod, zodObject } from "@/util/effect-zod"
|
||||
import { NonNegativeInt, withStatics } from "@/util/schema"
|
||||
import { zod, zodObject } from "@opencode-ai/core/effect-zod"
|
||||
import { NonNegativeInt, withStatics } from "@opencode-ai/core/schema"
|
||||
|
||||
const log = Log.create({ service: "vcs" })
|
||||
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({
|
||||
file: Schema.String,
|
||||
patch: Schema.String,
|
||||
additions: NonNegativeInt,
|
||||
deletions: NonNegativeInt,
|
||||
// Mirrors Snapshot.FileDiff (see #26574). The current producer always
|
||||
// populates patch, but loosening matches the sibling schema so a
|
||||
// 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"])),
|
||||
})
|
||||
.annotate({ identifier: "VcsFileDiff" })
|
||||
@@ -241,8 +244,8 @@ export type FileDiff = Schema.Schema.Type<typeof FileDiff>
|
||||
|
||||
export const FileStatus = Schema.Struct({
|
||||
file: Schema.String,
|
||||
additions: NonNegativeInt,
|
||||
deletions: NonNegativeInt,
|
||||
additions: Schema.Finite,
|
||||
deletions: Schema.Finite,
|
||||
status: Schema.Literals(["added", "deleted", "modified"]),
|
||||
})
|
||||
.annotate({ identifier: "VcsFileStatus" })
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { AuthOAuthResult, Hooks } from "@opencode-ai/plugin"
|
||||
import { Auth } from "@/auth"
|
||||
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 { optionalOmitUndefined, withStatics } from "@/util/schema"
|
||||
import { optionalOmitUndefined, withStatics } from "@opencode-ai/core/schema"
|
||||
import { Plugin } from "../plugin"
|
||||
import { ProviderID } from "./schema"
|
||||
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"
|
||||
@@ -8,6 +8,7 @@ import { Flock } from "@opencode-ai/core/util/flock"
|
||||
import { Hash } from "@opencode-ai/core/util/hash"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { withTransientReadRetry } from "@/util/effect-http-client"
|
||||
import { CatalogModelStatus } from "./model-status"
|
||||
|
||||
const Cost = Schema.Struct({
|
||||
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(
|
||||
Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) }),
|
||||
),
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Auth } from "../auth"
|
||||
import { Env } from "../env"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
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 { iife } from "@/util/iife"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
@@ -24,10 +24,11 @@ import { EffectBridge } from "@/effect/bridge"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
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 { ModelID, ProviderID } from "./schema"
|
||||
import { ModelStatus } from "./model-status"
|
||||
|
||||
const log = Log.create({ service: "provider" })
|
||||
|
||||
@@ -897,7 +898,7 @@ export const Model = Schema.Struct({
|
||||
capabilities: ProviderCapabilities,
|
||||
cost: ProviderCost,
|
||||
limit: ProviderLimit,
|
||||
status: Schema.Literals(["alpha", "beta", "deprecated", "active"]),
|
||||
status: ModelStatus,
|
||||
options: Schema.Record(Schema.String, Schema.Any),
|
||||
headers: Schema.Record(Schema.String, Schema.String),
|
||||
release_date: Schema.String,
|
||||
@@ -1162,7 +1163,7 @@ const layer: Layer.Layer<
|
||||
const pluginAuth = yield* auth.get(providerID).pipe(Effect.orDie)
|
||||
|
||||
provider.models = yield* Effect.promise(async () => {
|
||||
const next = await models(provider, { auth: pluginAuth })
|
||||
const next = await models(toPublicInfo(provider), { auth: pluginAuth })
|
||||
return Object.fromEntries(
|
||||
Object.entries(next).map(([id, model]) => [
|
||||
id,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Schema } from "effect"
|
||||
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { zod } from "@opencode-ai/core/effect-zod"
|
||||
import { withStatics } from "@opencode-ai/core/schema"
|
||||
|
||||
const providerIdSchema = Schema.String.pipe(Schema.brand("ProviderID"))
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import type { Proc } from "#pty"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { PtyID } from "./schema"
|
||||
import { Effect, Layer, Context, Schema, Types } from "effect"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { NonNegativeInt, PositiveInt, withStatics } from "@/util/schema"
|
||||
import { zod } from "@opencode-ai/core/effect-zod"
|
||||
import { NonNegativeInt, PositiveInt, withStatics } from "@opencode-ai/core/schema"
|
||||
|
||||
const log = Log.create({ service: "pty" })
|
||||
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { Schema } from "effect"
|
||||
|
||||
import { Identifier } from "@/id/id"
|
||||
import { zod, ZodOverride } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { zod } from "@opencode-ai/core/effect-zod"
|
||||
import { withStatics } from "@opencode-ai/core/schema"
|
||||
|
||||
const ptyIdSchema = Schema.String.annotate({ [ZodOverride]: Identifier.schema("pty") }).pipe(Schema.brand("PtyID"))
|
||||
const ptyPrefix = "pty"
|
||||
const ptyIdSchema = Schema.String.check(Schema.isStartsWith(ptyPrefix)).pipe(Schema.brand("PtyID"))
|
||||
|
||||
export type PtyID = typeof ptyIdSchema.Type
|
||||
|
||||
export const PtyID = ptyIdSchema.pipe(
|
||||
withStatics((schema: typeof ptyIdSchema) => ({
|
||||
ascending: (id?: string) => schema.make(Identifier.ascending("pty", id)),
|
||||
ascending: (id?: string) => schema.make(Identifier.ascending(ptyPrefix, id)),
|
||||
zod: zod(schema),
|
||||
})),
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ export * as PtyTicket from "./ticket"
|
||||
import { WorkspaceID } from "@/control-plane/schema"
|
||||
import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref"
|
||||
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"
|
||||
|
||||
const DEFAULT_TTL = Duration.seconds(60)
|
||||
|
||||
@@ -3,9 +3,9 @@ import { Bus } from "@/bus"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
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 { withStatics } from "@/util/schema"
|
||||
import { withStatics } from "@opencode-ai/core/schema"
|
||||
import { QuestionID } from "./schema"
|
||||
|
||||
const log = Log.create({ service: "question" })
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { Schema } from "effect"
|
||||
|
||||
import { Identifier } from "@/id/id"
|
||||
import { zod, ZodOverride } from "@/util/effect-zod"
|
||||
import { Newtype } from "@/util/schema"
|
||||
import { zod } from "@opencode-ai/core/effect-zod"
|
||||
import { Newtype } from "@opencode-ai/core/schema"
|
||||
|
||||
const questionPrefix = "que"
|
||||
|
||||
export class QuestionID extends Newtype<QuestionID>()(
|
||||
"QuestionID",
|
||||
Schema.String.check(Schema.isStartsWith("que")).annotate({ [ZodOverride]: Identifier.schema("question") }),
|
||||
Schema.String.check(Schema.isStartsWith(questionPrefix)),
|
||||
) {
|
||||
static ascending(id?: string): QuestionID {
|
||||
return this.make(Identifier.ascending("question", id))
|
||||
return this.make(Identifier.ascending(questionPrefix, id))
|
||||
}
|
||||
|
||||
static readonly zod = zod(this)
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as Stream from "effect/Stream"
|
||||
import { HttpServerResponse } from "effect/unstable/http"
|
||||
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||
import * as Sse from "effect/unstable/encoding/Sse"
|
||||
import { WorkspaceRoutingQuery } from "./middleware/workspace-routing"
|
||||
|
||||
const log = Log.create({ service: "server" })
|
||||
|
||||
@@ -16,6 +17,7 @@ export const EventApi = HttpApi.make("event").add(
|
||||
HttpApiGroup.make("event")
|
||||
.add(
|
||||
HttpApiEndpoint.get("subscribe", EventPaths.event, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: Schema.String.pipe(HttpApiSchema.asText({ contentType: "text/event-stream" })),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Provider } from "@/provider/provider"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { Authorization } from "../middleware/authorization"
|
||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
|
||||
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
|
||||
import { described } from "./metadata"
|
||||
|
||||
const root = "/config"
|
||||
@@ -13,6 +13,7 @@ export const ConfigApi = HttpApi.make("config")
|
||||
HttpApiGroup.make("config")
|
||||
.add(
|
||||
HttpApiEndpoint.get("get", root, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Config.Info, "Get config info"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -22,6 +23,7 @@ export const ConfigApi = HttpApi.make("config")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.patch("update", root, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: Config.Info,
|
||||
success: described(Config.Info, "Successfully updated config"),
|
||||
error: HttpApiError.BadRequest,
|
||||
@@ -33,6 +35,7 @@ export const ConfigApi = HttpApi.make("config")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("providers", `${root}/providers`, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Provider.ConfigProvidersResult, "List of providers"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
|
||||
@@ -3,13 +3,18 @@ import { MCP } from "@/mcp"
|
||||
import { ProviderID, ModelID } from "@/provider/schema"
|
||||
import { Session } from "@/session/session"
|
||||
import { Worktree } from "@/worktree"
|
||||
import { NonNegativeInt } from "@/util/schema"
|
||||
import { Schema, SchemaGetter } from "effect"
|
||||
import { NonNegativeInt } from "@opencode-ai/core/schema"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { Authorization } from "../middleware/authorization"
|
||||
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 { QueryBoolean } from "./query"
|
||||
|
||||
const ConsoleStateResponse = Schema.Struct({
|
||||
consoleManagedProviders: Schema.mutable(Schema.Array(Schema.String)),
|
||||
@@ -43,19 +48,14 @@ const ToolListItem = Schema.Struct({
|
||||
}).annotate({ identifier: "ToolListItem" })
|
||||
const ToolList = Schema.Array(ToolListItem).annotate({ identifier: "ToolList" })
|
||||
export const ToolListQuery = Schema.Struct({
|
||||
...WorkspaceRoutingQueryFields,
|
||||
provider: ProviderID,
|
||||
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)
|
||||
export const SessionListQuery = Schema.Struct({
|
||||
directory: Schema.optional(Schema.String),
|
||||
...WorkspaceRoutingQueryFields,
|
||||
roots: Schema.optional(QueryBoolean),
|
||||
start: Schema.optional(Schema.NumberFromString),
|
||||
cursor: Schema.optional(Schema.NumberFromString),
|
||||
@@ -81,6 +81,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
|
||||
HttpApiGroup.make("experimental")
|
||||
.add(
|
||||
HttpApiEndpoint.get("console", ExperimentalPaths.console, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(ConsoleStateResponse, "Active Console provider metadata"),
|
||||
error: HttpApiError.InternalServerError,
|
||||
}).annotateMerge(
|
||||
@@ -91,6 +92,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("consoleOrgs", ExperimentalPaths.consoleOrgs, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(ConsoleOrgList, "Switchable Console orgs"),
|
||||
error: HttpApiError.InternalServerError,
|
||||
}).annotateMerge(
|
||||
@@ -101,6 +103,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("consoleSwitch", ExperimentalPaths.consoleSwitch, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: ConsoleSwitchPayload,
|
||||
success: described(Schema.Boolean, "Switch success"),
|
||||
error: HttpApiError.BadRequest,
|
||||
@@ -124,6 +127,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("toolIDs", ExperimentalPaths.toolIDs, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(ToolIDs, "Tool IDs"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
@@ -135,6 +139,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("worktree", ExperimentalPaths.worktree, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(WorktreeList, "List of worktree directories"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -144,6 +149,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("worktreeCreate", ExperimentalPaths.worktree, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: Schema.optional(Worktree.CreateInput),
|
||||
success: described(Worktree.Info, "Worktree created"),
|
||||
error: HttpApiError.BadRequest,
|
||||
@@ -155,6 +161,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.delete("worktreeRemove", ExperimentalPaths.worktree, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: Worktree.RemoveInput,
|
||||
success: described(Schema.Boolean, "Worktree removed"),
|
||||
error: HttpApiError.BadRequest,
|
||||
@@ -166,6 +173,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("worktreeReset", ExperimentalPaths.worktreeReset, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: Worktree.ResetInput,
|
||||
success: described(Schema.Boolean, "Worktree reset"),
|
||||
error: HttpApiError.BadRequest,
|
||||
@@ -188,6 +196,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("resource", ExperimentalPaths.resource, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Record(Schema.String, MCP.Resource), "MCP resources"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
|
||||
@@ -5,18 +5,25 @@ import { Schema } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { Authorization } from "../middleware/authorization"
|
||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
|
||||
import {
|
||||
WorkspaceRoutingMiddleware,
|
||||
WorkspaceRoutingQuery,
|
||||
WorkspaceRoutingQueryFields,
|
||||
} from "../middleware/workspace-routing"
|
||||
import { described } from "./metadata"
|
||||
|
||||
export const FileQuery = Schema.Struct({
|
||||
...WorkspaceRoutingQueryFields,
|
||||
path: Schema.String,
|
||||
})
|
||||
|
||||
export const FindTextQuery = Schema.Struct({
|
||||
...WorkspaceRoutingQueryFields,
|
||||
pattern: Schema.String,
|
||||
})
|
||||
|
||||
export const FindFileQuery = Schema.Struct({
|
||||
...WorkspaceRoutingQueryFields,
|
||||
query: Schema.String,
|
||||
dirs: Schema.optional(Schema.Literals(["true", "false"])),
|
||||
type: Schema.optional(Schema.Literals(["file", "directory"])),
|
||||
@@ -26,6 +33,7 @@ export const FindFileQuery = Schema.Struct({
|
||||
})
|
||||
|
||||
export const FindSymbolQuery = Schema.Struct({
|
||||
...WorkspaceRoutingQueryFields,
|
||||
query: Schema.String,
|
||||
})
|
||||
|
||||
@@ -93,6 +101,7 @@ export const FileApi = HttpApi.make("file")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("status", FilePaths.status, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Array(File.Info), "File status"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
|
||||
@@ -8,7 +8,11 @@ import { Schema } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||
import { Authorization } from "../middleware/authorization"
|
||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
|
||||
import {
|
||||
WorkspaceRoutingMiddleware,
|
||||
WorkspaceRoutingQuery,
|
||||
WorkspaceRoutingQueryFields,
|
||||
} from "../middleware/workspace-routing"
|
||||
import { described } from "./metadata"
|
||||
|
||||
const PathInfo = Schema.Struct({
|
||||
@@ -20,6 +24,7 @@ const PathInfo = Schema.Struct({
|
||||
}).annotate({ identifier: "Path" })
|
||||
|
||||
export const VcsDiffQuery = Schema.Struct({
|
||||
...WorkspaceRoutingQueryFields,
|
||||
mode: Vcs.Mode,
|
||||
})
|
||||
|
||||
@@ -54,6 +59,7 @@ export const InstanceApi = HttpApi.make("instance")
|
||||
HttpApiGroup.make("instance")
|
||||
.add(
|
||||
HttpApiEndpoint.post("dispose", InstancePaths.dispose, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Boolean, "Instance disposed"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -63,6 +69,7 @@ export const InstanceApi = HttpApi.make("instance")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("path", InstancePaths.path, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: PathInfo,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -73,6 +80,7 @@ export const InstanceApi = HttpApi.make("instance")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("vcs", InstancePaths.vcs, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Vcs.Info, "VCS info"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -83,6 +91,7 @@ export const InstanceApi = HttpApi.make("instance")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("vcsStatus", InstancePaths.vcsStatus, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Array(Vcs.FileStatus), "VCS status"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -102,6 +111,7 @@ export const InstanceApi = HttpApi.make("instance")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("vcsDiffRaw", InstancePaths.vcsDiffRaw, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(
|
||||
Schema.String.pipe(HttpApiSchema.asText({ contentType: "text/x-diff; charset=utf-8" })),
|
||||
"Raw VCS diff",
|
||||
@@ -114,6 +124,7 @@ export const InstanceApi = HttpApi.make("instance")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("vcsApply", InstancePaths.vcsApply, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: Vcs.ApplyInput,
|
||||
success: described(Vcs.ApplyResult, "VCS patch applied"),
|
||||
error: ApiVcsApplyError,
|
||||
@@ -125,6 +136,7 @@ export const InstanceApi = HttpApi.make("instance")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("command", InstancePaths.command, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Array(Command.Info), "List of commands"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -134,6 +146,7 @@ export const InstanceApi = HttpApi.make("instance")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("agent", InstancePaths.agent, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Array(Agent.Info), "List of agents"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -143,6 +156,7 @@ export const InstanceApi = HttpApi.make("instance")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("skill", InstancePaths.skill, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Array(Skill.Info), "List of skills"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -152,6 +166,7 @@ export const InstanceApi = HttpApi.make("instance")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("lsp", InstancePaths.lsp, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Array(LSP.Status), "LSP server status"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -161,6 +176,7 @@ export const InstanceApi = HttpApi.make("instance")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("formatter", InstancePaths.formatter, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Array(Format.Status), "Formatter status"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Schema } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { Authorization } from "../middleware/authorization"
|
||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
|
||||
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
|
||||
import { described } from "./metadata"
|
||||
|
||||
export const AddPayload = Schema.Struct({
|
||||
@@ -42,6 +42,7 @@ export const McpApi = HttpApi.make("mcp")
|
||||
HttpApiGroup.make("mcp")
|
||||
.add(
|
||||
HttpApiEndpoint.get("status", McpPaths.status, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Record(Schema.String, MCP.Status), "MCP server status"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -51,6 +52,7 @@ export const McpApi = HttpApi.make("mcp")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("add", McpPaths.status, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: AddPayload,
|
||||
success: described(StatusMap, "MCP server added successfully"),
|
||||
error: HttpApiError.BadRequest,
|
||||
@@ -63,6 +65,7 @@ export const McpApi = HttpApi.make("mcp")
|
||||
),
|
||||
HttpApiEndpoint.post("authStart", McpPaths.auth, {
|
||||
params: { name: Schema.String },
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(AuthStartResponse, "OAuth flow started"),
|
||||
error: [UnsupportedOAuthError, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
@@ -74,6 +77,7 @@ export const McpApi = HttpApi.make("mcp")
|
||||
),
|
||||
HttpApiEndpoint.post("authCallback", McpPaths.authCallback, {
|
||||
params: { name: Schema.String },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: AuthCallbackPayload,
|
||||
success: described(MCP.Status, "OAuth authentication completed"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
@@ -87,6 +91,7 @@ export const McpApi = HttpApi.make("mcp")
|
||||
),
|
||||
HttpApiEndpoint.post("authAuthenticate", McpPaths.authAuthenticate, {
|
||||
params: { name: Schema.String },
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(MCP.Status, "OAuth authentication completed"),
|
||||
error: [UnsupportedOAuthError, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
@@ -98,6 +103,7 @@ export const McpApi = HttpApi.make("mcp")
|
||||
),
|
||||
HttpApiEndpoint.delete("authRemove", McpPaths.auth, {
|
||||
params: { name: Schema.String },
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(AuthRemoveResponse, "OAuth credentials removed"),
|
||||
error: HttpApiError.NotFound,
|
||||
}).annotateMerge(
|
||||
@@ -109,6 +115,7 @@ export const McpApi = HttpApi.make("mcp")
|
||||
),
|
||||
HttpApiEndpoint.post("connect", McpPaths.connect, {
|
||||
params: { name: Schema.String },
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Boolean, "MCP server connected successfully"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -118,6 +125,7 @@ export const McpApi = HttpApi.make("mcp")
|
||||
),
|
||||
HttpApiEndpoint.post("disconnect", McpPaths.disconnect, {
|
||||
params: { name: Schema.String },
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Boolean, "MCP server disconnected successfully"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Schema } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { Authorization } from "../middleware/authorization"
|
||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
|
||||
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
|
||||
import { described } from "./metadata"
|
||||
|
||||
const root = "/permission"
|
||||
@@ -18,6 +18,7 @@ export const PermissionApi = HttpApi.make("permission")
|
||||
HttpApiGroup.make("permission")
|
||||
.add(
|
||||
HttpApiEndpoint.get("list", root, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Array(Permission.Request), "List of pending permissions"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -28,6 +29,7 @@ export const PermissionApi = HttpApi.make("permission")
|
||||
),
|
||||
HttpApiEndpoint.post("reply", `${root}/:requestID/reply`, {
|
||||
params: { requestID: PermissionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: ReplyPayload,
|
||||
success: described(Schema.Boolean, "Permission processed successfully"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Schema } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { Authorization } from "../middleware/authorization"
|
||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
|
||||
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
|
||||
import { described } from "./metadata"
|
||||
|
||||
const root = "/project"
|
||||
@@ -19,6 +19,7 @@ export const ProjectApi = HttpApi.make("project")
|
||||
HttpApiGroup.make("project")
|
||||
.add(
|
||||
HttpApiEndpoint.get("list", root, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Array(Project.Info), "List of projects"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -28,6 +29,7 @@ export const ProjectApi = HttpApi.make("project")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("current", `${root}/current`, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Project.Info, "Current project information"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -37,6 +39,7 @@ export const ProjectApi = HttpApi.make("project")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("initGit", `${root}/git/init`, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Project.Info, "Project information after git initialization"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -47,6 +50,7 @@ export const ProjectApi = HttpApi.make("project")
|
||||
),
|
||||
HttpApiEndpoint.patch("update", `${root}/:projectID`, {
|
||||
params: { projectID: ProjectID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: UpdatePayload,
|
||||
success: described(Project.Info, "Updated project information"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Schema } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { Authorization } from "../middleware/authorization"
|
||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
|
||||
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
|
||||
import { described } from "./metadata"
|
||||
|
||||
const root = "/provider"
|
||||
@@ -15,6 +15,7 @@ export const ProviderApi = HttpApi.make("provider")
|
||||
HttpApiGroup.make("provider")
|
||||
.add(
|
||||
HttpApiEndpoint.get("list", root, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Provider.ListResult, "List of providers"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -24,6 +25,7 @@ export const ProviderApi = HttpApi.make("provider")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("auth", `${root}/auth`, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(ProviderAuth.Methods, "Provider auth methods"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -34,6 +36,7 @@ export const ProviderApi = HttpApi.make("provider")
|
||||
),
|
||||
HttpApiEndpoint.post("authorize", `${root}/:providerID/oauth/authorize`, {
|
||||
params: { providerID: ProviderID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: ProviderAuth.AuthorizeInput,
|
||||
success: described(Schema.UndefinedOr(ProviderAuth.Authorization), "Authorization URL and method"),
|
||||
error: HttpApiError.BadRequest,
|
||||
@@ -46,6 +49,7 @@ export const ProviderApi = HttpApi.make("provider")
|
||||
),
|
||||
HttpApiEndpoint.post("callback", `${root}/:providerID/oauth/callback`, {
|
||||
params: { providerID: ProviderID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: ProviderAuth.CallbackInput,
|
||||
success: described(Schema.Boolean, "OAuth callback processed successfully"),
|
||||
error: HttpApiError.BadRequest,
|
||||
|
||||
@@ -5,13 +5,20 @@ import { Schema } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { Authorization } from "../middleware/authorization"
|
||||
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 { described } from "./metadata"
|
||||
|
||||
const root = "/pty"
|
||||
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({
|
||||
path: Schema.String,
|
||||
name: Schema.String,
|
||||
@@ -34,6 +41,7 @@ export const PtyApi = HttpApi.make("pty")
|
||||
HttpApiGroup.make("pty")
|
||||
.add(
|
||||
HttpApiEndpoint.get("shells", PtyPaths.shells, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Array(ShellItem), "List of shells"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -43,6 +51,7 @@ export const PtyApi = HttpApi.make("pty")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("list", PtyPaths.list, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Array(Pty.Info), "List of sessions"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -52,6 +61,7 @@ export const PtyApi = HttpApi.make("pty")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("create", PtyPaths.create, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: Pty.CreateInput,
|
||||
success: described(Pty.Info, "Created session"),
|
||||
error: HttpApiError.BadRequest,
|
||||
@@ -64,6 +74,7 @@ export const PtyApi = HttpApi.make("pty")
|
||||
),
|
||||
HttpApiEndpoint.get("get", PtyPaths.get, {
|
||||
params: { ptyID: PtyID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Pty.Info, "Session info"),
|
||||
error: ApiNotFoundError,
|
||||
}).annotateMerge(
|
||||
@@ -75,6 +86,7 @@ export const PtyApi = HttpApi.make("pty")
|
||||
),
|
||||
HttpApiEndpoint.put("update", PtyPaths.update, {
|
||||
params: { ptyID: PtyID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: Pty.UpdateInput,
|
||||
success: described(Pty.Info, "Updated session"),
|
||||
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
||||
@@ -87,6 +99,7 @@ export const PtyApi = HttpApi.make("pty")
|
||||
),
|
||||
HttpApiEndpoint.delete("remove", PtyPaths.remove, {
|
||||
params: { ptyID: PtyID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Boolean, "Session removed"),
|
||||
error: ApiNotFoundError,
|
||||
}).annotateMerge(
|
||||
@@ -98,6 +111,7 @@ export const PtyApi = HttpApi.make("pty")
|
||||
),
|
||||
HttpApiEndpoint.post("connectToken", PtyPaths.connectToken, {
|
||||
params: { ptyID: PtyID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(PtyTicket.ConnectToken, "WebSocket connect token"),
|
||||
error: [HttpApiError.Forbidden, ApiNotFoundError],
|
||||
}).annotateMerge(
|
||||
@@ -126,6 +140,7 @@ export const PtyConnectApi = HttpApi.make("pty-connect").add(
|
||||
.add(
|
||||
HttpApiEndpoint.get("connect", PtyPaths.connect, {
|
||||
params: Params,
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Boolean, "Connected session"),
|
||||
error: [HttpApiError.Forbidden, HttpApiError.NotFound],
|
||||
}).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 { Authorization } from "../middleware/authorization"
|
||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
|
||||
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
|
||||
import { described } from "./metadata"
|
||||
|
||||
const root = "/question"
|
||||
@@ -19,6 +19,7 @@ export const QuestionApi = HttpApi.make("question")
|
||||
HttpApiGroup.make("question")
|
||||
.add(
|
||||
HttpApiEndpoint.get("list", root, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Array(Question.Request), "List of pending questions"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -29,6 +30,7 @@ export const QuestionApi = HttpApi.make("question")
|
||||
),
|
||||
HttpApiEndpoint.post("reply", `${root}/:requestID/reply`, {
|
||||
params: { requestID: QuestionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: ReplyPayload,
|
||||
success: described(Schema.Boolean, "Question answered successfully"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
@@ -41,6 +43,7 @@ export const QuestionApi = HttpApi.make("question")
|
||||
),
|
||||
HttpApiEndpoint.post("reject", `${root}/:requestID/reject`, {
|
||||
params: { requestID: QuestionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Boolean, "Question rejected successfully"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
|
||||
@@ -10,23 +10,22 @@ import { SessionSummary } from "@/session/summary"
|
||||
import { Todo } from "@/session/todo"
|
||||
import { MessageID, PartID, SessionID } from "@/session/schema"
|
||||
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 { Authorization } from "../middleware/authorization"
|
||||
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 { described } from "./metadata"
|
||||
import { QueryBoolean } from "./query"
|
||||
|
||||
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({
|
||||
directory: Schema.optional(Schema.String),
|
||||
...WorkspaceRoutingQueryFields,
|
||||
scope: Schema.optional(Schema.Literals(["project"])),
|
||||
path: Schema.optional(Schema.String),
|
||||
roots: Schema.optional(QueryBoolean),
|
||||
@@ -34,8 +33,12 @@ export const ListQuery = Schema.Struct({
|
||||
search: Schema.optional(Schema.String),
|
||||
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({
|
||||
...WorkspaceRoutingQueryFields,
|
||||
limit: Schema.optional(Schema.NumberFromString.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0))),
|
||||
before: Schema.optional(Schema.String),
|
||||
})
|
||||
@@ -112,6 +115,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("status", SessionPaths.status, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(StatusMap, "Get session status"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
@@ -123,6 +127,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
),
|
||||
HttpApiEndpoint.get("get", SessionPaths.get, {
|
||||
params: { sessionID: SessionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Session.Info, "Get session"),
|
||||
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
||||
}).annotateMerge(
|
||||
@@ -134,6 +139,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
),
|
||||
HttpApiEndpoint.get("children", SessionPaths.children, {
|
||||
params: { sessionID: SessionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Array(Session.Info), "List of children"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
@@ -145,6 +151,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
),
|
||||
HttpApiEndpoint.get("todo", SessionPaths.todo, {
|
||||
params: { sessionID: SessionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Array(Todo.Info), "Todo list"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
@@ -179,6 +186,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
),
|
||||
HttpApiEndpoint.get("message", SessionPaths.message, {
|
||||
params: { sessionID: SessionID, messageID: MessageID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(MessageV2.WithParts, "Message"),
|
||||
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
||||
}).annotateMerge(
|
||||
@@ -189,6 +197,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("create", SessionPaths.create, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: [HttpApiSchema.NoContent, Session.CreateInput],
|
||||
success: described(Session.Info, "Successfully created session"),
|
||||
error: HttpApiError.BadRequest,
|
||||
@@ -201,6 +210,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
),
|
||||
HttpApiEndpoint.delete("remove", SessionPaths.remove, {
|
||||
params: { sessionID: SessionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Boolean, "Successfully deleted session"),
|
||||
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
||||
}).annotateMerge(
|
||||
@@ -212,6 +222,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
),
|
||||
HttpApiEndpoint.patch("update", SessionPaths.update, {
|
||||
params: { sessionID: SessionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: UpdatePayload,
|
||||
success: described(Session.Info, "Successfully updated session"),
|
||||
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
||||
@@ -224,6 +235,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
),
|
||||
HttpApiEndpoint.post("fork", SessionPaths.fork, {
|
||||
params: { sessionID: SessionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: ForkPayload,
|
||||
success: described(Session.Info, "200"),
|
||||
error: ApiNotFoundError,
|
||||
@@ -236,6 +248,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
),
|
||||
HttpApiEndpoint.post("abort", SessionPaths.abort, {
|
||||
params: { sessionID: SessionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Boolean, "Aborted session"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
@@ -247,6 +260,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
),
|
||||
HttpApiEndpoint.post("init", SessionPaths.init, {
|
||||
params: { sessionID: SessionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: InitPayload,
|
||||
success: described(Schema.Boolean, "200"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
@@ -260,6 +274,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
),
|
||||
HttpApiEndpoint.post("share", SessionPaths.share, {
|
||||
params: { sessionID: SessionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Session.Info, "Successfully shared session"),
|
||||
error: [HttpApiError.InternalServerError, ApiNotFoundError],
|
||||
}).annotateMerge(
|
||||
@@ -271,6 +286,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
),
|
||||
HttpApiEndpoint.delete("unshare", SessionPaths.share, {
|
||||
params: { sessionID: SessionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Session.Info, "Successfully unshared session"),
|
||||
error: [HttpApiError.InternalServerError, ApiNotFoundError],
|
||||
}).annotateMerge(
|
||||
@@ -282,6 +298,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
),
|
||||
HttpApiEndpoint.post("summarize", SessionPaths.summarize, {
|
||||
params: { sessionID: SessionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: SummarizePayload,
|
||||
success: described(Schema.Boolean, "Summarized session"),
|
||||
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
||||
@@ -294,6 +311,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
),
|
||||
HttpApiEndpoint.post("prompt", SessionPaths.prompt, {
|
||||
params: { sessionID: SessionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: PromptPayload,
|
||||
success: described(MessageV2.WithParts, "Created message"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
@@ -306,6 +324,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
),
|
||||
HttpApiEndpoint.post("promptAsync", SessionPaths.promptAsync, {
|
||||
params: { sessionID: SessionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: PromptPayload,
|
||||
success: described(HttpApiSchema.NoContent, "Prompt accepted"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
@@ -319,6 +338,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
),
|
||||
HttpApiEndpoint.post("command", SessionPaths.command, {
|
||||
params: { sessionID: SessionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: CommandPayload,
|
||||
success: described(MessageV2.WithParts, "Created message"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
@@ -331,6 +351,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
),
|
||||
HttpApiEndpoint.post("shell", SessionPaths.shell, {
|
||||
params: { sessionID: SessionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: ShellPayload,
|
||||
success: described(MessageV2.WithParts, "Created message"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
@@ -343,6 +364,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
),
|
||||
HttpApiEndpoint.post("revert", SessionPaths.revert, {
|
||||
params: { sessionID: SessionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: RevertPayload,
|
||||
success: described(Session.Info, "Updated session"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
@@ -356,6 +378,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
),
|
||||
HttpApiEndpoint.post("unrevert", SessionPaths.unrevert, {
|
||||
params: { sessionID: SessionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Session.Info, "Updated session"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
@@ -367,6 +390,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
),
|
||||
HttpApiEndpoint.post("permissionRespond", SessionPaths.permissions, {
|
||||
params: { sessionID: SessionID, permissionID: PermissionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: PermissionResponsePayload,
|
||||
success: described(Schema.Boolean, "Permission processed successfully"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
@@ -380,6 +404,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
),
|
||||
HttpApiEndpoint.delete("deleteMessage", SessionPaths.deleteMessage, {
|
||||
params: { sessionID: SessionID, messageID: MessageID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Boolean, "Successfully deleted message"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
@@ -392,6 +417,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
),
|
||||
HttpApiEndpoint.delete("deletePart", SessionPaths.deletePart, {
|
||||
params: { sessionID: SessionID, messageID: MessageID, partID: PartID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Boolean, "Successfully deleted part"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
@@ -402,6 +428,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
),
|
||||
HttpApiEndpoint.patch("updatePart", SessionPaths.updatePart, {
|
||||
params: { sessionID: SessionID, messageID: MessageID, partID: PartID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: MessageV2.Part,
|
||||
success: described(MessageV2.Part, "Successfully updated part"),
|
||||
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 { Schema } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { Authorization } from "../middleware/authorization"
|
||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
|
||||
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
|
||||
import { described } from "./metadata"
|
||||
|
||||
const root = "/sync"
|
||||
@@ -46,6 +46,7 @@ export const SyncApi = HttpApi.make("sync")
|
||||
HttpApiGroup.make("sync")
|
||||
.add(
|
||||
HttpApiEndpoint.post("start", SyncPaths.start, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Boolean, "Workspace sync started"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -55,6 +56,7 @@ export const SyncApi = HttpApi.make("sync")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("replay", SyncPaths.replay, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: ReplayPayload,
|
||||
success: described(ReplayResponse, "Replayed sync events"),
|
||||
error: HttpApiError.BadRequest,
|
||||
@@ -66,6 +68,7 @@ export const SyncApi = HttpApi.make("sync")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("steal", SyncPaths.steal, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: SessionPayload,
|
||||
success: described(SessionPayload, "Session stolen into workspace"),
|
||||
error: HttpApiError.BadRequest,
|
||||
@@ -77,6 +80,7 @@ export const SyncApi = HttpApi.make("sync")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("history", SyncPaths.history, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: HistoryPayload,
|
||||
success: described(Schema.Array(HistoryEvent), "Sync events"),
|
||||
error: HttpApiError.BadRequest,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Schema } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { Authorization } from "../middleware/authorization"
|
||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
|
||||
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
|
||||
import { ApiNotFoundError } from "../errors"
|
||||
import { described } from "./metadata"
|
||||
|
||||
@@ -54,6 +54,7 @@ export const TuiApi = HttpApi.make("tui")
|
||||
HttpApiGroup.make("tui")
|
||||
.add(
|
||||
HttpApiEndpoint.post("appendPrompt", TuiPaths.appendPrompt, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: TuiEvent.PromptAppend.properties,
|
||||
success: described(Schema.Boolean, "Prompt processed successfully"),
|
||||
error: HttpApiError.BadRequest,
|
||||
@@ -65,6 +66,7 @@ export const TuiApi = HttpApi.make("tui")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("openHelp", TuiPaths.openHelp, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Boolean, "Help dialog opened successfully"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -74,6 +76,7 @@ export const TuiApi = HttpApi.make("tui")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("openSessions", TuiPaths.openSessions, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Boolean, "Session dialog opened successfully"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -83,6 +86,7 @@ export const TuiApi = HttpApi.make("tui")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("openThemes", TuiPaths.openThemes, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Boolean, "Theme dialog opened successfully"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -92,6 +96,7 @@ export const TuiApi = HttpApi.make("tui")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("openModels", TuiPaths.openModels, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Boolean, "Model dialog opened successfully"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -101,6 +106,7 @@ export const TuiApi = HttpApi.make("tui")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("submitPrompt", TuiPaths.submitPrompt, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Boolean, "Prompt submitted successfully"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -110,6 +116,7 @@ export const TuiApi = HttpApi.make("tui")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("clearPrompt", TuiPaths.clearPrompt, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Boolean, "Prompt cleared successfully"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -119,6 +126,7 @@ export const TuiApi = HttpApi.make("tui")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("executeCommand", TuiPaths.executeCommand, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: CommandPayload,
|
||||
success: described(Schema.Boolean, "Command executed successfully"),
|
||||
error: HttpApiError.BadRequest,
|
||||
@@ -130,6 +138,7 @@ export const TuiApi = HttpApi.make("tui")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("showToast", TuiPaths.showToast, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: TuiEvent.ToastShow.properties,
|
||||
success: described(Schema.Boolean, "Toast notification shown successfully"),
|
||||
}).annotateMerge(
|
||||
@@ -140,6 +149,7 @@ export const TuiApi = HttpApi.make("tui")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("publish", TuiPaths.publish, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: TuiPublishPayload,
|
||||
success: described(Schema.Boolean, "Event published successfully"),
|
||||
error: HttpApiError.BadRequest,
|
||||
@@ -151,6 +161,7 @@ export const TuiApi = HttpApi.make("tui")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("selectSession", TuiPaths.selectSession, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: TuiEvent.SessionSelect.properties,
|
||||
success: described(Schema.Boolean, "Session selected successfully"),
|
||||
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
||||
@@ -162,6 +173,7 @@ export const TuiApi = HttpApi.make("tui")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("controlNext", TuiPaths.controlNext, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(TuiRequestPayload, "Next TUI request"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -171,6 +183,7 @@ export const TuiApi = HttpApi.make("tui")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("controlResponse", TuiPaths.controlResponse, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: Schema.Unknown,
|
||||
success: described(Schema.Boolean, "Response submitted successfully"),
|
||||
}).annotateMerge(
|
||||
|
||||
@@ -3,46 +3,31 @@ import { SessionMessage } from "@/v2/session-message"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
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")
|
||||
.add(
|
||||
HttpApiEndpoint.get("messages", "/api/session/:sessionID/message", {
|
||||
params: { sessionID: SessionID },
|
||||
query: Schema.Union([
|
||||
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" }),
|
||||
query: MessagesQuery,
|
||||
success: Schema.Struct({
|
||||
items: Schema.Array(SessionMessage.Message),
|
||||
cursor: Schema.Struct({
|
||||
|
||||
@@ -1,67 +1,39 @@
|
||||
import { WorkspaceID } from "@/control-plane/schema"
|
||||
import { SessionID } from "@/session/schema"
|
||||
import { SessionMessage } from "@/v2/session-message"
|
||||
import { Prompt } from "@/v2/session-prompt"
|
||||
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 { Authorization } from "../../middleware/authorization"
|
||||
import { WorkspaceRoutingQuery, WorkspaceRoutingQueryFields } from "../../middleware/workspace-routing"
|
||||
import { QueryBoolean } from "../query"
|
||||
|
||||
export const SessionsQuery = Schema.Struct({
|
||||
...WorkspaceRoutingQueryFields,
|
||||
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.",
|
||||
}),
|
||||
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.",
|
||||
}),
|
||||
path: Schema.optional(Schema.String),
|
||||
roots: Schema.optional(QueryBoolean),
|
||||
start: Schema.optional(Schema.NumberFromString),
|
||||
search: Schema.optional(Schema.String),
|
||||
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 or filters.",
|
||||
}),
|
||||
),
|
||||
}).annotate({ identifier: "V2SessionsQuery" })
|
||||
|
||||
export const SessionGroup = HttpApiGroup.make("v2.session")
|
||||
.add(
|
||||
HttpApiEndpoint.get("sessions", "/api/session", {
|
||||
query: Schema.Union([
|
||||
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.",
|
||||
}),
|
||||
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.",
|
||||
}),
|
||||
directory: Schema.String.pipe(Schema.optional),
|
||||
path: Schema.String.pipe(Schema.optional),
|
||||
workspace: WorkspaceID.pipe(Schema.optional),
|
||||
roots: Schema.Literals(["true", "false"])
|
||||
.pipe(
|
||||
Schema.decodeTo(Schema.Boolean, {
|
||||
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:
|
||||
"Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order.",
|
||||
}),
|
||||
order: Schema.optional(Schema.Never),
|
||||
directory: Schema.optional(Schema.Never),
|
||||
path: Schema.optional(Schema.Never),
|
||||
workspace: Schema.optional(Schema.Never),
|
||||
roots: Schema.optional(Schema.Never),
|
||||
start: Schema.optional(Schema.Never),
|
||||
search: Schema.optional(Schema.Never),
|
||||
}),
|
||||
]).annotate({ identifier: "V2SessionsQuery" }),
|
||||
query: SessionsQuery,
|
||||
success: Schema.Struct({
|
||||
items: Schema.Array(SessionV2.Info),
|
||||
cursor: Schema.Struct({
|
||||
@@ -82,6 +54,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
|
||||
.add(
|
||||
HttpApiEndpoint.post("prompt", "/api/session/:sessionID/prompt", {
|
||||
params: { sessionID: SessionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: Schema.Struct({
|
||||
prompt: Prompt,
|
||||
delivery: SessionV2.Delivery.pipe(Schema.optional),
|
||||
@@ -98,6 +71,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
|
||||
.add(
|
||||
HttpApiEndpoint.post("compact", "/api/session/:sessionID/compact", {
|
||||
params: { sessionID: SessionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: HttpApiSchema.NoContent,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -110,6 +84,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
|
||||
.add(
|
||||
HttpApiEndpoint.post("wait", "/api/session/:sessionID/wait", {
|
||||
params: { sessionID: SessionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: HttpApiSchema.NoContent,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -122,6 +97,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
|
||||
.add(
|
||||
HttpApiEndpoint.get("context", "/api/session/:sessionID/context", {
|
||||
params: { sessionID: SessionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: Schema.Array(SessionMessage.Message),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
|
||||
@@ -5,7 +5,7 @@ import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, Op
|
||||
import { ApiVcsApplyError } from "./instance"
|
||||
import { Authorization } from "../middleware/authorization"
|
||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
|
||||
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
|
||||
import { described } from "./metadata"
|
||||
|
||||
const root = "/experimental/workspace"
|
||||
@@ -40,6 +40,7 @@ export const WorkspaceApi = HttpApi.make("workspace")
|
||||
HttpApiGroup.make("workspace")
|
||||
.add(
|
||||
HttpApiEndpoint.get("adapters", WorkspacePaths.adapters, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Array(WorkspaceAdapterEntry), "Workspace adapters"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -49,6 +50,7 @@ export const WorkspaceApi = HttpApi.make("workspace")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("list", WorkspacePaths.list, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Array(Workspace.Info), "Workspaces"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -58,6 +60,7 @@ export const WorkspaceApi = HttpApi.make("workspace")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("create", WorkspacePaths.list, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: CreatePayload,
|
||||
success: described(Workspace.Info, "Workspace created"),
|
||||
error: HttpApiError.BadRequest,
|
||||
@@ -69,6 +72,7 @@ export const WorkspaceApi = HttpApi.make("workspace")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("syncList", WorkspacePaths.syncList, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(HttpApiSchema.NoContent, "Workspace list synced"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -78,6 +82,7 @@ export const WorkspaceApi = HttpApi.make("workspace")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("status", WorkspacePaths.status, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.Array(Workspace.ConnectionStatus), "Workspace status"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -88,6 +93,7 @@ export const WorkspaceApi = HttpApi.make("workspace")
|
||||
),
|
||||
HttpApiEndpoint.delete("remove", WorkspacePaths.remove, {
|
||||
params: { id: Workspace.Info.fields.id },
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(Schema.UndefinedOr(Workspace.Info), "Workspace removed"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
@@ -98,6 +104,7 @@ export const WorkspaceApi = HttpApi.make("workspace")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("warp", WorkspacePaths.warp, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: WarpPayload,
|
||||
success: described(HttpApiSchema.NoContent, "Session warped"),
|
||||
error: [ApiWorkspaceWarpError, ApiVcsApplyError],
|
||||
|
||||
@@ -6,7 +6,7 @@ import { MCP } from "@/mcp"
|
||||
import { Project } from "@/project/project"
|
||||
import { Session } from "@/session/session"
|
||||
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 { Effect, Option } from "effect"
|
||||
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"
|
||||
|
||||
@@ -34,6 +34,7 @@ export const messageHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.message
|
||||
return handlers.handle(
|
||||
"messages",
|
||||
Effect.fn(function* (ctx) {
|
||||
if (ctx.query.cursor && ctx.query.order !== undefined) return yield* new HttpApiError.BadRequest({})
|
||||
const decoded = yield* Effect.try({
|
||||
try: () => (ctx.query.cursor ? cursor.decode(ctx.query.cursor) : undefined),
|
||||
catch: () => new HttpApiError.BadRequest({}),
|
||||
|
||||
@@ -22,6 +22,31 @@ type SessionCursor = typeof SessionCursor.Type
|
||||
|
||||
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 = {
|
||||
encode(
|
||||
session: SessionV2.Info,
|
||||
@@ -46,10 +71,12 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session
|
||||
.handle(
|
||||
"sessions",
|
||||
Effect.fn(function* (ctx) {
|
||||
if (ctx.query.cursor && hasCursorFilter(ctx.query)) return yield* new HttpApiError.BadRequest({})
|
||||
const decoded = yield* Effect.try({
|
||||
try: () => (ctx.query.cursor ? sessionCursor.decode(ctx.query.cursor) : undefined),
|
||||
catch: () => new HttpApiError.BadRequest({}),
|
||||
})
|
||||
if (hasCursorRoutingMismatch(ctx.query, decoded)) return yield* new HttpApiError.BadRequest({})
|
||||
const order = decoded?.order ?? ctx.query.order ?? "desc"
|
||||
const filters = decoded ?? {
|
||||
directory: ctx.query.directory,
|
||||
|
||||
+13
-1
@@ -9,11 +9,23 @@ import * as Fence from "@/server/shared/fence"
|
||||
import { getWorkspaceRouteSessionID, isLocalWorkspaceRoute, workspaceProxyURL } from "@/server/shared/workspace-routing"
|
||||
import { NotFoundError } from "@/storage/storage"
|
||||
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 { HttpApiMiddleware } from "effect/unstable/httpapi"
|
||||
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 RequestPlan = Data.TaggedEnum<{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user