Compare commits

...
52 changed files with 216 additions and 207 deletions
+35 -35
View File
@@ -32,7 +32,7 @@
},
"packages/ai": {
"name": "@opencode/ai",
"version": "2.0.6",
"version": "2.0.7",
"dependencies": {
"@aws-sdk/credential-providers": "3.1057.0",
"@opencode/schema": "workspace:*",
@@ -54,7 +54,7 @@
},
"packages/app": {
"name": "@opencode/app",
"version": "2.0.6",
"version": "2.0.7",
"dependencies": {
"@corvu/drawer": "catalog:",
"@dnd-kit/abstract": "0.5.0",
@@ -113,7 +113,7 @@
},
"packages/cli": {
"name": "@opencode/cli",
"version": "2.0.6",
"version": "2.0.7",
"bin": {
"opencode2": "./bin/opencode2.cjs",
},
@@ -177,7 +177,7 @@
},
"packages/client": {
"name": "@opencode/client",
"version": "2.0.6",
"version": "2.0.7",
"dependencies": {
"@opencode/protocol": "workspace:*",
"@opencode/schema": "workspace:*",
@@ -203,7 +203,7 @@
},
"packages/codemode": {
"name": "@opencode/codemode",
"version": "2.0.6",
"version": "2.0.7",
"dependencies": {
"acorn": "8.15.0",
"effect": "catalog:",
@@ -217,7 +217,7 @@
},
"packages/console/app": {
"name": "@opencode/console-app",
"version": "2.0.6",
"version": "2.0.7",
"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.6",
"version": "2.0.7",
"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.6",
"version": "2.0.7",
"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.6",
"version": "2.0.7",
"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.6",
"version": "2.0.7",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@opencode/console-core": "workspace:*",
@@ -341,7 +341,7 @@
},
"packages/core": {
"name": "@opencode/core",
"version": "2.0.6",
"version": "2.0.7",
"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.6",
"version": "2.0.7",
"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.6",
"version": "2.0.7",
"dependencies": {
"@hono/standard-validator": "catalog:",
"@opencode-ai/sdk": "1.18.21",
@@ -498,7 +498,7 @@
},
"packages/function": {
"name": "@opencode/function",
"version": "2.0.6",
"version": "2.0.7",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -514,7 +514,7 @@
},
"packages/http-recorder": {
"name": "@opencode/http-recorder",
"version": "2.0.6",
"version": "2.0.7",
"dependencies": {
"@effect/platform-node-shared": "4.0.0-rc.112",
},
@@ -533,7 +533,7 @@
},
"packages/httpapi-codegen": {
"name": "@opencode/httpapi-codegen",
"version": "2.0.6",
"version": "2.0.7",
"dependencies": {
"effect": "catalog:",
"prettier": "3.6.2",
@@ -546,7 +546,7 @@
},
"packages/latex": {
"name": "@opencode/latex",
"version": "2.0.6",
"version": "2.0.7",
"dependencies": {
"@opencode/plugin": "workspace:*",
"@opentui/core": "catalog:",
@@ -560,7 +560,7 @@
},
"packages/merman": {
"name": "@opencode/merman",
"version": "2.0.6",
"version": "2.0.7",
"dependencies": {
"@opencode/plugin": "workspace:*",
"@opentui/core": "catalog:",
@@ -575,7 +575,7 @@
},
"packages/plugin": {
"name": "@opencode/plugin",
"version": "2.0.6",
"version": "2.0.7",
"dependencies": {
"@ai-sdk/provider": "3.0.8",
"@opencode/ai": "workspace:*",
@@ -614,7 +614,7 @@
},
"packages/plugin-browser": {
"name": "@opencode/plugin-browser",
"version": "2.0.6",
"version": "2.0.7",
"dependencies": {
"@opencode/plugin": "workspace:*",
"@opencode/schema": "workspace:*",
@@ -644,7 +644,7 @@
},
"packages/protocol": {
"name": "@opencode/protocol",
"version": "2.0.6",
"version": "2.0.7",
"dependencies": {
"@opencode/schema": "workspace:*",
"effect": "catalog:",
@@ -659,7 +659,7 @@
},
"packages/schema": {
"name": "@opencode/schema",
"version": "2.0.6",
"version": "2.0.7",
"dependencies": {
"@standard-schema/spec": "catalog:",
"effect": "catalog:",
@@ -683,7 +683,7 @@
},
"packages/sdk": {
"name": "@opencode/sdk",
"version": "2.0.6",
"version": "2.0.7",
"dependencies": {
"@opencode/client": "workspace:*",
"@opencode/core": "workspace:*",
@@ -704,7 +704,7 @@
},
"packages/server": {
"name": "@opencode/server",
"version": "2.0.6",
"version": "2.0.7",
"dependencies": {
"@effect/platform-node": "catalog:",
"@effect/platform-node-shared": "catalog:",
@@ -726,7 +726,7 @@
},
"packages/session-ui": {
"name": "@opencode/session-ui",
"version": "2.0.6",
"version": "2.0.7",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode/client": "workspace:*",
@@ -761,7 +761,7 @@
},
"packages/simulation": {
"name": "@opencode/simulation",
"version": "2.0.6",
"version": "2.0.7",
"dependencies": {
"@opencode/ai": "workspace:*",
"@opencode/core": "workspace:*",
@@ -781,7 +781,7 @@
},
"packages/stats/app": {
"name": "@opencode/stats-app",
"version": "2.0.6",
"version": "2.0.7",
"dependencies": {
"@ibm/plex": "6.4.1",
"@kobalte/core": "catalog:",
@@ -815,7 +815,7 @@
},
"packages/stats/core": {
"name": "@opencode/stats-core",
"version": "2.0.6",
"version": "2.0.7",
"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.6",
"version": "2.0.7",
"dependencies": {
"@aws-sdk/client-firehose": "3.933.0",
"@effect/platform-node": "catalog:",
@@ -880,7 +880,7 @@
},
"packages/theme": {
"name": "@opencode/theme",
"version": "2.0.6",
"version": "2.0.7",
"dependencies": {
"@opentui/core": "catalog:",
"effect": "catalog:",
@@ -894,7 +894,7 @@
},
"packages/tui": {
"name": "@opencode/tui",
"version": "2.0.6",
"version": "2.0.7",
"dependencies": {
"@opencode/client": "workspace:*",
"@opencode/core": "workspace:*",
@@ -929,7 +929,7 @@
},
"packages/ui": {
"name": "@opencode/ui",
"version": "2.0.6",
"version": "2.0.7",
"dependencies": {
"@kobalte/core": "catalog:",
"@pierre/diffs": "catalog:",
@@ -964,7 +964,7 @@
},
"packages/util": {
"name": "@opencode/util",
"version": "2.0.6",
"version": "2.0.7",
"dependencies": {
"@effect/opentelemetry": "catalog:",
"@effect/platform-node": "catalog:",
@@ -997,7 +997,7 @@
},
"packages/web": {
"name": "@opencode/web",
"version": "2.0.6",
"version": "2.0.7",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -1038,7 +1038,7 @@
},
"services/update": {
"name": "@opencode/update",
"version": "2.0.6",
"version": "2.0.7",
"dependencies": {
"jose": "6.0.11",
"semver": "catalog:",
+1 -1
View File
@@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/package.json",
"name": "opencode",
"description": "AI-powered development tool",
"version": "2.0.6",
"version": "2.0.7",
"private": true,
"type": "module",
"packageManager": "bun@1.4.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "2.0.6",
"version": "2.0.7",
"name": "@opencode/ai",
"type": "module",
"license": "MIT",
@@ -1,5 +1,18 @@
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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/app",
"version": "2.0.6",
"version": "2.0.7",
"description": "",
"type": "module",
"exports": {
+2 -8
View File
@@ -11,7 +11,6 @@ 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"
@@ -256,7 +255,7 @@ function HomeProjectsPanel(props: HomeProjectsViewProps) {
collapsed={collapsed()}
health={props.serverHealth(item)}
/>
<Show when={authentication() || connecting()}>
<Show when={authentication()}>
<div class="mx-3 h-px bg-v2-border-border-base" />
<div class="px-1.5 py-1">
<Button
@@ -264,14 +263,9 @@ function HomeProjectsPanel(props: HomeProjectsViewProps) {
class="w-full"
size="small"
variant="neutral"
disabled={connecting()}
aria-busy={!!connecting()}
onClick={() => props.onAuthenticateServer?.(item)}
>
<Show when={connecting()}>
<Spinner class="size-3.5" />
</Show>
{props.language.t(connecting() ? "ssh.stage.connecting" : "ssh.action.authenticate")}
{props.language.t("ssh.action.authenticate")}
</Button>
</div>
</Show>
@@ -14,6 +14,7 @@ import { SshConnectionPanel } from "./connection-panel"
import { SshServerSettings } from "./settings"
function Fixture(props: {
home?: boolean
session?: boolean
settings?: boolean
incompatible?: boolean
@@ -25,10 +26,15 @@ function Fixture(props: {
const state: { item?: SshItem; before?: SshItem; step: number; timer?: ReturnType<typeof setTimeout> } = {
step: 0,
item:
props.initial === "required"
props.initial === "required" || props.home
? {
config: { id: "story", target: "ssh linuxbook", name: "" },
stage: props.session || props.settings ? "disconnected" : "authentication",
stage:
props.session || props.settings
? "disconnected"
: props.initial === "connecting"
? "connecting"
: "authentication",
saved: true,
detail: "",
}
@@ -141,7 +147,7 @@ function Fixture(props: {
<AuthenticationSettings />
) : props.session ? (
<AuthenticationSession />
) : props.initial === "required" ? (
) : props.initial === "required" || props.home ? (
<AuthenticationHome />
) : (
<Open initial={props.initial} />
@@ -277,6 +283,7 @@ 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 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/cli",
"version": "2.0.6",
"version": "2.0.7",
"type": "module",
"license": "MIT",
"bin": {
-16
View File
@@ -13,7 +13,6 @@ 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"
@@ -159,21 +158,6 @@ 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 -13
View File
@@ -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, Schedule } from "effect"
import { Context, Duration, Effect, FileSystem, Layer, Ref } from "effect"
import { ChildProcess } from "effect/unstable/process"
import { parse, type ParseError } from "jsonc-parser"
import path from "node:path"
@@ -24,18 +24,6 @@ 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 {
-24
View File
@@ -1,24 +0,0 @@
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 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/client",
"version": "2.0.6",
"version": "2.0.7",
"type": "module",
"license": "MIT",
"repository": {
@@ -2135,7 +2135,7 @@ export type ConfigEntry =
experimental?: {
portable_shell_scanner?: boolean
subagent_depth?: number
policies?: Array<{ action: "provider.use"; resource: string; effect: "allow" | "deny" }>
policies?: Array<{ action: "provider.use" | "permission"; resource: string; effect: "allow" | "deny" }>
}
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/codemode",
"version": "2.0.6",
"version": "2.0.7",
"description": "Effect-native confined code execution over schema-described tools",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/console-app",
"version": "2.0.6",
"version": "2.0.7",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/console-core",
"version": "2.0.6",
"version": "2.0.7",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/console-function",
"version": "2.0.6",
"version": "2.0.7",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/console-mail",
"version": "2.0.6",
"version": "2.0.7",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/console-support",
"version": "2.0.6",
"version": "2.0.7",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "2.0.6",
"version": "2.0.7",
"name": "@opencode/core",
"type": "module",
"license": "MIT",
+22 -4
View File
@@ -12,16 +12,34 @@ 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())
yield* ctx.provider.transform((providers) => {
// User-global policy takes priority over policy authored by a repository.
const policies = loaded.entries
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) => Wildcard.match(record.provider.id, policy.resource))
const policy = policies().findLast(
(policy) => policy.action === "provider.use" && 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"
}),
)
}),
})
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode/desktop",
"private": true,
"version": "2.0.6",
"version": "2.0.7",
"type": "module",
"license": "MIT",
"homepage": "https://opencode.ai",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/enterprise",
"version": "2.0.6",
"version": "2.0.7",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/function",
"version": "2.0.6",
"version": "2.0.7",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "2.0.6",
"version": "2.0.7",
"name": "@opencode/http-recorder",
"description": "Record and replay Effect HTTP and WebSocket traffic with deterministic cassettes",
"type": "module",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/httpapi-codegen",
"version": "2.0.6",
"version": "2.0.7",
"private": true,
"type": "module",
"exports": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/latex",
"version": "2.0.6",
"version": "2.0.7",
"private": true,
"type": "module",
"exports": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/merman",
"version": "2.0.6",
"version": "2.0.7",
"private": true,
"type": "module",
"exports": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/plugin-browser",
"version": "2.0.6",
"version": "2.0.7",
"description": "OpenCode's desktop browser plugin",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/plugin",
"version": "2.0.6",
"version": "2.0.7",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/protocol",
"version": "2.0.6",
"version": "2.0.7",
"type": "module",
"license": "MIT",
"repository": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/schema",
"version": "2.0.6",
"version": "2.0.7",
"type": "module",
"license": "MIT",
"repository": {
+1 -1
View File
@@ -6,7 +6,7 @@ export const Effect = Schema.Literals(["allow", "deny"])
export type Effect = typeof Effect.Type
export const Info = Schema.Struct({
action: Schema.Literal("provider.use"),
action: Schema.Literals(["provider.use", "permission"]),
resource: Schema.String,
effect: Effect,
})
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "2.0.6",
"version": "2.0.7",
"name": "@opencode/sdk",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/server",
"version": "2.0.6",
"version": "2.0.7",
"type": "module",
"license": "MIT",
"repository": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/session-ui",
"version": "2.0.6",
"version": "2.0.7",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/simulation",
"version": "2.0.6",
"version": "2.0.7",
"type": "module",
"license": "MIT",
"repository": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/stats-app",
"version": "2.0.6",
"version": "2.0.7",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/stats-core",
"version": "2.0.6",
"version": "2.0.7",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/stats-server",
"version": "2.0.6",
"version": "2.0.7",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/theme",
"version": "2.0.6",
"version": "2.0.7",
"type": "module",
"license": "MIT",
"repository": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/tui",
"version": "2.0.6",
"version": "2.0.7",
"private": true,
"type": "module",
"license": "MIT",
+2 -1
View File
@@ -968,7 +968,8 @@ function App(props: { pair?: DialogPairCredentials }) {
{
name: "opencode.update",
title: "Update OpenCode",
slash: { name: "update", aliases: ["upgrade"] },
description: "Update OpenCode (upgrade)",
slash: { name: "update" },
run: () => updater.open?.("manual"),
category: "System",
},
+12 -49
View File
@@ -11,8 +11,10 @@ import { readJson, writeJsonAtomic } from "../util/persistence"
import {
createModelPreferenceRepository,
cycleModelVariant,
favoriteModels,
modelPreferenceKey,
normalizeModelVariant,
recentModels,
type ModelPreference,
type ModelPreferenceModel,
} from "../model-preference"
@@ -24,19 +26,6 @@ import { usePermission } from "./permission"
import { useLocation } from "./location"
import { parse } from "../util/model"
export function recentModels(model: ModelPreferenceModel, recent: ModelPreferenceModel[]) {
const seen = new Set<string>()
return [model, ...recent]
.filter((item) => {
const key = modelPreferenceKey(item)
if (seen.has(key)) return false
seen.add(key)
return true
})
.slice(0, 10)
.map((item) => ({ providerID: item.providerID, modelID: item.modelID }))
}
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
name: "Local",
init: () => {
@@ -167,37 +156,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const pendingSelectionCommits = new Map<string, { agentID: string; selection: string }>()
const selectionKey = (value: ModelSelection) =>
`${modelPreferenceKey(value)}:${normalizeModelVariant(value.variant) ?? "default"}`
const saveState = {
pending: false,
}
function savePreferences() {
if (!preferences.ready) {
saveState.pending = true
return
}
saveState.pending = false
void repository
.patch({
recent: preferences.recent,
favorite: preferences.favorite,
variant: preferences.variant,
})
.catch(() => undefined)
}
repository
.load()
.then((value) => {
function applyPreferences(value: ModelPreference) {
batch(() => {
setPreferences("recent", value.recent)
setPreferences("favorite", value.favorite)
setPreferences("variant", value.variant)
})
.catch(() => {})
.finally(() => {
setPreferences("ready", true)
if (saveState.pending) savePreferences()
})
}
onCleanup(repository.subscribe(applyPreferences))
const configuredModel = createMemo(() => {
const entry = data.location.config
@@ -479,7 +448,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
if (!next) return
if (!selectModel({ ...next })) return
setPreferences("recent", recentModels(next, preferences.recent))
savePreferences()
void repository.addRecent(next).catch(() => undefined)
},
set(model: { providerID: string; modelID: string }, options?: { recent?: boolean }) {
batch(() => {
@@ -487,7 +456,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
if (!selectModel(model)) return
if (options?.recent) {
setPreferences("recent", recentModels(model, preferences.recent))
savePreferences()
void repository.addRecent(model).catch(() => undefined)
}
})
},
@@ -497,14 +466,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const exists = preferences.favorite.some(
(x) => x.providerID === model.providerID && x.modelID === model.modelID,
)
const next = exists
? preferences.favorite.filter((x) => x.providerID !== model.providerID || x.modelID !== model.modelID)
: [model, ...preferences.favorite]
setPreferences(
"favorite",
next.map((x) => ({ providerID: x.providerID, modelID: x.modelID })),
)
savePreferences()
setPreferences("favorite", favoriteModels(model, preferences.favorite, !exists))
void repository.setFavorite(model, !exists).catch(() => undefined)
})
},
variant: {
@@ -527,7 +490,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
setSessionDraft(route.data.sessionID, { ...m, variant: normalizeModelVariant(value) })
}
setPreferences("variant", modelPreferenceKey(m), value ?? "default")
savePreferences()
void repository.saveVariant(m, value).catch(() => undefined)
},
cycle() {
const variants = this.list()
+77 -12
View File
@@ -1,5 +1,8 @@
import { readJson, writeJsonAtomic } from "./util/persistence"
import { isRecord } from "./util/record"
import { Flock } from "@opencode/util/flock"
import { watch } from "node:fs"
import path from "node:path"
export type ModelPreferenceModel = {
providerID: string
@@ -43,6 +46,24 @@ export function modelPreferenceKey(model: ModelPreferenceModel) {
return `${model.providerID}/${model.modelID}`
}
export function recentModels(model: ModelPreferenceModel, recent: ModelPreferenceModel[]) {
const seen = new Set<string>()
return [model, ...recent]
.filter((item) => {
const key = modelPreferenceKey(item)
if (seen.has(key)) return false
seen.add(key)
return true
})
.slice(0, 10)
.map((item) => ({ providerID: item.providerID, modelID: item.modelID }))
}
export function favoriteModels(model: ModelPreferenceModel, favorite: ModelPreferenceModel[], enabled: boolean) {
const current = favorite.filter((item) => modelPreferenceKey(item) !== modelPreferenceKey(model))
return enabled ? [model, ...current] : current
}
export function cycleModelVariant(current: string | undefined, variants: string[]) {
const named = variants.filter((variant) => variant !== "default")
if (named.length === 0) return undefined
@@ -80,32 +101,76 @@ function patch(value: Partial<ModelPreference>) {
}
export function createModelPreferenceRepository(filePath: string) {
const state = {
pending: Promise.resolve(),
}
let pending = Promise.resolve()
let revision = 0
let watcher: ReturnType<typeof watch> | undefined
let reload: ReturnType<typeof setTimeout> | undefined
const listeners = new Set<(value: ModelPreference) => void>()
const read = () =>
readJson<unknown>(filePath)
.then(decodeModelPreference)
.catch(() => decodeModelPreference(undefined))
function update(change: (current: ModelPreference) => Partial<ModelPreference>) {
const result = state.pending.then(async () => {
const current = await read()
const next = { ...current, ...patch(change(preference(current))) }
await writeJsonAtomic(filePath, next)
})
state.pending = result.catch(() => undefined)
const result = pending.then(() =>
Flock.withLock(
filePath,
async () => {
const current = await read()
await writeJsonAtomic(filePath, { ...current, ...patch(change(preference(current))) })
},
{ dir: path.join(path.dirname(filePath), "locks") },
),
)
pending = result.then(
() => undefined,
() => undefined,
)
return result
}
function load() {
return state.pending.then(read).then(preference)
return pending.then(read).then(preference)
}
async function refresh() {
const current = ++revision
const value = await load()
if (current !== revision) return
listeners.forEach((listener) => listener(value))
}
return {
load,
patch(value: Partial<ModelPreference>) {
return update(() => value)
addRecent(model: ModelPreferenceModel) {
return update((current) => ({ recent: recentModels(model, current.recent) }))
},
setFavorite(model: ModelPreferenceModel, enabled: boolean) {
return update((current) => ({ favorite: favoriteModels(model, current.favorite, enabled) }))
},
subscribe(listener: (value: ModelPreference) => void) {
listeners.add(listener)
void refresh()
if (!watcher) {
watcher = watch(path.dirname(filePath), (_event, filename) => {
const changed = filename?.toString()
const name = path.basename(filePath)
if (changed !== undefined && changed !== name && !changed.startsWith(name + ".")) return
clearTimeout(reload)
reload = setTimeout(() => void refresh(), 50)
})
watcher.on("error", () => {
watcher?.close()
watcher = undefined
})
}
return () => {
listeners.delete(listener)
if (listeners.size > 0) return
clearTimeout(reload)
watcher?.close()
watcher = undefined
}
},
async resolveVariant(model: ModelPreferenceModel) {
return normalizeModelVariant((await load()).variant[modelPreferenceKey(model)])
+1 -1
View File
@@ -1,5 +1,5 @@
import { expect, test } from "bun:test"
import { recentModels } from "../../src/context/local"
import { recentModels } from "../../src/model-preference"
test("moves a model to the front, deduplicates, and limits recents", () => {
const recent = Array.from({ length: 12 }, (_, index) => ({
+2 -2
View File
@@ -19,7 +19,7 @@ test("repairs known model preferences and preserves unrelated fields", () => {
})
})
test("atomically serializes patches and variant updates", async () => {
test("atomically serializes model preference updates", async () => {
await using tmp = await tmpdir()
const file = path.join(tmp.path, "model.json")
await Bun.write(file, JSON.stringify({ unrelated: "keep", favorite: [], variant: {} }))
@@ -28,7 +28,7 @@ test("atomically serializes patches and variant updates", async () => {
const anthropic = { providerID: "anthropic", modelID: "claude/sonnet" }
await Promise.all([
repository.patch({ recent: [openai] }),
repository.addRecent(openai),
repository.saveVariant(openai, "high"),
repository.saveVariant(anthropic, "low"),
])
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/ui",
"version": "2.0.6",
"version": "2.0.7",
"type": "module",
"license": "MIT",
"repository": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/util",
"version": "2.0.6",
"version": "2.0.7",
"type": "module",
"license": "MIT",
"repository": {
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@opencode/web",
"type": "module",
"license": "MIT",
"version": "2.0.6",
"version": "2.0.7",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "2.0.6",
"version": "2.0.7",
"publisher": "sst-dev",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/update",
"version": "2.0.6",
"version": "2.0.7",
"private": true,
"type": "module",
"scripts": {