Compare commits

..
Author SHA1 Message Date
Developer ff6f032faa refactor(mcp): use Effect-native catch + tryPromise instead of try/catch
Per CLAUDE.md style guide ("Avoid try/catch where possible") and the
opencode Effect rules ("Use Effect.tryPromise for promise-based APIs",
"Use Effect.fnUntraced for internal helpers"), replace the plain
async function + try/catch in listTools with an Effect-native
listToolsTolerant that composes via Effect.tryPromise + Effect.catch.

Also drops the no-op `Effect.map((tools) => tools)` identity in defs(),
and extracts a tiny `wrapAsError` helper to remove the duplicated
"err instanceof Error ? ... : new Error(String(err))" expression.

No behavior change. 20/20 tests still green.
2026-05-09 19:15:36 -04:00
Developer d3a69ad910 fix(mcp): tolerate invalid tool output schemas
Closes #26529.

When an MCP server returns a tool whose \`outputSchema\` contains a
broken \`$ref\` (e.g. Google Stitch's \`#/$defs/ScreenInstance\`), the
SDK's typed \`listTools()\` validator throws and opencode marks the
ENTIRE server as failed — losing every other valid tool the server
exposes.

Catch the schema-reference errors and retry with a tolerant schema
(\`looseObject\` + \`outputSchema: z.unknown().optional()\`) via the raw
\`request\` path so the bad tool's schema is accepted as opaque while
the others load normally.

Equivalent fix shape to #26530 (nicolascancino) — kept his approach
since it's correct. Bundles our reproducer test from
\`kit/issue-reproducers\` so the regression is locked in.

Verified red → green → red → green:
- pre-fix: server marked \`failed\`
- post-fix: server stays \`connected\`, valid tool present
2026-05-09 19:00:34 -04:00
Kit LangtonandDeveloper 1c3950111a test(mcp): reproducer for #26529 — outputSchema unresolved refs fail whole server 2026-05-09 18:58:33 -04:00
opencode-agent[bot] 818b56dbd0 chore: generate 2026-05-09 22:47:54 +00:00
Kit LangtonandGitHub 29b5b24787 fix(tui): aggregate bootstrap request failures
TUI bootstrap now reports all parallel fetch failures together instead of losing sibling failures after the first rejection.
2026-05-09 18:46:52 -04:00
Kit LangtonandGitHub 11363170ca fix(sdk): wrap thrown error bodies in Error
SDK throwOnError paths now convert structured response bodies into real Error instances while preserving the original body and status in cause.
2026-05-09 18:46:43 -04:00
Kit LangtonandGitHub ba9e4b67ed fix(tool/read): match permission patterns against worktree-relative path
Read permission checks now use the same worktree-relative path basis as edit/write/apply_patch, so configured patterns apply consistently.
2026-05-09 18:46:29 -04:00
Kit LangtonandGitHub bd1029b19f test(server): cover HttpApi context inheritance
Adds regression coverage for request context inheritance in promptAsync and explicit context provisioning in stream bodies.
2026-05-09 18:46:10 -04:00
43b51f09d0 fix(httpapi): align runtime query schemas with workspace routing params (#26581)
Co-authored-by: Developer <temp@example.com>
2026-05-09 16:50:30 -04:00
opencode-agent[bot] c61ab51886 chore: generate 2026-05-09 20:45:29 +00:00
d373c562f2 fix(session): accept legacy summary diffs (#26579)
Co-authored-by: Developer <temp@example.com>
2026-05-09 16:44:24 -04:00
opencode-agent[bot] 5fa5d876fc chore: generate 2026-05-09 20:31:32 +00:00
805af011c9 test(session): regression test for #26574 + mirror loosening on Vcs.FileDiff (#26578)
Co-authored-by: Developer <temp@example.com>
2026-05-09 16:30:31 -04:00
opencode-agent[bot] 480aa8b23b chore: generate 2026-05-09 20:24:28 +00:00
OpeOginniandGitHub d62442bb5d fix(sessions): allow optional patch field in diff for migrated sessions (#26574) 2026-05-09 16:23:28 -04:00
Kit LangtonandGitHub 8602937a37 test(session): cover workspace-routed messages (#26576) 2026-05-09 16:19:06 -04:00
77da433e0a fix(session): accept routing params in message list (#26569)
Co-authored-by: Developer <temp@example.com>
2026-05-09 16:02:35 -04:00
opencode-agent[bot] ad79f3e0cf chore: generate 2026-05-09 19:59:00 +00:00
6c2dfd2f52 fix(tui): guard messages.data in session.sync against undefined (#26566)
Co-authored-by: Developer <temp@example.com>
2026-05-09 19:58:03 +00:00
SebastianandGitHub 9a8b54fe62 Plugin command API shim (#26564) 2026-05-09 21:56:49 +02:00
DaxandGitHub dcdbdb218f Move schema utilities into core (#26565) 2026-05-09 19:51:09 +00:00
5e49029e70 fix(provider): isolate plugin model mutations (#26561)
Co-authored-by: Developer <temp@example.com>
2026-05-09 15:47:07 -04:00
opencode 19abadaf27 sync release versions for v1.14.44 2026-05-09 19:33:26 +00:00
139 changed files with 1708 additions and 996 deletions
+17 -17
View File
@@ -29,7 +29,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.14.43",
"version": "1.14.44",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -85,7 +85,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.14.43",
"version": "1.14.44",
"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.43",
"version": "1.14.44",
"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.43",
"version": "1.14.44",
"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.43",
"version": "1.14.44",
"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.43",
"version": "1.14.44",
"bin": {
"opencode": "./bin/opencode",
},
@@ -229,7 +229,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.14.43",
"version": "1.14.44",
"dependencies": {
"drizzle-orm": "catalog:",
"effect": "catalog:",
@@ -283,7 +283,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.14.43",
"version": "1.14.44",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -313,7 +313,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.14.43",
"version": "1.14.44",
"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.43",
"version": "1.14.44",
"dependencies": {
"@effect/platform-node": "catalog:",
"effect": "catalog:",
@@ -342,7 +342,7 @@
},
"packages/llm": {
"name": "@opencode-ai/llm",
"version": "1.14.43",
"version": "1.14.44",
"dependencies": {
"@smithy/eventstream-codec": "4.2.14",
"@smithy/util-utf8": "4.2.2",
@@ -360,7 +360,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.14.43",
"version": "1.14.44",
"bin": {
"opencode": "./bin/opencode",
},
@@ -495,7 +495,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.14.43",
"version": "1.14.44",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"effect": "catalog:",
@@ -533,7 +533,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.14.43",
"version": "1.14.44",
"dependencies": {
"cross-spawn": "catalog:",
},
@@ -548,7 +548,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.14.43",
"version": "1.14.44",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -583,7 +583,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.14.43",
"version": "1.14.44",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -632,7 +632,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.14.43",
"version": "1.14.44",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
-240
View File
@@ -1,240 +0,0 @@
# Denotational Design: Part 1
## The Basic Idea
Denotational design means designing an abstraction by first asking:
> What does this thing mean?
Before choosing data structures, algorithms, callbacks, queues, caches, or runtime behavior, we give each core type a simple meaning. Then we define operations in terms of that meaning.
The implementation can be clever later. The meaning should be simple now.
## API vs Implementation vs Meaning
When designing a library, there are three related but different things:
| Layer | Question | Example |
| --- | --- | --- |
| API | What can users call? | `map(signal, f)` |
| Implementation | How does it run? | subscriptions, graphs, caching |
| Denotation | What does it mean? | a value changing over time |
Most designs jump between API and implementation. Denotational design adds the missing middle: a precise meaning.
## A Tiny FRP Example
Functional Reactive Programming is a good example because it starts with a very simple denotation.
A time-varying value, often called a `Behavior`, can be understood as:
```ts
Behavior<A> = Time -> A
```
That says:
> A `Behavior<A>` means: give me a time, and I can tell you the `A` value at that time.
This does not mean the implementation literally stores an infinite function. It means this is the specification. The implementation may use events, subscriptions, incremental recomputation, dependency graphs, or caching.
## Operations Follow From Meaning
Once we know what `Behavior<A>` means, operations become easier to define.
For example, `map` transforms the value inside a behavior:
```ts
map: (A -> B) -> Behavior<A> -> Behavior<B>
```
Its meaning is:
```ts
map(f, behavior)(time) = f(behavior(time))
```
That is the whole specification.
Similarly, a constant behavior:
```ts
constant: A -> Behavior<A>
```
means:
```ts
constant(value)(time) = value
```
The definitions are simple because the denotation is simple.
## Why This Helps
Denotational design is useful because it separates essence from machinery.
It helps library users because the abstraction has a clear mental model.
It helps implementers because correctness has a target independent of implementation details.
It helps API design because awkward operations become easier to spot. If an operation has no clean meaning, it may be exposing implementation machinery instead of domain meaning.
## The Design Loop
A practical denotational design loop looks like this:
1. Name the core type.
2. Write down what values of that type mean.
3. Define each operation by how it transforms meanings.
4. Notice what laws naturally follow.
5. Choose an implementation that preserves the meaning.
The key discipline is to delay implementation concerns until after the meaning is clear.
## Denotation Is Not Implementation
A denotation can look like an implementation because it is concrete enough to write down:
```ts
Behavior<A> = Time -> A
```
But this equation is not saying that a real FRP system must store a function from every possible time to an `A` value.
It is saying that this is the model we use to understand a behavior.
The implementation might use callbacks, mutable cells, event queues, dependency graphs, caching, sampling, or incremental recomputation. Those choices are representation. The denotation is the meaning those representations must preserve.
So there are two different questions:
| Question | Answer |
| --- | --- |
| What does a behavior mean? | A function from time to value |
| How do we run it efficiently? | Some concrete representation and algorithm |
A denotation should be simple and precise. An implementation should be executable and efficient. They do not have to be the same thing.
## Operations At Two Levels
Conal Elliott describes a useful pattern:
> The meaning of each method corresponds to the same method for the meaning.
This sentence is subtle because there are three things in play:
1. An abstract type, like `Behavior<A>`.
2. A meaning type, like `Time -> A`.
3. A meaning function that translates from the abstract type to the meaning type.
For behaviors, Conal often calls the meaning function `at`:
```ts
at: Behavior<A> -> (Time -> A)
```
Read this as:
> `at(behavior)` gives the meaning of `behavior`.
So `at` is just the specific FRP name for the more general idea:
```ts
meaning: Abstract<A> -> Model<A>
```
Now we can talk about operations.
There are two versions of "the same" operation.
One operation belongs to the abstract API:
```ts
mapBehavior: (A -> B) -> Behavior<A> -> Behavior<B>
```
The other operation belongs to the semantic model:
```ts
mapFunction: (A -> B) -> (Time -> A) -> (Time -> B)
```
They are not literally the same function. They live at different levels. But they are the same conceptual operation: mapping a pure function over a value inside some structure.
The homomorphism law says that translating meanings should not care which order we take these steps.
Path 1: operate first, then take the meaning.
```ts
behavior
-> mapBehavior(f, behavior)
-> at(mapBehavior(f, behavior))
```
Path 2: take the meaning first, then operate on the meaning.
```ts
behavior
-> at(behavior)
-> mapFunction(f, at(behavior))
```
The law says both paths produce the same meaning:
```ts
at(mapBehavior(f, behavior))
=
mapFunction(f, at(behavior))
```
Using the generic word `meaning`, the same law is:
```ts
meaning(operationOnAbstraction(x))
=
operationOnMeaning(meaning(x))
```
In pointwise form:
```ts
at(mapBehavior(f, behavior))(time)
=
f(at(behavior)(time))
```
So the operation on the abstraction must mean the corresponding operation on the model.
That is the homomorphism idea: the meaning function preserves structure. It translates the abstract operation into the corresponding model operation.
```ts
// Same shape, different levels:
mapBehavior(f, behavior) // abstract level
mapFunction(f, at(behavior)) // meaning/model level
// Connected by the meaning function:
at(mapBehavior(f, behavior))
=
mapFunction(f, at(behavior))
```
This gives us a correctness rule. If `Behavior` claims to support `map`, its `map` should behave like `map` on its denotation, which is a function of time.
## The Main Lesson
Denotational design does not ask, "How do we build this?" first.
It asks:
> What are we talking about?
For FRP, the answer begins with:
```ts
Behavior<A> = Time -> A
```
That small equation gives the abstraction a center of gravity. Everything else can be designed around it.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.14.43",
"version": "1.14.44",
"description": "",
"type": "module",
"exports": {
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.14.43",
"version": "1.14.44",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.14.43",
"version": "1.14.44",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.14.43",
"version": "1.14.44",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.14.43",
"version": "1.14.44",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.14.43",
"version": "1.14.44",
"name": "@opencode-ai/core",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.14.43",
"version": "1.14.44",
"type": "module",
"license": "MIT",
"homepage": "https://opencode.ai",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.14.43",
"version": "1.14.44",
"private": true,
"type": "module",
"license": "MIT",
+6 -6
View File
@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
version = "1.14.43"
version = "1.14.44"
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.43/opencode-darwin-arm64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.44/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.43/opencode-darwin-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.44/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.43/opencode-linux-arm64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.44/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.43/opencode-linux-x64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.44/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.43/opencode-windows-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.44/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.14.43",
"version": "1.14.44",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.14.43",
"version": "1.14.44",
"name": "@opencode-ai/http-recorder",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.14.43",
"version": "1.14.44",
"name": "@opencode-ai/llm",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.14.43",
"version": "1.14.44",
"name": "opencode",
"type": "module",
"license": "MIT",
+2 -2
View File
@@ -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
```
+3 -3
View File
@@ -8,7 +8,7 @@ Zod-first definitions to Effect Schema with Zod compatibility shims.
Use Effect Schema as the source of truth for domain models, IDs, inputs,
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,67 @@
# TUI Command Shim Removal
Problem:
- v1 keeps a deprecated `api.command` TUI plugin shim so older plugins do not fail during initialization
- v2 should expose only the keymap command API
- tests and fixtures should not encode legacy command behavior as expected behavior
## Remove Public Types
In `packages/plugin/src/tui.ts`, remove:
- `TuiCommand`
- `TuiCommandApi`
- `TuiPluginApi.command`
Keep `api.keymap` as the only TUI command registration and execution surface.
## Remove Runtime Shim
Delete `packages/opencode/src/cli/cmd/tui/plugin/command-shim.ts`.
In `packages/opencode/src/cli/cmd/tui/plugin/api.tsx`, remove:
- the `createCommandShim` import
- the `command: createCommandShim(...)` field from `createTuiApi(...)`
In `packages/opencode/src/cli/cmd/tui/plugin/runtime.ts`, remove:
- the `createCommandShim` import
- the `command: createCommandShim(...)` field from `pluginApi(...)`
## Migration Target
Plugin authors should replace old calls with keymap calls:
```ts
api.keymap.registerLayer({
commands: [
{
name: "plugin.command",
title: "Plugin Command",
namespace: "palette",
slashName: "plugin",
run() {
api.ui.dialog.clear()
},
},
],
bindings: [{ key: "ctrl+shift+p", cmd: "plugin.command" }],
})
```
Direct replacements:
- `api.command.register(cb)` -> `api.keymap.registerLayer({ commands, bindings })`
- `api.command.trigger(name)` -> `api.keymap.dispatchCommand(name)`
- `api.command.show()` -> `api.keymap.dispatchCommand("command.palette.show")`
- `onSelect(dialog)` -> use `api.ui.dialog` from the plugin API closure
## Verification
After removal, run from package directories:
- `bun typecheck` in `packages/plugin`
- `bun typecheck` in `packages/opencode`
- TUI plugin loader tests in `packages/opencode` if runtime plugin API wiring changed
+2 -2
View File
@@ -24,8 +24,8 @@ import { Effect, Context, Layer, Schema } from "effect"
import { InstanceState } from "@/effect/instance-state"
import * 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 }
+2 -2
View File
@@ -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"
+3 -3
View File
@@ -23,11 +23,11 @@ function span(id: string, value: { value: string; start: number; end: number })
}
}
function diff(kind: string, diffs: { file: string; patch: string }[] | undefined) {
function diff(kind: string, diffs: { file?: string; patch?: string }[] | undefined) {
return diffs?.map((item, i) => ({
...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),
}))
}
@@ -275,7 +275,6 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
}
const api = createTuiApi({
command,
tuiConfig,
dialog,
keymap,
@@ -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 -1
View File
@@ -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
@@ -1,10 +1,4 @@
import type {
TuiCommand,
TuiDialogSelectOption,
TuiPluginApi,
TuiRouteDefinition,
TuiSlotProps,
} from "@opencode-ai/plugin/tui"
import type { TuiDialogSelectOption, TuiPluginApi, TuiRouteDefinition, TuiSlotProps } from "@opencode-ai/plugin/tui"
import type { useEvent } from "@tui/context/event"
import type { useRoute } from "@tui/context/route"
import type { useSDK } from "@tui/context/sdk"
@@ -23,7 +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 type { useCommandPalette } from "../context/command-palette"
import { createCommandShim } from "./command-shim"
type RouteEntry = {
key: symbol
@@ -33,7 +27,6 @@ type RouteEntry = {
export type RouteMap = Map<string, RouteEntry[]>
type Input = {
command: ReturnType<typeof useCommandPalette>
tuiConfig: TuiConfig.Resolved
dialog: ReturnType<typeof useDialog>
keymap: ReturnType<typeof useOpencodeKeymap>
@@ -49,54 +42,6 @@ type Input = {
renderer: TuiPluginApi["renderer"]
}
let warnedLegacyCommand = false
function warnLegacyCommandApi() {
if (warnedLegacyCommand) return
warnedLegacyCommand = true
console.warn("[tui.plugin] api.command is deprecated; use api.keymap.registerLayer({ commands, bindings }) instead")
}
function commandBinding(command: TuiCommand, input: Input) {
if (!command.keybind) return []
return input.tuiConfig.keybinds.get(command.keybind).map((binding) => ({ ...binding, cmd: command.value }))
}
function legacyCommandApi(input: Input): TuiPluginApi["command"] {
return {
register(cb) {
warnLegacyCommandApi()
const list = cb()
return input.keymap.registerLayer({
commands: list.map((command) => ({
name: command.value,
title: command.title,
desc: command.description,
category: command.category,
namespace: "palette",
suggested: command.suggested,
hidden: command.hidden,
enabled: command.enabled,
slashName: command.slash?.name,
slashAliases: command.slash?.aliases,
run() {
command.onSelect?.()
},
})),
bindings: list.flatMap((command) => commandBinding(command, input)),
})
},
trigger(value) {
warnLegacyCommandApi()
input.keymap.dispatchCommand(value)
},
show() {
warnLegacyCommandApi()
input.command.show()
},
}
}
function routeRegister(routes: RouteMap, list: TuiRouteDefinition[], bump: () => void) {
const key = Symbol()
for (const item of list) {
@@ -203,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] ?? []
@@ -256,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)
@@ -265,7 +214,6 @@ export function createTuiApi(input: Input): TuiPluginApi {
},
},
keymap: input.keymap,
command: legacyCommandApi(input),
route: {
register(list) {
return routeRegister(input.routes, list, input.bump)
@@ -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 })
@@ -563,18 +564,6 @@ function pluginApi(runtime: RuntimeState, plugin: PluginEntry, scope: PluginScop
const keymap = createScopedKeymap(api.keymap, scope)
const command: TuiPluginApi["command"] = {
register(cb) {
return scope.track(api.command.register(cb))
},
trigger(value) {
api.command.trigger(value)
},
show() {
api.command.show()
},
}
let count = 0
const slots: TuiPluginApi["slots"] = {
@@ -588,9 +577,10 @@ 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,
command,
route,
ui: api.ui,
tuiConfig: api.tuiConfig,
+2 -2
View File
@@ -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 -2
View File
@@ -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"
+2 -2
View File
@@ -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"
+3 -3
View File
@@ -22,8 +22,8 @@ import { InstanceState } from "@/effect/instance-state"
import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "effect"
import { 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)),
+2 -2
View File
@@ -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),
+2 -2
View File
@@ -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" })
+2 -2
View File
@@ -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({
+2 -2
View File
@@ -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" }),
+2 -2
View File
@@ -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
+1 -1
View File
@@ -3,7 +3,7 @@ export * as ConfigParse from "./parse"
import { type ParseError as JsoncParseError, parse as parseJsoncImpl, printParseErrorCode } from "jsonc-parser"
import { 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>
+2 -2
View File
@@ -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 -2
View File
@@ -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) })))
+2 -2
View File
@@ -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 Model = Schema.Struct({
id: Schema.optional(Schema.String),
+2 -2
View File
@@ -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({
+2 -2
View File
@@ -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({
+2 -2
View File
@@ -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,8 +1,8 @@
import { Schema } from "effect"
import { Identifier } from "@/id/id"
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"
const workspaceIdSchema = Schema.String.annotate({ [ZodOverride]: Identifier.schema("workspace") }).pipe(
Schema.brand("WorkspaceID"),
+1 -1
View File
@@ -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,
+2 -2
View File
@@ -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,
+2 -2
View File
@@ -11,8 +11,8 @@ import { Global } from "@opencode-ai/core/global"
import * as Log from "@opencode-ai/core/util/log"
import { 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"
+2 -2
View File
@@ -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" })
+2 -2
View File
@@ -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" })
+47 -7
View File
@@ -30,12 +30,30 @@ 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 = z.looseObject({
name: z.string(),
description: z.string().optional(),
inputSchema: z
.object({
type: z.literal("object"),
properties: z.record(z.string(), z.unknown()).optional(),
required: z.array(z.string()).optional(),
})
.catchall(z.unknown()),
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 +137,32 @@ function remoteURL(key: string, value: string) {
log.warn("invalid remote mcp url", { key })
}
function isSchemaReferenceError(err: unknown) {
return err instanceof Error && /can't resolve reference|schema.*reference|reference.*schema/i.test(err.message)
}
const wrapAsError = (err: unknown) => (err instanceof Error ? err : new Error(String(err)))
function listToolsTolerant(key: string, client: MCPClient, timeout: number) {
return Effect.tryPromise({
try: () => client.listTools(undefined, { timeout }),
catch: wrapAsError,
}).pipe(
Effect.map((result) => result.tools),
Effect.catch((err) => {
if (!isSchemaReferenceError(err)) return Effect.fail(err)
log.warn("failed to validate MCP tool output schemas, retrying without output schema validation", {
key,
error: err,
})
return Effect.tryPromise({
try: () => client.request({ method: "tools/list" }, TolerantListToolsResultSchema, { timeout }),
catch: wrapAsError,
}).pipe(Effect.map((result) => result.tools as MCPToolDef[]))
}),
)
}
// Convert MCP tool definition to AI SDK Tool type
function convertMcpTool(mcpTool: MCPToolDef, client: MCPClient, timeout?: number): Tool {
const inputSchema = mcpTool.inputSchema
@@ -151,11 +195,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 listToolsTolerant(key, client, timeout ?? DEFAULT_TIMEOUT).pipe(
Effect.catch((err) => {
log.error("failed to get tools from client", { key, error: err })
return Effect.succeed(undefined)
+2 -2
View File
@@ -7,9 +7,9 @@ import { MessageID, SessionID } from "@/session/schema"
import { PermissionTable } from "@/session/session.sql"
import { 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"
+2 -2
View File
@@ -1,8 +1,8 @@
import { Schema } from "effect"
import { Identifier } from "@/id/id"
import { zod, ZodOverride } from "@/util/effect-zod"
import { Newtype } from "@/util/schema"
import { zod, ZodOverride } from "@opencode-ai/core/effect-zod"
import { Newtype } from "@opencode-ai/core/schema"
export class PermissionID extends Newtype<PermissionID>()(
"PermissionID",
+2 -2
View File
@@ -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" })
+2 -2
View File
@@ -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 -3
View File
@@ -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,7 +230,10 @@ export type Info = Schema.Schema.Type<typeof Info>
export const FileDiff = Schema.Struct({
file: Schema.String,
patch: Schema.String,
// 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: NonNegativeInt,
deletions: NonNegativeInt,
status: Schema.optional(Schema.Literals(["added", "deleted", "modified"])),
+2 -2
View File
@@ -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"
+3 -3
View File
@@ -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,7 +24,7 @@ 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"
@@ -1162,7 +1162,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,
+2 -2
View File
@@ -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"))
+2 -2
View File
@@ -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" })
+2 -2
View File
@@ -1,8 +1,8 @@
import { Schema } from "effect"
import { Identifier } from "@/id/id"
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"
const ptyIdSchema = Schema.String.annotate({ [ZodOverride]: Identifier.schema("pty") }).pipe(Schema.brand("PtyID"))
+1 -1
View File
@@ -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)
+2 -2
View File
@@ -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" })
+2 -2
View File
@@ -1,8 +1,8 @@
import { Schema } from "effect"
import { Identifier } from "@/id/id"
import { zod, ZodOverride } from "@/util/effect-zod"
import { Newtype } from "@/util/schema"
import { zod, ZodOverride } from "@opencode-ai/core/effect-zod"
import { Newtype } from "@opencode-ai/core/schema"
export class QuestionID extends Newtype<QuestionID>()(
"QuestionID",
@@ -2,14 +2,16 @@ import { Auth } from "@/auth"
import { ProviderID } from "@/provider/schema"
import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { withWorkspaceRouting } from "../query"
import { described } from "./metadata"
const AuthParams = Schema.Struct({
providerID: ProviderID,
})
const LogQuery = withWorkspaceRouting({})
const LogQuery = Schema.Struct({
directory: Schema.optional(Schema.String),
workspace: Schema.optional(Schema.String),
})
export const LogInput = Schema.Struct({
service: Schema.String.annotate({ description: "Service name for the log entry" }),
@@ -3,13 +3,12 @@ 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 { NonNegativeInt } from "@opencode-ai/core/schema"
import { Schema, SchemaGetter } 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 { withWorkspaceRouting } from "../query"
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQueryFields } from "../middleware/workspace-routing"
import { described } from "./metadata"
const ConsoleStateResponse = Schema.Struct({
@@ -43,7 +42,8 @@ const ToolListItem = Schema.Struct({
parameters: Schema.Unknown,
}).annotate({ identifier: "ToolListItem" })
const ToolList = Schema.Array(ToolListItem).annotate({ identifier: "ToolList" })
export const ToolListQuery = withWorkspaceRouting({
export const ToolListQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
provider: ProviderID,
model: ModelID,
})
@@ -55,8 +55,8 @@ const QueryBoolean = Schema.Literals(["true", "false"]).pipe(
}),
)
const WorktreeList = Schema.Array(Schema.String)
export const SessionListQuery = withWorkspaceRouting({
directory: Schema.optional(Schema.String),
export const SessionListQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
roots: Schema.optional(QueryBoolean),
start: Schema.optional(Schema.NumberFromString),
cursor: Schema.optional(Schema.NumberFromString),
@@ -5,19 +5,21 @@ 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 { withWorkspaceRouting } from "../query"
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQueryFields } from "../middleware/workspace-routing"
import { described } from "./metadata"
export const FileQuery = withWorkspaceRouting({
export const FileQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
path: Schema.String,
})
export const FindTextQuery = withWorkspaceRouting({
export const FindTextQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
pattern: Schema.String,
})
export const FindFileQuery = withWorkspaceRouting({
export const FindFileQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
query: Schema.String,
dirs: Schema.optional(Schema.Literals(["true", "false"])),
type: Schema.optional(Schema.Literals(["file", "directory"])),
@@ -26,7 +28,8 @@ export const FindFileQuery = withWorkspaceRouting({
),
})
export const FindSymbolQuery = withWorkspaceRouting({
export const FindSymbolQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
query: Schema.String,
})
@@ -8,8 +8,7 @@ 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 { withWorkspaceRouting } from "../query"
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQueryFields } from "../middleware/workspace-routing"
import { described } from "./metadata"
const PathInfo = Schema.Struct({
@@ -20,7 +19,8 @@ const PathInfo = Schema.Struct({
directory: Schema.String,
}).annotate({ identifier: "Path" })
export const VcsDiffQuery = withWorkspaceRouting({
export const VcsDiffQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
mode: Vcs.Mode,
})
@@ -5,13 +5,16 @@ 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, 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,
@@ -14,8 +14,7 @@ import { Schema, SchemaGetter, 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 { withWorkspaceRouting } from "../query"
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQueryFields } from "../middleware/workspace-routing"
import { ApiNotFoundError } from "../errors"
import { described } from "./metadata"
@@ -26,8 +25,8 @@ const QueryBoolean = Schema.Literals(["true", "false"]).pipe(
encode: SchemaGetter.transform((value) => (value ? "true" : "false")),
}),
)
export const ListQuery = withWorkspaceRouting({
directory: Schema.optional(Schema.String),
export const ListQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
scope: Schema.optional(Schema.Literals(["project"])),
path: Schema.optional(Schema.String),
roots: Schema.optional(QueryBoolean),
@@ -35,8 +34,12 @@ export const ListQuery = withWorkspaceRouting({
search: Schema.optional(Schema.String),
limit: Schema.optional(Schema.NumberFromString),
})
export const DiffQuery = withWorkspaceRouting(Struct.omit(SessionSummary.DiffInput.fields, ["sessionID"]))
export const MessagesQuery = withWorkspaceRouting({
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),
})
@@ -1,4 +1,4 @@
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"
@@ -3,7 +3,6 @@ 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 "../../query"
export const MessageGroup = HttpApiGroup.make("v2.message")
.add(
@@ -11,7 +10,6 @@ export const MessageGroup = HttpApiGroup.make("v2.message")
params: { sessionID: SessionID },
query: Schema.Union([
Schema.Struct({
...WorkspaceRoutingQueryFields,
limit: Schema.optional(
Schema.NumberFromString.check(
Schema.isInt(),
@@ -28,7 +26,6 @@ export const MessageGroup = HttpApiGroup.make("v2.message")
cursor: Schema.optional(Schema.Never),
}),
Schema.Struct({
...WorkspaceRoutingQueryFields,
limit: Schema.optional(
Schema.NumberFromString.check(
Schema.isInt(),
@@ -6,14 +6,12 @@ import { SessionV2 } from "@/v2/session"
import { Schema, SchemaGetter } from "effect"
import { HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../../middleware/authorization"
import { WorkspaceRoutingQueryFields } from "../../query"
export const SessionGroup = HttpApiGroup.make("v2.session")
.add(
HttpApiEndpoint.get("sessions", "/api/session", {
query: Schema.Union([
Schema.Struct({
...WorkspaceRoutingQueryFields,
limit: Schema.optional(
Schema.NumberFromString.check(
Schema.isInt(),
@@ -26,6 +24,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
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"])
@@ -41,7 +40,6 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
cursor: Schema.optional(Schema.Never),
}),
Schema.Struct({
...WorkspaceRoutingQueryFields,
limit: Schema.optional(
Schema.NumberFromString.check(
Schema.isInt(),
@@ -56,7 +54,9 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
"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),
@@ -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"
@@ -9,11 +9,21 @@ 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),
}
type RemoteTarget = Extract<Target, { type: "remote" }>
type RequestPlan = Data.TaggedEnum<{
@@ -1,13 +0,0 @@
import { Schema } from "effect"
export const WorkspaceRoutingQueryFields = {
directory: Schema.optional(Schema.String),
workspace: Schema.optional(Schema.String),
}
export function withWorkspaceRouting<T extends Schema.Struct.Fields>(fields: T) {
return Schema.Struct({
...WorkspaceRoutingQueryFields,
...fields,
})
}
+2 -2
View File
@@ -23,8 +23,8 @@ import type { SystemError } from "bun"
import type { Provider } from "@/provider/provider"
import { ModelID, ProviderID } from "@/provider/schema"
import { Effect, Schema, Types } from "effect"
import { zod, ZodOverride } from "@/util/effect-zod"
import { NonNegativeInt, withStatics } from "@/util/schema"
import { zod, ZodOverride } from "@opencode-ai/core/effect-zod"
import { NonNegativeInt, withStatics } from "@opencode-ai/core/schema"
import { namedSchemaError } from "@/util/named-schema-error"
import * as EffectLogger from "@opencode-ai/core/effect/logger"
+2 -2
View File
@@ -1,8 +1,8 @@
import { Schema } from "effect"
import { SessionID } from "./schema"
import { ModelID, ProviderID } from "../provider/schema"
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"
import { namedSchemaError } from "@/util/named-schema-error"
export const OutputLengthError = namedSchemaError("MessageOutputLengthError", {})
+3 -3
View File
@@ -1,6 +1,6 @@
import path from "path"
import os from "os"
import * as EffectZod from "@/util/effect-zod"
import * as EffectZod from "@opencode-ai/core/effect-zod"
import { SessionID, MessageID, PartID } from "./schema"
import { MessageV2 } from "./message-v2"
import * as Log from "@opencode-ai/core/util/log"
@@ -46,8 +46,8 @@ import { Truncate } from "@/tool/truncate"
import { decodeDataUrl } from "@/util/data-url"
import { Process } from "@/util/process"
import { Cause, Effect, Exit, Latch, Layer, Option, Scope, Context, Schema, Types } 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 EffectLogger from "@opencode-ai/core/effect/logger"
import { InstanceState } from "@/effect/instance-state"
import { TaskTool, type TaskPromptOps } from "@/tool/task"
+2 -2
View File
@@ -4,8 +4,8 @@ import { Snapshot } from "../snapshot"
import { Storage } from "@/storage/storage"
import { SyncEvent } from "../sync"
import * as Log from "@opencode-ai/core/util/log"
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 Session from "./session"
import { MessageV2 } from "./message-v2"
import { SessionID, MessageID, PartID } from "./schema"
+2 -2
View File
@@ -1,8 +1,8 @@
import { Schema } from "effect"
import { Identifier } from "@/id/id"
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"
export const SessionID = Schema.String.annotate({ [ZodOverride]: Identifier.schema("session") }).pipe(
Schema.brand("SessionID"),
+2 -2
View File
@@ -37,8 +37,8 @@ import type { Provider } from "@/provider/provider"
import { Permission } from "@/permission"
import { Global } from "@opencode-ai/core/global"
import { Effect, Layer, Option, Context, Schema, Types } from "effect"
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"
const log = Log.create({ service: "session" })
+2 -2
View File
@@ -2,8 +2,8 @@ import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import { InstanceState } from "@/effect/instance-state"
import { SessionID } from "./schema"
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"
import { Effect, Layer, Context, Schema } from "effect"
import z from "zod"
+3 -2
View File
@@ -2,8 +2,8 @@ import { Effect, Layer, Context, Schema } from "effect"
import { Bus } from "@/bus"
import { Snapshot } from "@/snapshot"
import { Storage } from "@/storage/storage"
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 Session from "./session"
import { MessageV2 } from "./message-v2"
import { SessionID, MessageID } from "./schema"
@@ -134,6 +134,7 @@ export const layer = Layer.effect(
.read<Snapshot.FileDiff[]>(["session_diff", input.sessionID])
.pipe(Effect.catch(() => Effect.succeed([] as Snapshot.FileDiff[])))
const next = diffs.map((item) => {
if (item.file === undefined) return item
const file = unquoteGitPath(item.file)
if (file === item.file) return item
return { ...item, file }
+2 -2
View File
@@ -1,8 +1,8 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import { SessionID } from "./schema"
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 { Effect, Layer, Context, Schema } from "effect"
import z from "zod"
import { Database } from "@/storage/db"
+2 -2
View File
@@ -2,8 +2,8 @@ import path from "path"
import { pathToFileURL } from "url"
import z from "zod"
import { Effect, Layer, Context, 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 { NamedError } from "@opencode-ai/core/util/error"
import type { Agent } from "@/agent/agent"
import { Bus } from "@/bus"
+7 -4
View File
@@ -10,8 +10,8 @@ import { Hash } from "@opencode-ai/core/util/hash"
import { Config } from "@/config/config"
import { Global } from "@opencode-ai/core/global"
import * as Log from "@opencode-ai/core/util/log"
import { NonNegativeInt, withStatics } from "@/util/schema"
import { zod } from "@/util/effect-zod"
import { NonNegativeInt, withStatics } from "@opencode-ai/core/schema"
import { zod } from "@opencode-ai/core/effect-zod"
export const Patch = Schema.Struct({
hash: Schema.String,
@@ -20,8 +20,11 @@ export const Patch = Schema.Struct({
export type Patch = typeof Patch.Type
export const FileDiff = Schema.Struct({
file: Schema.String,
patch: Schema.String,
// Optional because legacy/imported `summary_diffs` on disk may omit
// file details and patch text. Required Schema rejected the whole
// session response and broke session loading on Desktop.
file: Schema.optional(Schema.String),
patch: Schema.optional(Schema.String),
additions: NonNegativeInt,
deletions: NonNegativeInt,
status: Schema.optional(Schema.Literals(["added", "deleted", "modified"])),
+1 -1
View File
@@ -5,7 +5,7 @@ import { NamedError } from "@opencode-ai/core/util/error"
import z from "zod"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Effect, Exit, Layer, Option, RcMap, Schema, Context, TxReentrantLock } from "effect"
import { NonNegativeInt } from "@/util/schema"
import { NonNegativeInt } from "@opencode-ai/core/schema"
import { Git } from "@/git"
const log = Log.create({ service: "storage" })
+1 -1
View File
@@ -9,7 +9,7 @@ import type { WorkspaceID } from "@/control-plane/schema"
import { EventID } from "./schema"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Context, Effect, Layer, Schema as EffectSchema } from "effect"
import type { DeepMutable } from "@/util/schema"
import type { DeepMutable } from "@opencode-ai/core/schema"
import { makeRuntime } from "@/effect/run-service"
import { serviceUse } from "@/effect/service-use"
import { InstanceState } from "@/effect/instance-state"
+2 -2
View File
@@ -1,8 +1,8 @@
import { Schema } from "effect"
import { Identifier } from "@/id/id"
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"
export const EventID = Schema.String.annotate({ [ZodOverride]: Identifier.schema("event") }).pipe(
Schema.brand("EventID"),
+2 -2
View File
@@ -1,5 +1,5 @@
import { Effect, Option, Schema, Scope } from "effect"
import { NonNegativeInt } from "@/util/schema"
import { NonNegativeInt } from "@opencode-ai/core/schema"
import { createReadStream } from "fs"
import * as path from "path"
import { createInterface } from "readline"
@@ -178,7 +178,7 @@ export const ReadTool = Tool.define(
yield* ctx.ask({
permission: "read",
patterns: [filepath],
patterns: [path.relative(instance.worktree, filepath)],
always: ["*"],
metadata: {},
})
+1 -1
View File
@@ -17,7 +17,7 @@ import { Config } from "@/config/config"
import { type ToolContext as PluginToolContext, type ToolDefinition } from "@opencode-ai/plugin"
import { Schema } from "effect"
import z from "zod"
import { ZodOverride } from "@/util/effect-zod"
import { ZodOverride } from "@opencode-ai/core/effect-zod"
import { Plugin } from "../plugin"
import { Provider } from "@/provider/provider"
import { ProviderID, type ModelID } from "../provider/schema"
+2 -2
View File
@@ -1,8 +1,8 @@
import { Schema } from "effect"
import { Identifier } from "@/id/id"
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"
const toolIdSchema = Schema.String.annotate({ [ZodOverride]: Identifier.schema("tool") }).pipe(Schema.brand("ToolID"))
+1 -1
View File
@@ -1,6 +1,6 @@
import { Schema } from "effect"
import DESCRIPTION from "./shell.txt"
import { PositiveInt } from "@/util/schema"
import { PositiveInt } from "@opencode-ai/core/schema"
import { Global } from "@opencode-ai/core/global"
import { ShellID } from "./id"
@@ -1,6 +1,6 @@
import { Schema } from "effect"
import z from "zod"
import { zod } from "@/util/effect-zod"
import { zod } from "@opencode-ai/core/effect-zod"
/**
* Create a Schema-backed NamedError-shaped class.
+1 -1
View File
@@ -1,7 +1,7 @@
import path from "path"
import { Effect, Layer, Option, Schema, Context, SynchronizedRef } from "effect"
import { Identifier } from "@opencode-ai/core/util/identifier"
import { NonNegativeInt, withStatics } from "@/util/schema"
import { NonNegativeInt, withStatics } from "@opencode-ai/core/schema"
import { Global } from "@opencode-ai/core/global"
import { AppFileSystem } from "@opencode-ai/core/filesystem"

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