mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-27 02:44:47 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b084acc55e |
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"packages/ai": {
|
||||
"name": "@opencode/ai",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-providers": "3.1057.0",
|
||||
"@opencode/schema": "workspace:*",
|
||||
@@ -54,7 +54,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode/app",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@corvu/drawer": "catalog:",
|
||||
"@dnd-kit/abstract": "0.5.0",
|
||||
@@ -113,7 +113,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@opencode/cli",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"bin": {
|
||||
"opencode2": "./bin/opencode2.cjs",
|
||||
},
|
||||
@@ -177,7 +177,7 @@
|
||||
},
|
||||
"packages/client": {
|
||||
"name": "@opencode/client",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@opencode/protocol": "workspace:*",
|
||||
"@opencode/schema": "workspace:*",
|
||||
@@ -203,7 +203,7 @@
|
||||
},
|
||||
"packages/codemode": {
|
||||
"name": "@opencode/codemode",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"acorn": "8.15.0",
|
||||
"effect": "catalog:",
|
||||
@@ -217,7 +217,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode/console-app",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -253,7 +253,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode/console-core",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -280,7 +280,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode/console-function",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@opencode/console-core": "workspace:*",
|
||||
@@ -297,7 +297,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode/console-mail",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -321,7 +321,7 @@
|
||||
},
|
||||
"packages/console/support": {
|
||||
"name": "@opencode/console-support",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@opencode/console-core": "workspace:*",
|
||||
@@ -341,7 +341,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode/core",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@ai-sdk/cohere": "3.0.27",
|
||||
"@ai-sdk/gateway": "3.0.104",
|
||||
@@ -409,7 +409,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode/desktop",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"electron-context-menu": "4.1.2",
|
||||
@@ -461,7 +461,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode/enterprise",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@hono/standard-validator": "catalog:",
|
||||
"@opencode-ai/sdk": "1.18.21",
|
||||
@@ -498,7 +498,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode/function",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -514,7 +514,7 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode/http-recorder",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@effect/platform-node-shared": "4.0.0-rc.112",
|
||||
},
|
||||
@@ -533,7 +533,7 @@
|
||||
},
|
||||
"packages/httpapi-codegen": {
|
||||
"name": "@opencode/httpapi-codegen",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
"prettier": "3.6.2",
|
||||
@@ -546,7 +546,7 @@
|
||||
},
|
||||
"packages/latex": {
|
||||
"name": "@opencode/latex",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@opencode/plugin": "workspace:*",
|
||||
"@opentui/core": "catalog:",
|
||||
@@ -560,7 +560,7 @@
|
||||
},
|
||||
"packages/merman": {
|
||||
"name": "@opencode/merman",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@opencode/plugin": "workspace:*",
|
||||
"@opentui/core": "catalog:",
|
||||
@@ -575,7 +575,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode/plugin",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "3.0.8",
|
||||
"@opencode/ai": "workspace:*",
|
||||
@@ -614,7 +614,7 @@
|
||||
},
|
||||
"packages/plugin-browser": {
|
||||
"name": "@opencode/plugin-browser",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@opencode/plugin": "workspace:*",
|
||||
"@opencode/schema": "workspace:*",
|
||||
@@ -644,7 +644,7 @@
|
||||
},
|
||||
"packages/protocol": {
|
||||
"name": "@opencode/protocol",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@opencode/schema": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
@@ -659,7 +659,7 @@
|
||||
},
|
||||
"packages/schema": {
|
||||
"name": "@opencode/schema",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -683,7 +683,7 @@
|
||||
},
|
||||
"packages/sdk": {
|
||||
"name": "@opencode/sdk",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@opencode/client": "workspace:*",
|
||||
"@opencode/core": "workspace:*",
|
||||
@@ -704,7 +704,7 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@opencode/server",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@effect/platform-node-shared": "catalog:",
|
||||
@@ -726,7 +726,7 @@
|
||||
},
|
||||
"packages/session-ui": {
|
||||
"name": "@opencode/session-ui",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode/client": "workspace:*",
|
||||
@@ -761,7 +761,7 @@
|
||||
},
|
||||
"packages/simulation": {
|
||||
"name": "@opencode/simulation",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@opencode/ai": "workspace:*",
|
||||
"@opencode/core": "workspace:*",
|
||||
@@ -781,7 +781,7 @@
|
||||
},
|
||||
"packages/stats/app": {
|
||||
"name": "@opencode/stats-app",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -815,7 +815,7 @@
|
||||
},
|
||||
"packages/stats/core": {
|
||||
"name": "@opencode/stats-core",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-athena": "3.933.0",
|
||||
"@planetscale/database": "1.19.0",
|
||||
@@ -834,7 +834,7 @@
|
||||
},
|
||||
"packages/stats/server": {
|
||||
"name": "@opencode/stats-server",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-firehose": "3.933.0",
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -880,7 +880,7 @@
|
||||
},
|
||||
"packages/theme": {
|
||||
"name": "@opencode/theme",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@opentui/core": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -894,7 +894,7 @@
|
||||
},
|
||||
"packages/tui": {
|
||||
"name": "@opencode/tui",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@opencode/client": "workspace:*",
|
||||
"@opencode/core": "workspace:*",
|
||||
@@ -929,7 +929,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode/ui",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@pierre/diffs": "catalog:",
|
||||
@@ -964,7 +964,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode/util",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@effect/opentelemetry": "catalog:",
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -997,7 +997,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode/web",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -1038,7 +1038,7 @@
|
||||
},
|
||||
"services/update": {
|
||||
"name": "@opencode/update",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"jose": "6.0.11",
|
||||
"semver": "catalog:",
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "opencode",
|
||||
"description": "AI-powered development tool",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.4.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"name": "@opencode/ai",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,18 +1,5 @@
|
||||
import { expect, story } from "../../storybook/playwright/story"
|
||||
|
||||
story("home connection progress stays in the server row", async ({ mount, page }) => {
|
||||
const component = await mount("app-dialog-ssh--home-connecting")
|
||||
await component.getByRole("button", { name: "Projects" }).click()
|
||||
await expect(page.getByRole("status", { name: "Connecting over SSH…" })).toBeVisible()
|
||||
await expect(page.locator('[data-action="home-server-authenticate"]')).toHaveCount(0)
|
||||
})
|
||||
|
||||
story("home authentication action appears when required", async ({ mount, page }) => {
|
||||
const component = await mount("app-dialog-ssh--authentication-required")
|
||||
await component.getByRole("button", { name: "Projects" }).click()
|
||||
await expect(page.getByRole("button", { name: "Authenticate", exact: true })).toBeVisible()
|
||||
})
|
||||
|
||||
story("settings menu reconnect retains its prompt handler across server updates", async ({ mount, page }) => {
|
||||
const component = await mount("app-dialog-ssh--settings-reconnect")
|
||||
await component.getByRole("button", { name: "More options" }).click()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/app",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ProjectAvatar } from "@opencode/ui/project-avatar"
|
||||
import { Icon } from "@opencode/ui/icon"
|
||||
import { IconButton } from "@opencode/ui/icon-button"
|
||||
import { Button } from "@opencode/ui/button"
|
||||
import { Spinner } from "@opencode/ui/spinner"
|
||||
import { Menu } from "@opencode/ui/menu"
|
||||
import { Tooltip } from "@opencode/ui/tooltip"
|
||||
import { getProjectAvatarVariant, type HomeProjectSelection, type LocalProject } from "@/shell/state/layout"
|
||||
@@ -255,7 +256,7 @@ function HomeProjectsPanel(props: HomeProjectsViewProps) {
|
||||
collapsed={collapsed()}
|
||||
health={props.serverHealth(item)}
|
||||
/>
|
||||
<Show when={authentication()}>
|
||||
<Show when={authentication() || connecting()}>
|
||||
<div class="mx-3 h-px bg-v2-border-border-base" />
|
||||
<div class="px-1.5 py-1">
|
||||
<Button
|
||||
@@ -263,9 +264,14 @@ function HomeProjectsPanel(props: HomeProjectsViewProps) {
|
||||
class="w-full"
|
||||
size="small"
|
||||
variant="neutral"
|
||||
disabled={connecting()}
|
||||
aria-busy={!!connecting()}
|
||||
onClick={() => props.onAuthenticateServer?.(item)}
|
||||
>
|
||||
{props.language.t("ssh.action.authenticate")}
|
||||
<Show when={connecting()}>
|
||||
<Spinner class="size-3.5" />
|
||||
</Show>
|
||||
{props.language.t(connecting() ? "ssh.stage.connecting" : "ssh.action.authenticate")}
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
@@ -14,7 +14,6 @@ import { SshConnectionPanel } from "./connection-panel"
|
||||
import { SshServerSettings } from "./settings"
|
||||
|
||||
function Fixture(props: {
|
||||
home?: boolean
|
||||
session?: boolean
|
||||
settings?: boolean
|
||||
incompatible?: boolean
|
||||
@@ -26,15 +25,10 @@ function Fixture(props: {
|
||||
const state: { item?: SshItem; before?: SshItem; step: number; timer?: ReturnType<typeof setTimeout> } = {
|
||||
step: 0,
|
||||
item:
|
||||
props.initial === "required" || props.home
|
||||
props.initial === "required"
|
||||
? {
|
||||
config: { id: "story", target: "ssh linuxbook", name: "" },
|
||||
stage:
|
||||
props.session || props.settings
|
||||
? "disconnected"
|
||||
: props.initial === "connecting"
|
||||
? "connecting"
|
||||
: "authentication",
|
||||
stage: props.session || props.settings ? "disconnected" : "authentication",
|
||||
saved: true,
|
||||
detail: "",
|
||||
}
|
||||
@@ -147,7 +141,7 @@ function Fixture(props: {
|
||||
<AuthenticationSettings />
|
||||
) : props.session ? (
|
||||
<AuthenticationSession />
|
||||
) : props.initial === "required" || props.home ? (
|
||||
) : props.initial === "required" ? (
|
||||
<AuthenticationHome />
|
||||
) : (
|
||||
<Open initial={props.initial} />
|
||||
@@ -283,7 +277,6 @@ function Open(props: { initial?: string }) {
|
||||
|
||||
export default { title: "App/Dialogs/SSH", id: "app-dialog-ssh" }
|
||||
export const AuthenticationRequired = { render: () => <Fixture initial="required" /> }
|
||||
export const HomeConnecting = { render: () => <Fixture home initial="connecting" /> }
|
||||
export const SettingsReconnect = { render: () => <Fixture initial="required" settings connectionDelay={200} /> }
|
||||
export const IncompatibleHost = { render: () => <Fixture incompatible /> }
|
||||
export const IncompatibleSession = { render: () => <Fixture initial="required" session incompatible /> }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { ServerConnection } from "@/runtime/server/registry"
|
||||
import type { SshItem } from "@/servers/ssh/types"
|
||||
import { settingsProjects, settingsServers } from "./inventory"
|
||||
import { settingsServers } from "./inventory"
|
||||
|
||||
const ssh: SshItem = {
|
||||
config: { id: "build", target: "dev@example.com", name: "Build server" },
|
||||
@@ -17,30 +17,6 @@ const connection: ServerConnection.Ssh = {
|
||||
http: { url: "http://127.0.0.1:4000", password: "secret" },
|
||||
}
|
||||
|
||||
test("settings project inventory reads metadata without acquiring directory stores", () => {
|
||||
const projects = Array.from({ length: 40 }, (_, index) => ({
|
||||
id: `project-${index}`,
|
||||
worktree: `/projects/${index}`,
|
||||
name: `Project ${index}`,
|
||||
icon: { color: "orange" },
|
||||
commands: { start: "bun install" },
|
||||
time: { created: 1, updated: 1 },
|
||||
sandboxes: [],
|
||||
worktrees: [],
|
||||
}))
|
||||
const tracked = { ...projects[0], expanded: true, icon: { override: "local-icon" } }
|
||||
const inventory = settingsProjects({
|
||||
projects: { list: () => [tracked], closed: () => [projects[1].worktree] },
|
||||
sync: { data: { project: projects } },
|
||||
})
|
||||
|
||||
expect(inventory).toHaveLength(39)
|
||||
expect(inventory[0]).toBe(tracked)
|
||||
expect(inventory.some((project) => project.id === projects[1].id)).toBe(false)
|
||||
expect(inventory[1]).toEqual({ ...projects[2], expanded: false })
|
||||
expect(inventory[38]).toEqual({ ...projects[39], expanded: false })
|
||||
})
|
||||
|
||||
describe("settings server inventory", () => {
|
||||
test("includes saved SSH servers before they connect", () => {
|
||||
expect(settingsServers([], [], [ssh])).toEqual([
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { ServerCtx } from "@/runtime/server/runtime"
|
||||
import { pathKey } from "@/workspaces/path-key"
|
||||
|
||||
export function settingsProjects(context: {
|
||||
projects: Pick<ServerCtx["projects"], "list" | "closed">
|
||||
projects: Pick<ServerCtx["projects"], "list" | "closed" | "resolve">
|
||||
sync: { data: Pick<ServerCtx["sync"]["data"], "project"> }
|
||||
}) {
|
||||
const tracked = context.projects.list()
|
||||
@@ -16,10 +16,9 @@ export function settingsProjects(context: {
|
||||
const closed = new Set(context.projects.closed().map(pathKey))
|
||||
return [
|
||||
...tracked,
|
||||
// Inventory reads must not allocate directory stores: async cache hydration can trigger an eviction/reload loop.
|
||||
...context.sync.data.project
|
||||
.filter((project) => !paths.has(pathKey(project.worktree)) && !closed.has(pathKey(project.worktree)))
|
||||
.map((project) => ({ ...project, expanded: false })),
|
||||
.map((project) => context.projects.resolve({ worktree: project.worktree, expanded: false })),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/cli",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
||||
@@ -6,16 +6,10 @@ import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { UpdateArtifact } from "../../../script/update-artifact"
|
||||
|
||||
if (Script.channel !== "beta" && Script.channel !== "latest") {
|
||||
throw new Error("AUR publishing requires the beta or latest channel")
|
||||
}
|
||||
if (Script.channel !== "beta") throw new Error("AUR publishing requires the beta channel")
|
||||
const name = "opencode-beta"
|
||||
const command = "opencode"
|
||||
const valid =
|
||||
Script.channel === "beta"
|
||||
? /^\d+\.\d+\.\d+-beta[.-]\d+(?:\.\d+)?$/.test(Script.version)
|
||||
: /^\d+\.\d+\.\d+$/.test(Script.version)
|
||||
if (!valid) throw new Error(`Expected a ${Script.channel} release version`)
|
||||
if (!/^\d+\.\d+\.\d+-beta[.-]\d+(?:\.\d+)?$/.test(Script.version)) throw new Error("Expected a beta release version")
|
||||
|
||||
const dir = fileURLToPath(new URL("..", import.meta.url))
|
||||
const root = path.resolve(process.env.OPENCODE_CLI_DIST ?? path.join(dir, "dist"))
|
||||
|
||||
@@ -147,7 +147,7 @@ if (Script.channel === "latest" && Script.release && !dryRun) {
|
||||
await $`docker buildx build --platform linux/amd64,linux/arm64 --tag ghcr.io/anomalyco/opencode:${Script.version} --push .`
|
||||
}
|
||||
|
||||
if ((Script.channel === "beta" || Script.channel === "latest") && Script.release) {
|
||||
if (Script.channel === "beta" && Script.release) {
|
||||
await $`bun ./script/publish-aur.ts ${dryRun ? ["--dry-run"] : []}`.env({ ...process.env, OPENCODE_CLI_DIST: root })
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +1,42 @@
|
||||
import { agent, RequestError, type Stream } from "@agentclientprotocol/sdk"
|
||||
import {
|
||||
RequestError,
|
||||
type Agent,
|
||||
type AgentSideConnection,
|
||||
type AuthenticateRequest,
|
||||
type CancelNotification,
|
||||
type CloseSessionRequest,
|
||||
type DeleteSessionRequest,
|
||||
type ForkSessionRequest,
|
||||
type InitializeRequest,
|
||||
type ListSessionsRequest,
|
||||
type LoadSessionRequest,
|
||||
type NewSessionRequest,
|
||||
type PromptRequest,
|
||||
type ResumeSessionRequest,
|
||||
type SetSessionConfigOptionRequest,
|
||||
type SetSessionModeRequest,
|
||||
} from "@agentclientprotocol/sdk"
|
||||
import type { OpenCodeClient } from "@opencode/client/promise"
|
||||
import { ACPConnection } from "./connection"
|
||||
import { ACPError } from "./error"
|
||||
import { ACPService } from "./service"
|
||||
|
||||
export function connect(client: OpenCodeClient, stream: Stream) {
|
||||
const connection = agent({ name: "opencode" })
|
||||
.onRequest("initialize", (ctx) => run(service.initialize(ctx.params)))
|
||||
.onRequest("authenticate", (ctx) => run(service.authenticate(ctx.params)))
|
||||
.onRequest("session/new", (ctx) => run(service.newSession(ctx.params)))
|
||||
.onRequest("session/load", (ctx) => run(service.loadSession(ctx.params)))
|
||||
.onRequest("session/list", (ctx) => run(service.listSessions(ctx.params)))
|
||||
.onRequest("session/delete", (ctx) => run(service.deleteSession(ctx.params)))
|
||||
.onRequest("session/resume", (ctx) => run(service.resumeSession(ctx.params)))
|
||||
.onRequest("session/close", (ctx) => run(service.closeSession(ctx.params)))
|
||||
.onRequest("session/fork", (ctx) => run(service.forkSession(ctx.params)))
|
||||
.onRequest("session/set_config_option", (ctx) => run(service.setSessionConfigOption(ctx.params)))
|
||||
.onRequest("session/set_mode", (ctx) => run(service.setSessionMode(ctx.params)))
|
||||
.onRequest("session/prompt", (ctx) => run(service.prompt(ctx.params, ctx.signal)))
|
||||
.onNotification("session/cancel", (ctx) => run(service.cancel(ctx.params)))
|
||||
.connect(stream)
|
||||
// Inbound dispatch starts after the stream's async read loop yields, so handlers never observe this before assignment.
|
||||
const service = ACPService.make({ client, connection: ACPConnection.make(connection) })
|
||||
return connection
|
||||
export function create(client: OpenCodeClient, connection: AgentSideConnection) {
|
||||
const service = ACPService.make({ client, connection })
|
||||
return {
|
||||
initialize: (params: InitializeRequest) => run(service.initialize(params)),
|
||||
authenticate: (params: AuthenticateRequest) => run(service.authenticate(params)),
|
||||
newSession: (params: NewSessionRequest) => run(service.newSession(params)),
|
||||
loadSession: (params: LoadSessionRequest) => run(service.loadSession(params)),
|
||||
listSessions: (params: ListSessionsRequest) => run(service.listSessions(params)),
|
||||
deleteSession: (params: DeleteSessionRequest) => run(service.deleteSession(params)),
|
||||
resumeSession: (params: ResumeSessionRequest) => run(service.resumeSession(params)),
|
||||
closeSession: (params: CloseSessionRequest) => run(service.closeSession(params)),
|
||||
unstable_forkSession: (params: ForkSessionRequest) => run(service.forkSession(params)),
|
||||
setSessionConfigOption: (params: SetSessionConfigOptionRequest) => run(service.setSessionConfigOption(params)),
|
||||
setSessionMode: (params: SetSessionModeRequest) => run(service.setSessionMode(params)),
|
||||
prompt: (params: PromptRequest) => run(service.prompt(params)),
|
||||
cancel: (params: CancelNotification) => run(service.cancel(params)),
|
||||
} satisfies Agent
|
||||
}
|
||||
|
||||
async function run<A>(promise: Promise<A>) {
|
||||
@@ -38,7 +52,6 @@ async function run<A>(promise: Promise<A>) {
|
||||
function isACPError(error: unknown): error is ACPError.Error {
|
||||
return (
|
||||
error instanceof ACPError.SessionNotFoundError ||
|
||||
error instanceof ACPError.SessionDirectoryMismatchError ||
|
||||
error instanceof ACPError.InvalidConfigOptionError ||
|
||||
error instanceof ACPError.InvalidModelError ||
|
||||
error instanceof ACPError.InvalidEffortError ||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import {
|
||||
methods,
|
||||
type AgentConnection,
|
||||
type RequestPermissionRequest,
|
||||
type RequestPermissionResponse,
|
||||
type SendRequestOptions,
|
||||
type SessionNotification,
|
||||
type WriteTextFileRequest,
|
||||
type WriteTextFileResponse,
|
||||
} from "@agentclientprotocol/sdk"
|
||||
|
||||
export type Connection = {
|
||||
readonly signal?: AbortSignal
|
||||
sessionUpdate(params: SessionNotification): Promise<void>
|
||||
requestPermission(params: RequestPermissionRequest, options?: SendRequestOptions): Promise<RequestPermissionResponse>
|
||||
writeTextFile?(params: WriteTextFileRequest, options?: SendRequestOptions): Promise<WriteTextFileResponse>
|
||||
extNotification?(method: string, params: Record<string, unknown>): Promise<void>
|
||||
}
|
||||
|
||||
export function make(connection: AgentConnection): Connection {
|
||||
return {
|
||||
signal: connection.signal,
|
||||
sessionUpdate: (params) => connection.client.notify(methods.client.session.update, params),
|
||||
requestPermission: (params, options) =>
|
||||
connection.client.request(methods.client.session.requestPermission, params, options),
|
||||
writeTextFile: (params, options) => connection.client.request(methods.client.fs.writeTextFile, params, options),
|
||||
extNotification: (method, params) => connection.client.notify(method, params),
|
||||
}
|
||||
}
|
||||
|
||||
export * as ACPConnection from "./connection"
|
||||
@@ -5,11 +5,6 @@ export class SessionNotFoundError extends Schema.TaggedError<SessionNotFoundErro
|
||||
sessionId: Schema.String,
|
||||
}) {}
|
||||
|
||||
export class SessionDirectoryMismatchError extends Schema.TaggedError<SessionDirectoryMismatchError>()(
|
||||
"ACPSessionDirectoryMismatchError",
|
||||
{ sessionId: Schema.String, cwd: Schema.String },
|
||||
) {}
|
||||
|
||||
export class InvalidConfigOptionError extends Schema.TaggedError<InvalidConfigOptionError>()(
|
||||
"ACPInvalidConfigOptionError",
|
||||
{ configId: Schema.String },
|
||||
@@ -42,7 +37,6 @@ export class ServiceFailureError extends Schema.TaggedError<ServiceFailureError>
|
||||
|
||||
export type Error =
|
||||
| SessionNotFoundError
|
||||
| SessionDirectoryMismatchError
|
||||
| InvalidConfigOptionError
|
||||
| InvalidModelError
|
||||
| InvalidEffortError
|
||||
@@ -55,11 +49,6 @@ export function toRequestError(error: Error): RequestError {
|
||||
switch (error._tag) {
|
||||
case "ACPSessionNotFoundError":
|
||||
return RequestError.invalidParams({ sessionId: error.sessionId }, `session not found: ${error.sessionId}`)
|
||||
case "ACPSessionDirectoryMismatchError":
|
||||
return RequestError.invalidParams(
|
||||
{ sessionId: error.sessionId, cwd: error.cwd },
|
||||
`session ${error.sessionId} does not belong to cwd: ${error.cwd}`,
|
||||
)
|
||||
case "ACPInvalidConfigOptionError":
|
||||
return RequestError.invalidParams({ configId: error.configId }, `unknown config option: ${error.configId}`)
|
||||
case "ACPInvalidModelError":
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { PromptResponse, SessionUpdate } from "@agentclientprotocol/sdk"
|
||||
import type { AgentSideConnection, PromptResponse, SessionUpdate } from "@agentclientprotocol/sdk"
|
||||
import type {
|
||||
EventSubscribeOutput,
|
||||
OpenCodeClient,
|
||||
SessionMessageAssistant,
|
||||
SessionMessageInfo,
|
||||
} from "@opencode/client/promise"
|
||||
import type { ACPConnection } from "./connection"
|
||||
import { partsToContentChunks, type ReplayPart } from "./content"
|
||||
import { ACPError } from "./error"
|
||||
import { replyPermission, syncEditedFiles } from "./permission"
|
||||
@@ -18,11 +17,11 @@ import {
|
||||
type ToolInput,
|
||||
} from "./tool"
|
||||
|
||||
type Connection = Pick<ACPConnection.Connection, "sessionUpdate" | "requestPermission" | "writeTextFile">
|
||||
type Connection = Pick<AgentSideConnection, "sessionUpdate" | "requestPermission"> &
|
||||
Partial<Pick<AgentSideConnection, "writeTextFile">>
|
||||
|
||||
export type TurnControl = {
|
||||
cancelled: boolean
|
||||
// Aborted whenever the turn is cancelled or closed, so it also cancels the turn's outbound client requests.
|
||||
readonly admission: AbortController
|
||||
}
|
||||
|
||||
@@ -161,7 +160,6 @@ export async function streamTurn(input: {
|
||||
clientSessionID: input.sessionID,
|
||||
cwd: input.cwd,
|
||||
tool,
|
||||
signal: control.admission.signal,
|
||||
...(child ? { toolCallPrefix: child.id, titlePrefix: child.title } : {}),
|
||||
})
|
||||
continue
|
||||
@@ -271,7 +269,6 @@ export async function streamTurn(input: {
|
||||
toolName: current.name,
|
||||
toolInput: current.input,
|
||||
metadata: event.data.metadata ?? {},
|
||||
signal: control.admission.signal,
|
||||
}).catch(() => {})
|
||||
await send({
|
||||
sessionUpdate: "tool_call_update",
|
||||
@@ -420,7 +417,7 @@ function projectChildUpdate(update: SessionUpdate, child: ChildSession) {
|
||||
}
|
||||
|
||||
export async function replayMessages(
|
||||
connection: Pick<Connection, "sessionUpdate">,
|
||||
connection: Pick<AgentSideConnection, "sessionUpdate">,
|
||||
sessionID: string,
|
||||
cwd: string,
|
||||
messages: readonly SessionMessageInfo[],
|
||||
@@ -429,7 +426,7 @@ export async function replayMessages(
|
||||
}
|
||||
|
||||
async function replayMessage(
|
||||
connection: Pick<Connection, "sessionUpdate">,
|
||||
connection: Pick<AgentSideConnection, "sessionUpdate">,
|
||||
sessionID: string,
|
||||
cwd: string,
|
||||
message: SessionMessageInfo,
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import type { PermissionOption, ToolCallContent, ToolCallLocation } from "@agentclientprotocol/sdk"
|
||||
import type { AgentSideConnection, PermissionOption, ToolCallContent, ToolCallLocation } from "@agentclientprotocol/sdk"
|
||||
import type { EventSubscribeOutput, OpenCodeClient } from "@opencode/client/promise"
|
||||
import { Patch } from "@opencode/util/patch"
|
||||
import { Result } from "effect"
|
||||
import { isAbsolute, resolve } from "node:path"
|
||||
import type { ACPConnection } from "./connection"
|
||||
import { pendingToolCall, stringValue, toLocations, toToolKind, type ToolInput } from "./tool"
|
||||
|
||||
type PermissionEvent = Extract<EventSubscribeOutput, { type: "permission.asked" }>
|
||||
type Connection = Pick<ACPConnection.Connection, "requestPermission" | "writeTextFile">
|
||||
type Connection = Pick<AgentSideConnection, "requestPermission"> & Partial<Pick<AgentSideConnection, "writeTextFile">>
|
||||
type Tool = { readonly name: string; readonly input: ToolInput }
|
||||
|
||||
const options: PermissionOption[] = [
|
||||
@@ -26,34 +25,31 @@ export async function replyPermission(input: {
|
||||
readonly tool?: Tool
|
||||
readonly toolCallPrefix?: string
|
||||
readonly titlePrefix?: string
|
||||
readonly signal?: AbortSignal
|
||||
}) {
|
||||
const toolName = input.tool?.name ?? input.event.data.action
|
||||
const toolInput = { ...input.event.data.metadata, ...input.tool?.input }
|
||||
const previews = await permissionPreviews(toolName, toolInput, input.cwd)
|
||||
const toolCallID = input.event.data.source?.id ?? input.event.data.id
|
||||
const title = permissionTitle(toolName, toolInput, previews)
|
||||
const request = {
|
||||
sessionId: input.clientSessionID ?? input.sessionID,
|
||||
toolCall: {
|
||||
...pendingToolCall({
|
||||
toolCallId: input.toolCallPrefix ? `${input.toolCallPrefix}:${toolCallID}` : toolCallID,
|
||||
toolName,
|
||||
state: {
|
||||
input: toolInput,
|
||||
title: prefixedTitle(input.titlePrefix, title),
|
||||
},
|
||||
cwd: input.cwd,
|
||||
}),
|
||||
locations: permissionLocations(toolName, toolInput, input.event.data.resources, input.cwd, previews),
|
||||
...(previews.length > 0 ? { content: previews } : {}),
|
||||
},
|
||||
options,
|
||||
}
|
||||
// An already-cancelled turn skips the round-trip; the SDK would still send the request and then cancel it.
|
||||
const result = input.signal?.aborted
|
||||
? undefined
|
||||
: await input.connection.requestPermission(request, { cancellationSignal: input.signal }).catch(() => undefined)
|
||||
const result = await input.connection
|
||||
.requestPermission({
|
||||
sessionId: input.clientSessionID ?? input.sessionID,
|
||||
toolCall: {
|
||||
...pendingToolCall({
|
||||
toolCallId: input.toolCallPrefix ? `${input.toolCallPrefix}:${toolCallID}` : toolCallID,
|
||||
toolName,
|
||||
state: {
|
||||
input: toolInput,
|
||||
title: prefixedTitle(input.titlePrefix, title),
|
||||
},
|
||||
cwd: input.cwd,
|
||||
}),
|
||||
locations: permissionLocations(toolName, toolInput, input.event.data.resources, input.cwd, previews),
|
||||
...(previews.length > 0 ? { content: previews } : {}),
|
||||
},
|
||||
options,
|
||||
})
|
||||
.catch(() => undefined)
|
||||
const selected = result?.outcome.outcome === "selected" ? result.outcome.optionId : undefined
|
||||
const reply = selected === "once" || selected === "always" ? selected : "reject"
|
||||
await input.client.permission.reply({
|
||||
@@ -70,14 +66,13 @@ function prefixedTitle(prefix: string | undefined, title: string | undefined) {
|
||||
}
|
||||
|
||||
export async function syncEditedFiles(input: {
|
||||
readonly connection: Pick<ACPConnection.Connection, "writeTextFile">
|
||||
readonly connection: Partial<Pick<AgentSideConnection, "writeTextFile">>
|
||||
readonly writeTextFile: boolean
|
||||
readonly sessionID: string
|
||||
readonly cwd: string
|
||||
readonly toolName: string
|
||||
readonly toolInput: ToolInput
|
||||
readonly metadata: Readonly<Record<string, unknown>>
|
||||
readonly signal?: AbortSignal
|
||||
}) {
|
||||
if (!input.writeTextFile || !input.connection.writeTextFile || toToolKind(input.toolName) !== "edit") return
|
||||
const files = Array.isArray(input.metadata.files)
|
||||
@@ -94,10 +89,7 @@ export async function syncEditedFiles(input: {
|
||||
const target = resolvePath(path, input.cwd)
|
||||
const file = Bun.file(target)
|
||||
if (!(await file.exists())) return
|
||||
await input.connection.writeTextFile?.(
|
||||
{ sessionId: input.sessionID, path: target, content: await file.text() },
|
||||
{ cancellationSignal: input.signal },
|
||||
)
|
||||
await input.connection.writeTextFile?.({ sessionId: input.sessionID, path: target, content: await file.text() })
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ import {
|
||||
type SessionInfo,
|
||||
type SessionMessageInfo,
|
||||
} from "@opencode/client/promise"
|
||||
import { FSUtil } from "@opencode/util/fs-util"
|
||||
import { withTimestampedFallback } from "@opencode/util/session-title-fallback"
|
||||
import type {
|
||||
AgentSideConnection,
|
||||
AuthenticateRequest,
|
||||
AuthenticateResponse,
|
||||
AuthMethod,
|
||||
@@ -46,7 +46,6 @@ import {
|
||||
parseModelSelection,
|
||||
type ConfigOptionProvider,
|
||||
} from "./config-option"
|
||||
import type { ACPConnection } from "./connection"
|
||||
import { promptContentToParts } from "./content"
|
||||
import {
|
||||
ChildSessionUpdateMethod,
|
||||
@@ -61,6 +60,9 @@ import { ACPError } from "./error"
|
||||
|
||||
export const AuthMethodID = "opencode-login"
|
||||
|
||||
type Connection = Pick<AgentSideConnection, "sessionUpdate" | "requestPermission"> &
|
||||
Partial<Pick<AgentSideConnection, "writeTextFile" | "extNotification" | "signal">>
|
||||
|
||||
type Catalog = {
|
||||
readonly providers: ConfigOptionProvider[]
|
||||
readonly models: ModelInfo[]
|
||||
@@ -100,18 +102,15 @@ export interface Interface {
|
||||
forkSession(input: ForkSessionRequest): Promise<ForkSessionResponse>
|
||||
setSessionConfigOption(input: SetSessionConfigOptionRequest): Promise<SetSessionConfigOptionResponse>
|
||||
setSessionMode(input: SetSessionModeRequest): Promise<SetSessionModeResponse>
|
||||
prompt(input: PromptRequest, signal?: AbortSignal): Promise<PromptResponse>
|
||||
prompt(input: PromptRequest): Promise<PromptResponse>
|
||||
cancel(input: CancelNotification): Promise<void>
|
||||
}
|
||||
|
||||
export function make(input: {
|
||||
readonly client: OpenCodeClient
|
||||
readonly connection: ACPConnection.Connection
|
||||
}): Interface {
|
||||
export function make(input: { readonly client: OpenCodeClient; readonly connection: Connection }): Interface {
|
||||
const sessions = new Map<string, Attached>()
|
||||
const catalogs = new Map<string, Promise<Catalog>>()
|
||||
const registeredMcp = new Map<string, Set<string>>()
|
||||
const active = new Map<string, { readonly control: TurnControl; readonly turn: Promise<PromptResponse> }>()
|
||||
const active = new Map<string, TurnControl>()
|
||||
const capabilities = { writeTextFile: false, childSessionUpdates: false }
|
||||
|
||||
const catalog = (cwd: string) => {
|
||||
@@ -137,15 +136,6 @@ export function make(input: {
|
||||
registeredMcp.delete(sessionID)
|
||||
}
|
||||
|
||||
const cancelTurn = (sessionID: string) => {
|
||||
const turn = active.get(sessionID)
|
||||
if (turn) {
|
||||
turn.control.cancelled = true
|
||||
turn.control.admission.abort()
|
||||
}
|
||||
return input.client.session.interrupt({ sessionID })
|
||||
}
|
||||
|
||||
const attach = async (session: SessionInfo, cwd: string, mcpServers: readonly McpServer[]) => {
|
||||
const currentCatalog = await catalog(cwd)
|
||||
sessions.get(session.id)?.abort.abort()
|
||||
@@ -226,7 +216,7 @@ export function make(input: {
|
||||
return { sessionId: state.id, configOptions: configOptions(state) }
|
||||
},
|
||||
loadSession: async (params) => {
|
||||
const session = await getSession(input.client, params.sessionId, params.cwd)
|
||||
const session = await getSession(input.client, params.sessionId)
|
||||
const state = await attach(session, session.location.directory, params.mcpServers)
|
||||
await replay(state)
|
||||
return { configOptions: configOptions(state) }
|
||||
@@ -256,17 +246,18 @@ export function make(input: {
|
||||
return {}
|
||||
},
|
||||
resumeSession: async (params) => {
|
||||
const session = await getSession(input.client, params.sessionId, params.cwd)
|
||||
const session = await getSession(input.client, params.sessionId)
|
||||
const state = await attach(session, session.location.directory, params.mcpServers ?? [])
|
||||
return { configOptions: configOptions(state) }
|
||||
},
|
||||
closeSession: async (params) => {
|
||||
const turn = active.get(params.sessionId)
|
||||
await cancelTurn(params.sessionId).catch((error) => {
|
||||
if (!isSessionNotFoundError(error)) throw error
|
||||
})
|
||||
await turn?.turn.catch(() => {})
|
||||
detach(params.sessionId)
|
||||
const turn = active.get(params.sessionId)
|
||||
if (turn) {
|
||||
turn.cancelled = true
|
||||
turn.admission.abort()
|
||||
}
|
||||
await input.client.session.interrupt({ sessionID: params.sessionId }).catch(() => {})
|
||||
return {}
|
||||
},
|
||||
forkSession: async (params) => {
|
||||
@@ -312,7 +303,7 @@ export function make(input: {
|
||||
await selectMode(input.client, await requireSession(params.sessionId), params.modeId)
|
||||
return {}
|
||||
},
|
||||
prompt: async (params, signal) => {
|
||||
prompt: async (params) => {
|
||||
const state = await requireSession(params.sessionId)
|
||||
if (active.has(state.id)) {
|
||||
throw new ACPError.ServiceFailureError({
|
||||
@@ -328,9 +319,8 @@ export function make(input: {
|
||||
capabilities.childSessionUpdates && extNotification
|
||||
? (update: ChildSessionUpdate) => extNotification(ChildSessionUpdateMethod, update).then(() => {})
|
||||
: undefined
|
||||
// A `$/cancel_request` for this prompt behaves like `session/cancel` for its turn.
|
||||
const cancel = () => void cancelTurn(state.id).catch(() => {})
|
||||
const turn = streamTurn({
|
||||
active.set(state.id, control)
|
||||
const response = await streamTurn({
|
||||
client: input.client,
|
||||
connection: input.connection,
|
||||
sessionID: state.id,
|
||||
@@ -343,23 +333,19 @@ export function make(input: {
|
||||
sessionSignal: state.abort.signal,
|
||||
submit: (signal) => submitPrompt(input.client, state, prepared, signal),
|
||||
...(childSessionUpdate ? { childSessionUpdate } : {}),
|
||||
}).finally(() => {
|
||||
if (active.get(state.id) === control) active.delete(state.id)
|
||||
})
|
||||
.then(async (response) => {
|
||||
await sendUsageUpdate(input.client, input.connection, state, response.usage?.totalTokens).catch(() => {})
|
||||
return response
|
||||
})
|
||||
.finally(() => {
|
||||
signal?.removeEventListener("abort", cancel)
|
||||
if (active.get(state.id)?.control === control) active.delete(state.id)
|
||||
})
|
||||
active.set(state.id, { control, turn })
|
||||
signal?.addEventListener("abort", cancel, { once: true })
|
||||
// The cancel may already be buffered behind the awaits above.
|
||||
if (signal?.aborted) cancel()
|
||||
return turn
|
||||
await sendUsageUpdate(input.client, input.connection, state, response.usage?.totalTokens).catch(() => {})
|
||||
return response
|
||||
},
|
||||
cancel: async (params) => {
|
||||
await cancelTurn(params.sessionId).catch(() => {})
|
||||
const current = active.get(params.sessionId)
|
||||
if (current) {
|
||||
current.cancelled = true
|
||||
current.admission.abort()
|
||||
}
|
||||
await input.client.session.interrupt({ sessionID: params.sessionId }).catch(() => {})
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -486,15 +472,11 @@ async function selectMode(client: OpenCodeClient, state: Attached, modeID: strin
|
||||
await client.session.switchAgent({ sessionID: state.id, agent: modeID })
|
||||
}
|
||||
|
||||
async function getSession(client: OpenCodeClient, sessionID: string, cwd: string) {
|
||||
const session = await client.session.get({ sessionID }).catch((error) => {
|
||||
async function getSession(client: OpenCodeClient, sessionID: string) {
|
||||
return client.session.get({ sessionID }).catch((error) => {
|
||||
if (isSessionNotFoundError(error)) throw new ACPError.SessionNotFoundError({ sessionId: sessionID })
|
||||
throw error
|
||||
})
|
||||
if (FSUtil.resolve(cwd) !== FSUtil.resolve(session.location.directory)) {
|
||||
throw new ACPError.SessionDirectoryMismatchError({ sessionId: sessionID, cwd })
|
||||
}
|
||||
return session
|
||||
}
|
||||
|
||||
async function messages(client: OpenCodeClient, sessionID: string) {
|
||||
@@ -560,12 +542,7 @@ function stableStringify(value: unknown): string {
|
||||
.join(",")}}`
|
||||
}
|
||||
|
||||
async function sendUsageUpdate(
|
||||
client: OpenCodeClient,
|
||||
connection: ACPConnection.Connection,
|
||||
session: Attached,
|
||||
used?: number,
|
||||
) {
|
||||
async function sendUsageUpdate(client: OpenCodeClient, connection: Connection, session: Attached, used?: number) {
|
||||
if (!used) return
|
||||
const model = session.catalog.models.find(
|
||||
(item) => item.providerID === session.model.providerID && item.id === session.model.id,
|
||||
|
||||
@@ -163,10 +163,6 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
|
||||
Argument.optional,
|
||||
),
|
||||
method: Flag.string("method").pipe(Flag.withDescription("Authentication method ID"), Flag.optional),
|
||||
answer: Flag.string("answer").pipe(
|
||||
Flag.withDescription("Provider form answer (key=value; repeat for multiple fields)"),
|
||||
Flag.atMost(100),
|
||||
),
|
||||
},
|
||||
}),
|
||||
Spec.make("logout", {
|
||||
@@ -488,12 +484,6 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Spec.make("reload", {
|
||||
description: "Reload configuration",
|
||||
params: {
|
||||
...ServerParams,
|
||||
},
|
||||
}),
|
||||
Spec.make("pair", {
|
||||
description: "Show server pairing information",
|
||||
params: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ndJsonStream } from "@agentclientprotocol/sdk"
|
||||
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"
|
||||
import { OpenCode } from "@opencode/client/promise"
|
||||
import { Service } from "@opencode/client/effect/service"
|
||||
import { Effect } from "effect"
|
||||
@@ -26,7 +26,8 @@ export default Runtime.handler(
|
||||
process.stdin.on("error", (error) => controller.error(error))
|
||||
},
|
||||
})
|
||||
const connection = ACP.connect(client, ndJsonStream(input, output))
|
||||
const stream = ndJsonStream(input, output)
|
||||
const connection = new AgentSideConnection((connection) => ACP.create(client, connection), stream)
|
||||
process.stdin.resume()
|
||||
yield* Effect.promise(() => connection.closed)
|
||||
// EOF owns this stdio process; exiting also closes the private server's lease pipe.
|
||||
|
||||
@@ -1,72 +1,29 @@
|
||||
import { confirm, log, multiselect, password, select, text, type Option } from "@clack/prompts"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Form } from "@opencode/schema/form"
|
||||
import { formValidateValue } from "@opencode/tui/util/form"
|
||||
import { Effect } from "effect"
|
||||
import type { FormAnswer, FormField, FormFields } from "@opencode/client"
|
||||
import { openUrl, prompt, requireInteractive } from "../../../ui/prompt"
|
||||
|
||||
const skip = Symbol("skip")
|
||||
const custom = Symbol("custom")
|
||||
const decodeAnswer = Schema.decodeUnknownOption(Schema.fromJsonString(Form.Value))
|
||||
|
||||
export const answerForm = Effect.fn("cli.auth.form")(function* (
|
||||
fields: FormFields | undefined,
|
||||
input: ReadonlyArray<string> = [],
|
||||
) {
|
||||
const supplied = new Map<string, string>()
|
||||
for (const item of input) {
|
||||
const separator = item.indexOf("=")
|
||||
if (separator < 1) return yield* Effect.fail(new Error("Expected --answer key=value"))
|
||||
const key = item.slice(0, separator)
|
||||
if (!fields?.some((field) => field.key === key)) {
|
||||
return yield* Effect.fail(new Error(`Unknown form field: ${key}`))
|
||||
}
|
||||
supplied.set(key, item.slice(separator + 1))
|
||||
}
|
||||
export const answerForm = Effect.fn("cli.auth.form")(function* (fields: FormFields | undefined) {
|
||||
if (!fields) return undefined
|
||||
yield* requireInteractive("Authentication form input requires an interactive terminal")
|
||||
const answer: FormAnswer = {}
|
||||
for (const field of fields) {
|
||||
if (!active(field, answer)) {
|
||||
if (supplied.has(field.key)) {
|
||||
return yield* Effect.fail(new Error(`Form field is not active: ${field.key}`))
|
||||
}
|
||||
continue
|
||||
}
|
||||
const provided = supplied.get(field.key)
|
||||
const value =
|
||||
provided !== undefined
|
||||
? yield* suppliedAnswer(field, provided)
|
||||
: field.type !== "external" && field.hidden
|
||||
? field.default
|
||||
: yield* answerField(field)
|
||||
if (field.type !== "external") {
|
||||
const invalid = formValidateValue(field, value)
|
||||
if (invalid) return yield* Effect.fail(new Error(`${field.key}: ${invalid}`))
|
||||
}
|
||||
if (!active(field, answer)) continue
|
||||
const value = yield* answerField(field)
|
||||
if (value !== undefined) answer[field.key] = value
|
||||
}
|
||||
return answer
|
||||
})
|
||||
|
||||
const suppliedAnswer = Effect.fn("cli.auth.form.answer")(function* (field: FormField, input: string) {
|
||||
if (field.type === "external") {
|
||||
return yield* Effect.fail(new Error(`External form field requires interactive confirmation: ${field.key}`))
|
||||
}
|
||||
if (field.type === "string") return input
|
||||
const value = decodeAnswer(input)
|
||||
if (value._tag === "None") {
|
||||
return yield* Effect.fail(new Error(`Expected a JSON value for form field: ${field.key}`))
|
||||
}
|
||||
return typeof value.value === "object" ? [...value.value] : value.value
|
||||
})
|
||||
|
||||
export const secret = Effect.fn("cli.auth.secret")(function* (message: string) {
|
||||
yield* requireInteractive("API key input requires an interactive terminal")
|
||||
return yield* prompt<string>(() => password({ message, validate: (value) => (!value ? "Required" : undefined) }))
|
||||
})
|
||||
|
||||
const answerField = Effect.fn("cli.auth.form.field")(function* (field: FormField) {
|
||||
yield* requireInteractive("Authentication form input requires an interactive terminal")
|
||||
const message = field.title ?? field.key
|
||||
if (field.description) log.info(field.description)
|
||||
if (field.type === "external") {
|
||||
|
||||
@@ -33,7 +33,6 @@ export default Runtime.handler(
|
||||
login({
|
||||
target: Option.getOrUndefined(input.target),
|
||||
method: Option.getOrUndefined(input.method),
|
||||
answer: input.answer,
|
||||
server: Option.getOrUndefined(input.server),
|
||||
standalone: input.standalone,
|
||||
}).pipe(handlePromptErrors),
|
||||
@@ -43,7 +42,6 @@ export default Runtime.handler(
|
||||
const login = Effect.fn("cli.auth.login.run")(function* (input: {
|
||||
target?: string
|
||||
method?: string
|
||||
answer?: ReadonlyArray<string>
|
||||
server?: string
|
||||
standalone: boolean
|
||||
}) {
|
||||
@@ -55,7 +53,7 @@ const login = Effect.fn("cli.auth.login.run")(function* (input: {
|
||||
const methods = connectMethods(integration)
|
||||
if (methods.length === 0) yield* Effect.fail(new Error(`${integration.name} has no interactive login methods`))
|
||||
const method = yield* chooseMethod(methods, input.method)
|
||||
const answer = yield* answerForm(method.type === "command" ? undefined : method.form, input.answer)
|
||||
const answer = method.type === "command" ? undefined : yield* answerForm(method.form)
|
||||
yield* authenticate(client, integration, method, answer)
|
||||
outro("Done")
|
||||
})
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { EOL } from "node:os"
|
||||
import { Effect, Option } from "effect"
|
||||
import { OpenCode } from "@opencode/client"
|
||||
import { Service } from "@opencode/client/effect/service"
|
||||
import { Commands } from "../commands"
|
||||
import { Runtime } from "../../framework/runtime"
|
||||
import { ServerConnection } from "../../services/server-connection"
|
||||
|
||||
export default Runtime.handler(
|
||||
Commands.commands.reload,
|
||||
Effect.fn("cli.reload")(function* (input: Runtime.Input<typeof Commands.commands.reload>) {
|
||||
const server = yield* ServerConnection.resolve({
|
||||
server: Option.getOrUndefined(input.server),
|
||||
standalone: input.standalone,
|
||||
})
|
||||
const client = OpenCode.make({ baseUrl: server.endpoint.url, headers: Service.headers(server.endpoint) })
|
||||
yield* Effect.tryPromise({
|
||||
try: (signal) => client.location.reload({ signal }),
|
||||
catch: (cause) => cause,
|
||||
})
|
||||
process.stdout.write("Configuration reloaded" + EOL)
|
||||
}),
|
||||
)
|
||||
@@ -55,7 +55,6 @@ const Handlers = Runtime.handlers(Commands, {
|
||||
mini: () => import("./commands/handlers/mini"),
|
||||
run: () => import("./commands/handlers/run"),
|
||||
pair: () => import("./commands/handlers/pair"),
|
||||
reload: () => import("./commands/handlers/reload"),
|
||||
session: {
|
||||
list: () => import("./commands/handlers/session/list"),
|
||||
delete: () => import("./commands/handlers/session/delete"),
|
||||
|
||||
@@ -13,6 +13,7 @@ import { HttpServer } from "effect/unstable/http"
|
||||
import { Env } from "./env"
|
||||
import { ServiceConfig } from "./services/service-config"
|
||||
import { ServiceRegistration } from "./services/service-registration"
|
||||
import { Updater } from "./services/updater"
|
||||
import { WebUi } from "./services/web-ui"
|
||||
import { databasePath } from "./database-path"
|
||||
|
||||
@@ -158,6 +159,21 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
|
||||
const url = HttpServer.formatAddress(server.address)
|
||||
console.log(options.mode === "stdio" ? JSON.stringify({ url }) : `server listening on ${url}`)
|
||||
if (foreground && !environmentPassword) console.log(`server password ${password}`)
|
||||
yield* Updater.Service.pipe(
|
||||
Effect.flatMap((updater) =>
|
||||
Updater.pollUpdates({
|
||||
check: updater.run().pipe(
|
||||
Effect.flatMap((result) => {
|
||||
if (!result) return Effect.void
|
||||
if (result.type === "available") return server.updateAvailable(result.version)
|
||||
return server.updated(result.version)
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
Effect.provide(Updater.layer),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
return yield* options.mode === "service"
|
||||
? server.shutdown
|
||||
: options.mode === "stdio"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Global } from "@opencode/util/global"
|
||||
import { AppProcess } from "@opencode/util/process"
|
||||
import { OPENCODE_ARTIFACT, OPENCODE_CHANNEL, OPENCODE_LOCAL, OPENCODE_VERSION } from "../version"
|
||||
import { Context, Duration, Effect, FileSystem, Layer, Ref } from "effect"
|
||||
import { Context, Duration, Effect, FileSystem, Layer, Ref, Schedule } from "effect"
|
||||
import { ChildProcess } from "effect/unstable/process"
|
||||
import { parse, type ParseError } from "jsonc-parser"
|
||||
import path from "node:path"
|
||||
@@ -24,6 +24,18 @@ export interface Interface {
|
||||
| undefined
|
||||
}
|
||||
|
||||
export const pollUpdates = Effect.fnUntraced(function* (input: {
|
||||
readonly check: Effect.Effect<unknown>
|
||||
readonly initialDelay?: Duration.Input
|
||||
readonly interval?: Duration.Input
|
||||
}) {
|
||||
const interval = input.interval ?? "10 minutes"
|
||||
return yield* input.check.pipe(
|
||||
Effect.repeat(Schedule.spaced(interval)),
|
||||
Effect.delay(input.initialDelay ?? "1 minute"),
|
||||
)
|
||||
})
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/cli/Updater") {}
|
||||
|
||||
export function decodePolicy(text: string): Policy | undefined {
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import type { CloseSessionResponse, PromptResponse } from "@agentclientprotocol/sdk"
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { createAcpFixture, expectOk, initialize, newSession } from "./subprocess"
|
||||
|
||||
// The mock model holds any completion whose request mentions "hold" until the test releases it,
|
||||
// so cancellation can be exercised while the model turn is genuinely in flight.
|
||||
function heldModel() {
|
||||
const started = Promise.withResolvers<void>()
|
||||
const release = Promise.withResolvers<string>()
|
||||
return {
|
||||
started: started.promise,
|
||||
release: () => release.resolve("released"),
|
||||
respond(request: unknown) {
|
||||
if (!JSON.stringify(request).includes("hold")) return "accepted"
|
||||
started.resolve()
|
||||
return release.promise
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe("acp cancel subprocess", () => {
|
||||
test("$/cancel_request cancels the in-flight prompt and the session stays usable", async () => {
|
||||
const model = heldModel()
|
||||
await using fixture = await createAcpFixture({ respond: model.respond })
|
||||
const acp = fixture.spawn()
|
||||
await initialize(acp)
|
||||
const session = await newSession(acp, fixture.home)
|
||||
|
||||
const prompt = acp.send<PromptResponse>("session/prompt", {
|
||||
sessionId: session.sessionId,
|
||||
prompt: [{ type: "text", text: "hold" }],
|
||||
})
|
||||
await model.started
|
||||
await acp.notify("$/cancel_request", { requestId: prompt.id })
|
||||
|
||||
expect(expectOk(await prompt.response).stopReason).toBe("cancelled")
|
||||
model.release()
|
||||
const next = expectOk(
|
||||
await acp.request<PromptResponse>("session/prompt", {
|
||||
sessionId: session.sessionId,
|
||||
prompt: [{ type: "text", text: "again" }],
|
||||
}),
|
||||
)
|
||||
expect(next.stopReason).toBe("end_turn")
|
||||
}, 60_000)
|
||||
|
||||
test("session/close settles the active prompt before responding and leaves other sessions alone", async () => {
|
||||
const model = heldModel()
|
||||
await using fixture = await createAcpFixture({ respond: model.respond })
|
||||
const acp = fixture.spawn()
|
||||
await initialize(acp)
|
||||
const closing = await newSession(acp, fixture.home)
|
||||
const other = await newSession(acp, fixture.home)
|
||||
|
||||
const prompt = acp.send<PromptResponse>("session/prompt", {
|
||||
sessionId: closing.sessionId,
|
||||
prompt: [{ type: "text", text: "hold" }],
|
||||
})
|
||||
await model.started
|
||||
const order: string[] = []
|
||||
const close = acp
|
||||
.request<CloseSessionResponse>("session/close", { sessionId: closing.sessionId })
|
||||
.then((response) => {
|
||||
order.push("close")
|
||||
return response
|
||||
})
|
||||
const cancelled = await prompt.response.then((response) => {
|
||||
order.push("prompt")
|
||||
return response
|
||||
})
|
||||
|
||||
expect(expectOk(cancelled).stopReason).toBe("cancelled")
|
||||
expect(expectOk(await close)).toEqual({})
|
||||
expect(order).toEqual(["prompt", "close"])
|
||||
model.release()
|
||||
const next = expectOk(
|
||||
await acp.request<PromptResponse>("session/prompt", {
|
||||
sessionId: other.sessionId,
|
||||
prompt: [{ type: "text", text: "still here" }],
|
||||
}),
|
||||
)
|
||||
expect(next.stopReason).toBe("end_turn")
|
||||
}, 60_000)
|
||||
})
|
||||
@@ -51,12 +51,6 @@ describe("acp lifecycle subprocess", () => {
|
||||
)
|
||||
|
||||
expect(selectConfigOption(loaded.configOptions, "model")?.category).toBe("model")
|
||||
const mismatched = await acp.request<LoadSessionResponse>("session/load", {
|
||||
cwd: fixture.root,
|
||||
sessionId: session.sessionId,
|
||||
mcpServers: [],
|
||||
})
|
||||
expect(mismatched.error?.code).toBe(-32602)
|
||||
}, 60_000)
|
||||
|
||||
test("list request includes a live ACP-created session", async () => {
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type {
|
||||
RequestPermissionRequest,
|
||||
RequestPermissionResponse,
|
||||
SessionNotification,
|
||||
WriteTextFileRequest,
|
||||
} from "@agentclientprotocol/sdk"
|
||||
import type { AgentSideConnection, RequestPermissionRequest, RequestPermissionResponse } from "@agentclientprotocol/sdk"
|
||||
import fs from "node:fs/promises"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import type { ACPConnection } from "../../src/acp/connection"
|
||||
import { streamTurn } from "../../src/acp/event"
|
||||
import { syncEditedFiles } from "../../src/acp/permission"
|
||||
import { createSseFixture, durableEvent, ephemeralEvent, withTimeout } from "./sse-fixture"
|
||||
|
||||
type SessionUpdateParams = SessionNotification
|
||||
type Connection = Pick<ACPConnection.Connection, "sessionUpdate" | "requestPermission" | "writeTextFile">
|
||||
type SessionUpdateParams = Parameters<AgentSideConnection["sessionUpdate"]>[0]
|
||||
type Connection = Pick<AgentSideConnection, "sessionUpdate" | "requestPermission"> &
|
||||
Partial<Pick<AgentSideConnection, "writeTextFile">>
|
||||
type Fixture = ReturnType<typeof createSseFixture>
|
||||
|
||||
describe("acp permission behavior", () => {
|
||||
test("does not sync edits when writeTextFile was not advertised", async () => {
|
||||
const writes: WriteTextFileRequest[] = []
|
||||
const writes: Parameters<AgentSideConnection["writeTextFile"]>[0][] = []
|
||||
|
||||
await syncEditedFiles({
|
||||
connection: {
|
||||
@@ -221,7 +216,7 @@ describe("acp permission behavior", () => {
|
||||
const file = path.join(cwd, "file.ts")
|
||||
await fs.writeFile(file, "before")
|
||||
const permissionRequests: RequestPermissionRequest[] = []
|
||||
const writes: WriteTextFileRequest[] = []
|
||||
const writes: Parameters<AgentSideConnection["writeTextFile"]>[0][] = []
|
||||
const fixture = createSseFixture({
|
||||
onPrompt({ id, send }) {
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_edit", inboxID: id }))
|
||||
@@ -311,7 +306,7 @@ describe("acp permission behavior", () => {
|
||||
"*** End Patch",
|
||||
].join("\n")
|
||||
const permissionRequests: RequestPermissionRequest[] = []
|
||||
const writes: WriteTextFileRequest[] = []
|
||||
const writes: Parameters<AgentSideConnection["writeTextFile"]>[0][] = []
|
||||
const fixture = createSseFixture({
|
||||
onPrompt({ id, send }) {
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_patch", inboxID: id }))
|
||||
@@ -541,55 +536,6 @@ describe("acp permission behavior", () => {
|
||||
await fixture.stop()
|
||||
}
|
||||
})
|
||||
|
||||
test("cancelling the turn cancels its pending permission request and rejects the permission", async () => {
|
||||
const requested = Promise.withResolvers<AbortSignal | undefined>()
|
||||
const fixture = createSseFixture({
|
||||
onPrompt({ id, send }) {
|
||||
send(durableEvent("session.inbox.delivered", { sessionID: "ses_cancel", inboxID: id }))
|
||||
send(permissionAsked("ses_cancel", "perm_cancel"))
|
||||
},
|
||||
onPermissionReply({ send }) {
|
||||
send(durableEvent("session.execution.interrupted", { sessionID: "ses_cancel", reason: "user" }))
|
||||
},
|
||||
})
|
||||
const connection = {
|
||||
sessionUpdate: async () => {},
|
||||
// Behaves like a client answering the agent's `$/cancel_request` with a cancelled outcome.
|
||||
requestPermission: (_request, options) =>
|
||||
new Promise<RequestPermissionResponse>((resolve) => {
|
||||
options?.cancellationSignal?.addEventListener("abort", () => resolve({ outcome: { outcome: "cancelled" } }), {
|
||||
once: true,
|
||||
})
|
||||
requested.resolve(options?.cancellationSignal)
|
||||
}),
|
||||
} satisfies Connection
|
||||
const control = { cancelled: false, admission: new AbortController() }
|
||||
const result = streamTurn({
|
||||
client: fixture.client,
|
||||
connection,
|
||||
sessionID: "ses_cancel",
|
||||
cwd: "/workspace",
|
||||
start: { type: "input", id: "input_cancel" },
|
||||
writeTextFile: false,
|
||||
control,
|
||||
submit: (signal) =>
|
||||
fixture.client.session.prompt({ sessionID: "ses_cancel", id: "input_cancel", text: "hello" }, { signal }),
|
||||
})
|
||||
|
||||
try {
|
||||
const signal = await withTimeout(requested.promise, "permission was not requested")
|
||||
expect(signal?.aborted).toBe(false)
|
||||
control.cancelled = true
|
||||
control.admission.abort()
|
||||
|
||||
expect(await withTimeout(result, "cancelled turn did not finish")).toMatchObject({ stopReason: "cancelled" })
|
||||
expect(permissionReplies(fixture)).toEqual([["perm_cancel", "reject"]])
|
||||
} finally {
|
||||
await result.catch(() => undefined)
|
||||
await fixture.stop()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function startTurn(fixture: Fixture, connection: Connection, sessionID: string, inboxID: string, cwd = "/workspace") {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SessionNotification } from "@agentclientprotocol/sdk"
|
||||
import type { AgentSideConnection } from "@agentclientprotocol/sdk"
|
||||
import {
|
||||
OpenCode,
|
||||
type AgentInfo,
|
||||
@@ -114,7 +114,7 @@ export function makeSession(
|
||||
|
||||
export function makeACPFixture(options: FixtureOptions = {}) {
|
||||
const requests: FixtureRequest[] = []
|
||||
const updates: SessionNotification[] = []
|
||||
const updates: Parameters<AgentSideConnection["sessionUpdate"]>[0][] = []
|
||||
const encoder = new TextEncoder()
|
||||
let eventController: ReadableStreamDefaultController<Uint8Array> | undefined
|
||||
const models = options.models ?? [testModel, secondModel]
|
||||
|
||||
@@ -94,24 +94,20 @@ describe("acp service lifecycle", () => {
|
||||
})
|
||||
|
||||
const loaded = await fixture.service.loadSession({
|
||||
cwd: "/workspace",
|
||||
cwd: "/ignored",
|
||||
sessionId: "ses_loaded",
|
||||
mcpServers: [],
|
||||
})
|
||||
const resumed = await fixture.service.resumeSession({
|
||||
cwd: "/workspace",
|
||||
cwd: "/ignored",
|
||||
sessionId: "ses_resume",
|
||||
mcpServers: [],
|
||||
})
|
||||
const forked = await fixture.service.forkSession({
|
||||
cwd: "/workspace",
|
||||
cwd: "/ignored",
|
||||
sessionId: "ses_loaded",
|
||||
mcpServers: [],
|
||||
})
|
||||
const mismatched = await fixture.service
|
||||
.loadSession({ cwd: "/elsewhere", sessionId: "ses_loaded", mcpServers: [] })
|
||||
.catch((error: unknown) => error)
|
||||
expect(mismatched).toMatchObject({ _tag: "ACPSessionDirectoryMismatchError", cwd: "/elsewhere" })
|
||||
|
||||
expect(currentValue(loaded, "model")).toBe("test/second-model")
|
||||
expect(currentValue(loaded, "effort")).toBe("medium")
|
||||
@@ -218,8 +214,7 @@ describe("acp service lifecycle", () => {
|
||||
])
|
||||
})
|
||||
|
||||
test("cancel preserves the attachment while close removes it and surfaces interrupt failures", async () => {
|
||||
const interrupt = { fail: true }
|
||||
test("cancel preserves the attachment while close removes it and interrupts best-effort", async () => {
|
||||
await using fixture = makeACPFixture({
|
||||
fetch(request) {
|
||||
if (request.method === "POST" && request.path === "/api/session") {
|
||||
@@ -228,14 +223,8 @@ describe("acp service lifecycle", () => {
|
||||
if (request.method === "POST" && request.path === "/api/session/ses_lifecycle/model") {
|
||||
return new Response(null, { status: 204 })
|
||||
}
|
||||
if (request.method === "POST" && request.path === "/api/session/missing/interrupt") {
|
||||
return Response.json(
|
||||
{ _tag: "SessionNotFoundError", sessionID: "missing", message: "session not found" },
|
||||
{ status: 404 },
|
||||
)
|
||||
}
|
||||
if (request.method === "POST" && request.path.endsWith("/interrupt")) {
|
||||
return interrupt.fail ? new Response(null, { status: 500 }) : Response.json({ interrupted: false })
|
||||
return new Response(null, { status: 500 })
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
@@ -250,9 +239,6 @@ describe("acp service lifecycle", () => {
|
||||
})
|
||||
|
||||
expect(currentValue(updated, "effort")).toBe("high")
|
||||
const failed = await fixture.service.closeSession({ sessionId: created.sessionId }).catch((error: unknown) => error)
|
||||
expect(failed).toMatchObject({ name: "ClientError", reason: "UnexpectedStatus" })
|
||||
interrupt.fail = false
|
||||
expect(await fixture.service.closeSession({ sessionId: created.sessionId })).toEqual({})
|
||||
const missing = await fixture.service
|
||||
.setSessionConfigOption({
|
||||
@@ -266,7 +252,6 @@ describe("acp service lifecycle", () => {
|
||||
expect(
|
||||
fixture.requests.filter((request) => request.path.endsWith("/interrupt")).map((request) => request.path),
|
||||
).toEqual([
|
||||
"/api/session/ses_lifecycle/interrupt",
|
||||
"/api/session/ses_lifecycle/interrupt",
|
||||
"/api/session/ses_lifecycle/interrupt",
|
||||
"/api/session/missing/interrupt",
|
||||
|
||||
@@ -46,11 +46,6 @@ type Waiter = {
|
||||
|
||||
export type AcpProcess = {
|
||||
readonly request: <T>(method: string, params?: unknown) => Promise<JsonRpcResponse<T>>
|
||||
readonly send: <T>(
|
||||
method: string,
|
||||
params?: unknown,
|
||||
) => { readonly id: number; readonly response: Promise<JsonRpcResponse<T>> }
|
||||
readonly notify: (method: string, params: unknown) => Promise<void>
|
||||
readonly waitForNotification: <T>(
|
||||
method: string,
|
||||
predicate: (params: T) => boolean,
|
||||
@@ -69,9 +64,7 @@ description: Verifier compatibility skill.
|
||||
# Verifier Skill
|
||||
`
|
||||
|
||||
export async function createAcpFixture(
|
||||
options: { readonly skill?: string; readonly respond?: (request: unknown) => string | Promise<string> } = {},
|
||||
) {
|
||||
export async function createAcpFixture(options: { readonly skill?: string } = {}) {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-cli-acp-"))
|
||||
const home = path.join(root, "workspace")
|
||||
const config = path.join(root, "config")
|
||||
@@ -91,9 +84,8 @@ export async function createAcpFixture(
|
||||
if (request.method !== "POST" || new URL(request.url).pathname !== "/v1/chat/completions") {
|
||||
return new Response("Not found", { status: 404 })
|
||||
}
|
||||
const body: unknown = await request.json().catch(() => undefined)
|
||||
requests.push(body)
|
||||
return new Response(completion(await (options.respond?.(body) ?? "accepted")), {
|
||||
requests.push(await request.json().catch(() => undefined))
|
||||
return new Response(completion("accepted"), {
|
||||
headers: { "content-type": "text/event-stream" },
|
||||
})
|
||||
},
|
||||
@@ -302,28 +294,18 @@ function spawnAcp(input: { readonly env: Record<string, string | undefined> }):
|
||||
})
|
||||
}
|
||||
|
||||
const write = async (message: JsonRpcRequest | JsonRpcNotification<unknown>) => {
|
||||
if (inputClosed) throw new Error("ACP stdin is closed")
|
||||
await child.stdin.write(encoder.encode(`${JSON.stringify(message)}\n`))
|
||||
await child.stdin.flush()
|
||||
}
|
||||
|
||||
const send = <T>(method: string, params?: unknown) => {
|
||||
const id = nextID++
|
||||
const request: JsonRpcRequest =
|
||||
params === undefined ? { jsonrpc: "2.0", id, method } : { jsonrpc: "2.0", id, method, params }
|
||||
const response = write(request).then(async () => {
|
||||
return {
|
||||
async request<T>(method: string, params?: unknown) {
|
||||
if (inputClosed) throw new Error("ACP stdin is closed")
|
||||
const id = nextID++
|
||||
const request: JsonRpcRequest =
|
||||
params === undefined ? { jsonrpc: "2.0", id, method } : { jsonrpc: "2.0", id, method, params }
|
||||
await child.stdin.write(encoder.encode(`${JSON.stringify(request)}\n`))
|
||||
await child.stdin.flush()
|
||||
const response = await take((message) => isResponse(message) && message.id === id, 20_000, `${method} response`)
|
||||
if (!isResponse<T>(response)) throw new Error(`Invalid ACP response: ${JSON.stringify(response)}`)
|
||||
return response
|
||||
})
|
||||
return { id, response }
|
||||
}
|
||||
|
||||
return {
|
||||
request: <T>(method: string, params?: unknown) => send<T>(method, params).response,
|
||||
send,
|
||||
notify: (method: string, params: unknown) => write({ jsonrpc: "2.0", method, params }),
|
||||
},
|
||||
async waitForNotification<T>(method: string, predicate: (params: T) => boolean, timeoutMs = 20_000) {
|
||||
const notification = await take(
|
||||
(message) => isNotification<T>(message) && message.method === method && predicate(message.params),
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import type { FormFields } from "@opencode/client"
|
||||
import { answerForm } from "../src/commands/handlers/auth/form"
|
||||
|
||||
test("resolves hidden defaults without an interactive terminal", async () => {
|
||||
const fields = [
|
||||
{ type: "string", key: "server", hidden: true, default: "https://example.com/console" },
|
||||
{ type: "boolean", key: "enabled", hidden: true, default: false },
|
||||
{ type: "integer", key: "count", hidden: true, default: 0 },
|
||||
{ type: "string", key: "optional", hidden: true },
|
||||
] satisfies FormFields
|
||||
expect(await Effect.runPromise(answerForm(fields))).toEqual({
|
||||
server: "https://example.com/console",
|
||||
enabled: false,
|
||||
count: 0,
|
||||
})
|
||||
expect(await Effect.runPromise(answerForm(fields, ["server=https://staging.example.com/console"]))).toEqual({
|
||||
server: "https://staging.example.com/console",
|
||||
enabled: false,
|
||||
count: 0,
|
||||
})
|
||||
})
|
||||
|
||||
test("parses typed answers and evaluates conditions in form order", async () => {
|
||||
const fields = [
|
||||
{ type: "string", key: "deployment", options: [{ value: "enterprise", label: "Enterprise" }] },
|
||||
{ type: "string", key: "host", when: [{ key: "deployment", op: "eq", value: "enterprise" }] },
|
||||
{ type: "boolean", key: "enabled" },
|
||||
{ type: "integer", key: "count", minimum: 0 },
|
||||
{ type: "number", key: "rate" },
|
||||
{ type: "multiselect", key: "scopes", options: [{ value: "read", label: "Read" }] },
|
||||
] satisfies FormFields
|
||||
expect(
|
||||
await Effect.runPromise(
|
||||
answerForm(fields, [
|
||||
"host=example.com",
|
||||
"deployment=enterprise",
|
||||
"enabled=false",
|
||||
"count=0",
|
||||
"rate=1.5",
|
||||
'scopes=["read"]',
|
||||
]),
|
||||
),
|
||||
).toEqual({ deployment: "enterprise", host: "example.com", enabled: false, count: 0, rate: 1.5, scopes: ["read"] })
|
||||
})
|
||||
|
||||
test("preserves empty strings and equals signs in supplied text", async () => {
|
||||
expect(
|
||||
await Effect.runPromise(
|
||||
answerForm(
|
||||
[
|
||||
{ type: "string", key: "server" },
|
||||
{ type: "string", key: "optional" },
|
||||
],
|
||||
["server=https://example.com/console?tenant=a=b", "optional="],
|
||||
),
|
||||
),
|
||||
).toEqual({ server: "https://example.com/console?tenant=a=b", optional: "" })
|
||||
})
|
||||
|
||||
const invalidAnswers: { fields: FormFields | undefined; supplied: string[]; error: string }[] = [
|
||||
{ fields: undefined, supplied: ["missing-separator"], error: "Expected --answer key=value" },
|
||||
{ fields: undefined, supplied: ["=missing-key"], error: "Expected --answer key=value" },
|
||||
{ fields: undefined, supplied: ["unknown=value"], error: "Unknown form field: unknown" },
|
||||
{
|
||||
fields: [{ type: "string", key: "known" }],
|
||||
supplied: ["unknown=value"],
|
||||
error: "Unknown form field: unknown",
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{ type: "boolean", key: "enabled", hidden: true, default: false },
|
||||
{ type: "string", key: "host", when: [{ key: "enabled", op: "eq", value: true }] },
|
||||
],
|
||||
supplied: ["host=example.com"],
|
||||
error: "Form field is not active: host",
|
||||
},
|
||||
{ fields: [{ type: "integer", key: "count" }], supplied: ["count=1.5"], error: "Expected an integer" },
|
||||
{ fields: [{ type: "boolean", key: "enabled" }], supplied: ["enabled=yes"], error: "Expected a JSON value" },
|
||||
{
|
||||
fields: [{ type: "string", key: "server", format: "uri", hidden: true }],
|
||||
supplied: ["server=not a URL"],
|
||||
error: "Expected a URL",
|
||||
},
|
||||
{
|
||||
fields: [{ type: "string", key: "required", hidden: true, required: true }],
|
||||
supplied: [],
|
||||
error: "Answer required",
|
||||
},
|
||||
{
|
||||
fields: [{ type: "multiselect", key: "scopes", options: [{ value: "read", label: "Read" }] }],
|
||||
supplied: ['scopes=["write"]'],
|
||||
error: "Select only available options",
|
||||
},
|
||||
{
|
||||
fields: [{ type: "external", key: "approve", url: "https://example.com" }],
|
||||
supplied: ["approve=true"],
|
||||
error: "requires interactive confirmation",
|
||||
},
|
||||
]
|
||||
|
||||
test.each(invalidAnswers)("rejects invalid form answers: $error", async (input) => {
|
||||
await expect(Effect.runPromise(answerForm(input.fields, input.supplied))).rejects.toThrow(input.error)
|
||||
})
|
||||
@@ -1,11 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import path from "node:path"
|
||||
import { OPENCODE_VERSION } from "../src/version"
|
||||
import type { FormFields } from "@opencode/client"
|
||||
|
||||
const consoleForm = [
|
||||
{ type: "string", key: "server", hidden: true, format: "uri", default: "https://opencode.ai/console" },
|
||||
] satisfies FormFields
|
||||
|
||||
describe("auth command", () => {
|
||||
test("registers authentication commands", async () => {
|
||||
@@ -33,7 +28,6 @@ describe("auth command", () => {
|
||||
expect(login.stdout).toContain("opencode auth login [flags] [<target>]")
|
||||
expect(login.stdout).toContain("Integration ID, name, or well-known provider URL")
|
||||
expect(login.stdout).toContain("--method")
|
||||
expect(login.stdout).toContain("--answer")
|
||||
expect(logout.exitCode).toBe(0)
|
||||
expect(logout.stdout).toContain("opencode auth logout [flags] [<target>] [<credential>]")
|
||||
})
|
||||
@@ -141,52 +135,23 @@ describe("auth command", () => {
|
||||
expect(requests).toContainEqual({ method: "DELETE", path: "/api/integration/company/connect/command/con_test" })
|
||||
})
|
||||
|
||||
test.each([
|
||||
{ id: "openai", method: "browser", form: undefined, args: [], answer: undefined },
|
||||
{
|
||||
id: "opencode",
|
||||
method: "device",
|
||||
form: consoleForm,
|
||||
args: [],
|
||||
answer: { server: "https://opencode.ai/console" },
|
||||
},
|
||||
{
|
||||
id: "opencode",
|
||||
method: "device",
|
||||
form: consoleForm,
|
||||
args: ["--answer", "server=https://staging.example.com/console"],
|
||||
answer: { server: "https://staging.example.com/console" },
|
||||
},
|
||||
{
|
||||
id: "company",
|
||||
method: "browser",
|
||||
form: [
|
||||
{ type: "string", key: "tenant", required: true },
|
||||
{ type: "boolean", key: "enabled", required: true },
|
||||
] satisfies FormFields,
|
||||
args: ["--answer", "tenant=team=one", "--answer", "enabled=false"],
|
||||
answer: { tenant: "team=one", enabled: false },
|
||||
},
|
||||
])("completes $id/$method OAuth with supplied answers $args", async (input) => {
|
||||
test("completes automatic OAuth authentication", async () => {
|
||||
const requests: Array<{ method: string; path: string }> = []
|
||||
const bodies: unknown[] = []
|
||||
const endpoint = `/api/integration/${input.id}/connect/oauth`
|
||||
using server = authServer(async (request, url) => {
|
||||
using server = authServer((request, url) => {
|
||||
requests.push({ method: request.method, path: url.pathname })
|
||||
if (url.pathname === "/api/integration") {
|
||||
return Response.json(
|
||||
located([
|
||||
{
|
||||
id: input.id,
|
||||
name: input.id,
|
||||
methods: [{ id: input.method, type: "oauth", label: "Browser", form: input.form }],
|
||||
id: "openai",
|
||||
name: "OpenAI",
|
||||
methods: [{ id: "browser", type: "oauth", label: "Browser" }],
|
||||
connections: [],
|
||||
},
|
||||
]),
|
||||
)
|
||||
}
|
||||
if (url.pathname === endpoint && request.method === "POST") {
|
||||
bodies.push(await request.json())
|
||||
if (url.pathname === "/api/integration/openai/connect/oauth" && request.method === "POST") {
|
||||
return Response.json(
|
||||
located({
|
||||
attemptID: "con_oauth",
|
||||
@@ -197,23 +162,22 @@ describe("auth command", () => {
|
||||
}),
|
||||
)
|
||||
}
|
||||
if (url.pathname === `${endpoint}/con_oauth` && request.method === "GET") {
|
||||
if (url.pathname === "/api/integration/openai/connect/oauth/con_oauth" && request.method === "GET") {
|
||||
return Response.json(located({ status: "complete", time: { created: 1, expires: 2 } }))
|
||||
}
|
||||
if (url.pathname === `${endpoint}/con_oauth` && request.method === "DELETE") {
|
||||
if (url.pathname === "/api/integration/openai/connect/oauth/con_oauth" && request.method === "DELETE") {
|
||||
return new Response(null, { status: 204 })
|
||||
}
|
||||
return new Response("Not found", { status: 404 })
|
||||
})
|
||||
|
||||
const result = await cli(["auth", "login", input.id, "--server", server.url.toString(), ...input.args])
|
||||
const result = await cli(["auth", "login", "openai", "--server", server.url.toString()])
|
||||
expect({ exitCode: result.exitCode, stderr: result.stderr }).toEqual({ exitCode: 0, stderr: "" })
|
||||
expect(result.stdout).toContain("https://example.com/authorize")
|
||||
expect(result.stdout).toContain(`Connected to ${input.id}`)
|
||||
expect(bodies).toEqual([{ methodID: input.method, ...(input.answer ? { answer: input.answer } : {}) }])
|
||||
expect(requests).toContainEqual({ method: "POST", path: endpoint })
|
||||
expect(requests).toContainEqual({ method: "GET", path: `${endpoint}/con_oauth` })
|
||||
expect(requests).toContainEqual({ method: "DELETE", path: `${endpoint}/con_oauth` })
|
||||
expect(result.stdout).toContain("Connected to OpenAI")
|
||||
expect(requests).toContainEqual({ method: "POST", path: "/api/integration/openai/connect/oauth" })
|
||||
expect(requests).toContainEqual({ method: "GET", path: "/api/integration/openai/connect/oauth/con_oauth" })
|
||||
expect(requests).toContainEqual({ method: "DELETE", path: "/api/integration/openai/connect/oauth/con_oauth" })
|
||||
})
|
||||
|
||||
test("settles the OAuth spinner when status polling fails", async () => {
|
||||
|
||||
@@ -63,7 +63,6 @@ function stepStarted(): V2Event {
|
||||
type: "session.step.started",
|
||||
durable: { aggregateID: "ses_1", seq: 1, version: 1 },
|
||||
data: {
|
||||
started: 1,
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_assistant",
|
||||
agent: "build",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { expect } from "bun:test"
|
||||
import { Effect, Layer, Queue } from "effect"
|
||||
import { TestClock } from "effect/testing"
|
||||
import { testEffect } from "../../core/test/lib/effect"
|
||||
import { Updater } from "../src/services/updater"
|
||||
|
||||
const it = testEffect(Layer.empty)
|
||||
|
||||
it.effect("polls after 1 minute and every 10 minutes after that", () =>
|
||||
Effect.gen(function* () {
|
||||
const checks = yield* Queue.unbounded<void>()
|
||||
yield* Updater.pollUpdates({ check: Queue.offer(checks, undefined).pipe(Effect.asVoid) }).pipe(Effect.forkScoped)
|
||||
|
||||
yield* Effect.yieldNow
|
||||
expect(yield* Queue.size(checks)).toBe(0)
|
||||
yield* TestClock.adjust("59 seconds")
|
||||
expect(yield* Queue.size(checks)).toBe(0)
|
||||
yield* TestClock.adjust("1 second")
|
||||
yield* Queue.take(checks)
|
||||
yield* Effect.yieldNow
|
||||
yield* TestClock.adjust("10 minutes")
|
||||
yield* Queue.take(checks)
|
||||
}),
|
||||
)
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/client",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -817,7 +817,6 @@ export type SessionLogOutput =
|
||||
readonly agent: Agent.ID
|
||||
readonly model: Model.Ref
|
||||
readonly snapshot?: (string & Brand.Brand<"Snapshot.ID">) | undefined
|
||||
readonly started: number
|
||||
}
|
||||
}
|
||||
| {
|
||||
|
||||
@@ -712,14 +712,7 @@ export type SessionStepStarted = {
|
||||
type: "session.step.started"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: {
|
||||
sessionID: string
|
||||
assistantMessageID: string
|
||||
agent: string
|
||||
model: ModelRef
|
||||
snapshot?: string
|
||||
started: number
|
||||
}
|
||||
data: { sessionID: string; assistantMessageID: string; agent: string; model: ModelRef; snapshot?: string }
|
||||
}
|
||||
|
||||
export type SessionStepStreamed = {
|
||||
@@ -1367,7 +1360,6 @@ export type FormNumberField = {
|
||||
title?: string
|
||||
description?: string
|
||||
required?: boolean
|
||||
hidden?: boolean
|
||||
when?: Array<FormWhen>
|
||||
type: "number"
|
||||
minimum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
@@ -1380,7 +1372,6 @@ export type FormIntegerField = {
|
||||
title?: string
|
||||
description?: string
|
||||
required?: boolean
|
||||
hidden?: boolean
|
||||
when?: Array<FormWhen>
|
||||
type: "integer"
|
||||
minimum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
@@ -1393,7 +1384,6 @@ export type FormBooleanField = {
|
||||
title?: string
|
||||
description?: string
|
||||
required?: boolean
|
||||
hidden?: boolean
|
||||
when?: Array<FormWhen>
|
||||
type: "boolean"
|
||||
default?: boolean
|
||||
@@ -1404,7 +1394,6 @@ export type FormStringField = {
|
||||
title?: string
|
||||
description?: string
|
||||
required?: boolean
|
||||
hidden?: boolean
|
||||
when?: Array<FormWhen>
|
||||
type: "string"
|
||||
format?: "email" | "uri" | "date" | "date-time"
|
||||
@@ -1422,7 +1411,6 @@ export type FormMultiselectField = {
|
||||
title?: string
|
||||
description?: string
|
||||
required?: boolean
|
||||
hidden?: boolean
|
||||
when?: Array<FormWhen>
|
||||
type: "multiselect"
|
||||
options: Array<FormOption>
|
||||
@@ -1580,7 +1568,6 @@ export type FormStringField1 = {
|
||||
title?: string
|
||||
description?: string
|
||||
required?: boolean
|
||||
hidden?: boolean
|
||||
when?: Array<FormWhen1>
|
||||
type: "string"
|
||||
format?: "email" | "uri" | "date" | "date-time"
|
||||
@@ -1598,7 +1585,6 @@ export type FormNumberField1 = {
|
||||
title?: string
|
||||
description?: string
|
||||
required?: boolean
|
||||
hidden?: boolean
|
||||
when?: Array<FormWhen1>
|
||||
type: "number"
|
||||
minimum?: number
|
||||
@@ -1611,7 +1597,6 @@ export type FormIntegerField1 = {
|
||||
title?: string
|
||||
description?: string
|
||||
required?: boolean
|
||||
hidden?: boolean
|
||||
when?: Array<FormWhen1>
|
||||
type: "integer"
|
||||
minimum?: number
|
||||
@@ -1624,7 +1609,6 @@ export type FormBooleanField1 = {
|
||||
title?: string
|
||||
description?: string
|
||||
required?: boolean
|
||||
hidden?: boolean
|
||||
when?: Array<FormWhen1>
|
||||
type: "boolean"
|
||||
default?: boolean
|
||||
@@ -1635,7 +1619,6 @@ export type FormMultiselectField1 = {
|
||||
title?: string
|
||||
description?: string
|
||||
required?: boolean
|
||||
hidden?: boolean
|
||||
when?: Array<FormWhen1>
|
||||
type: "multiselect"
|
||||
options: Array<FormOption>
|
||||
@@ -2135,7 +2118,7 @@ export type ConfigEntry =
|
||||
experimental?: {
|
||||
portable_shell_scanner?: boolean
|
||||
subagent_depth?: number
|
||||
policies?: Array<{ action: "provider.use" | "permission"; resource: string; effect: "allow" | "deny" }>
|
||||
policies?: Array<{ action: "provider.use"; resource: string; effect: "allow" | "deny" }>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4468,7 +4451,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -4493,7 +4475,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -4509,7 +4490,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -4525,7 +4505,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -4539,7 +4518,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -4570,7 +4548,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -4595,7 +4572,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -4611,7 +4587,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -4627,7 +4602,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -4641,7 +4615,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -4679,7 +4652,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -4704,7 +4676,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -4720,7 +4691,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -4736,7 +4706,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -4750,7 +4719,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -4781,7 +4749,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -4806,7 +4773,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -4822,7 +4788,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -4838,7 +4803,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -4852,7 +4816,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -4890,7 +4853,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -4915,7 +4877,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -4931,7 +4892,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -4947,7 +4907,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -4961,7 +4920,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -4992,7 +4950,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -5017,7 +4974,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -5033,7 +4989,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -5049,7 +5004,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -5063,7 +5017,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -5101,7 +5054,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -5126,7 +5078,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -5142,7 +5093,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -5158,7 +5108,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -5172,7 +5121,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -5203,7 +5151,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -5228,7 +5175,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -5244,7 +5190,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -5260,7 +5205,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
@@ -5274,7 +5218,6 @@ export type SessionFormCreateInput = {
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly hidden?: boolean
|
||||
readonly when?: ReadonlyArray<{
|
||||
readonly key: string
|
||||
readonly op: "eq" | "neq"
|
||||
|
||||
@@ -847,7 +847,6 @@ export function createData(config: CreateDataInput) {
|
||||
existing.finish = undefined
|
||||
existing.rawFinish = undefined
|
||||
existing.providerState = undefined
|
||||
existing.time.created = event.data.started
|
||||
existing.time.streamed = undefined
|
||||
existing.time.completed = undefined
|
||||
if (event.data.snapshot) existing.snapshot = { ...existing.snapshot, start: event.data.snapshot }
|
||||
@@ -866,7 +865,7 @@ export function createData(config: CreateDataInput) {
|
||||
metadata: event.metadata,
|
||||
content: [],
|
||||
snapshot: event.data.snapshot ? { start: event.data.snapshot } : undefined,
|
||||
time: { created: event.data.started },
|
||||
time: { created: event.created },
|
||||
})
|
||||
})
|
||||
return
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/codemode",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"description": "Effect-native confined code execution over schema-described tools",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/console-app",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/console-core",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/console-function",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/console-mail",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/console-support",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"name": "@opencode/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -12,34 +12,16 @@ export const Plugin = define({
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const config = yield* Config.Service
|
||||
const loaded = yield* ConfigEntryObserver.observe(config, ctx.event, ctx.provider.reload())
|
||||
const policies = () =>
|
||||
loaded.entries
|
||||
yield* ctx.provider.transform((providers) => {
|
||||
// User-global policy takes priority over policy authored by a repository.
|
||||
const policies = loaded.entries
|
||||
.filter((entry): entry is Document => entry.type === "document")
|
||||
.toReversed()
|
||||
.flatMap((entry) => entry.info.experimental?.policies ?? [])
|
||||
yield* ctx.provider.transform((providers) => {
|
||||
// User-global policy takes priority over policy authored by a repository.
|
||||
for (const record of providers.list()) {
|
||||
const policy = policies().findLast(
|
||||
(policy) => policy.action === "provider.use" && Wildcard.match(record.provider.id, policy.resource),
|
||||
)
|
||||
const policy = policies.findLast((policy) => Wildcard.match(record.provider.id, policy.resource))
|
||||
if (policy?.effect === "deny") providers.remove(record.provider.id)
|
||||
}
|
||||
})
|
||||
yield* ctx.permission.hook("evaluate", (event) =>
|
||||
Effect.sync(() => {
|
||||
const current = policies()
|
||||
const denied = event.resources.some((resource) => {
|
||||
const policy = current.findLast(
|
||||
(policy) =>
|
||||
policy.action === "permission" && Wildcard.match(`${event.action}:${resource}`, policy.resource),
|
||||
)
|
||||
return policy?.effect === "deny"
|
||||
})
|
||||
if (!denied) return
|
||||
event.effect = "deny"
|
||||
event.message = "Blocked by configuration policy"
|
||||
}),
|
||||
)
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -45,15 +45,6 @@ function oauth(http: HttpClient.HttpClient) {
|
||||
id: methodID,
|
||||
type: "oauth",
|
||||
label: "OpenCode Console account",
|
||||
form: [
|
||||
{
|
||||
key: "server",
|
||||
type: "string",
|
||||
format: "uri",
|
||||
hidden: true,
|
||||
default: defaultServer,
|
||||
},
|
||||
],
|
||||
},
|
||||
authorize: (answer) =>
|
||||
Effect.gen(function* () {
|
||||
|
||||
@@ -223,7 +223,6 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
|
||||
draft.finish = undefined
|
||||
draft.rawFinish = undefined
|
||||
draft.providerState = undefined
|
||||
draft.time.created = DateTime.makeUnsafe(event.data.started)
|
||||
draft.time.streamed = undefined
|
||||
draft.time.completed = undefined
|
||||
if (event.data.snapshot) draft.snapshot = { ...draft.snapshot, start: event.data.snapshot }
|
||||
@@ -247,7 +246,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
|
||||
agent: event.data.agent,
|
||||
model: event.data.model,
|
||||
metadata: event.metadata,
|
||||
time: { created: DateTime.makeUnsafe(event.data.started) },
|
||||
time: { created },
|
||||
content: [],
|
||||
snapshot: event.data.snapshot ? { start: event.data.snapshot } : undefined,
|
||||
}),
|
||||
|
||||
@@ -20,7 +20,6 @@ type Input = {
|
||||
readonly model: Model.Ref
|
||||
readonly providerMetadataKey: string
|
||||
readonly snapshot?: Snapshot.ID
|
||||
readonly started: number
|
||||
readonly assistantMessageID: SessionMessage.ID
|
||||
}
|
||||
|
||||
@@ -105,7 +104,6 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
|
||||
model: input.model,
|
||||
assistantMessageID,
|
||||
snapshot: input.snapshot,
|
||||
started: input.started,
|
||||
})
|
||||
return assistantMessageID
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
type ToolCall,
|
||||
} from "@opencode/ai"
|
||||
import type { Agent } from "@opencode/schema/agent"
|
||||
import { Cause, Clock, Data, Effect, Exit, Fiber, Option, Stream } from "effect"
|
||||
import { Cause, Data, Effect, Exit, Fiber, Option, Stream } from "effect"
|
||||
import { SessionError } from "@opencode/schema/session-error"
|
||||
import { Bus } from "../../bus.js"
|
||||
import { Permission } from "../../permission.js"
|
||||
@@ -78,7 +78,6 @@ export const make = Effect.gen(function* () {
|
||||
model: input.model.ref,
|
||||
providerMetadataKey: input.model.model.route.providerMetadataKey ?? input.model.model.provider,
|
||||
snapshot: startSnapshot,
|
||||
started: yield* Clock.currentTimeMillis,
|
||||
})
|
||||
const toolRuns: Array<{
|
||||
readonly call: ToolCall
|
||||
|
||||
@@ -113,9 +113,6 @@ describe("OpencodePlugin", () => {
|
||||
id: Integration.MethodID.make("device"),
|
||||
type: "oauth",
|
||||
label: "OpenCode Console account",
|
||||
form: [
|
||||
{ key: "server", type: "string", format: "uri", hidden: true, default: "https://opencode.ai/console" },
|
||||
],
|
||||
},
|
||||
{ type: "key", label: "API key (service account)" },
|
||||
])
|
||||
@@ -353,7 +350,7 @@ describe("OpencodePlugin", () => {
|
||||
})
|
||||
.pipe(Effect.flip)
|
||||
expect(error).toBeInstanceOf(Integration.AuthorizationError)
|
||||
expect(String(error.cause)).toContain("Expected string for form field: server")
|
||||
expect(String(error.cause)).toContain("Invalid OpenCode server URL: expected string")
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -709,7 +709,6 @@ describe("Session.create", () => {
|
||||
assistantMessageID,
|
||||
agent: Agent.ID.make("build"),
|
||||
model,
|
||||
started: 0,
|
||||
})
|
||||
yield* bus.publish(SessionEvent.Tool.Input.Started, {
|
||||
sessionID: parent.id,
|
||||
@@ -820,7 +819,6 @@ describe("Session.create", () => {
|
||||
assistantMessageID,
|
||||
agent: Agent.ID.make("build"),
|
||||
model,
|
||||
started: 0,
|
||||
})
|
||||
yield* bus.publish(SessionEvent.Step.Ended, {
|
||||
sessionID: parent.id,
|
||||
@@ -1232,7 +1230,6 @@ describe("SessionTransfer", () => {
|
||||
assistantMessageID: SessionMessage.ID.create(),
|
||||
agent: Agent.ID.make("build"),
|
||||
model: Model.Ref.make({ id: Model.ID.make("model"), providerID: Provider.ID.make("provider") }),
|
||||
started: 0,
|
||||
})
|
||||
yield* bus.publish(SessionEvent.Shell.Started, {
|
||||
sessionID: source.id,
|
||||
|
||||
@@ -92,7 +92,6 @@ describe("Session.diff", () => {
|
||||
agent: Agent.defaultID,
|
||||
model: { id: Model.ID.make("test-model"), providerID: Provider.ID.make("test-provider") },
|
||||
snapshot: before,
|
||||
started: 0,
|
||||
})
|
||||
yield* Effect.promise(edit)
|
||||
if (end === "running") return assistantMessageID
|
||||
|
||||
@@ -275,7 +275,6 @@ it.effect(
|
||||
assistantMessageID: settledAssistant,
|
||||
agent: Agent.ID.make("build"),
|
||||
model: { id: ID.make("generate-model"), providerID: Provider.ID.make("test") },
|
||||
started: 0,
|
||||
})
|
||||
yield* bus.publish(SessionEvent.Text.Started, {
|
||||
sessionID,
|
||||
@@ -294,7 +293,6 @@ it.effect(
|
||||
assistantMessageID: activeAssistant,
|
||||
agent: Agent.ID.make("build"),
|
||||
model: { id: ID.make("generate-model"), providerID: Provider.ID.make("test") },
|
||||
started: 0,
|
||||
})
|
||||
yield* bus.publish(SessionEvent.Tool.Input.Started, {
|
||||
sessionID,
|
||||
|
||||
@@ -177,7 +177,6 @@ describe("Session-owned handles", () => {
|
||||
assistantMessageID: messageID,
|
||||
agent: Agent.ID.make("build"),
|
||||
model: { ...model, id: Model.ID.make("initial-model") },
|
||||
started: 0,
|
||||
})
|
||||
yield* fixture.bus.publish(SessionEvent.Step.Ended, {
|
||||
sessionID,
|
||||
|
||||
@@ -485,7 +485,6 @@ describe("SessionProjector", () => {
|
||||
assistantMessageID: id,
|
||||
agent: build,
|
||||
model,
|
||||
started: 0,
|
||||
})
|
||||
.pipe(Effect.exit)
|
||||
|
||||
@@ -502,13 +501,7 @@ describe("SessionProjector", () => {
|
||||
const bus = yield* Bus.Service
|
||||
const first = SessionMessage.ID.make("msg_retry_first")
|
||||
const second = SessionMessage.ID.make("msg_retry_second")
|
||||
yield* bus.publish(SessionEvent.Step.Started, {
|
||||
sessionID,
|
||||
assistantMessageID: first,
|
||||
agent: build,
|
||||
model,
|
||||
started: 0,
|
||||
})
|
||||
yield* bus.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID: first, agent: build, model })
|
||||
yield* bus.publish(SessionEvent.RetryScheduled, {
|
||||
sessionID,
|
||||
assistantMessageID: first,
|
||||
@@ -530,13 +523,7 @@ describe("SessionProjector", () => {
|
||||
retry: { attempt: 2, at: DateTime.makeUnsafe(2_000), error: { type: "provider.transport" } },
|
||||
})
|
||||
|
||||
yield* bus.publish(SessionEvent.Step.Started, {
|
||||
sessionID,
|
||||
assistantMessageID: second,
|
||||
agent: build,
|
||||
model,
|
||||
started: 0,
|
||||
})
|
||||
yield* bus.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID: second, agent: build, model })
|
||||
yield* bus.publish(SessionEvent.RetryScheduled, {
|
||||
sessionID,
|
||||
assistantMessageID: second,
|
||||
|
||||
@@ -76,7 +76,6 @@ describe("Session.revert files", () => {
|
||||
agent: Agent.defaultID,
|
||||
model: { id: Model.ID.make("test-model"), providerID: Provider.ID.make("test-provider") },
|
||||
snapshot: before,
|
||||
started: 0,
|
||||
})
|
||||
yield* Effect.promise(() => fs.rename(original, renamed))
|
||||
const after = yield* snapshot.capture()
|
||||
|
||||
@@ -60,7 +60,6 @@ const capture = (
|
||||
providerID: Provider.ID.opencode,
|
||||
},
|
||||
providerMetadataKey,
|
||||
started: 0,
|
||||
assistantMessageID: SessionMessage.ID.create(),
|
||||
}),
|
||||
}
|
||||
@@ -164,7 +163,6 @@ testEffect(
|
||||
agent: Agent.defaultID,
|
||||
model: { id: Model.ID.make("test-model"), providerID: Provider.ID.opencode },
|
||||
providerMetadataKey: "openai",
|
||||
started: 0,
|
||||
},
|
||||
)
|
||||
yield* publisher.publish(LLMEvent.toolCall({ ...call, providerExecuted: true }))
|
||||
|
||||
@@ -4132,7 +4132,6 @@ describe("SessionRunnerLLM", () => {
|
||||
assistantMessageID,
|
||||
agent: Agent.ID.make("build"),
|
||||
model: { id: ID.make("fake-model"), providerID: Provider.ID.make("fake") },
|
||||
started: 0,
|
||||
})
|
||||
yield* s.bus.publish(SessionEvent.Tool.Input.Started, {
|
||||
sessionID,
|
||||
@@ -4179,7 +4178,6 @@ describe("SessionRunnerLLM", () => {
|
||||
assistantMessageID,
|
||||
agent: Agent.ID.make("build"),
|
||||
model: { id: ID.make("fake-model"), providerID: Provider.ID.make("fake") },
|
||||
started: 0,
|
||||
})
|
||||
yield* s.bus.publish(SessionEvent.Tool.Input.Started, {
|
||||
sessionID,
|
||||
@@ -4245,7 +4243,6 @@ describe("SessionRunnerLLM", () => {
|
||||
assistantMessageID,
|
||||
agent: Agent.ID.make("build"),
|
||||
model: { id: ID.make("fake-model"), providerID: Provider.ID.make("fake") },
|
||||
started: 0,
|
||||
})
|
||||
yield* s.bus.publish(SessionEvent.Tool.Input.Started, {
|
||||
sessionID,
|
||||
@@ -4293,7 +4290,6 @@ describe("SessionRunnerLLM", () => {
|
||||
assistantMessageID,
|
||||
agent: Agent.ID.make("build"),
|
||||
model: { id: ID.make("fake-model"), providerID: Provider.ID.make("fake") },
|
||||
started: 0,
|
||||
})
|
||||
yield* s.bus.publish(SessionEvent.Tool.Input.Started, {
|
||||
sessionID,
|
||||
|
||||
@@ -441,7 +441,6 @@ it.effect("regenerates an existing title using the title agent", () =>
|
||||
assistantMessageID,
|
||||
agent: Agent.ID.make("build"),
|
||||
model: Model.Ref.make({ id: Model.ID.make("title-model"), providerID: Provider.ID.make("test") }),
|
||||
started: 0,
|
||||
})
|
||||
yield* events.publish(SessionEvent.Reasoning.Started, { sessionID, assistantMessageID, ordinal: 0 })
|
||||
yield* events.publish(SessionEvent.Reasoning.Ended, {
|
||||
|
||||
@@ -58,7 +58,6 @@ describe("Session tool progress", () => {
|
||||
assistantMessageID,
|
||||
agent: Agent.ID.make("build"),
|
||||
model,
|
||||
started: 0,
|
||||
})
|
||||
const readAssistant = Effect.gen(function* () {
|
||||
const row = yield* db
|
||||
|
||||
@@ -92,7 +92,6 @@ const executionNode = makeGlobalNode({
|
||||
assistantMessageID,
|
||||
agent: session.agent ?? Agent.ID.make("code"),
|
||||
model: sessionModel,
|
||||
started: 0,
|
||||
})
|
||||
yield* bus.publish(SessionEvent.Text.Started, {
|
||||
sessionID: id,
|
||||
|
||||
@@ -76,7 +76,6 @@ const executionNode = makeGlobalNode({
|
||||
assistantMessageID,
|
||||
agent: Agent.ID.make("reviewer"),
|
||||
model: childModel,
|
||||
started: 0,
|
||||
})
|
||||
yield* bus.publish(SessionEvent.Text.Started, {
|
||||
sessionID,
|
||||
|
||||
@@ -18,18 +18,6 @@ const channels = [
|
||||
] as const
|
||||
|
||||
for (const channel of channels) {
|
||||
test(`disables security code AutoFill by default for ${channel.channel}`, async () => {
|
||||
const previous = process.env.OPENCODE_CHANNEL
|
||||
process.env.OPENCODE_CHANNEL = channel.channel
|
||||
try {
|
||||
const config = (await import(`./electron-builder.config.ts?autofill=${channel.channel}`)).default as Configuration
|
||||
expect(config.mac?.extendInfo?.NSAutoFillRequiresTextContentTypeForOneTimeCodeOnMac).toBe(true)
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env.OPENCODE_CHANNEL
|
||||
else process.env.OPENCODE_CHANNEL = previous
|
||||
}
|
||||
})
|
||||
|
||||
test(`includes the Windows sandbox permission hook for ${channel.channel}`, async () => {
|
||||
const previous = process.env.OPENCODE_CHANNEL
|
||||
process.env.OPENCODE_CHANNEL = channel.channel
|
||||
|
||||
@@ -104,9 +104,6 @@ const getBase = (appId: string): Configuration => ({
|
||||
mac: {
|
||||
category: "public.app-category.developer-tools",
|
||||
icon: `resources/icons/icon.icns`,
|
||||
extendInfo: {
|
||||
NSAutoFillRequiresTextContentTypeForOneTimeCodeOnMac: true,
|
||||
},
|
||||
hardenedRuntime: true,
|
||||
gatekeeperAssess: false,
|
||||
entitlements: "resources/entitlements.plist",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode/desktop",
|
||||
"private": true,
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
|
||||
@@ -27,7 +27,6 @@ export async function prepareDevElectron() {
|
||||
await $`plutil -replace ${key} -string ${"OpenCode Dev"} ${plist}`
|
||||
}
|
||||
await $`plutil -replace CFBundleIdentifier -string ai.opencode.desktop.dev ${plist}`
|
||||
await $`plutil -insert NSAutoFillRequiresTextContentTypeForOneTimeCodeOnMac -bool true ${plist}`
|
||||
await Bun.write(join(bundle, "Contents/Resources/electron.icns"), icon)
|
||||
// Changing the bundle resources invalidates Electron's signature.
|
||||
await $`codesign --force --deep --sign - ${bundle}`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/enterprise",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/function",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"name": "@opencode/http-recorder",
|
||||
"description": "Record and replay Effect HTTP and WebSocket traffic with deterministic cassettes",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/httpapi-codegen",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/latex",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/merman",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/plugin-browser",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"description": "OpenCode's desktop browser plugin",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/plugin",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -163,7 +163,7 @@
|
||||
}
|
||||
},
|
||||
"description": "Shut down and rebuild every loaded location. Pending permissions and forms are cancelled; running sessions continue with fresh services at the next step boundary. Emits location.shutdown for client recovery and responds once all replacement builds settle.",
|
||||
"summary": "Reload configuration"
|
||||
"summary": "Reload locations"
|
||||
}
|
||||
},
|
||||
"/api/agent": {
|
||||
@@ -8440,109 +8440,6 @@
|
||||
"summary": "Find files"
|
||||
}
|
||||
},
|
||||
"/api/experimental/fs/write": {
|
||||
"post": {
|
||||
"tags": ["filesystem"],
|
||||
"operationId": "experimental.fs.write",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "location",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"directory": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": false,
|
||||
"style": "deepObject",
|
||||
"explode": true
|
||||
},
|
||||
{
|
||||
"name": "path",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"description": "An absolute path or a path relative to the requested location. Missing parent directories are created."
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"security": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"$ref": "#/components/schemas/Location.PublicRef"
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/FileSystem.Write"
|
||||
}
|
||||
},
|
||||
"required": ["location", "data"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "InvalidRequestError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvalidRequestErrorEncoded"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "UnauthorizedError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UnauthorizedErrorEncoded"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Write the raw request body to an absolute path or a path relative to the requested location, creating parent directories, and return the resolved absolute path. Unlike read, the target is not confined to the location. Experimental: may change without compatibility guarantees.",
|
||||
"summary": "Write file",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/octet-stream": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/command": {
|
||||
"get": {
|
||||
"tags": ["command"],
|
||||
@@ -13640,16 +13537,6 @@
|
||||
"required": ["path", "type"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"FileSystem.Write": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["path"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ForbiddenErrorEncoded": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -13685,9 +13572,6 @@
|
||||
"required": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"when": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -13877,9 +13761,6 @@
|
||||
"required": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"when": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -13945,9 +13826,6 @@
|
||||
"required": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"when": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -14000,9 +13878,6 @@
|
||||
"required": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"when": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -14131,9 +14006,6 @@
|
||||
"required": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"when": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/protocol",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -54,7 +54,7 @@ export const makeLocationGroup = <LocationId extends HttpApiMiddleware.AnyId, Lo
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "location.reload",
|
||||
summary: "Reload configuration",
|
||||
summary: "Reload locations",
|
||||
description:
|
||||
"Shut down and rebuild every loaded location. Pending permissions and forms are cancelled; running sessions continue with fresh services at the next step boundary. Emits location.shutdown for client recovery and responds once all replacement builds settle.",
|
||||
}),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/schema",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -6,7 +6,7 @@ export const Effect = Schema.Literals(["allow", "deny"])
|
||||
export type Effect = typeof Effect.Type
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
action: Schema.Literals(["provider.use", "permission"]),
|
||||
action: Schema.Literal("provider.use"),
|
||||
resource: Schema.String,
|
||||
effect: Effect,
|
||||
})
|
||||
|
||||
@@ -41,9 +41,6 @@ const FieldBase = {
|
||||
title: Schema.String.pipe(optional),
|
||||
description: Schema.String.pipe(optional),
|
||||
required: Schema.Boolean.pipe(optional),
|
||||
hidden: Schema.Boolean.pipe(optional).annotate({
|
||||
description: "Skip the interactive authentication prompt and use the default unless an answer is supplied",
|
||||
}),
|
||||
when: Schema.Array(When).pipe(optional),
|
||||
}
|
||||
|
||||
|
||||
@@ -328,8 +328,6 @@ export namespace Step {
|
||||
agent: Agent.ID,
|
||||
model: Model.Ref,
|
||||
snapshot: Snapshot.ID.pipe(optional),
|
||||
/** Request dispatch time, before waiting for provider output. */
|
||||
started: NonNegativeInt,
|
||||
},
|
||||
})
|
||||
export type Started = typeof Started.Type
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"name": "@opencode/sdk",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/server",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Integration } from "@opencode/core/integration"
|
||||
import { Plugin } from "@opencode/core/plugin"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
@@ -30,7 +29,6 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration"
|
||||
.handle(
|
||||
"integration.list",
|
||||
Effect.fn(function* () {
|
||||
yield* Plugin.awaitActivation
|
||||
const service = yield* Integration.Service
|
||||
return yield* response(service.list())
|
||||
}),
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { expect } from "bun:test"
|
||||
import { SdkPlugins } from "@opencode/core/plugin/sdk"
|
||||
import { Plugin } from "@opencode/plugin/effect"
|
||||
import { Context, Deferred, Effect, Fiber, Layer } from "effect"
|
||||
import { Context, Deferred, Effect, Layer } from "effect"
|
||||
import { HttpEffect, HttpRouter, HttpServer } from "effect/unstable/http"
|
||||
import { tmpdirScoped } from "../../core/test/fixture/tmpdir"
|
||||
import { it } from "../../core/test/lib/effect"
|
||||
import { createRoutes } from "../src/routes"
|
||||
|
||||
it.live(
|
||||
"provider reads stay nonblocking while authentication discovery waits for plugins",
|
||||
"lists and gets providers without blocking on plugin initialization",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* tmpdirScoped("opencode-provider-endpoints-")
|
||||
@@ -65,7 +65,6 @@ it.live(
|
||||
// Config providers activate after SDK plugins; reads must return the current snapshot without waiting.
|
||||
const list = yield* request("GET", "/api/provider").pipe(Effect.timeout("2 seconds"))
|
||||
yield* Deferred.await(started)
|
||||
const integrations = yield* request("GET", "/api/integration").pipe(Effect.forkScoped)
|
||||
expect(list.status).toBe(200)
|
||||
expect(yield* Effect.promise(() => list.json())).toMatchObject({
|
||||
location: { directory: tmp.path },
|
||||
@@ -78,11 +77,6 @@ it.live(
|
||||
providerID: "custom",
|
||||
})
|
||||
yield* Deferred.succeed(release, undefined)
|
||||
const discovered = yield* Fiber.join(integrations)
|
||||
expect(discovered.status).toBe(200)
|
||||
expect(yield* Effect.promise(() => discovered.json())).toMatchObject({
|
||||
data: expect.arrayContaining([expect.objectContaining({ id: "opencode" })]),
|
||||
})
|
||||
}),
|
||||
15_000,
|
||||
)
|
||||
|
||||
@@ -37,7 +37,6 @@ it.live("serves turn diffs by user message with range validation", () =>
|
||||
assistantMessageID: ids.assistant,
|
||||
agent: Agent.defaultID,
|
||||
model: { id: Model.ID.make("model"), providerID: Provider.ID.make("provider") },
|
||||
started: 0,
|
||||
})
|
||||
yield* bus.publish(SessionEvent.Step.Ended, {
|
||||
sessionID,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/session-ui",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/simulation",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/stats-app",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/stats-core",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/stats-server",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/theme",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/tui",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.6",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
@@ -41,7 +41,6 @@
|
||||
"./parsers-config": "./src/parsers-config.ts",
|
||||
"./util/error": "./src/util/error.ts",
|
||||
"./util/filetype": "./src/util/filetype.ts",
|
||||
"./util/form": "./src/util/form.ts",
|
||||
"./util/locale": "./src/util/locale.ts",
|
||||
"./util/persistence": "./src/util/persistence.ts",
|
||||
"./util/record": "./src/util/record.ts",
|
||||
|
||||
@@ -158,7 +158,6 @@ const appBindingCommands = [
|
||||
"opencode.update",
|
||||
"server.pair",
|
||||
"service.restart",
|
||||
"location.reload",
|
||||
"opencode.debug",
|
||||
"theme.switch",
|
||||
"theme.switch_mode",
|
||||
@@ -968,8 +967,7 @@ function App(props: { pair?: DialogPairCredentials }) {
|
||||
{
|
||||
name: "opencode.update",
|
||||
title: "Update OpenCode",
|
||||
description: "Update OpenCode (upgrade)",
|
||||
slash: { name: "update" },
|
||||
slash: { name: "update", aliases: ["upgrade"] },
|
||||
run: () => updater.open?.("manual"),
|
||||
category: "System",
|
||||
},
|
||||
@@ -1007,15 +1005,15 @@ function App(props: { pair?: DialogPairCredentials }) {
|
||||
: []),
|
||||
{
|
||||
name: "location.reload",
|
||||
title: "Reload configuration",
|
||||
title: "Reload locations",
|
||||
slash: { name: "reload" },
|
||||
run: async () => {
|
||||
dialog.clear()
|
||||
toast.show({ variant: "info", message: "Reloading configuration…", duration: 30000 })
|
||||
toast.show({ variant: "info", message: "Reloading all locations…", duration: 30000 })
|
||||
await client.api.location
|
||||
.reload()
|
||||
.then(() => {
|
||||
toast.show({ variant: "success", message: "Configuration reloaded" })
|
||||
toast.show({ variant: "success", message: "Locations reloaded" })
|
||||
})
|
||||
.catch(toast.error)
|
||||
},
|
||||
|
||||
@@ -766,7 +766,7 @@ async function formAnswer(dialog: ReturnType<typeof useDialog>, title: string, f
|
||||
const answer: FormAnswer = {}
|
||||
for (const field of fields) {
|
||||
if (!active(field, answer)) continue
|
||||
const value = field.type !== "external" && field.hidden ? field.default : await fieldAnswer(dialog, title, field)
|
||||
const value = await fieldAnswer(dialog, title, field)
|
||||
if (value === CANCELLED) return null
|
||||
if (value !== undefined) answer[field.key] = value
|
||||
}
|
||||
|
||||
@@ -21,8 +21,6 @@ export function Spinner(props: { children?: JSX.Element; color?: RGBA; shimmer?:
|
||||
const timer = setInterval(() => setFrame((value) => (value + 1) % SPINNER_FRAMES.length), 80)
|
||||
onCleanup(() => clearInterval(timer))
|
||||
})
|
||||
// A bare spinner beside a long sibling shrinks to a fractional width that rounds to 0, so the
|
||||
// glyph paints on top of the row gap. Only a labeled spinner may shrink (to wrap its text).
|
||||
return (
|
||||
<Show
|
||||
when={config.animations ?? true}
|
||||
@@ -31,10 +29,8 @@ export function Spinner(props: { children?: JSX.Element; color?: RGBA; shimmer?:
|
||||
<Show
|
||||
when={props.shimmer}
|
||||
fallback={
|
||||
<box flexDirection="row" gap={1} flexShrink={props.children ? 1 : 0}>
|
||||
<box flexShrink={0}>
|
||||
<spinner frames={SPINNER_FRAMES} interval={80} color={color()} />
|
||||
</box>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<spinner frames={SPINNER_FRAMES} interval={80} color={color()} />
|
||||
<Show when={props.children}>
|
||||
<text fg={color()}>{props.children}</text>
|
||||
</Show>
|
||||
|
||||
@@ -60,7 +60,6 @@ export const Definitions = {
|
||||
"opencode.settings": keybind("none", "Open settings"),
|
||||
"server.pair": keybind("none", "Pair device"),
|
||||
"service.restart": keybind("none", "Restart service"),
|
||||
"location.reload": keybind("none", "Reload configuration"),
|
||||
"diff.open": keybind("none", "Open diff viewer"),
|
||||
"diff.close": keybind("escape,q", "Close diff viewer"),
|
||||
"diff.down": keybind("j,down", "Move diff viewer down"),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user