mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-18 14:56:27 +00:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62daf48a8a | ||
|
|
3e3a4ae46b | ||
|
|
fe0d9579a7 | ||
|
|
a2594ddefb | ||
|
|
3555cef1c8 | ||
|
|
0fa881efd0 | ||
|
|
37ff501cc6 | ||
|
|
fa126d68e8 |
@@ -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
@@ -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,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,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/app",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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,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,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,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/console-app",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -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,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,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,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/console-support",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,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,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/enterprise",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -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,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,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,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,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,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,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,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,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": {
|
||||
|
||||
@@ -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,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,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": {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { expect, story } from "../../storybook/playwright/story"
|
||||
|
||||
story("shows Code Mode child calls and expands their inputs", async ({ mount }) => {
|
||||
const timeline = await mount("current-session-terminal-work--execute-code")
|
||||
const calls = timeline.locator('[data-component="execute-tool-call"]')
|
||||
await expect(calls).toHaveCount(3)
|
||||
|
||||
const completed = calls.nth(0)
|
||||
const trigger = completed.getByRole("button")
|
||||
await expect(trigger).toHaveAttribute("aria-expanded", "false")
|
||||
await expect(trigger).toContainText("planetscale.planetscale_execute_write_query")
|
||||
await expect(trigger).toContainText("organization=anomalyco")
|
||||
|
||||
await trigger.click()
|
||||
await expect(trigger).toHaveAttribute("aria-expanded", "true")
|
||||
await expect(completed.locator("dt")).toHaveText([
|
||||
"organization",
|
||||
"database",
|
||||
"branch",
|
||||
"confirm_destructive",
|
||||
"query",
|
||||
])
|
||||
await expect(completed.locator("dd")).toContainText([
|
||||
"anomalyco",
|
||||
"opencode",
|
||||
"production",
|
||||
"true",
|
||||
"UPDATE workspace",
|
||||
])
|
||||
await expect(calls.nth(2)).toHaveAttribute("data-status", "completed")
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/session-ui",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -200,6 +200,107 @@
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="execute-tool-calls"] {
|
||||
margin-inline-start: 12px;
|
||||
padding-inline-start: 12px;
|
||||
border-inline-start: 1px solid var(--v2-border-border-weak);
|
||||
}
|
||||
|
||||
[data-component="execute-tool-call"] {
|
||||
min-width: 0;
|
||||
|
||||
&[data-open="true"] [data-slot="execute-tool-call-status"] [data-slot="icon-svg"] {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
&[data-status="error"] [data-slot="execute-tool-call-status"] {
|
||||
color: var(--text-error);
|
||||
}
|
||||
|
||||
[data-slot="execute-tool-call-trigger"] {
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--v2-text-text-muted);
|
||||
text-align: start;
|
||||
cursor: pointer;
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled),
|
||||
&:focus-visible {
|
||||
color: var(--v2-text-text-base);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="execute-tool-call-status"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex: 0 0 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
[data-slot="icon-svg"] {
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
[data-component="session-progress-indicator-v2"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="execute-tool-call-title"] {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: 13px;
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-compact);
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
[data-slot="execute-tool-call-details"] {
|
||||
margin: 0;
|
||||
padding: 2px 0 8px 22px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-slot="execute-tool-call-detail"] {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: max-content minmax(0, 1fr);
|
||||
column-gap: 8px;
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: 13px;
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-base);
|
||||
|
||||
dt {
|
||||
color: var(--v2-text-text-muted);
|
||||
}
|
||||
|
||||
dd {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
color: var(--v2-text-text-base);
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="task-tool-card"] {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
|
||||
@@ -545,6 +545,31 @@ export const executeCodeDocument = document([
|
||||
code: 'const greeting = "Code Mode execute completed"\nreturn { greeting, timestamp: new Date().toISOString() }',
|
||||
},
|
||||
output: '{\n "greeting": "Code Mode execute completed",\n "timestamp": "2026-08-17T09:01:43.590Z"\n}',
|
||||
metadata: {
|
||||
toolCalls: [
|
||||
{
|
||||
tool: "planetscale.planetscale_execute_write_query",
|
||||
status: "completed",
|
||||
input: {
|
||||
organization: "anomalyco",
|
||||
database: "opencode",
|
||||
branch: "production",
|
||||
confirm_destructive: true,
|
||||
query: "UPDATE workspace SET time_deleted = UTC_TIMESTAMP(3) WHERE time_deleted IS NULL LIMIT 50000",
|
||||
},
|
||||
},
|
||||
{
|
||||
tool: "planetscale.planetscale_execute_write_query",
|
||||
status: "completed",
|
||||
input: { organization: "anomalyco", database: "opencode", branch: "production" },
|
||||
},
|
||||
{
|
||||
tool: "planetscale.planetscale_execute_write_query",
|
||||
status: "completed",
|
||||
input: { organization: "anomalyco", database: "opencode", branch: "production", limit: 50000 },
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
|
||||
@@ -1724,37 +1724,105 @@ ToolRegistry.register({
|
||||
const pending = () => props.status === "streaming" || props.status === "running"
|
||||
const code = createMemo(() => (typeof props.input.code === "string" ? props.input.code : ""))
|
||||
const output = () => stripAnsi(props.output ?? "").replace(/\r\n?/g, "\n")
|
||||
const calls = createMemo(() => executeCalls(props.metadata.toolCalls))
|
||||
const sawPending = pending()
|
||||
return (
|
||||
<BasicTool
|
||||
{...props}
|
||||
icon="console"
|
||||
rail={false}
|
||||
hasContent
|
||||
compact
|
||||
allowOpenWhilePending
|
||||
trigger={(open) => (
|
||||
<div data-slot="basic-tool-tool-info-structured">
|
||||
<div data-slot="basic-tool-tool-info-main">
|
||||
<span data-slot="basic-tool-tool-title">
|
||||
<TextShimmer text={i18n.t("ui.tool.execute")} active={pending()} />
|
||||
</span>
|
||||
<Show when={!open() && code()}>
|
||||
<ShellSubmessage text={code().split("\n", 1)[0]} animate={sawPending} />
|
||||
</Show>
|
||||
<div data-component="execute-tool">
|
||||
<BasicTool
|
||||
{...props}
|
||||
icon="console"
|
||||
rail={false}
|
||||
hasContent
|
||||
compact
|
||||
allowOpenWhilePending
|
||||
trigger={(open) => (
|
||||
<div data-slot="basic-tool-tool-info-structured">
|
||||
<div data-slot="basic-tool-tool-info-main">
|
||||
<span data-slot="basic-tool-tool-title">
|
||||
<TextShimmer text={i18n.t("ui.tool.execute")} active={pending()} />
|
||||
</span>
|
||||
<Show when={!open() && code()}>
|
||||
<ShellSubmessage text={code().split("\n", 1)[0]} animate={sawPending} />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<ConsoleOutput copy={code()} variant="shell">
|
||||
<span data-slot="bash-command">{code()}</span>
|
||||
<Show when={output()}>{(value) => <span data-slot="bash-result">{value()}</span>}</Show>
|
||||
</ConsoleOutput>
|
||||
</BasicTool>
|
||||
<Show when={calls().length > 0}>
|
||||
<div data-component="execute-tool-calls">
|
||||
<Index each={calls()}>{(call) => <ExecuteCallRow call={call} />}</Index>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<ConsoleOutput copy={code()} variant="shell">
|
||||
<span data-slot="bash-command">{code()}</span>
|
||||
<Show when={output()}>{(value) => <span data-slot="bash-result">{value()}</span>}</Show>
|
||||
</ConsoleOutput>
|
||||
</BasicTool>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
type ExecuteCall = {
|
||||
tool: string
|
||||
status: "running" | "completed" | "error"
|
||||
input?: Record<string, unknown>
|
||||
}
|
||||
|
||||
function executeCalls(value: unknown): ExecuteCall[] {
|
||||
if (!Array.isArray(value)) return []
|
||||
return value.flatMap((call) => {
|
||||
if (!record(call) || typeof call.tool !== "string") return []
|
||||
if (call.status !== "running" && call.status !== "completed" && call.status !== "error") return []
|
||||
return [{ tool: call.tool, status: call.status, input: record(call.input) ? call.input : undefined }]
|
||||
})
|
||||
}
|
||||
|
||||
function ExecuteCallRow(props: { call: () => ExecuteCall }) {
|
||||
const [open, setOpen] = createSignal(false)
|
||||
const input = createMemo(() => Object.entries(props.call().input ?? {}))
|
||||
const summary = createMemo(() => {
|
||||
const args = input()
|
||||
.filter(([, value]) => typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
||||
.map(([key, value]) => `${key}=${String(value)}`)
|
||||
.join(", ")
|
||||
.replace(/\s+/g, " ")
|
||||
return `${props.call().tool}${args ? ` [${args}]` : ""}`
|
||||
})
|
||||
return (
|
||||
<div data-component="execute-tool-call" data-status={props.call().status} data-open={open() ? "true" : "false"}>
|
||||
<button
|
||||
type="button"
|
||||
data-slot="execute-tool-call-trigger"
|
||||
disabled={input().length === 0}
|
||||
aria-expanded={input().length > 0 ? open() : undefined}
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
>
|
||||
<span data-slot="execute-tool-call-status" aria-hidden="true">
|
||||
<Show when={props.call().status !== "running"} fallback={<SessionProgressIndicatorV2 />}>
|
||||
<Show when={props.call().status === "error"} fallback={<Icon name="chevron-right" size="small" />}>
|
||||
×
|
||||
</Show>
|
||||
</Show>
|
||||
</span>
|
||||
<span data-slot="execute-tool-call-title">{open() ? props.call().tool : summary()}</span>
|
||||
</button>
|
||||
<Show when={open()}>
|
||||
<dl data-slot="execute-tool-call-details">
|
||||
<For each={input()}>
|
||||
{([key, value]) => (
|
||||
<div data-slot="execute-tool-call-detail">
|
||||
<dt>{key}</dt>
|
||||
<dd>{typeof value === "string" ? value : (JSON.stringify(value, null, 2) ?? String(value))}</dd>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</dl>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ToolRegistry.register({
|
||||
name: "shell",
|
||||
render(props) {
|
||||
|
||||
@@ -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,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,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,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,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,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",
|
||||
|
||||
@@ -1114,7 +1114,17 @@ export function Prompt(props: PromptProps) {
|
||||
if (!trimmed && (!props.sessionID || store.mode === "shell" || delivery === "queue"))
|
||||
return delivery === "steer" ? (await props.onEmptySubmit?.()) === true : false
|
||||
const exitWord = trimmed === "exit" || trimmed === "quit" || trimmed === ":q"
|
||||
const slash = argumentSlash(store.prompt.text, keymapCommands())
|
||||
const inputText = expandTrackedPastedText(
|
||||
store.prompt.text,
|
||||
input.extmarks.getAllForTypeId(promptPartTypeId).flatMap((extmark) => {
|
||||
const ref = store.extmarkToPart.get(extmark.id)
|
||||
if (ref?.type !== "pasted") return []
|
||||
const part = store.prompt.pasted[ref.index]
|
||||
if (!part) return []
|
||||
return [{ start: extmark.start, end: extmark.end, text: part.text }]
|
||||
}),
|
||||
)
|
||||
const slash = argumentSlash(inputText, keymapCommands())
|
||||
if (delivery === "queue" && (store.mode === "shell" || exitWord || slash)) {
|
||||
toast.show({ message: "This prompt cannot be queued", variant: "warning" })
|
||||
return false
|
||||
@@ -1128,16 +1138,6 @@ export function Prompt(props: PromptProps) {
|
||||
await slash.command.run(slash.input)
|
||||
return true
|
||||
}
|
||||
const inputText = expandTrackedPastedText(
|
||||
store.prompt.text,
|
||||
input.extmarks.getAllForTypeId(promptPartTypeId).flatMap((extmark) => {
|
||||
const ref = store.extmarkToPart.get(extmark.id)
|
||||
if (ref?.type !== "pasted") return []
|
||||
const part = store.prompt.pasted[ref.index]
|
||||
if (!part) return []
|
||||
return [{ start: extmark.start, end: extmark.end, text: part.text }]
|
||||
}),
|
||||
)
|
||||
const slashHead = parseSlashHead(inputText, /\s/)
|
||||
const isCommand =
|
||||
slashHead !== undefined &&
|
||||
|
||||
@@ -126,6 +126,7 @@ export const Definitions = {
|
||||
"session.interrupt": keybind("escape", "Interrupt current session"),
|
||||
"session.background": keybind("ctrl+b", "Background blocking session tools"),
|
||||
"session.compact": keybind("<leader>c", "Compact the session"),
|
||||
"session.aside": keybind("none", "Ask a side question"),
|
||||
"session.cd": keybind("none", "Change working directory"),
|
||||
"session.queued_prompts": keybind("<leader>q", "Manage queued prompts"),
|
||||
"queued_prompt.delete": keybind("ctrl+d", "Delete queued prompt"),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import { Plugin } from "@opencode/plugin/tui"
|
||||
import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core"
|
||||
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
|
||||
import { createMemo, createSignal, Show } from "solid-js"
|
||||
import { Spinner } from "../../component/spinner"
|
||||
import { useConfig } from "../../config"
|
||||
import { useClipboard } from "../../context/clipboard"
|
||||
import { Keymap } from "../../context/keymap"
|
||||
import { useTheme, useThemes } from "../../context/theme"
|
||||
import { usePlugin } from "../../plugin/context"
|
||||
import { useDialog } from "../../ui/dialog"
|
||||
import { useToast } from "../../ui/toast"
|
||||
import { getScrollAcceleration } from "../../util/scroll"
|
||||
|
||||
// session.generate exposes the session's tools but runs no tool loop, so a
|
||||
// tool call would surface as an empty answer.
|
||||
const instructions = [
|
||||
"The user is asking a quick side question about the conversation so far.",
|
||||
"Answer directly and concisely in markdown from what you already know.",
|
||||
"Do not call any tools and do not take any actions.",
|
||||
].join(" ")
|
||||
|
||||
export default Plugin.define({
|
||||
id: "opencode.btw",
|
||||
setup(context) {
|
||||
const [pending, setPending] = createSignal(0)
|
||||
|
||||
context.ui.slot({
|
||||
append: "prompt.footer.status",
|
||||
render: () => {
|
||||
const theme = useTheme()
|
||||
return (
|
||||
<Show when={pending() > 0}>
|
||||
<box flexShrink={0}>
|
||||
<Spinner color={theme.text.status.running}>/btw</Spinner>
|
||||
</box>
|
||||
</Show>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
context.ui.slot({
|
||||
append: "app",
|
||||
render() {
|
||||
const toast = useToast()
|
||||
context.keymap.layer(() => ({
|
||||
mode: "global",
|
||||
commands: [
|
||||
{
|
||||
id: "session.aside",
|
||||
title: "Ask a side question",
|
||||
description: "One-shot answer from the session's context without adding to the conversation",
|
||||
group: "Session",
|
||||
palette: true,
|
||||
slash: { name: "btw", arguments: true },
|
||||
async run(input) {
|
||||
const route = context.ui.router.current()
|
||||
if (route.type !== "session") {
|
||||
toast.show({ message: "Open a session first", variant: "warning" })
|
||||
return
|
||||
}
|
||||
const question =
|
||||
input?.trim() || (await context.ui.dialog.prompt({ title: "/btw", placeholder: "Ask anything" }))
|
||||
if (!question) return
|
||||
setPending((count) => count + 1)
|
||||
await context.client.session
|
||||
.generate({ sessionID: route.sessionID, prompt: [instructions, question].join("\n\n") })
|
||||
.then((result) => {
|
||||
context.ui.dialog.show(() => <Answer question={question} answer={result.text.trim()} />)
|
||||
context.ui.dialog.set({ size: "large", centered: true })
|
||||
})
|
||||
.catch((cause: unknown) => toast.error(cause))
|
||||
.finally(() => setPending((count) => count - 1))
|
||||
},
|
||||
},
|
||||
],
|
||||
}))
|
||||
return null
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
function Answer(props: { question: string; answer: string }) {
|
||||
const dialog = useDialog()
|
||||
const toast = useToast()
|
||||
const clipboard = useClipboard()
|
||||
const plugins = usePlugin()
|
||||
const theme = useTheme("elevated")
|
||||
const overlay = useTheme("overlay")
|
||||
const syntax = useThemes().currentSyntax
|
||||
const config = useConfig().data
|
||||
const dimensions = useTerminalDimensions()
|
||||
const maxHeight = createMemo(() => Math.max(3, Math.floor(dimensions().height / 2)))
|
||||
const [copied, setCopied] = createSignal(false)
|
||||
let scroll: ScrollBoxRenderable | undefined
|
||||
|
||||
const copy = () => {
|
||||
void clipboard
|
||||
.write(props.answer)
|
||||
.then(() => setCopied(true))
|
||||
.catch(toast.error)
|
||||
}
|
||||
|
||||
Keymap.createLayer(() => ({
|
||||
mode: "modal",
|
||||
commands: [{ bind: "c", title: "Copy answer", group: "Dialog", run: copy }],
|
||||
}))
|
||||
|
||||
useKeyboard((event) => {
|
||||
if (!scroll) return
|
||||
if (event.name === "up") return scroll.scrollBy(-1)
|
||||
if (event.name === "down") return scroll.scrollBy(1)
|
||||
if (event.name === "pageup") return scroll.scrollBy(-maxHeight())
|
||||
if (event.name === "pagedown") return scroll.scrollBy(maxHeight())
|
||||
if (event.name === "home") return scroll.scrollTo(0)
|
||||
if (event.name === "end") return scroll.scrollTo(scroll.scrollHeight)
|
||||
})
|
||||
|
||||
return (
|
||||
<box gap={1}>
|
||||
<box paddingLeft={2} paddingRight={2}>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text attributes={TextAttributes.BOLD} fg={theme.text.default}>
|
||||
/btw
|
||||
</text>
|
||||
<text fg={theme.text.subdued} onMouseUp={() => dialog.clear()}>
|
||||
esc
|
||||
</text>
|
||||
</box>
|
||||
<text fg={theme.text.subdued} wrapMode="word">
|
||||
{props.question}
|
||||
</text>
|
||||
</box>
|
||||
<scrollbox
|
||||
ref={(element: ScrollBoxRenderable) => (scroll = element)}
|
||||
maxHeight={maxHeight()}
|
||||
backgroundColor={overlay.background.default}
|
||||
scrollbarOptions={{ visible: false }}
|
||||
scrollAcceleration={getScrollAcceleration(config)}
|
||||
>
|
||||
<box paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1}>
|
||||
<markdown
|
||||
syntaxStyle={syntax()}
|
||||
renderNode={plugins.markdown()}
|
||||
content={props.answer}
|
||||
conceal
|
||||
internalBlockMode="top-level"
|
||||
tableOptions={{ style: "grid", cellPaddingX: 1 }}
|
||||
fg={overlay.markdown.text}
|
||||
bg={overlay.background.default}
|
||||
/>
|
||||
</box>
|
||||
</scrollbox>
|
||||
<box flexDirection="row" gap={3} paddingLeft={2} paddingRight={2} paddingBottom={1}>
|
||||
<text onMouseUp={copy}>
|
||||
<span style={{ fg: copied() ? theme.text.feedback.success.default : theme.text.default }}>
|
||||
<b>{copied() ? "✓ copied" : "c"}</b>
|
||||
</span>
|
||||
<span style={{ fg: theme.text.subdued }}>{copied() ? "" : " copy"}</span>
|
||||
</text>
|
||||
<text fg={theme.text.subdued}>↑/↓ scroll</text>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
@@ -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,4 +1,5 @@
|
||||
import HomeFooter from "../feature-plugins/home/footer"
|
||||
import PromptBtw from "../feature-plugins/prompt/btw"
|
||||
import PromptFooter from "../feature-plugins/prompt/footer"
|
||||
import SidebarContext from "../feature-plugins/sidebar/context"
|
||||
import SidebarFooter from "../feature-plugins/sidebar/footer"
|
||||
@@ -14,6 +15,7 @@ import Merman from "@opencode/merman/plugin"
|
||||
export const builtins = [
|
||||
HomeFooter,
|
||||
PromptFooter,
|
||||
PromptBtw,
|
||||
SidebarContext,
|
||||
SidebarMcp,
|
||||
SidebarFooter,
|
||||
|
||||
@@ -2787,6 +2787,7 @@ function BlockToolContent(props: BlockToolProps & { borderColor: RGBA }) {
|
||||
return (
|
||||
<box
|
||||
border={["left"]}
|
||||
flexShrink={0}
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode/ui",
|
||||
"version": "2.0.6",
|
||||
"version": "2.0.7",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -10,12 +10,6 @@ const dir = fileURLToPath(new URL("..", import.meta.url))
|
||||
process.chdir(dir)
|
||||
const tag = `v${Script.version}`
|
||||
|
||||
if (Script.channel === "beta" && Script.release) {
|
||||
console.log("\n=== desktop beta release ===\n")
|
||||
await $`bun ./packages/desktop/scripts/publish.ts`
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const pkgjsons = await Array.fromAsync(
|
||||
new Bun.Glob("**/package.json").scan({
|
||||
absolute: true,
|
||||
|
||||
@@ -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,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": {
|
||||
|
||||
@@ -194,6 +194,7 @@ The retired `diff.toggle`, `diff.expand`, `diff.expand_all`, `diff.collapse`, an
|
||||
| `session.interrupt` | `escape` | Interrupt current session |
|
||||
| `session.background` | `ctrl+b` | Background blocking session tools |
|
||||
| `session.compact` | `<leader>c` | Compact the session |
|
||||
| `session.aside` | `none` | Ask a side question |
|
||||
| `session.cd` | `none` | Change working directory |
|
||||
| `session.queued_prompts` | `<leader>q` | Manage queued prompts |
|
||||
| `queued_prompt.delete` | `ctrl+d` | Delete queued prompt |
|
||||
|
||||
@@ -50,6 +50,14 @@ Type `/` to list slash commands. Continue typing to filter the list, then press
|
||||
|
||||
Common commands include `/new`, `/sessions`, `/models`, `/agents`, `/undo`, `/redo`, and `/editor`. Press **Ctrl+P** to open the command palette for every action available in the current view.
|
||||
|
||||
## Side questions
|
||||
|
||||
Run `/btw <question>` to ask a quick question about the conversation so far. The answer uses the session's context and current model but is not added to the conversation, so it does not affect later prompts. A `/btw` indicator shows in the prompt footer while it runs, and the answer opens in a dialog; press **C** to copy it.
|
||||
|
||||
```text
|
||||
/btw why did the test in step 2 fail?
|
||||
```
|
||||
|
||||
## Models
|
||||
|
||||
Press **Ctrl+X**, then **M** to choose a model, or run `/models`. Press **F2** to cycle through recently used models.
|
||||
|
||||
Reference in New Issue
Block a user