Compare commits

..
60 changed files with 627 additions and 235 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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/app",
"version": "2.0.6",
"version": "2.0.7",
"description": "",
"type": "module",
"exports": {
+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": {
@@ -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 -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",
@@ -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 },
},
],
},
}),
],
}),
+91 -23
View File
@@ -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 -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",
+11 -11
View File
@@ -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 &&
+1
View File
@@ -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"),
+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()
@@ -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>
)
}
+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)])
+2
View File
@@ -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 -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",
-6
View File
@@ -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,
+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": {
@@ -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.