Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e53975314 |
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.14.43",
|
||||
"version": "1.14.42",
|
||||
"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.42",
|
||||
"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.42",
|
||||
"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.42",
|
||||
"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.42",
|
||||
"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.42",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -229,7 +229,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.14.43",
|
||||
"version": "1.14.42",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -283,7 +283,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.14.43",
|
||||
"version": "1.14.42",
|
||||
"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.42",
|
||||
"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.42",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -342,7 +342,7 @@
|
||||
},
|
||||
"packages/llm": {
|
||||
"name": "@opencode-ai/llm",
|
||||
"version": "1.14.43",
|
||||
"version": "1.14.42",
|
||||
"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.42",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -495,7 +495,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.14.43",
|
||||
"version": "1.14.42",
|
||||
"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.42",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -548,7 +548,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.14.43",
|
||||
"version": "1.14.42",
|
||||
"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.42",
|
||||
"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.42",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.14.43",
|
||||
"version": "1.14.42",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.14.43",
|
||||
"version": "1.14.42",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.14.43",
|
||||
"version": "1.14.42",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.14.43",
|
||||
"version": "1.14.42",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.14.43",
|
||||
"version": "1.14.42",
|
||||
"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.43",
|
||||
"version": "1.14.42",
|
||||
"name": "@opencode-ai/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.14.43",
|
||||
"version": "1.14.42",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.14.43",
|
||||
"version": "1.14.42",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The open source coding agent."
|
||||
version = "1.14.43"
|
||||
version = "1.14.42"
|
||||
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.42/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.42/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.42/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.42/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.42/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.14.43",
|
||||
"version": "1.14.42",
|
||||
"$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.43",
|
||||
"version": "1.14.42",
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.14.43",
|
||||
"version": "1.14.42",
|
||||
"name": "@opencode-ai/llm",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1 +1 @@
|
||||
ALTER TABLE `workspace` ADD `time_used` integer NOT NULL DEFAULT 0;
|
||||
ALTER TABLE `workspace` ADD `time_used` integer NOT NULL;
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.14.43",
|
||||
"version": "1.14.42",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -275,7 +275,6 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||
}
|
||||
|
||||
const api = createTuiApi({
|
||||
command,
|
||||
tuiConfig,
|
||||
dialog,
|
||||
keymap,
|
||||
|
||||
@@ -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,6 @@ 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"
|
||||
|
||||
type RouteEntry = {
|
||||
key: symbol
|
||||
@@ -33,7 +26,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 +41,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) {
|
||||
@@ -265,7 +209,6 @@ export function createTuiApi(input: Input): TuiPluginApi {
|
||||
},
|
||||
},
|
||||
keymap: input.keymap,
|
||||
command: legacyCommandApi(input),
|
||||
route: {
|
||||
register(list) {
|
||||
return routeRegister(input.routes, list, input.bump)
|
||||
|
||||
@@ -563,18 +563,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"] = {
|
||||
@@ -590,7 +578,6 @@ function pluginApi(runtime: RuntimeState, plugin: PluginEntry, scope: PluginScop
|
||||
app: api.app,
|
||||
keys: api.keys,
|
||||
keymap,
|
||||
command,
|
||||
route,
|
||||
ui: api.ui,
|
||||
tuiConfig: api.tuiConfig,
|
||||
|
||||
@@ -935,16 +935,6 @@ export const ConfigProvidersResult = Schema.Struct({
|
||||
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type ConfigProvidersResult = Types.DeepMutable<Schema.Schema.Type<typeof ConfigProvidersResult>>
|
||||
|
||||
export function toPublicInfo(provider: Info): Info {
|
||||
return JSON.parse(
|
||||
JSON.stringify(provider, (_, value) => {
|
||||
if (typeof value === "function" || typeof value === "symbol" || value === undefined) return undefined
|
||||
if (typeof value === "bigint") return value.toString()
|
||||
return value
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export function defaultModelIDs<T extends { models: Record<string, { id: string }> }>(providers: Record<string, T>) {
|
||||
return mapValues(providers, (item) => sort(Object.values(item.models))[0].id)
|
||||
}
|
||||
@@ -1309,7 +1299,7 @@ const layer: Layer.Layer<
|
||||
const options = yield* Effect.promise(() =>
|
||||
plugin.auth!.loader!(
|
||||
() => bridge.promise(auth.get(providerID).pipe(Effect.orDie)) as any,
|
||||
toPublicInfo(database[plugin.auth!.provider]),
|
||||
database[plugin.auth!.provider],
|
||||
),
|
||||
)
|
||||
const opts = options ?? {}
|
||||
|
||||
@@ -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" }),
|
||||
|
||||
@@ -9,7 +9,6 @@ import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "e
|
||||
import { Authorization } from "../middleware/authorization"
|
||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
|
||||
import { withWorkspaceRouting } from "../query"
|
||||
import { described } from "./metadata"
|
||||
|
||||
const ConsoleStateResponse = Schema.Struct({
|
||||
@@ -43,7 +42,7 @@ 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({
|
||||
provider: ProviderID,
|
||||
model: ModelID,
|
||||
})
|
||||
@@ -55,7 +54,7 @@ const QueryBoolean = Schema.Literals(["true", "false"]).pipe(
|
||||
}),
|
||||
)
|
||||
const WorktreeList = Schema.Array(Schema.String)
|
||||
export const SessionListQuery = withWorkspaceRouting({
|
||||
export const SessionListQuery = Schema.Struct({
|
||||
directory: Schema.optional(Schema.String),
|
||||
roots: Schema.optional(QueryBoolean),
|
||||
start: Schema.optional(Schema.NumberFromString),
|
||||
|
||||
@@ -6,18 +6,17 @@ import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable
|
||||
import { Authorization } from "../middleware/authorization"
|
||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
|
||||
import { withWorkspaceRouting } from "../query"
|
||||
import { described } from "./metadata"
|
||||
|
||||
export const FileQuery = withWorkspaceRouting({
|
||||
export const FileQuery = Schema.Struct({
|
||||
path: Schema.String,
|
||||
})
|
||||
|
||||
export const FindTextQuery = withWorkspaceRouting({
|
||||
export const FindTextQuery = Schema.Struct({
|
||||
pattern: Schema.String,
|
||||
})
|
||||
|
||||
export const FindFileQuery = withWorkspaceRouting({
|
||||
export const FindFileQuery = Schema.Struct({
|
||||
query: Schema.String,
|
||||
dirs: Schema.optional(Schema.Literals(["true", "false"])),
|
||||
type: Schema.optional(Schema.Literals(["file", "directory"])),
|
||||
@@ -26,7 +25,7 @@ export const FindFileQuery = withWorkspaceRouting({
|
||||
),
|
||||
})
|
||||
|
||||
export const FindSymbolQuery = withWorkspaceRouting({
|
||||
export const FindSymbolQuery = Schema.Struct({
|
||||
query: Schema.String,
|
||||
})
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "
|
||||
import { Authorization } from "../middleware/authorization"
|
||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
|
||||
import { withWorkspaceRouting } from "../query"
|
||||
import { described } from "./metadata"
|
||||
|
||||
const PathInfo = Schema.Struct({
|
||||
@@ -20,7 +19,7 @@ const PathInfo = Schema.Struct({
|
||||
directory: Schema.String,
|
||||
}).annotate({ identifier: "Path" })
|
||||
|
||||
export const VcsDiffQuery = withWorkspaceRouting({
|
||||
export const VcsDiffQuery = Schema.Struct({
|
||||
mode: Vcs.Mode,
|
||||
})
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, Op
|
||||
import { Authorization } from "../middleware/authorization"
|
||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
|
||||
import { withWorkspaceRouting } from "../query"
|
||||
import { ApiNotFoundError } from "../errors"
|
||||
import { described } from "./metadata"
|
||||
|
||||
@@ -26,7 +25,7 @@ const QueryBoolean = Schema.Literals(["true", "false"]).pipe(
|
||||
encode: SchemaGetter.transform((value) => (value ? "true" : "false")),
|
||||
}),
|
||||
)
|
||||
export const ListQuery = withWorkspaceRouting({
|
||||
export const ListQuery = Schema.Struct({
|
||||
directory: Schema.optional(Schema.String),
|
||||
scope: Schema.optional(Schema.Literals(["project"])),
|
||||
path: Schema.optional(Schema.String),
|
||||
@@ -35,8 +34,8 @@ 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(Struct.omit(SessionSummary.DiffInput.fields, ["sessionID"]))
|
||||
export const MessagesQuery = Schema.Struct({
|
||||
limit: Schema.optional(Schema.NumberFromString.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0))),
|
||||
before: Schema.optional(Schema.String),
|
||||
})
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -24,7 +24,7 @@ export const configHandlers = HttpApiBuilder.group(InstanceHttpApi, "config", (h
|
||||
const providers = Effect.fn("ConfigHttpApi.providers")(function* () {
|
||||
const providers = yield* providerSvc.list()
|
||||
return {
|
||||
providers: Object.values(providers).map(Provider.toPublicInfo),
|
||||
providers: Object.values(providers),
|
||||
default: Provider.defaultModelIDs(providers),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -30,7 +30,7 @@ export const providerHandlers = HttpApiBuilder.group(InstanceHttpApi, "provider"
|
||||
connected,
|
||||
)
|
||||
return {
|
||||
all: Object.values(providers).map(Provider.toPublicInfo),
|
||||
all: Object.values(providers),
|
||||
default: Provider.defaultModelIDs(providers),
|
||||
connected: Object.keys(connected),
|
||||
}
|
||||
|
||||
@@ -6,13 +6,39 @@ import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Cause, Effect } from "effect"
|
||||
import { HttpRouter, HttpServerError, HttpServerRespondable, HttpServerResponse } from "effect/unstable/http"
|
||||
import { HttpApiError } from "effect/unstable/httpapi"
|
||||
import { HttpApiSchemaError } from "effect/unstable/httpapi/HttpApiError"
|
||||
|
||||
const log = Log.create({ service: "server" })
|
||||
|
||||
function badRequestResponse() {
|
||||
return HttpServerResponse.jsonUnsafe(
|
||||
{
|
||||
data: {},
|
||||
errors: [],
|
||||
success: false,
|
||||
},
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
function normalizeEmptyBadRequest(response: HttpServerResponse.HttpServerResponse) {
|
||||
if (response.status !== 400 || response.body._tag !== "Empty") return response
|
||||
return badRequestResponse()
|
||||
}
|
||||
|
||||
// Keep typed HttpApi failures on their declared error path; this boundary only replaces defect-only empty 500s.
|
||||
export const errorLayer = HttpRouter.middleware<{ handles: unknown }>()((effect) =>
|
||||
effect.pipe(
|
||||
Effect.catch((error) => {
|
||||
if (error instanceof HttpApiError.BadRequest) return Effect.succeed(badRequestResponse())
|
||||
return Effect.fail(error)
|
||||
}),
|
||||
Effect.map(normalizeEmptyBadRequest),
|
||||
Effect.catchCause((cause) => {
|
||||
const schemaError = cause.reasons.filter(Cause.isDieReason).find((reason) => HttpApiSchemaError.is(reason.defect))
|
||||
if (schemaError) return Effect.succeed(badRequestResponse())
|
||||
|
||||
const defect = cause.reasons.filter(Cause.isDieReason).find((reason) => {
|
||||
if (HttpServerResponse.isHttpServerResponse(reason.defect)) return false
|
||||
if (HttpServerError.isHttpServerError(reason.defect)) return false
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -776,79 +776,6 @@ test("auto-disposes plugin keymap layers", async () => {
|
||||
}
|
||||
})
|
||||
|
||||
test("supports legacy plugin command API", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
const file = path.join(dir, "legacy-command-plugin.ts")
|
||||
const spec = pathToFileURL(file).href
|
||||
const marker = path.join(dir, "legacy-command.txt")
|
||||
|
||||
await Bun.write(
|
||||
file,
|
||||
`export default {
|
||||
id: "demo.command.legacy",
|
||||
tui: async (api) => {
|
||||
api.command.register(() => [{
|
||||
title: "Legacy command",
|
||||
value: "demo.command.legacy.run",
|
||||
onSelect() {
|
||||
Bun.write(${JSON.stringify(marker)}, "called")
|
||||
},
|
||||
}])
|
||||
api.command.trigger("demo.command.legacy.run")
|
||||
api.command.show()
|
||||
},
|
||||
}
|
||||
`,
|
||||
)
|
||||
|
||||
return { spec, marker }
|
||||
},
|
||||
})
|
||||
|
||||
let add = 0
|
||||
let drop = 0
|
||||
let show = 0
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
|
||||
try {
|
||||
await TuiPluginRuntime.init({
|
||||
api: createTuiPluginApi({
|
||||
command: {
|
||||
register(cb) {
|
||||
add += 1
|
||||
const list = cb()
|
||||
return () => {
|
||||
drop += list.length
|
||||
}
|
||||
},
|
||||
trigger(value) {
|
||||
expect(value).toBe("demo.command.legacy.run")
|
||||
Bun.write(tmp.extra.marker, "called")
|
||||
},
|
||||
show() {
|
||||
show += 1
|
||||
},
|
||||
},
|
||||
}),
|
||||
config: createTuiResolvedConfig({
|
||||
plugin: [tmp.extra.spec],
|
||||
plugin_origins: [{ spec: tmp.extra.spec, scope: "local", source: path.join(tmp.path, "tui.json") }],
|
||||
}),
|
||||
})
|
||||
|
||||
expect(add).toBe(1)
|
||||
expect(show).toBe(1)
|
||||
await expect(fs.readFile(tmp.extra.marker, "utf8")).resolves.toBe("called")
|
||||
} finally {
|
||||
await TuiPluginRuntime.dispose()
|
||||
expect(drop).toBe(1)
|
||||
cwd.mockRestore()
|
||||
wait.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
test("plugin keymap proxy preserves real keymap receiver", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
|
||||
@@ -82,7 +82,6 @@ function themeCurrent(): HostPluginApi["theme"]["current"] {
|
||||
|
||||
type Opts = {
|
||||
client?: HostPluginApi["client"] | (() => HostPluginApi["client"])
|
||||
command?: Partial<HostPluginApi["command"]>
|
||||
renderer?: HostPluginApi["renderer"]
|
||||
count?: Count
|
||||
keymap?: HostPluginApi["keymap"]
|
||||
@@ -132,7 +131,6 @@ export function createTuiPluginApi(opts: Opts = {}): HostPluginApi {
|
||||
? () => opts.client as HostPluginApi["client"]
|
||||
: fallback
|
||||
const client = () => read()
|
||||
const commands: ReturnType<Parameters<HostPluginApi["command"]["register"]>[0]> = []
|
||||
let depth = 0
|
||||
let size: "medium" | "large" | "xlarge" = "medium"
|
||||
const has = opts.theme?.has ?? (() => false)
|
||||
@@ -189,25 +187,6 @@ export function createTuiPluginApi(opts: Opts = {}): HostPluginApi {
|
||||
formatSequence: () => "",
|
||||
formatBindings: () => undefined,
|
||||
},
|
||||
command: {
|
||||
register(cb) {
|
||||
if (opts.command?.register) return opts.command.register(cb)
|
||||
const list = cb()
|
||||
commands.push(...list)
|
||||
if (count) count.command_add += 1
|
||||
return () => {
|
||||
if (count) count.command_drop += 1
|
||||
commands.splice(0, commands.length, ...commands.filter((command) => !list.includes(command)))
|
||||
}
|
||||
},
|
||||
trigger(value) {
|
||||
if (opts.command?.trigger) return opts.command.trigger(value)
|
||||
commands.find((command) => command.value === value)?.onSelect?.()
|
||||
},
|
||||
show() {
|
||||
opts.command?.show?.()
|
||||
},
|
||||
},
|
||||
get client() {
|
||||
return client()
|
||||
},
|
||||
|
||||
@@ -21,22 +21,6 @@ function app() {
|
||||
return Server.Default().app
|
||||
}
|
||||
|
||||
function providerListHasFetch(list: unknown) {
|
||||
if (!Array.isArray(list)) return false
|
||||
return list.some((item: unknown) => {
|
||||
if (typeof item !== "object" || item === null || !("id" in item) || !("options" in item)) return false
|
||||
if (item.id !== "google") return false
|
||||
if (typeof item.options !== "object" || item.options === null) return false
|
||||
return "fetch" in item.options
|
||||
})
|
||||
}
|
||||
|
||||
function hasProviderWithFetch(input: unknown, key: "all" | "providers") {
|
||||
if (typeof input !== "object" || input === null) return false
|
||||
if (key === "all") return "all" in input && providerListHasFetch(input.all)
|
||||
return "providers" in input && providerListHasFetch(input.providers)
|
||||
}
|
||||
|
||||
function requestAuthorize(input: {
|
||||
app: ReturnType<typeof app>
|
||||
providerID: string
|
||||
@@ -92,39 +76,6 @@ function writeProviderAuthPlugin(dir: string) {
|
||||
})
|
||||
}
|
||||
|
||||
function writeFunctionOptionsPlugin(dir: string) {
|
||||
return Effect.gen(function* () {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const path = yield* Path.Path
|
||||
|
||||
yield* fs.makeDirectory(path.join(dir, ".opencode", "plugin"), { recursive: true })
|
||||
yield* fs.writeFileString(
|
||||
path.join(dir, ".opencode", "plugin", "provider-function-options.ts"),
|
||||
[
|
||||
"export default {",
|
||||
' id: "test.provider-function-options",',
|
||||
" server: async () => ({",
|
||||
" auth: {",
|
||||
' provider: "google",',
|
||||
" loader: async (_getAuth, provider) => {",
|
||||
" for (const model of Object.values(provider.models ?? {})) {",
|
||||
" model.cost = { input: 0, output: 0 }",
|
||||
" }",
|
||||
" return {",
|
||||
' apiKey: "",',
|
||||
" fetch: async (input, init) => fetch(input, init),",
|
||||
" }",
|
||||
" },",
|
||||
" methods: [{ type: 'api', label: 'API key' }],",
|
||||
" },",
|
||||
" }),",
|
||||
"}",
|
||||
"",
|
||||
].join("\n"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function withProviderProject<A, E, R>(self: (dir: string) => Effect.Effect<A, E, R>) {
|
||||
return Effect.gen(function* () {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
@@ -185,43 +136,4 @@ describe("provider HttpApi", () => {
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("serves provider lists when auth loaders add runtime fetch options", () =>
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const path = yield* Path.Path
|
||||
const dir = yield* fs.makeTempDirectoryScoped({ prefix: "opencode-test-" })
|
||||
const previous = process.env.OPENCODE_AUTH_CONTENT
|
||||
|
||||
yield* fs.writeFileString(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({ $schema: "https://opencode.ai/config.json", formatter: false, lsp: false }),
|
||||
)
|
||||
yield* writeFunctionOptionsPlugin(dir)
|
||||
yield* Effect.sync(() => {
|
||||
process.env.OPENCODE_AUTH_CONTENT = JSON.stringify({
|
||||
google: { type: "oauth", refresh: "dummy", access: "dummy", expires: 9999999999999 },
|
||||
})
|
||||
})
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.sync(() => {
|
||||
if (previous === undefined) delete process.env.OPENCODE_AUTH_CONTENT
|
||||
if (previous !== undefined) process.env.OPENCODE_AUTH_CONTENT = previous
|
||||
}),
|
||||
)
|
||||
const headers = { "x-opencode-directory": dir }
|
||||
const providerResponse = yield* Effect.promise(() => Promise.resolve(app().request("/provider", { headers })))
|
||||
const configResponse = yield* Effect.promise(() =>
|
||||
Promise.resolve(app().request("/config/providers", { headers })),
|
||||
)
|
||||
|
||||
expect(providerResponse.status).toBe(200)
|
||||
expect(configResponse.status).toBe(200)
|
||||
|
||||
const providerBody = yield* Effect.promise(() => providerResponse.json())
|
||||
const configBody = yield* Effect.promise(() => configResponse.json())
|
||||
expect(hasProviderWithFetch(providerBody, "all")).toBe(false)
|
||||
expect(hasProviderWithFetch(configBody, "providers")).toBe(false)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
import { afterEach, describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { WithInstance } from "../../src/project/with-instance"
|
||||
import { Session } from "@/session/session"
|
||||
import { MessageID, PartID, SessionID, type SessionID as SessionIDType } from "../../src/session/schema"
|
||||
import { ModelID, ProviderID } from "../../src/provider/schema"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
import { it } from "../lib/effect"
|
||||
import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session"
|
||||
import { FilePaths } from "../../src/server/routes/instance/httpapi/groups/file"
|
||||
|
||||
void (await import("@opencode-ai/core/util/log")).init({ print: false })
|
||||
|
||||
const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
|
||||
|
||||
function runSession<A, E>(fx: Effect.Effect<A, E, Session.Service>) {
|
||||
return Effect.runPromise(fx.pipe(Effect.provide(Session.defaultLayer)))
|
||||
}
|
||||
|
||||
function pathFor(path: string, params: Record<string, string>) {
|
||||
return Object.entries(params).reduce((result, [key, value]) => result.replace(`:${key}`, value), path)
|
||||
}
|
||||
|
||||
function createSession(directory: string, input?: Session.CreateInput) {
|
||||
return Effect.promise(
|
||||
async () =>
|
||||
await WithInstance.provide({
|
||||
directory,
|
||||
fn: () => runSession(Session.Service.use((svc) => svc.create(input))),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function createTextMessage(directory: string, sessionID: SessionIDType, text: string) {
|
||||
return Effect.promise(
|
||||
async () =>
|
||||
await WithInstance.provide({
|
||||
directory,
|
||||
fn: () =>
|
||||
runSession(
|
||||
Effect.gen(function* () {
|
||||
const svc = yield* Session.Service
|
||||
const info = yield* svc.updateMessage({
|
||||
id: MessageID.ascending(),
|
||||
role: "user",
|
||||
sessionID,
|
||||
agent: "build",
|
||||
model: { providerID: ProviderID.make("test"), modelID: ModelID.make("test") },
|
||||
time: { created: Date.now() },
|
||||
})
|
||||
const part = yield* svc.updatePart({
|
||||
id: PartID.ascending(),
|
||||
sessionID,
|
||||
messageID: info.id,
|
||||
type: "text",
|
||||
text,
|
||||
})
|
||||
return { info, part }
|
||||
}),
|
||||
),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function request(path: string, init?: RequestInit) {
|
||||
return Effect.promise(async () => {
|
||||
const { Server } = await import("../../src/server/server")
|
||||
return Server.Default().app.request(path, init)
|
||||
})
|
||||
}
|
||||
|
||||
function withTmp<A, E, R>(
|
||||
options: Parameters<typeof tmpdir>[0],
|
||||
fn: (tmp: Awaited<ReturnType<typeof tmpdir>>) => Effect.Effect<A, E, R>,
|
||||
) {
|
||||
return Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir(options)),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
).pipe(Effect.flatMap(fn))
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = originalWorkspaces
|
||||
await disposeAllInstances()
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
/**
|
||||
* Reproducer for: runtime HttpApi query schemas must accept directory/workspace
|
||||
*
|
||||
* Previously, the OpenAPI spec was manually injected with directory/workspace query
|
||||
* params (InstanceQueryParameters in public.ts), but the runtime query schemas
|
||||
* did not include these fields. This caused a drift where:
|
||||
* 1. Generated SDKs would send requests with ?directory=...&workspace=...
|
||||
* 2. But runtime validation would reject these as unknown fields
|
||||
* 3. Resulting in 400 Bad Request errors
|
||||
*
|
||||
* The fix adds directory/workspace to all instance route query schemas using the
|
||||
* extendWithInstanceQuery helper in query.ts.
|
||||
*/
|
||||
describe("query schema drift fix", () => {
|
||||
it.live(
|
||||
"accepts directory and workspace query params on session.messages route",
|
||||
withTmp({ git: true, config: { formatter: false, lsp: false } }, (tmp) =>
|
||||
Effect.gen(function* () {
|
||||
const headers = { "x-opencode-directory": tmp.path }
|
||||
const session = yield* createSession(tmp.path, { title: "drift test" })
|
||||
yield* createTextMessage(tmp.path, session.id, "test message")
|
||||
|
||||
// This should NOT return 400 - previously it would fail validation
|
||||
// because MessagesQuery didn't include directory/workspace fields
|
||||
const response = yield* request(
|
||||
`${pathFor(SessionPaths.messages, { sessionID: session.id })}?limit=1&directory=${encodeURIComponent(tmp.path)}`,
|
||||
{ headers },
|
||||
)
|
||||
|
||||
// Should be 200 OK, not 400 Bad Request due to unknown query params
|
||||
// Note: workspace param is omitted because an invalid workspace ID would cause 500
|
||||
expect(response.status).toBe(200)
|
||||
|
||||
const body = yield* Effect.promise(() => response.json())
|
||||
expect(Array.isArray(body)).toBe(true)
|
||||
expect(body.length).toBe(1)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.live(
|
||||
"accepts directory and workspace query params on file.list route",
|
||||
withTmp({ git: true, config: { formatter: false, lsp: false } }, (tmp) =>
|
||||
Effect.gen(function* () {
|
||||
const headers = { "x-opencode-directory": tmp.path }
|
||||
|
||||
// Create a test file
|
||||
const testFile = `${tmp.path}/test.txt`
|
||||
yield* Effect.promise(() => Bun.write(testFile, "test content"))
|
||||
|
||||
// This should NOT return 400 - previously FileQuery didn't include directory/workspace
|
||||
const response = yield* request(
|
||||
`${FilePaths.list}?path=${encodeURIComponent(tmp.path)}&directory=${encodeURIComponent(tmp.path)}`,
|
||||
{ headers },
|
||||
)
|
||||
|
||||
// Should be 200 OK, not 400 Bad Request
|
||||
// Note: workspace param is omitted because an invalid workspace ID would cause 500
|
||||
expect(response.status).toBe(200)
|
||||
|
||||
const body = yield* Effect.promise(() => response.json())
|
||||
expect(Array.isArray(body)).toBe(true)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.live(
|
||||
"accepts directory and workspace query params on session.list route",
|
||||
withTmp({ git: true, config: { formatter: false, lsp: false } }, (tmp) =>
|
||||
Effect.gen(function* () {
|
||||
const headers = { "x-opencode-directory": tmp.path }
|
||||
yield* createSession(tmp.path, { title: "list drift test" })
|
||||
|
||||
// This should NOT return 400 - ListQuery already had directory but now includes workspace
|
||||
// Use only directory parameter since invalid workspace ID would cause 500
|
||||
const response = yield* request(
|
||||
`${SessionPaths.list}?directory=${encodeURIComponent(tmp.path)}`,
|
||||
{ headers },
|
||||
)
|
||||
|
||||
// Should be 200 OK, not 400 Bad Request
|
||||
expect(response.status).toBe(200)
|
||||
|
||||
const body = yield* Effect.promise(() => response.json())
|
||||
expect(Array.isArray(body)).toBe(true)
|
||||
expect(body.length).toBeGreaterThan(0)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.live(
|
||||
"accepts directory and workspace query params on find.file route",
|
||||
withTmp({ git: true, config: { formatter: false, lsp: false } }, (tmp) =>
|
||||
Effect.gen(function* () {
|
||||
const headers = { "x-opencode-directory": tmp.path }
|
||||
|
||||
// Create a test file
|
||||
const testFile = `${tmp.path}/findme.txt`
|
||||
yield* Effect.promise(() => Bun.write(testFile, "test content"))
|
||||
|
||||
// This should NOT return 400 - FindFileQuery now includes directory/workspace
|
||||
// Use only directory parameter since invalid workspace ID would cause 500
|
||||
const response = yield* request(
|
||||
`${FilePaths.findFile}?query=findme&directory=${encodeURIComponent(tmp.path)}`,
|
||||
{ headers },
|
||||
)
|
||||
|
||||
// Should be 200 OK, not 400 Bad Request
|
||||
expect(response.status).toBe(200)
|
||||
|
||||
const body = yield* Effect.promise(() => response.json())
|
||||
expect(Array.isArray(body)).toBe(true)
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
@@ -331,36 +331,18 @@ describe("HttpApi SDK", () => {
|
||||
|
||||
httpapi(
|
||||
"uses the generated SDK for safe instance routes",
|
||||
withProject("raw", { setup: writeStandardFiles }, ({ sdk }) =>
|
||||
withProject("raw", { git: false, setup: writeStandardFiles }, ({ sdk }) =>
|
||||
Effect.gen(function* () {
|
||||
const file = yield* call(() => sdk.file.read({ path: "hello.txt" }))
|
||||
const files = yield* call(() => sdk.file.list({ path: "." }))
|
||||
const status = yield* call(() => sdk.file.status())
|
||||
const findText = yield* call(() => sdk.find.text({ pattern: "sdk-parity" }))
|
||||
const findFiles = yield* call(() => sdk.find.files({ query: "hello", limit: 10 }))
|
||||
const findSymbols = yield* call(() => sdk.find.symbols({ query: "hello" }))
|
||||
const session = yield* call(() => sdk.session.create({ title: "sdk" }))
|
||||
const listed = yield* call(() => sdk.session.list({ roots: true, limit: 10 }))
|
||||
const messages = yield* call(() => sdk.session.messages({ sessionID: String(record(session.data).id), limit: 1 }))
|
||||
const diff = yield* call(() => sdk.session.diff({ sessionID: String(record(session.data).id) }))
|
||||
const vcsDiff = yield* call(() => sdk.vcs.diff({ mode: "git" }))
|
||||
const tools = yield* call(() => sdk.tool.list({ provider: "opencode", model: "gpt-5" }))
|
||||
|
||||
expect(file.response.status).toBe(200)
|
||||
expect(file.data).toMatchObject({ content: "hello" })
|
||||
expect(files.response.status).toBe(200)
|
||||
expect(status.response.status).toBe(200)
|
||||
expect(findText.response.status).toBe(200)
|
||||
expect(findFiles.response.status).toBe(200)
|
||||
expect(findSymbols.response.status).toBe(200)
|
||||
expect(session.response.status).toBe(200)
|
||||
expect(session.data).toMatchObject({ title: "sdk" })
|
||||
expect(listed.response.status).toBe(200)
|
||||
expect(listed.data?.map((item) => item.id)).toContain(session.data?.id)
|
||||
expect(messages.response.status).toBe(200)
|
||||
expect(diff.response.status).toBe(200)
|
||||
expect(vcsDiff.response.status).toBe(200)
|
||||
expect(tools.response.status).toBe(200)
|
||||
|
||||
yield* Effect.all([
|
||||
expectStatus(() => sdk.project.current(), 200),
|
||||
@@ -482,11 +464,9 @@ describe("HttpApi SDK", () => {
|
||||
const fileStatus = yield* capture(() => sdk.file.status())
|
||||
const findFiles = yield* capture(() => sdk.find.files({ query: "hello", limit: 10 }))
|
||||
const findText = yield* capture(() => sdk.find.text({ pattern: "sdk-parity" }))
|
||||
const vcsDiff = yield* capture(() => sdk.vcs.diff({ mode: "git" }))
|
||||
const agents = yield* capture(() => sdk.app.agents())
|
||||
const skills = yield* capture(() => sdk.app.skills())
|
||||
const tools = yield* capture(() => sdk.tool.ids())
|
||||
const modelTools = yield* capture(() => sdk.tool.list({ provider: "opencode", model: "gpt-5" }))
|
||||
const vcs = yield* capture(() => sdk.vcs.get())
|
||||
const formatter = yield* capture(() => sdk.formatter.status())
|
||||
const lsp = yield* capture(() => sdk.lsp.status())
|
||||
@@ -503,11 +483,9 @@ describe("HttpApi SDK", () => {
|
||||
fileStatus,
|
||||
findFiles,
|
||||
findText,
|
||||
vcsDiff,
|
||||
agents,
|
||||
skills,
|
||||
tools,
|
||||
modelTools,
|
||||
vcs,
|
||||
formatter,
|
||||
lsp,
|
||||
@@ -537,7 +515,6 @@ describe("HttpApi SDK", () => {
|
||||
const all = yield* capture(() => sdk.session.list({ roots: false, limit: 10 }))
|
||||
const children = yield* capture(() => sdk.session.children({ sessionID: parentID }))
|
||||
const todo = yield* capture(() => sdk.session.todo({ sessionID: parentID }))
|
||||
const diff = yield* capture(() => sdk.session.diff({ sessionID: parentID }))
|
||||
const status = yield* capture(() => sdk.session.status())
|
||||
const messages = yield* capture(() => sdk.session.messages({ sessionID: parentID }))
|
||||
const missingGet = yield* capture(() => sdk.session.get({ sessionID: "ses_missing" }))
|
||||
@@ -558,7 +535,6 @@ describe("HttpApi SDK", () => {
|
||||
all,
|
||||
children,
|
||||
todo,
|
||||
diff,
|
||||
status,
|
||||
messages,
|
||||
missingGet,
|
||||
|
||||
@@ -290,10 +290,8 @@ describe("session HttpApi", () => {
|
||||
)
|
||||
|
||||
expect(
|
||||
(yield* requestJson<{ items: SessionMessage.Message[] }>(
|
||||
`/api/session/${parent.id}/message?directory=${encodeURIComponent(tmp.path)}`,
|
||||
{ headers },
|
||||
)).items,
|
||||
(yield* requestJson<{ items: SessionMessage.Message[] }>(`/api/session/${parent.id}/message`, { headers }))
|
||||
.items,
|
||||
).toMatchObject([{ type: "assistant" }])
|
||||
}),
|
||||
),
|
||||
@@ -366,12 +364,8 @@ describe("session HttpApi", () => {
|
||||
headers: { "x-opencode-directory": tmp.path, "content-type": "application/json" },
|
||||
body: JSON.stringify({ title: "workspace session" }),
|
||||
})
|
||||
const messages = yield* request(`${pathFor(SessionPaths.messages, { sessionID: created.id })}?workspace=${workspace.id}`, {
|
||||
headers: { "x-opencode-directory": tmp.path },
|
||||
})
|
||||
|
||||
expect(created).toMatchObject({ id: created.id, workspaceID: workspace.id })
|
||||
expect(messages.status).toBe(200)
|
||||
expect(
|
||||
yield* Effect.sync(() =>
|
||||
Database.use((db) =>
|
||||
|
||||
@@ -122,7 +122,7 @@ describe("sync HttpApi", () => {
|
||||
}
|
||||
})
|
||||
|
||||
test.todo("returns structured validation errors", async () => {
|
||||
test("returns structured validation errors", async () => {
|
||||
await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
|
||||
const response = await ExperimentalHttpApiServer.webHandler().handler(
|
||||
new Request(`http://localhost${SyncPaths.history}`, {
|
||||
|
||||
@@ -165,28 +165,4 @@ describe("session messages endpoint", () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test("accepts workspace routing query params with paginated message requests", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await withoutWatcher(() =>
|
||||
WithInstance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await svc.create({})
|
||||
await fill(session.id, 1)
|
||||
const app = Server.Default().app
|
||||
|
||||
const directory = await app.request(
|
||||
`/session/${session.id}/message?limit=80&directory=${encodeURIComponent(tmp.path)}`,
|
||||
)
|
||||
const workspace = await app.request(`/session/${session.id}/message?limit=80&workspace=wrk_test`)
|
||||
|
||||
expect(directory.status).toBe(200)
|
||||
expect(workspace.status).toBe(200)
|
||||
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Database } from "bun:sqlite"
|
||||
import { drizzle } from "drizzle-orm/bun-sqlite"
|
||||
import { migrate } from "drizzle-orm/bun-sqlite/migrator"
|
||||
import { readFileSync, readdirSync } from "fs"
|
||||
import path from "path"
|
||||
|
||||
const target = "20260507164347_add_workspace_time"
|
||||
|
||||
function migrations() {
|
||||
return readdirSync(path.join(import.meta.dirname, "../../migration"), { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => ({
|
||||
name: entry.name,
|
||||
timestamp: Number(entry.name.split("_")[0]),
|
||||
sql: readFileSync(path.join(import.meta.dirname, "../../migration", entry.name, "migration.sql"), "utf-8"),
|
||||
}))
|
||||
.sort((a, b) => a.timestamp - b.timestamp)
|
||||
}
|
||||
|
||||
describe("workspace time migration", () => {
|
||||
test("migrates existing workspace rows", () => {
|
||||
const sqlite = new Database(":memory:")
|
||||
const db = drizzle({ client: sqlite })
|
||||
const entries = migrations()
|
||||
const index = entries.findIndex((entry) => entry.name === target)
|
||||
|
||||
expect(index).toBeGreaterThan(0)
|
||||
|
||||
migrate(db, entries.slice(0, index))
|
||||
sqlite.run(
|
||||
"INSERT INTO project (id, worktree, vcs, name, time_created, time_updated, sandboxes) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
["project_1", "/tmp/project", "git", "project", 1, 1, "[]"],
|
||||
)
|
||||
sqlite.run(
|
||||
"INSERT INTO workspace (id, type, name, branch, directory, extra, project_id) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
["workspace_1", "local", "main", "main", "/tmp/project", null, "project_1"],
|
||||
)
|
||||
|
||||
expect(() => migrate(db, entries.slice(index))).not.toThrow()
|
||||
expect(sqlite.query("SELECT time_used FROM workspace WHERE id = ?").get("workspace_1")).toEqual({ time_used: 0 })
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.14.43",
|
||||
"version": "1.14.42",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -70,23 +70,6 @@ export type TuiRouteDefinition = {
|
||||
render: (input: { params?: Record<string, unknown> }) => JSX.Element
|
||||
}
|
||||
|
||||
/** @deprecated Use api.keymap.registerLayer({ commands, bindings }) instead. */
|
||||
export type TuiCommand = {
|
||||
title: string
|
||||
value: string
|
||||
description?: string
|
||||
category?: string
|
||||
keybind?: string
|
||||
suggested?: boolean
|
||||
hidden?: boolean
|
||||
enabled?: boolean
|
||||
slash?: {
|
||||
name: string
|
||||
aliases?: string[]
|
||||
}
|
||||
onSelect?: () => void
|
||||
}
|
||||
|
||||
export type TuiKeys = {
|
||||
formatSequence: (parts: readonly KeySequenceFormatPart[] | undefined) => string
|
||||
formatBindings: (bindings: readonly SequenceBindingLike[] | undefined) => string | undefined
|
||||
@@ -480,12 +463,6 @@ export type TuiPluginApi = {
|
||||
app: TuiApp
|
||||
keys: TuiKeys
|
||||
keymap: TuiKeymap
|
||||
/** @deprecated Use api.keymap.registerLayer({ commands, bindings }) instead. */
|
||||
command: {
|
||||
register: (cb: () => TuiCommand[]) => () => void
|
||||
trigger: (value: string) => void
|
||||
show: () => void
|
||||
}
|
||||
route: {
|
||||
register: (routes: TuiRouteDefinition[]) => () => void
|
||||
navigate: (name: string, params?: Record<string, unknown>) => void
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.14.43",
|
||||
"version": "1.14.42",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.14.43",
|
||||
"version": "1.14.42",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.14.43",
|
||||
"version": "1.14.42",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@opencode-ai/web",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "1.14.43",
|
||||
"version": "1.14.42",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "opencode",
|
||||
"displayName": "opencode",
|
||||
"description": "opencode for VS Code",
|
||||
"version": "1.14.43",
|
||||
"version": "1.14.42",
|
||||
"publisher": "sst-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Reference in New Issue
Block a user