diff --git a/bun.lock b/bun.lock index bd770d5251a..a02f2398935 100644 --- a/bun.lock +++ b/bun.lock @@ -41,7 +41,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.0.83", + "version": "1.0.85", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -68,7 +68,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.0.83", + "version": "1.0.85", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -92,7 +92,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.0.83", + "version": "1.0.85", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -116,7 +116,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.0.83", + "version": "1.0.85", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -156,7 +156,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.0.83", + "version": "1.0.85", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "22.0.0", @@ -172,7 +172,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.0.83", + "version": "1.0.85", "bin": { "opencode": "./bin/opencode", }, @@ -251,7 +251,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.0.83", + "version": "1.0.85", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -271,7 +271,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.0.83", + "version": "1.0.85", "devDependencies": { "@hey-api/openapi-ts": "0.81.0", "@tsconfig/node22": "catalog:", @@ -282,7 +282,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.0.83", + "version": "1.0.85", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -295,7 +295,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.0.83", + "version": "1.0.85", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -325,7 +325,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.0.83", + "version": "1.0.85", "dependencies": { "zod": "catalog:", }, @@ -335,7 +335,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.0.83", + "version": "1.0.85", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/flake.lock b/flake.lock index cdab71ce74a..1150e275150 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1763464769, - "narHash": "sha256-AJHrsT7VoeQzErpBRlLJM1SODcaayp0joAoEA35yiwM=", + "lastModified": 1763618868, + "narHash": "sha256-v5afmLjn/uyD9EQuPBn7nZuaZVV9r+JerayK/4wvdWA=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "6f374686605df381de8541c072038472a5ea2e2d", + "rev": "a8d610af3f1a5fb71e23e08434d8d61a466fc942", "type": "github" }, "original": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 071041da9ca..745bf6bebbd 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -7,7 +7,7 @@ "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev", "build": "./script/generate-sitemap.ts && vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json", "start": "vinxi start", - "version": "1.0.83" + "version": "1.0.85" }, "dependencies": { "@ibm/plex": "6.4.1", diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 770ccc3aa8a..1ff03db3451 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.83", + "version": "1.0.85", "private": true, "type": "module", "dependencies": { diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 5192c5f625e..74588bbd17b 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.83", + "version": "1.0.85", "$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 9a12d93ce31..c92fb685310 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.83", + "version": "1.0.85", "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 ba75cf84dfc..cc05656c396 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/desktop", - "version": "1.0.83", + "version": "1.0.85", "description": "", "type": "module", "scripts": { diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 3f850d46f34..e1ceeaf8059 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.83" +version = "1.0.85" 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.83/opencode-darwin-arm64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.85/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.83/opencode-darwin-x64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.85/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/sst/opencode/releases/download/v1.0.83/opencode-linux-arm64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.85/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.83/opencode-linux-x64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.85/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.83/opencode-windows-x64.zip" +archive = "https://github.com/sst/opencode/releases/download/v1.0.85/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index c87be58f590..9a8e3350262 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.0.83", + "version": "1.0.85", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 86244e780da..4a919d7cf66 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.83", + "version": "1.0.85", "name": "opencode", "type": "module", "private": true, diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 4f990e76e9f..7bb10de899a 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -2,10 +2,11 @@ import { render, useKeyboard, useRenderer, useTerminalDimensions } from "@opentu import { Clipboard } from "@tui/util/clipboard" import { TextAttributes } from "@opentui/core" import { RouteProvider, useRoute } from "@tui/context/route" -import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch } from "solid-js" +import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show } from "solid-js" import { Installation } from "@/installation" import { Global } from "@/global" import { DialogProvider, useDialog } from "@tui/ui/dialog" +import { DialogProvider as DialogProviderList } from "@tui/component/dialog-provider" import { SDKProvider, useSDK } from "@tui/context/sdk" import { SyncProvider, useSync } from "@tui/context/sync" import { LocalProvider, useLocal } from "@tui/context/local" @@ -293,6 +294,14 @@ function App() { }, category: "System", }, + { + title: "Connect provider", + value: "provider.connect", + onSelect: () => { + dialog.replace(() => ) + }, + category: "System", + }, { title: `Switch to ${mode() === "dark" ? "light" : "dark"} mode`, value: "theme.switch_mode", @@ -451,16 +460,18 @@ function App() { {process.cwd().replace(Global.Path.home, "~")} - - - tab - - {""} - - {local.agent.current().name.toUpperCase()} - AGENT - - + + + + tab + + {""} + + {local.agent.current().name.toUpperCase()} + AGENT + + + ) diff --git a/packages/opencode/src/cli/cmd/tui/component/border.tsx b/packages/opencode/src/cli/cmd/tui/component/border.tsx index 9cbb96068d3..333071020c4 100644 --- a/packages/opencode/src/cli/cmd/tui/component/border.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/border.tsx @@ -1,16 +1,21 @@ +export const EmptyBorder = { + topLeft: "", + bottomLeft: "", + vertical: "", + topRight: "", + bottomRight: "", + horizontal: " ", + bottomT: "", + topT: "", + cross: "", + leftT: "", + rightT: "", +} + export const SplitBorder = { border: ["left" as const, "right" as const], customBorderChars: { - topLeft: "", - bottomLeft: "", + ...EmptyBorder, vertical: "┃", - topRight: "", - bottomRight: "", - horizontal: "", - bottomT: "", - topT: "", - cross: "", - leftT: "", - rightT: "", }, } diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx index bcd1d98d56a..c25e7e370ab 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx @@ -1,15 +1,10 @@ import { createMemo, createSignal } from "solid-js" import { useLocal } from "@tui/context/local" import { useSync } from "@tui/context/sync" -import { map, pipe, flatMap, entries, filter, isDeepEqual, sortBy } from "remeda" +import { map, pipe, flatMap, entries, filter, isDeepEqual, sortBy, take } from "remeda" import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select" import { useDialog } from "@tui/ui/dialog" -import { useTheme } from "../context/theme" - -function Free() { - const { theme } = useTheme() - return Free -} +import { createDialogProviderOptions, DialogProvider } from "./dialog-provider" export function DialogModel() { const local = useLocal() @@ -17,9 +12,16 @@ export function DialogModel() { const dialog = useDialog() const [ref, setRef] = createSignal>() + const connected = createMemo(() => + sync.data.provider.some((x) => x.id !== "opencode" || Object.values(x.models).some((y) => y.cost?.input !== 0)), + ) + + const showRecent = createMemo(() => !ref()?.filter && local.model.recent().length > 0 && connected()) + const providers = createDialogProviderOptions() + const options = createMemo(() => { return [ - ...(!ref()?.filter + ...(showRecent() ? local.model.recent().flatMap((item) => { const provider = sync.data.provider.find((x) => x.id === item.providerID)! if (!provider) return [] @@ -35,7 +37,17 @@ export function DialogModel() { title: model.name ?? item.modelID, description: provider.name, category: "Recent", - footer: model.cost?.input === 0 && provider.id === "opencode" ? : undefined, + footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined, + onSelect: () => { + dialog.clear() + local.model.set( + { + providerID: provider.id, + modelID: model.id, + }, + { recent: true }, + ) + }, }, ] }) @@ -56,28 +68,56 @@ export function DialogModel() { modelID: model, }, title: info.name ?? model, - description: provider.name, - category: provider.name, - footer: info.cost?.input === 0 && provider.id === "opencode" ? : undefined, + description: connected() ? provider.name : undefined, + category: connected() ? provider.name : undefined, + disabled: provider.id === "opencode" && model.includes("-nano"), + footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined, + onSelect() { + dialog.clear() + local.model.set( + { + providerID: provider.id, + modelID: model, + }, + { recent: true }, + ) + }, })), - filter((x) => Boolean(ref()?.filter) || !local.model.recent().find((y) => isDeepEqual(y, x.value))), + filter((x) => !showRecent() || !local.model.recent().find((y) => isDeepEqual(y, x.value))), sortBy((x) => x.title), ), ), ), + ...(!connected() + ? pipe( + providers(), + map((option) => { + return { + ...option, + category: "Popular providers", + } + }), + take(6), + ) + : []), ] }) return ( ) + }, + }, + ]} ref={setRef} title="Select model" current={local.model.current()} options={options()} - onSelect={(option) => { - dialog.clear() - local.model.set(option.value, { recent: true }) - }} /> ) } diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx new file mode 100644 index 00000000000..109d4d25ade --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx @@ -0,0 +1,222 @@ +import { createMemo, createSignal, onMount, Show } from "solid-js" +import { useSync } from "@tui/context/sync" +import { map, pipe, sortBy } from "remeda" +import { DialogSelect } from "@tui/ui/dialog-select" +import { useDialog } from "@tui/ui/dialog" +import { useSDK } from "../context/sdk" +import { DialogPrompt } from "../ui/dialog-prompt" +import { useTheme } from "../context/theme" +import { TextAttributes } from "@opentui/core" +import type { ProviderAuthAuthorization } from "@opencode-ai/sdk" +import { DialogModel } from "./dialog-model" + +const PROVIDER_PRIORITY: Record = { + opencode: 0, + anthropic: 1, + "github-copilot": 2, + openai: 3, + google: 4, + openrouter: 5, +} + +export function createDialogProviderOptions() { + const sync = useSync() + const dialog = useDialog() + const sdk = useSDK() + const options = createMemo(() => { + return pipe( + sync.data.provider_next.all, + map((provider) => ({ + title: provider.name, + value: provider.id, + footer: { + opencode: "Recommended", + anthropic: "Claude Max or API key", + }[provider.id], + async onSelect() { + const methods = sync.data.provider_auth[provider.id] ?? [ + { + type: "api", + label: "API key", + }, + ] + let index: number | null = 0 + if (methods.length > 1) { + index = await new Promise((resolve) => { + dialog.replace( + () => ( + ({ + title: x.label, + value: index, + }))} + onSelect={(option) => resolve(option.value)} + /> + ), + () => resolve(null), + ) + }) + } + if (index == null) return + const method = methods[index] + if (method.type === "oauth") { + const result = await sdk.client.provider.oauth.authorize({ + path: { + id: provider.id, + }, + body: { + method: index, + }, + }) + if (result.data?.method === "code") { + dialog.replace(() => ( + + )) + } + if (result.data?.method === "auto") { + dialog.replace(() => ( + + )) + } + } + if (method.type === "api") { + return dialog.replace(() => ) + } + }, + })), + sortBy((x) => PROVIDER_PRIORITY[x.value] ?? 99), + ) + }) + return options +} + +export function DialogProvider() { + const options = createDialogProviderOptions() + return +} + +interface AutoMethodProps { + index: number + providerID: string + title: string + authorization: ProviderAuthAuthorization +} +function AutoMethod(props: AutoMethodProps) { + const { theme } = useTheme() + const sdk = useSDK() + const dialog = useDialog() + const sync = useSync() + + onMount(async () => { + const result = await sdk.client.provider.oauth.callback({ + path: { + id: props.providerID, + }, + body: { + method: props.index, + }, + }) + if (result.error) { + dialog.clear() + return + } + await sdk.client.instance.dispose() + await sync.bootstrap() + dialog.replace(() => ) + }) + + return ( + + + {props.title} + esc + + + {props.authorization.url} + {props.authorization.instructions} + + Waiting for authorization... + + ) +} + +interface CodeMethodProps { + index: number + title: string + providerID: string + authorization: ProviderAuthAuthorization +} +function CodeMethod(props: CodeMethodProps) { + const { theme } = useTheme() + const sdk = useSDK() + const sync = useSync() + const dialog = useDialog() + const [error, setError] = createSignal(false) + + return ( + { + const { error } = await sdk.client.provider.oauth.callback({ + path: { + id: props.providerID, + }, + body: { + method: props.index, + code: value, + }, + }) + if (!error) { + await sdk.client.instance.dispose() + await sync.bootstrap() + dialog.replace(() => ) + return + } + setError(true) + }} + description={() => ( + + {props.authorization.instructions} + {props.authorization.url} + + Invalid code + + + )} + /> + ) +} + +interface ApiMethodProps { + providerID: string + title: string +} +function ApiMethod(props: ApiMethodProps) { + const dialog = useDialog() + const sdk = useSDK() + const sync = useSync() + + return ( + { + if (!value) return + sdk.client.auth.set({ + path: { + id: props.providerID, + }, + body: { + type: "api", + key: value, + }, + }) + await sdk.client.instance.dispose() + await sync.bootstrap() + dialog.replace(() => ) + }} + /> + ) +} 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 8281ab617aa..b04cb7c6043 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -1,18 +1,8 @@ -import { - TextAttributes, - BoxRenderable, - TextareaRenderable, - MouseEvent, - PasteEvent, - t, - dim, - fg, - type KeyBinding, -} from "@opentui/core" -import { createEffect, createMemo, Match, Switch, type JSX, onMount } from "solid-js" +import { BoxRenderable, TextareaRenderable, MouseEvent, PasteEvent, t, dim, fg, type KeyBinding } from "@opentui/core" +import { createEffect, createMemo, type JSX, onMount, createSignal, onCleanup, Show, Switch, Match } from "solid-js" import { useLocal } from "@tui/context/local" import { useTheme } from "@tui/context/theme" -import { SplitBorder } from "@tui/component/border" +import { EmptyBorder } from "@tui/component/border" import { useSDK } from "@tui/context/sdk" import { useRoute } from "@tui/context/route" import { useSync } from "@tui/context/sync" @@ -29,6 +19,8 @@ import { Clipboard } from "../../util/clipboard" import type { FilePart } from "@opencode-ai/sdk" import { TuiEvent } from "../../event" import { iife } from "@/util/iife" +import { Locale } from "@/util/locale" +import { Shimmer } from "../../ui/shimmer" export type PromptProps = { sessionID?: string @@ -57,7 +49,7 @@ export function Prompt(props: PromptProps) { const sdk = useSDK() const route = useRoute() const sync = useSync() - const status = createMemo(() => (props.sessionID ? sync.session.status(props.sessionID) : "idle")) + const status = createMemo(() => sync.data.session_status[props.sessionID ?? ""] ?? { type: "idle" }) const history = usePromptHistory() const command = useCommandDialog() const renderer = useRenderer() @@ -222,12 +214,17 @@ export function Prompt(props: PromptProps) { title: "Interrupt session", value: "session.interrupt", keybind: "session_interrupt", - disabled: status() !== "working", + disabled: status().type === "idle", category: "Session", onSelect: (dialog) => { - if (!props.sessionID) return if (autocomplete.visible) return if (!input.focused) return + // TODO: this should be its own command + if (store.mode === "shell") { + setStore("mode", "normal") + return + } + if (!props.sessionID) return setStore("interrupt", store.interrupt + 1) @@ -542,6 +539,16 @@ export function Prompt(props: PromptProps) { return } + const highlight = createMemo(() => { + if (keybind.leader) return theme.border + if (store.mode === "shell") return theme.primary + return local.agent.color(local.agent.current().name) + }) + + createEffect(() => { + renderer.setCursorColor(highlight()) + }) + return ( <> (anchor = r)}> - - - {store.mode === "normal" ? ">" : "!"} - - - +