diff --git a/.github/last-synced-tag b/.github/last-synced-tag index b0397e1b986..d53b6bb1f63 100644 --- a/.github/last-synced-tag +++ b/.github/last-synced-tag @@ -1 +1 @@ -v1.0.132 +v1.0.133 diff --git a/.github/workflows/review.yml b/.github/workflows/review.yml index 32c7c7b1144..ac93ca94e7e 100644 --- a/.github/workflows/review.yml +++ b/.github/workflows/review.yml @@ -1,35 +1,20 @@ name: Guidelines Check on: - pull_request_target: - types: [opened, ready_for_review] issue_comment: types: [created] jobs: check-guidelines: if: | - (github.event_name == 'pull_request_target' && - github.event.pull_request.draft == false) || - (github.event_name == 'issue_comment' && - github.event.issue.pull_request && - startsWith(github.event.comment.body, '/review')) - runs-on: ubuntu-latest + github.event.issue.pull_request && + startsWith(github.event.comment.body, '/review') && + contains(fromJson('["OWNER","MEMBER"]'), github.event.comment.author_association) + runs-on: blacksmith-4vcpu-ubuntu-2404 permissions: contents: read pull-requests: write steps: - - name: Check if user has write permission - if: github.event_name == 'issue_comment' - run: | - PERMISSION=$(gh api /repos/${{ github.repository }}/collaborators/${{ github.event.comment.user.login }}/permission --jq '.permission') - if [[ "$PERMISSION" != "write" && "$PERMISSION" != "admin" ]]; then - echo "User does not have write permission" - exit 1 - fi - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Get PR number id: pr-number run: | diff --git a/STATS.md b/STATS.md index 25678e915d5..72198597234 100644 --- a/STATS.md +++ b/STATS.md @@ -159,3 +159,4 @@ | 2025-12-01 | 925,898 (+9,782) | 876,500 (+6,306) | 1,802,398 (+16,088) | | 2025-12-02 | 939,250 (+13,352) | 890,919 (+14,419) | 1,830,169 (+27,771) | | 2025-12-03 | 952,249 (+12,999) | 903,713 (+12,794) | 1,855,962 (+25,793) | +| 2025-12-04 | 965,611 (+13,362) | 916,471 (+12,758) | 1,882,082 (+26,120) | diff --git a/bun.lock b/bun.lock index 8047cdeede5..4342fc7fa9e 100644 --- a/bun.lock +++ b/bun.lock @@ -20,7 +20,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.0.132", + "version": "1.0.133", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -48,7 +48,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.0.132", + "version": "1.0.133", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -75,7 +75,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.0.132", + "version": "1.0.133", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -99,7 +99,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.0.132", + "version": "1.0.133", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -123,7 +123,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.0.132", + "version": "1.0.133", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -164,7 +164,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.0.132", + "version": "1.0.133", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -192,7 +192,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.0.132", + "version": "1.0.133", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "22.0.0", @@ -208,7 +208,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.0.132", + "version": "1.0.133", "bin": { "opencode": "./bin/opencode", }, @@ -298,7 +298,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.0.132", + "version": "1.0.133", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -318,7 +318,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.0.132", + "version": "1.0.133", "devDependencies": { "@hey-api/openapi-ts": "0.81.0", "@tsconfig/node22": "catalog:", @@ -329,7 +329,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.0.132", + "version": "1.0.133", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -342,7 +342,7 @@ }, "packages/tauri": { "name": "@opencode-ai/tauri", - "version": "1.0.132", + "version": "1.0.133", "dependencies": { "@tauri-apps/api": "^2", "@tauri-apps/plugin-opener": "^2", @@ -355,7 +355,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.0.132", + "version": "1.0.133", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -387,7 +387,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.0.132", + "version": "1.0.133", "dependencies": { "zod": "catalog:", }, @@ -398,7 +398,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.0.132", + "version": "1.0.133", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/github/action.yml b/github/action.yml index 0138395f865..d83535fb0d0 100644 --- a/github/action.yml +++ b/github/action.yml @@ -13,6 +13,10 @@ inputs: description: "Share the opencode session (defaults to true for public repos)" required: false + prompt: + description: "Custom prompt to override the default prompt" + required: false + runs: using: "composite" steps: @@ -26,5 +30,6 @@ runs: env: MODEL: ${{ inputs.model }} SHARE: ${{ inputs.share }} + PROMPT: ${{ inputs.prompt }} OPENCODE_BIN_PATH: ${{ github.workspace }}/packages/opencode/dist/shuvcode-linux-x64/bin/opencode run: bun packages/opencode/bin/opencode github run diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 0a2c9c61bbb..15f00d6f322 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.0.132", + "version": "1.0.133", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index 623c931277e..7844a3ab079 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -21,6 +21,7 @@ import { oaCompatHelper } from "./provider/openai-compatible" import { createRateLimiter } from "./rateLimiter" import { createDataDumper } from "./dataDumper" import { createTrialLimiter } from "./trialLimiter" +import { createStickyTracker } from "./stickyProviderTracker" type ZenData = Awaited> type RetryOptions = { @@ -68,9 +69,11 @@ export async function handler( const isTrial = await trialLimiter?.isTrial() const rateLimiter = createRateLimiter(modelInfo.id, modelInfo.rateLimit, ip) await rateLimiter?.check() + const stickyTracker = createStickyTracker(modelInfo.stickyProvider ?? false, sessionId) + const stickyProvider = await stickyTracker?.get() const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => { - const providerInfo = selectProvider(zenData, modelInfo, sessionId, isTrial ?? false, retry) + const providerInfo = selectProvider(zenData, modelInfo, sessionId, isTrial ?? false, retry, stickyProvider) const authInfo = await authenticate(modelInfo, providerInfo) validateBilling(authInfo, modelInfo) validateModelSettings(authInfo) @@ -121,6 +124,9 @@ export async function handler( dataDumper?.provideModel(providerInfo.storeModel) dataDumper?.provideRequest(reqBody) + // Store sticky provider + await stickyTracker?.set(providerInfo.id) + // Scrub response headers const resHeaders = new Headers() const keepHeaders = ["content-type", "cache-control"] @@ -289,12 +295,18 @@ export async function handler( sessionId: string, isTrial: boolean, retry: RetryOptions, + stickyProvider: string | undefined, ) { const provider = (() => { if (isTrial) { return modelInfo.providers.find((provider) => provider.id === modelInfo.trial!.provider) } + if (stickyProvider) { + const provider = modelInfo.providers.find((provider) => provider.id === stickyProvider) + if (provider) return provider + } + if (retry.retryCount === MAX_RETRIES) { return modelInfo.providers.find((provider) => provider.id === modelInfo.fallbackProvider) } diff --git a/packages/console/app/src/routes/zen/util/stickyProviderTracker.ts b/packages/console/app/src/routes/zen/util/stickyProviderTracker.ts new file mode 100644 index 00000000000..63cbb0a68c9 --- /dev/null +++ b/packages/console/app/src/routes/zen/util/stickyProviderTracker.ts @@ -0,0 +1,16 @@ +import { Resource } from "@opencode-ai/console-resource" + +export function createStickyTracker(stickyProvider: boolean, session: string) { + if (!stickyProvider) return + if (!session) return + const key = `sticky:${session}` + + return { + get: async () => { + return await Resource.GatewayKv.get(key) + }, + set: async (providerId: string) => { + await Resource.GatewayKv.put(key, providerId, { expirationTtl: 86400 }) + }, + } +} diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 290127d3ef6..d324589b94b 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.0.132", + "version": "1.0.133", "private": true, "type": "module", "dependencies": { diff --git a/packages/console/core/src/model.ts b/packages/console/core/src/model.ts index 8cc181b7ca8..5a4a98fe94e 100644 --- a/packages/console/core/src/model.ts +++ b/packages/console/core/src/model.ts @@ -24,6 +24,7 @@ export namespace ZenData { cost: ModelCostSchema, cost200K: ModelCostSchema.optional(), allowAnonymous: z.boolean().optional(), + stickyProvider: z.boolean().optional(), trial: z .object({ limit: z.number(), diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 4f0955fd79a..0a3149cb1df 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.0.132", + "version": "1.0.133", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 3d6099b4d8b..9f55fd8f26e 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.0.132", + "version": "1.0.133", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index f2f8768cbb8..483cf85ef5a 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/desktop", - "version": "1.0.132", + "version": "1.0.133", "description": "", "type": "module", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 5a99f880ff4..a007bdcd959 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.0.132", + "version": "1.0.133", "private": true, "type": "module", "scripts": { diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 89598d4db6e..8a603582aae 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The AI coding agent built for the terminal" -version = "1.0.132" +version = "1.0.133" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/sst/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.132/opencode-darwin-arm64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.133/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.132/opencode-darwin-x64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.133/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.132/opencode-linux-arm64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.133/opencode-linux-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.132/opencode-linux-x64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.133/opencode-linux-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.132/opencode-windows-x64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.133/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 7bacb052bb5..be6e1b65c00 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.0.132", + "version": "1.0.133", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index eb6a728d4ee..c95ad1ba14e 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.0.132", + "version": "1.0.133", "name": "opencode", "type": "module", "private": true, diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts index 423b169183c..4fe27b151b8 100644 --- a/packages/opencode/src/cli/cmd/github.ts +++ b/packages/opencode/src/cli/cmd/github.ts @@ -562,6 +562,11 @@ export const GithubRunCommand = cmd({ } async function getUserPrompt() { + const customPrompt = process.env["PROMPT"] + if (customPrompt) { + return { userPrompt: customPrompt, promptFiles: [] } + } + const reviewContext = getReviewCommentContext() let prompt = (() => { const body = payload.comment.body.trim() diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 3fcda7d4563..7a90274dcab 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -32,6 +32,7 @@ import { KVProvider, useKV } from "./context/kv" import { Provider } from "@/provider/provider" import { ArgsProvider, useArgs, type Args } from "./context/args" import open from "open" +import { PromptRefProvider, usePromptRef } from "./context/prompt" async function getTerminalBackgroundColor(): Promise<"dark" | "light"> { // can't set raw mode if not a TTY @@ -119,7 +120,9 @@ export function tui(input: { url: string; args: Args; onExit?: () => Promise - + + + @@ -160,6 +163,7 @@ function App() { const { theme, mode, setMode } = useTheme() const sync = useSync() const exit = useExit() + const promptRef = usePromptRef() createEffect(() => { console.log(JSON.stringify(route.data)) @@ -225,8 +229,12 @@ function App() { keybind: "session_new", category: "Session", onSelect: () => { + const current = promptRef.current + // Don't require focus - if there's any text, preserve it + const currentPrompt = current?.current?.input ? current.current : undefined route.navigate({ type: "home", + initialPrompt: currentPrompt, }) dialog.clear() }, diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index 585b3f41362..c453ced7e86 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -357,8 +357,8 @@ export function Autocomplete(props: { function select() { const selected = options()[store.selected] if (!selected) return - selected.onSelect?.() hide() + selected.onSelect?.() } function show(mode: "@" | "/") { @@ -379,6 +379,10 @@ export function Autocomplete(props: { if (store.visible === "/" && !text.endsWith(" ") && text.startsWith("/")) { const cursor = props.input().logicalCursor props.input().deleteRange(0, 0, cursor.row, cursor.col) + // Sync the prompt store immediately since onContentChange is async + props.setPrompt((draft) => { + draft.input = props.input().plainText + }) } command.keybinds(true) setStore("visible", false) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 307b520f510..ba30ba799c8 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -39,6 +39,7 @@ export type PromptProps = { export type PromptRef = { focused: boolean text: string + current: PromptInfo set(prompt: PromptInfo): void reset(): void blur(): void @@ -271,7 +272,7 @@ export function Prompt(props: PromptProps) { createEffect(() => { if (props.disabled) input.cursorColor = theme.backgroundElement - if (!props.disabled) input.cursorColor = theme.primary + if (!props.disabled) input.cursorColor = theme.text }) const [store, setStore] = createStore<{ @@ -386,6 +387,9 @@ export function Prompt(props: PromptProps) { get text() { return input.plainText }, + get current() { + return store.prompt + }, focus() { input.focus() }, @@ -829,12 +833,12 @@ export function Prompt(props: PromptProps) { ref={(r: TextareaRenderable) => { input = r setTimeout(() => { - input.cursorColor = highlight() + input.cursorColor = theme.text }, 0) }} onMouseDown={(r: MouseEvent) => r.target?.focus()} focusedBackgroundColor={theme.backgroundElement} - cursorColor={highlight()} + cursorColor={theme.text} syntaxStyle={syntax()} /> diff --git a/packages/opencode/src/cli/cmd/tui/context/prompt.tsx b/packages/opencode/src/cli/cmd/tui/context/prompt.tsx new file mode 100644 index 00000000000..efbb050645e --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/context/prompt.tsx @@ -0,0 +1,18 @@ +import { createSimpleContext } from "./helper" +import type { PromptRef } from "../component/prompt" + +export const { use: usePromptRef, provider: PromptRefProvider } = createSimpleContext({ + name: "PromptRef", + init: () => { + let current: PromptRef | undefined + + return { + get current() { + return current + }, + set(ref: PromptRef | undefined) { + current = ref + }, + } + }, +}) diff --git a/packages/opencode/src/cli/cmd/tui/context/route.tsx b/packages/opencode/src/cli/cmd/tui/context/route.tsx index b906de99b38..22333a0589e 100644 --- a/packages/opencode/src/cli/cmd/tui/context/route.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/route.tsx @@ -1,8 +1,10 @@ import { createStore } from "solid-js/store" import { createSimpleContext } from "./helper" +import type { PromptInfo } from "../component/prompt/history" export type HomeRoute = { type: "home" + initialPrompt?: PromptInfo } export type SessionRoute = { diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx index 33942c2a50b..df99eac885f 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/home.tsx @@ -1,15 +1,14 @@ import { Prompt, type PromptRef } from "@tui/component/prompt" -import { createMemo, Match, onMount, Show, Switch, type ParentProps } from "solid-js" +import { createMemo, Match, onMount, Show, Switch } from "solid-js" import { useTheme } from "@tui/context/theme" -import { useKeybind } from "../context/keybind" -import type { KeybindsConfig } from "@opencode-ai/sdk" import { Logo } from "../component/logo" import { Locale } from "@/util/locale" import { useSync } from "../context/sync" import { Toast } from "../ui/toast" import { useArgs } from "../context/args" -import { Global } from "@/global" import { useDirectory } from "../context/directory" +import { useRoute, useRouteData } from "@tui/context/route" +import { usePromptRef } from "../context/prompt" // TODO: what is the best way to do this? let once = false @@ -17,6 +16,8 @@ let once = false export function Home() { const sync = useSync() const { theme } = useTheme() + const route = useRouteData("home") + const promptRef = usePromptRef() const mcp = createMemo(() => Object.keys(sync.data.mcp).length > 0) const mcpError = createMemo(() => { return Object.values(sync.data.mcp).some((x) => x.status === "failed") @@ -45,7 +46,10 @@ export function Home() { const args = useArgs() onMount(() => { if (once) return - if (args.prompt) { + if (route.initialPrompt) { + prompt.set(route.initialPrompt) + once = true + } else if (args.prompt) { prompt.set({ input: args.prompt, parts: [] }) once = true } @@ -57,7 +61,13 @@ export function Home() { - (prompt = r)} hint={Hint} /> + { + prompt = r + promptRef.set(r) + }} + hint={Hint} + /> diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 016172333ca..a7c16f3cc5e 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -66,6 +66,7 @@ import { SearchInput, type SearchInputRef } from "../../component/prompt/search. import { Footer } from "./footer.tsx" import { extend } from "@opentui/solid" import { TerminalBufferRenderable } from "opentui-ansi-vt/terminal-buffer" +import { usePromptRef } from "../../context/prompt" addDefaultParsers(parsers.parsers) @@ -122,6 +123,7 @@ export function Session() { const sync = useSync() const kv = useKV() const { theme } = useTheme() + const promptRef = usePromptRef() const session = createMemo(() => sync.session.get(route.sessionID)!) const messages = createMemo(() => sync.data.message[route.sessionID] ?? []) const permissions = createMemo(() => sync.data.permission[route.sessionID] ?? []) @@ -1162,7 +1164,10 @@ export function Session() { (prompt = r)} + ref={(r) => { + prompt = r + promptRef.set(r) + }} disabled={permissions().length > 0} onSubmit={() => { toBottom() diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts index 50274f44200..7754b4a3953 100644 --- a/packages/opencode/src/cli/cmd/tui/worker.ts +++ b/packages/opencode/src/cli/cmd/tui/worker.ts @@ -2,6 +2,7 @@ import { Installation } from "@/installation" import { Server } from "@/server/server" import { Log } from "@/util/log" import { Instance } from "@/project/instance" +import { InstanceBootstrap } from "@/project/bootstrap" import { Rpc } from "@/util/rpc" import { upgrade } from "@/cli/upgrade" @@ -43,6 +44,7 @@ export const rpc = { async checkUpgrade(input: { directory: string }) { await Instance.provide({ directory: input.directory, + init: InstanceBootstrap, fn: async () => { await upgrade().catch(() => {}) }, diff --git a/packages/opencode/src/global/index.ts b/packages/opencode/src/global/index.ts index de0d0182368..04a9093a529 100644 --- a/packages/opencode/src/global/index.ts +++ b/packages/opencode/src/global/index.ts @@ -30,7 +30,7 @@ await Promise.all([ fs.mkdir(Global.Path.bin, { recursive: true }), ]) -const CACHE_VERSION = "12" +const CACHE_VERSION = "13" const version = await Bun.file(path.join(Global.Path.cache, "version")) .text() diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index e617e045418..9571a63481d 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -28,8 +28,8 @@ export namespace Plugin { } const plugins = [...(config.plugin ?? [])] if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) { - plugins.push("opencode-copilot-auth@0.0.7") - plugins.push("opencode-anthropic-auth@0.0.3") + plugins.push("opencode-copilot-auth@0.0.8") + plugins.push("opencode-anthropic-auth@0.0.4") } for (let plugin of plugins) { log.info("loading plugin", { path: plugin }) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 54367bcfeb3..f60bcfa4756 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -285,6 +285,17 @@ export namespace Provider { }, } }, + "sap-ai-core": async () => { + const auth = await Auth.get("sap-ai-core") + const serviceKey = Env.get("SAP_AI_SERVICE_KEY") || (auth?.type === "api" ? auth.key : undefined) + const deploymentId = Env.get("SAP_AI_DEPLOYMENT_ID") || "d65d81e7c077e583" + const resourceGroup = Env.get("SAP_AI_RESOURCE_GROUP") || "default" + + return { + autoload: !!serviceKey, + options: serviceKey ? { serviceKey, deploymentId, resourceGroup } : {}, + } + }, zenmux: async () => { return { autoload: false, @@ -446,7 +457,8 @@ export namespace Provider { const state = Instance.state(async () => { using _ = log.time("state") const config = await Config.get() - const database = mapValues(await ModelsDev.get(), fromModelsDevProvider) + const modelsDev = await ModelsDev.get() + const database = mapValues(modelsDev, fromModelsDevProvider) const disabled = new Set(config.disabled_providers ?? []) const enabled = config.enabled_providers ? new Set(config.enabled_providers) : null @@ -504,56 +516,57 @@ export namespace Provider { } for (const [modelID, model] of Object.entries(provider.models ?? {})) { - const existing = parsed.models[model.id ?? modelID] + const existingModel = parsed.models[model.id ?? modelID] const name = iife(() => { if (model.name) return model.name if (model.id && model.id !== modelID) return modelID - return existing?.name ?? modelID + return existingModel?.name ?? modelID }) const parsedModel: Model = { id: modelID, api: { - id: model.id ?? existing?.api.id ?? modelID, - npm: model.provider?.npm ?? provider.npm ?? existing?.api.npm ?? providerID, - url: provider?.api ?? existing?.api.url, + id: model.id ?? existingModel?.api.id ?? modelID, + npm: + model.provider?.npm ?? provider.npm ?? existingModel?.api.npm ?? modelsDev[providerID]?.npm ?? providerID, + url: provider?.api ?? existingModel?.api.url, }, - status: model.status ?? existing?.status ?? "active", + status: model.status ?? existingModel?.status ?? "active", name, providerID, capabilities: { - temperature: model.temperature ?? existing?.capabilities.temperature ?? false, - reasoning: model.reasoning ?? existing?.capabilities.reasoning ?? false, - attachment: model.attachment ?? existing?.capabilities.attachment ?? false, - toolcall: model.tool_call ?? existing?.capabilities.toolcall ?? true, + temperature: model.temperature ?? existingModel?.capabilities.temperature ?? false, + reasoning: model.reasoning ?? existingModel?.capabilities.reasoning ?? false, + attachment: model.attachment ?? existingModel?.capabilities.attachment ?? false, + toolcall: model.tool_call ?? existingModel?.capabilities.toolcall ?? true, input: { - text: model.modalities?.input?.includes("text") ?? existing?.capabilities.input.text ?? true, - audio: model.modalities?.input?.includes("audio") ?? existing?.capabilities.input.audio ?? false, - image: model.modalities?.input?.includes("image") ?? existing?.capabilities.input.image ?? false, - video: model.modalities?.input?.includes("video") ?? existing?.capabilities.input.video ?? false, - pdf: model.modalities?.input?.includes("pdf") ?? existing?.capabilities.input.pdf ?? false, + text: model.modalities?.input?.includes("text") ?? existingModel?.capabilities.input.text ?? true, + audio: model.modalities?.input?.includes("audio") ?? existingModel?.capabilities.input.audio ?? false, + image: model.modalities?.input?.includes("image") ?? existingModel?.capabilities.input.image ?? false, + video: model.modalities?.input?.includes("video") ?? existingModel?.capabilities.input.video ?? false, + pdf: model.modalities?.input?.includes("pdf") ?? existingModel?.capabilities.input.pdf ?? false, }, output: { - text: model.modalities?.output?.includes("text") ?? existing?.capabilities.output.text ?? true, - audio: model.modalities?.output?.includes("audio") ?? existing?.capabilities.output.audio ?? false, - image: model.modalities?.output?.includes("image") ?? existing?.capabilities.output.image ?? false, - video: model.modalities?.output?.includes("video") ?? existing?.capabilities.output.video ?? false, - pdf: model.modalities?.output?.includes("pdf") ?? existing?.capabilities.output.pdf ?? false, + text: model.modalities?.output?.includes("text") ?? existingModel?.capabilities.output.text ?? true, + audio: model.modalities?.output?.includes("audio") ?? existingModel?.capabilities.output.audio ?? false, + image: model.modalities?.output?.includes("image") ?? existingModel?.capabilities.output.image ?? false, + video: model.modalities?.output?.includes("video") ?? existingModel?.capabilities.output.video ?? false, + pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false, }, }, cost: { - input: model?.cost?.input ?? existing?.cost?.input ?? 0, - output: model?.cost?.output ?? existing?.cost?.output ?? 0, + input: model?.cost?.input ?? existingModel?.cost?.input ?? 0, + output: model?.cost?.output ?? existingModel?.cost?.output ?? 0, cache: { - read: model?.cost?.cache_read ?? existing?.cost?.cache.read ?? 0, - write: model?.cost?.cache_write ?? existing?.cost?.cache.write ?? 0, + read: model?.cost?.cache_read ?? existingModel?.cost?.cache.read ?? 0, + write: model?.cost?.cache_write ?? existingModel?.cost?.cache.write ?? 0, }, }, - options: mergeDeep(existing?.options ?? {}, model.options ?? {}), + options: mergeDeep(existingModel?.options ?? {}, model.options ?? {}), limit: { - context: model.limit?.context ?? existing?.limit?.context ?? 0, - output: model.limit?.output ?? existing?.limit?.output ?? 0, + context: model.limit?.context ?? existingModel?.limit?.context ?? 0, + output: model.limit?.output ?? existingModel?.limit?.output ?? 0, }, - headers: mergeDeep(existing?.headers ?? {}, model.headers ?? {}), + headers: mergeDeep(existingModel?.headers ?? {}, model.headers ?? {}), } parsed.models[modelID] = parsedModel } @@ -776,7 +789,7 @@ export namespace Provider { const mod = await import(installedPath) const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!] - const loaded = fn({ + const loaded = await fn({ name: model.providerID, ...options, }) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index b125425814c..68683469268 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -685,7 +685,7 @@ export namespace SessionPrompt { abort: options.abortSignal!, messageID: input.processor.message.id, callID: options.toolCallId, - extra: input.model, + extra: { model: input.model }, agent: input.agent.name, metadata: async (val) => { const match = input.processor.partFromToolCall(options.toolCallId) @@ -919,12 +919,13 @@ export namespace SessionPrompt { await ReadTool.init() .then(async (t) => { + const model = await Provider.getModel(info.model.providerID, info.model.modelID) const result = await t.execute(args, { sessionID: input.sessionID, abort: new AbortController().signal, agent: input.agent!, messageID: info.id, - extra: { bypassCwdCheck: true, ...info.model }, + extra: { bypassCwdCheck: true, model }, metadata: async () => {}, }) pieces.push({ diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index 2fc0cfee137..bdd688469de 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -335,17 +335,24 @@ export const BashTool = Tool.define("bash", async () => { }) }) + let resultMetadata: String[] = [""] + if (output.length > MAX_OUTPUT_LENGTH) { output = output.slice(0, MAX_OUTPUT_LENGTH) - output += "\n\n(Output was truncated due to length limit)" + resultMetadata.push(`Output exceeded length limit of ${MAX_OUTPUT_LENGTH} chars`) } if (timedOut) { - output += `\n\n(Command timed out after ${timeout} ms)` + resultMetadata.push(`Command terminated after exceeding timeout ${timeout} ms`) } if (aborted) { - output += "\n\n(Command was aborted)" + resultMetadata.push("Command aborted by user") + } + + if (resultMetadata.length > 1) { + resultMetadata.push("") + output += "\n\n" + resultMetadata.join("\n") } return { diff --git a/packages/opencode/src/tool/bash.txt b/packages/opencode/src/tool/bash.txt index 0a4d9f16de1..cbb66bba532 100644 --- a/packages/opencode/src/tool/bash.txt +++ b/packages/opencode/src/tool/bash.txt @@ -35,6 +35,8 @@ Usage notes: # Committing changes with git +IMPORTANT: ONLY COMMIT IF THE USER ASKS YOU TO. + If and only if the user asks you to create a new git commit, follow these steps carefully: 1. You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. ALWAYS run the following bash commands in parallel, each using the Bash tool: @@ -42,9 +44,8 @@ If and only if the user asks you to create a new git commit, follow these steps - Run a git diff command to see both staged and unstaged changes that will be committed. - Run a git log command to see recent commit messages, so that you can follow this repository's commit message style. -2. Analyze all staged changes (both previously staged and newly added) and draft a commit message. Wrap your analysis process in tags: +2. Analyze all staged changes (both previously staged and newly added) and draft a commit message. When analyzing: - - List the files that have been changed or added - Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.) - Brainstorm the purpose or motivation behind these changes @@ -55,7 +56,6 @@ If and only if the user asks you to create a new git commit, follow these steps - Ensure the message accurately reflects the changes and their purpose (i.e. "add" means a wholly new feature, "update" means an enhancement to an existing feature, "fix" means a bug fix, etc.) - Ensure the message is not generic (avoid words like "Update" or "Fix" without context) - Review the draft message to ensure it accurately reflects the changes and their purpose - 3. You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. ALWAYS run the following commands in parallel: - Add relevant untracked files to the staging area. diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index 7e01246b539..7d01a198148 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -95,14 +95,8 @@ export const ReadTool = Tool.define("read", { } const isImage = isImageFile(filepath) - const supportsImages = await (async () => { - if (!ctx.extra?.["providerID"] || !ctx.extra?.["modelID"]) return false - const providerID = ctx.extra["providerID"] as string - const modelID = ctx.extra["modelID"] as string - const model = await Provider.getModel(providerID, modelID).catch(() => undefined) - if (!model) return false - return model.capabilities.input.image - })() + const model = ctx.extra?.model as Provider.Model | undefined + const supportsImages = model?.capabilities.input.image ?? false if (isImage) { if (!supportsImages) { throw new Error(`Failed to read image: ${filepath}, model may not be able to read images`) diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 698fdddfb42..e6eb0c7b464 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -1727,3 +1727,39 @@ test("provider options are deeply merged", async () => { }, }) }) + +test("custom model inherits npm package from models.dev provider config", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + openai: { + models: { + "my-custom-model": { + name: "My Custom Model", + tool_call: true, + limit: { context: 8000, output: 2000 }, + }, + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("OPENAI_API_KEY", "test-api-key") + }, + fn: async () => { + const providers = await Provider.list() + const model = providers["openai"].models["my-custom-model"] + expect(model).toBeDefined() + expect(model.api.npm).toBe("@ai-sdk/openai") + }, + }) +}) diff --git a/packages/plugin/package.json b/packages/plugin/package.json index da5e3450e3e..b8833364d06 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.0.132", + "version": "1.0.133", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index a811885e1dc..aee34752836 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.0.132", + "version": "1.0.133", "type": "module", "scripts": { "typecheck": "tsgo --noEmit", diff --git a/packages/slack/package.json b/packages/slack/package.json index 4808543d592..2570f0877a4 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.0.132", + "version": "1.0.133", "type": "module", "scripts": { "dev": "bun run src/index.ts", diff --git a/packages/tauri/package.json b/packages/tauri/package.json index b368f4a08b6..9c3964dbab1 100644 --- a/packages/tauri/package.json +++ b/packages/tauri/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/tauri", "private": true, - "version": "1.0.132", + "version": "1.0.133", "type": "module", "scripts": { "dev": "vite", diff --git a/packages/ui/package.json b/packages/ui/package.json index ec2d740414a..a20d19f6214 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.0.132", + "version": "1.0.133", "type": "module", "exports": { "./*": "./src/components/*.tsx", diff --git a/packages/util/package.json b/packages/util/package.json index 75f8bec7485..4ac022e0cbe 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.0.132", + "version": "1.0.133", "private": true, "type": "module", "exports": { diff --git a/packages/web/package.json b/packages/web/package.json index 666d40a33f9..5e5b178f766 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/web", "type": "module", - "version": "1.0.132", + "version": "1.0.133", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/packages/web/src/content/docs/acp.mdx b/packages/web/src/content/docs/acp.mdx index bca30923c25..9d421235c1b 100644 --- a/packages/web/src/content/docs/acp.mdx +++ b/packages/web/src/content/docs/acp.mdx @@ -100,6 +100,27 @@ If you need to pass environment variables: --- +### CodeCompanion.nvim + +To use OpenCode as an ACP agent in [CodeCompanion.nvim](https://github.com/olimorris/codecompanion.nvim), add the following to your Neovim config: + +```lua +require("codecompanion").setup({ + strategies = { + chat = { + adapter = { + name = "opencode", + model = "claude-sonnet-4", + }, + }, + }, +}) +``` + +This config sets up CodeCompanion to use OpenCode as the ACP agent for chat. + +If you need to pass environment variables (like `OPENCODE_API_KEY`), refer to [Configuring Adapters: Environment Variables](https://codecompanion.olimorris.dev/configuration/adapters#environment-variables-setting-an-api-key) in the CodeCompanion.nvim documentation for full details. + ## Support OpenCode works the same via ACP as it does in the terminal. All features are supported: diff --git a/packages/web/src/content/docs/github.mdx b/packages/web/src/content/docs/github.mdx index 19c7782ef04..b0e0397e114 100644 --- a/packages/web/src/content/docs/github.mdx +++ b/packages/web/src/content/docs/github.mdx @@ -82,6 +82,7 @@ Or you can set it up manually. - `model`: The model to use with opencode. Takes the format of `provider/model`. This is **required**. - `share`: Whether to share the opencode session. Defaults to **true** for public repositories. +- `prompt`: Optional custom prompt to override the default behavior. Use this to customize how opencode processes requests. - `token`: Optional GitHub access token for performing operations such as creating comments, committing changes, and opening pull requests. By default, opencode uses the installation access token from the opencode GitHub App, so commits, comments, and pull requests appear as coming from the app. Alternatively, you can use the GitHub Action runner's [built-in `GITHUB_TOKEN`](https://docs.github.com/en/actions/tutorials/authenticate-with-github_token) without installing the opencode GitHub App. Just make sure to grant the required permissions in your workflow: @@ -98,6 +99,25 @@ Or you can set it up manually. --- +## Custom prompts + +Override the default prompt to customize opencode's behavior for your workflow. + +```yaml title=".github/workflows/opencode.yml" +- uses: sst/opencode/github@latest + with: + model: anthropic/claude-sonnet-4-5 + prompt: | + Review this pull request: + - Check for code quality issues + - Look for potential bugs + - Suggest improvements +``` + +This is useful for enforcing specific review criteria, coding standards, or focus areas relevant to your project. + +--- + ## Examples Here are some examples of how you can use opencode in GitHub. diff --git a/packages/web/src/content/docs/providers.mdx b/packages/web/src/content/docs/providers.mdx index bb284901f53..e8534ceee93 100644 --- a/packages/web/src/content/docs/providers.mdx +++ b/packages/web/src/content/docs/providers.mdx @@ -891,6 +891,61 @@ OpenCode Zen is a list of tested and verified models provided by the OpenCode te --- +### SAP AI Core + +SAP AI Core provides access to 40+ models from OpenAI, Anthropic, Google, Amazon, Meta, Mistral, and AI21 through a unified platform. + +1. Go to your [SAP BTP Cockpit](https://account.hana.ondemand.com/), navigate to your SAP AI Core service instance, and create a service key. + + :::tip + The service key is a JSON object containing `clientid`, `clientsecret`, `url`, and `serviceurls.AI_API_URL`. You can find your AI Core instance under **Services** > **Instances and Subscriptions** in the BTP Cockpit. + ::: + +2. Run the `/connect` command and search for **SAP AI Core**. + + ```txt + /connect + ``` + +3. Enter your service key JSON. + + ```txt + ┌ Service key + │ + │ + └ enter + ``` + + Or set the `SAP_AI_SERVICE_KEY` environment variable: + + ```bash + SAP_AI_SERVICE_KEY='{"clientid":"...","clientsecret":"...","url":"...","serviceurls":{"AI_API_URL":"..."}}' opencode + ``` + + Or add it to your bash profile: + + ```bash title="~/.bash_profile" + export SAP_AI_SERVICE_KEY='{"clientid":"...","clientsecret":"...","url":"...","serviceurls":{"AI_API_URL":"..."}}' + ``` + +4. Optionally set deployment ID and resource group: + + ```bash + SAP_AI_DEPLOYMENT_ID=your-deployment-id SAP_AI_RESOURCE_GROUP=your-resource-group opencode + ``` + + :::note + If not set, uses deployment ID `d65d81e7c077e583` (general-purpose) and resource group `default`. Configure these for your specific setup. + ::: + +5. Run the `/models` command to select from 40+ available models. + + ```txt + /models + ``` + +--- + ### OVHcloud AI Endpoints 1. Head over to the [OVHcloud panel](https://ovh.com/manager). Navigate to the `Public Cloud` section, `AI & Machine Learning` > `AI Endpoints` and in `API Keys` tab, click **Create a new API key**. diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 448489ac5a3..f71048523cd 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.0.132", + "version": "1.0.133", "publisher": "sst-dev", "repository": { "type": "git",