Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2afb05fd15 | ||
|
|
c460a7d957 | ||
|
|
8a2e0a0471 | ||
|
|
5cdd764123 | ||
|
|
938a53b5a2 | ||
|
|
5e1986c0bc | ||
|
|
fc2a5b851b |
@@ -30,7 +30,7 @@ scoop bucket add extras; scoop install extras/opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install opencode # macOS and Linux
|
||||
paru -S opencode-bin # Arch Linux
|
||||
mise use -g opencode # Any OS
|
||||
mise use -g github:sst/opencode # Any OS
|
||||
nix run nixpkgs#opencode # or github:sst/opencode for latest dev branch
|
||||
```
|
||||
|
||||
|
||||
@@ -186,4 +186,3 @@
|
||||
| 2025-12-28 | 1,390,388 (+18,617) | 1,245,690 (+7,454) | 2,636,078 (+26,071) |
|
||||
| 2025-12-29 | 1,415,560 (+25,172) | 1,257,101 (+11,411) | 2,672,661 (+36,583) |
|
||||
| 2025-12-30 | 1,445,450 (+29,890) | 1,272,689 (+15,588) | 2,718,139 (+45,478) |
|
||||
| 2025-12-31 | 1,479,598 (+34,148) | 1,293,235 (+20,546) | 2,772,833 (+54,694) |
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -70,7 +70,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -98,7 +98,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -125,7 +125,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
@@ -149,7 +149,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -173,7 +173,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@solid-primitives/storage": "catalog:",
|
||||
@@ -201,7 +201,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"dependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
@@ -230,7 +230,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -246,7 +246,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -271,7 +271,6 @@
|
||||
"@ai-sdk/provider": "2.0.0",
|
||||
"@ai-sdk/provider-utils": "3.0.19",
|
||||
"@ai-sdk/togetherai": "1.0.30",
|
||||
"@ai-sdk/vercel": "1.0.31",
|
||||
"@ai-sdk/xai": "2.0.42",
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@hono/standard-validator": "0.1.5",
|
||||
@@ -348,7 +347,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
@@ -368,7 +367,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.88.1",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
@@ -379,7 +378,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -392,7 +391,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -430,7 +429,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"dependencies": {
|
||||
"zod": "catalog:",
|
||||
},
|
||||
@@ -441,7 +440,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -572,8 +571,6 @@
|
||||
|
||||
"@ai-sdk/togetherai": ["@ai-sdk/togetherai@1.0.30", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.29", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9bxQbIXnWSN4bNismrza3NvIo+ui/Y3pj3UN6e9vCszCWFCN45RgISi4oDe10RqmzaJ/X8cfO/Tem+K8MT3wGQ=="],
|
||||
|
||||
"@ai-sdk/vercel": ["@ai-sdk/vercel@1.0.31", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ggvwAMt/KsbqcdR6ILQrjwrRONLV/8aG6rOLbjcOGvV0Ai+WdZRRKQj5nOeQ06PvwVQtKdkp7S4IinpXIhCiHg=="],
|
||||
|
||||
"@ai-sdk/xai": ["@ai-sdk/xai@2.0.42", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.29", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-wlwO4yRoZ/d+ca29vN8SDzxus7POdnL7GBTyRdSrt6icUF0hooLesauC8qRUC4aLxtqvMEc1YHtJOU7ZnLWbTQ=="],
|
||||
|
||||
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||
@@ -3920,12 +3917,6 @@
|
||||
|
||||
"@ai-sdk/togetherai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
|
||||
|
||||
"@ai-sdk/vercel/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="],
|
||||
|
||||
"@ai-sdk/vercel/@ai-sdk/provider": ["@ai-sdk/provider@2.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng=="],
|
||||
|
||||
"@ai-sdk/vercel/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
|
||||
|
||||
"@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
|
||||
|
||||
"@astrojs/cloudflare/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"nodeModules": "sha256-7zMUWgMCnoe2As8WdEKazkKiGEcUIk5rP4zFvX9USgA="
|
||||
"nodeModules": "sha256-2Wbnxy9SPcZkO03Sis3uiypPXa87jc5TzKbo6PvMlxY="
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { Session } from "@opencode-ai/sdk/v2/client"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { Mark } from "@opencode-ai/ui/logo"
|
||||
import { Popover } from "@opencode-ai/ui/popover"
|
||||
import { Select } from "@opencode-ai/ui/select"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { base64Decode } from "@opencode-ai/util/encode"
|
||||
import { useCommand } from "@/context/command"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { A, useParams } from "@solidjs/router"
|
||||
import { createMemo, createResource, Show } from "solid-js"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { iife } from "@opencode-ai/util/iife"
|
||||
|
||||
export function Header(props: {
|
||||
navigateToProject: (directory: string) => void
|
||||
navigateToSession: (session: Session | undefined) => void
|
||||
onMobileMenuToggle?: () => void
|
||||
}) {
|
||||
const globalSync = useGlobalSync()
|
||||
const globalSDK = useGlobalSDK()
|
||||
const layout = useLayout()
|
||||
const params = useParams()
|
||||
const command = useCommand()
|
||||
|
||||
return (
|
||||
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
|
||||
<button
|
||||
type="button"
|
||||
class="xl:hidden w-12 shrink-0 flex items-center justify-center border-r border-border-weak-base hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active transition-colors"
|
||||
onClick={props.onMobileMenuToggle}
|
||||
>
|
||||
<Icon name="menu" size="small" />
|
||||
</button>
|
||||
<A
|
||||
href="/"
|
||||
classList={{
|
||||
"hidden xl:flex": true,
|
||||
"w-12 shrink-0 px-4 py-3.5": true,
|
||||
"items-center justify-start self-stretch": true,
|
||||
"border-r border-border-weak-base": true,
|
||||
}}
|
||||
style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<Mark class="shrink-0" />
|
||||
</A>
|
||||
<div class="pl-4 px-6 flex items-center justify-between gap-4 w-full">
|
||||
<Show when={layout.projects.list().length > 0 && params.dir}>
|
||||
{(directory) => {
|
||||
const currentDirectory = createMemo(() => base64Decode(directory()))
|
||||
const store = createMemo(() => globalSync.child(currentDirectory())[0])
|
||||
const sessions = createMemo(() => (store().session ?? []).filter((s) => !s.parentID))
|
||||
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
|
||||
const shareEnabled = createMemo(() => store().config.share !== "disabled")
|
||||
return (
|
||||
<>
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<div class="hidden xl:flex items-center gap-2">
|
||||
<Select
|
||||
options={layout.projects.list().map((project) => project.worktree)}
|
||||
current={currentDirectory()}
|
||||
label={(x) => getFilename(x)}
|
||||
onSelect={(x) => (x ? props.navigateToProject(x) : undefined)}
|
||||
class="text-14-regular text-text-base"
|
||||
variant="ghost"
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
{(i) => (
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name="folder" size="small" />
|
||||
<div class="text-text-strong">{getFilename(i)}</div>
|
||||
</div>
|
||||
)}
|
||||
</Select>
|
||||
<div class="text-text-weaker">/</div>
|
||||
</div>
|
||||
<Select
|
||||
options={sessions()}
|
||||
current={currentSession()}
|
||||
placeholder="New session"
|
||||
label={(x) => x.title}
|
||||
value={(x) => x.id}
|
||||
onSelect={props.navigateToSession}
|
||||
class="text-14-regular text-text-base max-w-[calc(100vw-180px)] md:max-w-md"
|
||||
variant="ghost"
|
||||
/>
|
||||
</div>
|
||||
<Show when={currentSession()}>
|
||||
<Tooltip
|
||||
class="hidden xl:block"
|
||||
value={
|
||||
<div class="flex items-center gap-2">
|
||||
<span>New session</span>
|
||||
<span class="text-icon-base text-12-medium">{command.keybind("session.new")}</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button as={A} href={`/${params.dir}/session`} icon="plus-small">
|
||||
New session
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<Show when={currentSession()?.summary?.files}>
|
||||
<Tooltip
|
||||
class="hidden md:block shrink-0"
|
||||
value={
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Toggle review</span>
|
||||
<span class="text-icon-base text-12-medium">{command.keybind("review.toggle")}</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button variant="ghost" class="group/review-toggle size-6 p-0" onClick={layout.review.toggle}>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
name={layout.review.opened() ? "layout-right" : "layout-left"}
|
||||
size="small"
|
||||
class="group-hover/review-toggle:hidden"
|
||||
/>
|
||||
<Icon
|
||||
name={layout.review.opened() ? "layout-right-partial" : "layout-left-partial"}
|
||||
size="small"
|
||||
class="hidden group-hover/review-toggle:inline-block"
|
||||
/>
|
||||
<Icon
|
||||
name={layout.review.opened() ? "layout-right-full" : "layout-left-full"}
|
||||
size="small"
|
||||
class="hidden group-active/review-toggle:inline-block"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
<Tooltip
|
||||
class="hidden md:block shrink-0"
|
||||
value={
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Toggle terminal</span>
|
||||
<span class="text-icon-base text-12-medium">{command.keybind("terminal.toggle")}</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
size="small"
|
||||
name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
|
||||
class="group-hover/terminal-toggle:hidden"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name="layout-bottom-partial"
|
||||
class="hidden group-hover/terminal-toggle:inline-block"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
|
||||
class="hidden group-active/terminal-toggle:inline-block"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Show when={shareEnabled() && currentSession()}>
|
||||
<Popover
|
||||
title="Share session"
|
||||
trigger={
|
||||
<Tooltip class="shrink-0" value="Share session">
|
||||
<IconButton icon="share" variant="ghost" class="" />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{iife(() => {
|
||||
const [url] = createResource(
|
||||
() => currentSession(),
|
||||
async (session) => {
|
||||
if (!session) return
|
||||
let shareURL = session.share?.url
|
||||
if (!shareURL) {
|
||||
shareURL = await globalSDK.client.session
|
||||
.share({ sessionID: session.id, directory: currentDirectory() })
|
||||
.then((r) => r.data?.share?.url)
|
||||
.catch((e) => {
|
||||
console.error("Failed to share session", e)
|
||||
return undefined
|
||||
})
|
||||
}
|
||||
return shareURL
|
||||
},
|
||||
)
|
||||
return (
|
||||
<Show when={url()}>
|
||||
{(url) => <TextField value={url()} readOnly copyable class="w-72" />}
|
||||
</Show>
|
||||
)
|
||||
})}
|
||||
</Popover>
|
||||
</Show>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</Show>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -1126,7 +1126,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
providerID: currentModel.provider.id,
|
||||
}
|
||||
const agent = currentAgent.name
|
||||
const variant = local.model.variant.current()
|
||||
|
||||
if (isShellMode) {
|
||||
sdk.client.session
|
||||
@@ -1154,7 +1153,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
arguments: args.join(" "),
|
||||
agent,
|
||||
model: `${model.providerID}/${model.modelID}`,
|
||||
variant,
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("Failed to send command", e)
|
||||
@@ -1191,7 +1189,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
model,
|
||||
messageID,
|
||||
parts: requestParts,
|
||||
variant,
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("Failed to send prompt", e)
|
||||
@@ -1378,14 +1375,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<Tooltip
|
||||
placement="top"
|
||||
value={
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Choose model</span>
|
||||
<span class="text-icon-base text-12-medium">{command.keybind("model.choose")}</span>
|
||||
</div>
|
||||
<Show when={local.model.current()?.provider.name}>
|
||||
<span class="text-text-weak">{local.model.current()?.provider.name}</span>
|
||||
</Show>
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Choose model</span>
|
||||
<span class="text-icon-base text-12-medium">{command.keybind("model.choose")}</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
@@ -1399,25 +1391,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}
|
||||
>
|
||||
{local.model.current()?.name ?? "Select model"}
|
||||
<span class="hidden md:block ml-0.5 text-text-weak text-12-regular">
|
||||
{local.model.current()?.provider.name}
|
||||
</span>
|
||||
<Icon name="chevron-down" size="small" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Show when={local.model.variant.list().length > 0}>
|
||||
<Tooltip placement="top" value="Cycle effort level">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => local.model.variant.cycle()}
|
||||
classList={{
|
||||
"text-icon-warning": !!local.model.variant.current(),
|
||||
}}
|
||||
>
|
||||
<Icon name="brain" size="small" />
|
||||
<Show when={local.model.variant.current()}>
|
||||
<span class="text-12-regular">{local.model.variant.current()}</span>
|
||||
</Show>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { createMemo, Show, type ParentProps } from "solid-js"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useServer } from "@/context/server"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { DialogSelectServer } from "@/components/dialog-select-server"
|
||||
|
||||
export function StatusBar(props: ParentProps) {
|
||||
const dialog = useDialog()
|
||||
const server = useServer()
|
||||
const sync = useSync()
|
||||
const globalSync = useGlobalSync()
|
||||
|
||||
const directoryDisplay = createMemo(() => {
|
||||
const directory = sync.data.path.directory || ""
|
||||
const home = globalSync.data.path.home || ""
|
||||
const short = home && directory.startsWith(home) ? directory.replace(home, "~") : directory
|
||||
const branch = sync.data.vcs?.branch
|
||||
return branch ? `${short}:${branch}` : short
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="h-8 w-full shrink-0 flex items-center justify-between px-2 border-t border-border-weak-base bg-background-base">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
size="small"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
dialog.show(() => <DialogSelectServer />)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
classList={{
|
||||
"size-1.5 rounded-full": true,
|
||||
"bg-icon-success-base": server.healthy() === true,
|
||||
"bg-icon-critical-base": server.healthy() === false,
|
||||
"bg-border-weak-base": server.healthy() === undefined,
|
||||
}}
|
||||
/>
|
||||
|
||||
<span class="text-12-regular text-text-weak">{server.name}</span>
|
||||
</Button>
|
||||
</div>
|
||||
<Show when={directoryDisplay()}>
|
||||
<span class="text-12-regular text-text-weak">{directoryDisplay()}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="flex items-center">{props.children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -30,8 +30,6 @@ type SessionTabs = {
|
||||
|
||||
export type LocalProject = Partial<Project> & { worktree: string; expanded: boolean }
|
||||
|
||||
export type ReviewDiffStyle = "unified" | "split"
|
||||
|
||||
export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
|
||||
name: "Layout",
|
||||
init: () => {
|
||||
@@ -51,7 +49,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
},
|
||||
review: {
|
||||
opened: true,
|
||||
diffStyle: "split" as ReviewDiffStyle,
|
||||
},
|
||||
session: {
|
||||
width: 600,
|
||||
@@ -159,14 +156,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
},
|
||||
review: {
|
||||
opened: createMemo(() => store.review?.opened ?? true),
|
||||
diffStyle: createMemo(() => store.review?.diffStyle ?? "split"),
|
||||
setDiffStyle(diffStyle: ReviewDiffStyle) {
|
||||
if (!store.review) {
|
||||
setStore("review", { opened: true, diffStyle })
|
||||
return
|
||||
}
|
||||
setStore("review", "diffStyle", diffStyle)
|
||||
},
|
||||
open() {
|
||||
setStore("review", "opened", true)
|
||||
},
|
||||
|
||||
@@ -115,11 +115,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
createStore<{
|
||||
user: (ModelKey & { visibility: "show" | "hide"; favorite?: boolean })[]
|
||||
recent: ModelKey[]
|
||||
variant?: Record<string, string | undefined>
|
||||
}>({
|
||||
user: [],
|
||||
recent: [],
|
||||
variant: {},
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -274,45 +272,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
setVisibility(model: ModelKey, visible: boolean) {
|
||||
updateVisibility(model, visible ? "show" : "hide")
|
||||
},
|
||||
variant: {
|
||||
current() {
|
||||
const m = current()
|
||||
if (!m) return undefined
|
||||
const key = `${m.provider.id}/${m.id}`
|
||||
return store.variant?.[key]
|
||||
},
|
||||
list() {
|
||||
const m = current()
|
||||
if (!m) return []
|
||||
if (!m.variants) return []
|
||||
return Object.keys(m.variants)
|
||||
},
|
||||
set(value: string | undefined) {
|
||||
const m = current()
|
||||
if (!m) return
|
||||
const key = `${m.provider.id}/${m.id}`
|
||||
if (!store.variant) {
|
||||
setStore("variant", { [key]: value })
|
||||
} else {
|
||||
setStore("variant", key, value)
|
||||
}
|
||||
},
|
||||
cycle() {
|
||||
const variants = this.list()
|
||||
if (variants.length === 0) return
|
||||
const currentVariant = this.current()
|
||||
if (!currentVariant) {
|
||||
this.set(variants[0])
|
||||
return
|
||||
}
|
||||
const index = variants.indexOf(currentVariant)
|
||||
if (index === -1 || index === variants.length - 1) {
|
||||
this.set(undefined)
|
||||
return
|
||||
}
|
||||
this.set(variants[index + 1])
|
||||
},
|
||||
},
|
||||
}
|
||||
})()
|
||||
|
||||
|
||||
@@ -117,12 +117,10 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
|
||||
const origin = createMemo(() => projectsKey(active()))
|
||||
const projectsList = createMemo(() => store.projects[origin()] ?? [])
|
||||
const isLocal = createMemo(() => origin() === "local")
|
||||
|
||||
return {
|
||||
ready: isReady,
|
||||
healthy,
|
||||
isLocal,
|
||||
get url() {
|
||||
return active()
|
||||
},
|
||||
|
||||
@@ -63,7 +63,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
async sync(sessionID: string, _isRetry = false) {
|
||||
const [session, messages, todo, diff] = await Promise.all([
|
||||
retry(() => sdk.client.session.get({ sessionID })),
|
||||
retry(() => sdk.client.session.messages({ sessionID, limit: 1000 })),
|
||||
retry(() => sdk.client.session.messages({ sessionID, limit: 100 })),
|
||||
retry(() => sdk.client.session.todo({ sessionID })),
|
||||
retry(() => sdk.client.session.diff({ sessionID })),
|
||||
])
|
||||
|
||||
@@ -2,7 +2,6 @@ import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { Logo } from "@opencode-ai/ui/logo"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Component, Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
|
||||
@@ -182,25 +181,6 @@ interface ErrorPageProps {
|
||||
|
||||
export const ErrorPage: Component<ErrorPageProps> = (props) => {
|
||||
const platform = usePlatform()
|
||||
const [store, setStore] = createStore({
|
||||
checking: false,
|
||||
version: undefined as string | undefined,
|
||||
})
|
||||
|
||||
async function checkForUpdates() {
|
||||
if (!platform.checkUpdate) return
|
||||
setStore("checking", true)
|
||||
const result = await platform.checkUpdate()
|
||||
setStore("checking", false)
|
||||
if (result.updateAvailable && result.version) setStore("version", result.version)
|
||||
}
|
||||
|
||||
async function installUpdate() {
|
||||
if (!platform.update || !platform.restart) return
|
||||
await platform.update()
|
||||
await platform.restart()
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="relative flex-1 h-screen w-screen min-h-0 flex flex-col items-center justify-center bg-background-base font-sans">
|
||||
<div class="w-2/3 max-w-3xl flex flex-col items-center justify-center gap-8">
|
||||
@@ -218,25 +198,9 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
|
||||
label="Error Details"
|
||||
hideLabel
|
||||
/>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button size="large" onClick={platform.restart}>
|
||||
Restart
|
||||
</Button>
|
||||
<Show when={platform.checkUpdate}>
|
||||
<Show
|
||||
when={store.version}
|
||||
fallback={
|
||||
<Button size="large" variant="ghost" onClick={checkForUpdates} disabled={store.checking}>
|
||||
{store.checking ? "Checking..." : "Check for updates"}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Button size="large" onClick={installUpdate}>
|
||||
Update to {store.version}
|
||||
</Button>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
<Button size="large" onClick={platform.restart}>
|
||||
Restart
|
||||
</Button>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
Please report this error to the OpenCode team
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function Home() {
|
||||
}
|
||||
}
|
||||
|
||||
if (platform.openDirectoryPickerDialog && server.isLocal()) {
|
||||
if (platform.openDirectoryPickerDialog) {
|
||||
const result = await platform.openDirectoryPickerDialog?.({
|
||||
title: "Open project",
|
||||
multiple: true,
|
||||
|
||||
@@ -26,7 +26,6 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { Collapsible } from "@opencode-ai/ui/collapsible"
|
||||
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||
import { Mark } from "@opencode-ai/ui/logo"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||
import { Session } from "@opencode-ai/sdk/v2/client"
|
||||
@@ -46,7 +45,7 @@ import { showToast, Toast, toaster } from "@opencode-ai/ui/toast"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useNotification } from "@/context/notification"
|
||||
import { Binary } from "@opencode-ai/util/binary"
|
||||
|
||||
import { Header } from "@/components/header"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
|
||||
import { DialogSelectProvider } from "@/components/dialog-select-provider"
|
||||
@@ -55,7 +54,6 @@ import { DialogSelectServer } from "@/components/dialog-select-server"
|
||||
import { useCommand, type CommandOption } from "@/context/command"
|
||||
import { ConstrainDragXAxis } from "@/utils/solid-dnd"
|
||||
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
|
||||
import { useServer } from "@/context/server"
|
||||
|
||||
export default function Layout(props: ParentProps) {
|
||||
const [store, setStore] = createStore({
|
||||
@@ -90,7 +88,6 @@ export default function Layout(props: ParentProps) {
|
||||
const globalSync = useGlobalSync()
|
||||
const layout = useLayout()
|
||||
const platform = usePlatform()
|
||||
const server = useServer()
|
||||
const notification = useNotification()
|
||||
const navigate = useNavigate()
|
||||
const providers = useProviders()
|
||||
@@ -479,7 +476,7 @@ export default function Layout(props: ParentProps) {
|
||||
}
|
||||
}
|
||||
|
||||
if (platform.openDirectoryPickerDialog && server.isLocal()) {
|
||||
if (platform.openDirectoryPickerDialog) {
|
||||
const result = await platform.openDirectoryPickerDialog?.({
|
||||
title: "Open project",
|
||||
multiple: true,
|
||||
@@ -874,11 +871,6 @@ export default function Layout(props: ParentProps) {
|
||||
return (
|
||||
<>
|
||||
<div class="flex flex-col items-start self-stretch gap-4 p-2 min-h-0 overflow-hidden">
|
||||
<Show when={!sidebarProps.mobile}>
|
||||
<A href="/" class="shrink-0 h-8 flex items-center justify-start px-2" data-tauri-drag-region>
|
||||
<Mark class="shrink-0" />
|
||||
</A>
|
||||
</Show>
|
||||
<Show when={!sidebarProps.mobile}>
|
||||
<Tooltip
|
||||
class="shrink-0"
|
||||
@@ -1024,6 +1016,11 @@ export default function Layout(props: ParentProps) {
|
||||
|
||||
return (
|
||||
<div class="relative flex-1 min-h-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
|
||||
<Header
|
||||
navigateToProject={navigateToProject}
|
||||
navigateToSession={navigateToSession}
|
||||
onMobileMenuToggle={mobileSidebar.toggle}
|
||||
/>
|
||||
<div class="flex-1 min-h-0 flex">
|
||||
<div
|
||||
classList={{
|
||||
|
||||
@@ -51,26 +51,17 @@ import { DialogSelectFile } from "@/components/dialog-select-file"
|
||||
import { DialogSelectModel } from "@/components/dialog-select-model"
|
||||
import { DialogSelectMcp } from "@/components/dialog-select-mcp"
|
||||
import { useCommand } from "@/context/command"
|
||||
import { A, useNavigate, useParams } from "@solidjs/router"
|
||||
import { useNavigate, useParams } from "@solidjs/router"
|
||||
import { UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { usePrompt } from "@/context/prompt"
|
||||
import { extractPromptFromParts } from "@/utils/prompt"
|
||||
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
|
||||
import { StatusBar } from "@/components/status-bar"
|
||||
import { SessionMcpIndicator } from "@/components/session-mcp-indicator"
|
||||
import { SessionLspIndicator } from "@/components/session-lsp-indicator"
|
||||
import { usePermission } from "@/context/permission"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { useServer } from "@/context/server"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { DialogSelectServer } from "@/components/dialog-select-server"
|
||||
import { SessionLspIndicator } from "@/components/session-lsp-indicator"
|
||||
import { SessionMcpIndicator } from "@/components/session-mcp-indicator"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { Popover } from "@opencode-ai/ui/popover"
|
||||
import { Select } from "@opencode-ai/ui/select"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
import { iife } from "@opencode-ai/util/iife"
|
||||
import { Session } from "@opencode-ai/sdk/v2/client"
|
||||
|
||||
function same<T>(a: readonly T[], b: readonly T[]) {
|
||||
if (a === b) return true
|
||||
@@ -78,212 +69,6 @@ function same<T>(a: readonly T[], b: readonly T[]) {
|
||||
return a.every((x, i) => x === b[i])
|
||||
}
|
||||
|
||||
function Header(props: { onMobileMenuToggle?: () => void }) {
|
||||
const globalSDK = useGlobalSDK()
|
||||
const layout = useLayout()
|
||||
const params = useParams()
|
||||
const navigate = useNavigate()
|
||||
const command = useCommand()
|
||||
const server = useServer()
|
||||
const dialog = useDialog()
|
||||
const sync = useSync()
|
||||
|
||||
const sessions = createMemo(() => (sync.data.session ?? []).filter((s) => !s.parentID))
|
||||
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
|
||||
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
|
||||
const branch = createMemo(() => sync.data.vcs?.branch)
|
||||
|
||||
function navigateToProject(directory: string) {
|
||||
navigate(`/${base64Encode(directory)}`)
|
||||
}
|
||||
|
||||
function navigateToSession(session: Session | undefined) {
|
||||
if (!session) return
|
||||
navigate(`/${params.dir}/session/${session.id}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
|
||||
<button
|
||||
type="button"
|
||||
class="xl:hidden w-12 shrink-0 flex items-center justify-center border-r border-border-weak-base hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active transition-colors"
|
||||
onClick={props.onMobileMenuToggle}
|
||||
>
|
||||
<Icon name="menu" size="small" />
|
||||
</button>
|
||||
<div class="px-4 flex items-center justify-between gap-4 w-full">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<div class="hidden xl:flex items-center gap-2">
|
||||
<Select
|
||||
options={layout.projects.list().map((project) => project.worktree)}
|
||||
current={sync.directory}
|
||||
label={(x) => {
|
||||
const name = getFilename(x)
|
||||
const b = x === sync.directory ? branch() : undefined
|
||||
return b ? `${name}:${b}` : name
|
||||
}}
|
||||
onSelect={(x) => (x ? navigateToProject(x) : undefined)}
|
||||
class="text-14-regular text-text-base"
|
||||
variant="ghost"
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
{(i) => (
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name="folder" size="small" />
|
||||
<div class="text-text-strong">{getFilename(i)}</div>
|
||||
</div>
|
||||
)}
|
||||
</Select>
|
||||
<div class="text-text-weaker">/</div>
|
||||
</div>
|
||||
<Select
|
||||
options={sessions()}
|
||||
current={currentSession()}
|
||||
placeholder="New session"
|
||||
label={(x) => x.title}
|
||||
value={(x) => x.id}
|
||||
onSelect={navigateToSession}
|
||||
class="text-14-regular text-text-base max-w-[calc(100vw-180px)] md:max-w-md"
|
||||
variant="ghost"
|
||||
/>
|
||||
</div>
|
||||
<Show when={currentSession()}>
|
||||
<Tooltip
|
||||
class="hidden xl:block"
|
||||
value={
|
||||
<div class="flex items-center gap-2">
|
||||
<span>New session</span>
|
||||
<span class="text-icon-base text-12-medium">{command.keybind("session.new")}</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<IconButton as={A} href={`/${params.dir}/session`} icon="edit-small-2" variant="ghost" />
|
||||
</Tooltip>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="hidden md:flex items-center gap-1">
|
||||
<Button
|
||||
size="small"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
dialog.show(() => <DialogSelectServer />)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
classList={{
|
||||
"size-1.5 rounded-full": true,
|
||||
"bg-icon-success-base": server.healthy() === true,
|
||||
"bg-icon-critical-base": server.healthy() === false,
|
||||
"bg-border-weak-base": server.healthy() === undefined,
|
||||
}}
|
||||
/>
|
||||
<Icon name="server" size="small" class="text-icon-weak" />
|
||||
<span class="text-12-regular text-text-weak truncate max-w-[200px]">{server.name}</span>
|
||||
</Button>
|
||||
<SessionLspIndicator />
|
||||
<SessionMcpIndicator />
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Show when={currentSession()?.summary?.files}>
|
||||
<Tooltip
|
||||
class="hidden md:block shrink-0"
|
||||
value={
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Toggle review</span>
|
||||
<span class="text-icon-base text-12-medium">{command.keybind("review.toggle")}</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button variant="ghost" class="group/review-toggle size-6 p-0" onClick={layout.review.toggle}>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
name={layout.review.opened() ? "layout-right" : "layout-left"}
|
||||
size="small"
|
||||
class="group-hover/review-toggle:hidden"
|
||||
/>
|
||||
<Icon
|
||||
name={layout.review.opened() ? "layout-right-partial" : "layout-left-partial"}
|
||||
size="small"
|
||||
class="hidden group-hover/review-toggle:inline-block"
|
||||
/>
|
||||
<Icon
|
||||
name={layout.review.opened() ? "layout-right-full" : "layout-left-full"}
|
||||
size="small"
|
||||
class="hidden group-active/review-toggle:inline-block"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
<Tooltip
|
||||
class="hidden md:block shrink-0"
|
||||
value={
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Toggle terminal</span>
|
||||
<span class="text-icon-base text-12-medium">{command.keybind("terminal.toggle")}</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
size="small"
|
||||
name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
|
||||
class="group-hover/terminal-toggle:hidden"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name="layout-bottom-partial"
|
||||
class="hidden group-hover/terminal-toggle:inline-block"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
|
||||
class="hidden group-active/terminal-toggle:inline-block"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Show when={shareEnabled() && currentSession()}>
|
||||
<Popover
|
||||
title="Share session"
|
||||
trigger={
|
||||
<Tooltip class="shrink-0" value="Share session">
|
||||
<IconButton icon="share" variant="ghost" class="" />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{iife(() => {
|
||||
const [url] = createResource(
|
||||
() => currentSession(),
|
||||
async (session) => {
|
||||
if (!session) return
|
||||
let shareURL = session.share?.url
|
||||
if (!shareURL) {
|
||||
shareURL = await globalSDK.client.session
|
||||
.share({ sessionID: session.id, directory: sync.directory })
|
||||
.then((r) => r.data?.share?.url)
|
||||
.catch((e) => {
|
||||
console.error("Failed to share session", e)
|
||||
return undefined
|
||||
})
|
||||
}
|
||||
return shareURL
|
||||
},
|
||||
)
|
||||
return <Show when={url()}>{(url) => <TextField value={url()} readOnly copyable class="w-72" />}</Show>
|
||||
})}
|
||||
</Popover>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const layout = useLayout()
|
||||
const local = useLocal()
|
||||
@@ -933,7 +718,6 @@ export default function Page() {
|
||||
|
||||
return (
|
||||
<div class="relative bg-background-base size-full overflow-hidden flex flex-col">
|
||||
<Header />
|
||||
<div class="md:hidden flex-1 min-h-0 flex flex-col bg-background-stronger">
|
||||
<Switch>
|
||||
<Match when={!params.id}>
|
||||
@@ -958,8 +742,6 @@ export default function Page() {
|
||||
<div class="relative h-full mt-6 overflow-y-auto no-scrollbar">
|
||||
<SessionReview
|
||||
diffs={diffs()}
|
||||
diffStyle={layout.review.diffStyle()}
|
||||
onDiffStyleChange={layout.review.setDiffStyle}
|
||||
classes={{
|
||||
root: "pb-32",
|
||||
header: "px-4",
|
||||
@@ -1085,8 +867,7 @@ export default function Page() {
|
||||
container: "px-6",
|
||||
}}
|
||||
diffs={diffs()}
|
||||
diffStyle={layout.review.diffStyle()}
|
||||
onDiffStyleChange={layout.review.setDiffStyle}
|
||||
split
|
||||
/>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
@@ -1218,6 +999,10 @@ export default function Page() {
|
||||
</DragDropProvider>
|
||||
</div>
|
||||
</Show>
|
||||
<StatusBar>
|
||||
<SessionLspIndicator />
|
||||
<SessionMcpIndicator />
|
||||
</StatusBar>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { json, action, useParams, createAsync, useSubmission } from "@solidjs/router"
|
||||
import { createEffect, Show, createMemo } from "solid-js"
|
||||
import { createEffect, Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { withActor } from "~/context/auth.withActor"
|
||||
import { Billing } from "@opencode-ai/console-core/billing.js"
|
||||
@@ -68,12 +68,6 @@ export function ReloadSection() {
|
||||
reloadTrigger: "",
|
||||
})
|
||||
|
||||
const processingFee = createMemo(() => {
|
||||
const reloadAmount = billingInfo()?.reloadAmount
|
||||
if (!reloadAmount) return "0.00"
|
||||
return (((reloadAmount + 0.3) / 0.956) * 0.044 + 0.3).toFixed(2)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!setReloadSubmission.pending && setReloadSubmission.result && !(setReloadSubmission.result as any).error) {
|
||||
setStore("show", false)
|
||||
@@ -110,8 +104,8 @@ export function ReloadSection() {
|
||||
}
|
||||
>
|
||||
<p>
|
||||
Auto reload is <b>enabled</b>. We'll reload <b>${billingInfo()?.reloadAmount}</b> (+${processingFee()}{" "}
|
||||
processing fee) when balance reaches <b>${billingInfo()?.reloadTrigger}</b>.
|
||||
Auto reload is <b>enabled</b>. We'll reload <b>${billingInfo()?.reloadAmount}</b> (+$1.23 processing fee)
|
||||
when balance reaches <b>${billingInfo()?.reloadTrigger}</b>.
|
||||
</p>
|
||||
</Show>
|
||||
<button data-color="primary" type="button" onClick={() => show()}>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo -b",
|
||||
|
||||
@@ -107,10 +107,7 @@ const platform: Platform = {
|
||||
|
||||
await Promise.resolve()
|
||||
.then(() => {
|
||||
const notification = new Notification(title, {
|
||||
body: description ?? "",
|
||||
icon: "https://opencode.ai/favicon-96x96.png",
|
||||
})
|
||||
const notification = new Notification(title, { body: description ?? "" })
|
||||
notification.onclick = () => {
|
||||
const win = getCurrentWindow()
|
||||
void win.show().catch(() => undefined)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -33,7 +33,7 @@ const ClientOnlyCode = clientOnly(() => import("@opencode-ai/ui/code").then((m)
|
||||
const ClientOnlyWorkerPoolProvider = clientOnly(() =>
|
||||
import("@opencode-ai/ui/pierre/worker").then((m) => ({
|
||||
default: (props: { children: any }) => (
|
||||
<WorkerPoolProvider pools={m.getWorkerPools()}>{props.children}</WorkerPoolProvider>
|
||||
<WorkerPoolProvider pool={m.workerPool}>{props.children}</WorkerPoolProvider>
|
||||
),
|
||||
})),
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The open source coding agent."
|
||||
version = "1.0.220"
|
||||
version = "1.0.217"
|
||||
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.220/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.217/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.220/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.217/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.220/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.217/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.220/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.217/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.220/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.217/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
@@ -66,7 +66,6 @@
|
||||
"@ai-sdk/provider": "2.0.0",
|
||||
"@ai-sdk/provider-utils": "3.0.19",
|
||||
"@ai-sdk/togetherai": "1.0.30",
|
||||
"@ai-sdk/vercel": "1.0.31",
|
||||
"@ai-sdk/xai": "2.0.42",
|
||||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@hono/standard-validator": "0.1.5",
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { EOL } from "os"
|
||||
import { basename } from "path"
|
||||
import { Agent } from "../../../agent/agent"
|
||||
import { Provider } from "../../../provider/provider"
|
||||
import { ToolRegistry } from "../../../tool/registry"
|
||||
import { Wildcard } from "../../../util/wildcard"
|
||||
import { bootstrap } from "../../bootstrap"
|
||||
import { cmd } from "../cmd"
|
||||
|
||||
export const AgentCommand = cmd({
|
||||
command: "agent <name>",
|
||||
builder: (yargs) =>
|
||||
yargs.positional("name", {
|
||||
type: "string",
|
||||
demandOption: true,
|
||||
description: "Agent name",
|
||||
}),
|
||||
async handler(args) {
|
||||
await bootstrap(process.cwd(), async () => {
|
||||
const agentName = args.name as string
|
||||
const agent = await Agent.get(agentName)
|
||||
if (!agent) {
|
||||
process.stderr.write(
|
||||
`Agent ${agentName} not found, run '${basename(process.execPath)} agent list' to get an agent list` + EOL,
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
const resolvedTools = await resolveTools(agent)
|
||||
const output = {
|
||||
...agent,
|
||||
tools: resolvedTools,
|
||||
toolOverrides: agent.tools,
|
||||
}
|
||||
process.stdout.write(JSON.stringify(output, null, 2) + EOL)
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
async function resolveTools(agent: Agent.Info) {
|
||||
const providerID = agent.model?.providerID ?? (await Provider.defaultModel()).providerID
|
||||
const toolOverrides = {
|
||||
...agent.tools,
|
||||
...(await ToolRegistry.enabled(agent)),
|
||||
}
|
||||
const availableTools = await ToolRegistry.tools(providerID, agent)
|
||||
const resolved: Record<string, boolean> = {}
|
||||
for (const tool of availableTools) {
|
||||
resolved[tool.id] = Wildcard.all(tool.id, toolOverrides) !== false
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import { RipgrepCommand } from "./ripgrep"
|
||||
import { ScrapCommand } from "./scrap"
|
||||
import { SkillCommand } from "./skill"
|
||||
import { SnapshotCommand } from "./snapshot"
|
||||
import { AgentCommand } from "./agent"
|
||||
|
||||
export const DebugCommand = cmd({
|
||||
command: "debug",
|
||||
@@ -21,7 +20,6 @@ export const DebugCommand = cmd({
|
||||
.command(ScrapCommand)
|
||||
.command(SkillCommand)
|
||||
.command(SnapshotCommand)
|
||||
.command(AgentCommand)
|
||||
.command(PathsCommand)
|
||||
.command({
|
||||
command: "wait",
|
||||
|
||||
@@ -20,17 +20,6 @@ interface SessionStats {
|
||||
}
|
||||
}
|
||||
toolUsage: Record<string, number>
|
||||
modelUsage: Record<
|
||||
string,
|
||||
{
|
||||
messages: number
|
||||
tokens: {
|
||||
input: number
|
||||
output: number
|
||||
}
|
||||
cost: number
|
||||
}
|
||||
>
|
||||
dateRange: {
|
||||
earliest: number
|
||||
latest: number
|
||||
@@ -54,9 +43,6 @@ export const StatsCommand = cmd({
|
||||
describe: "number of tools to show (default: all)",
|
||||
type: "number",
|
||||
})
|
||||
.option("models", {
|
||||
describe: "show model statistics (default: hidden). Pass a number to show top N, otherwise shows all",
|
||||
})
|
||||
.option("project", {
|
||||
describe: "filter by project (default: all projects, empty string: current project)",
|
||||
type: "string",
|
||||
@@ -65,15 +51,7 @@ export const StatsCommand = cmd({
|
||||
handler: async (args) => {
|
||||
await bootstrap(process.cwd(), async () => {
|
||||
const stats = await aggregateSessionStats(args.days, args.project)
|
||||
|
||||
let modelLimit: number | undefined
|
||||
if (args.models === true) {
|
||||
modelLimit = Infinity
|
||||
} else if (typeof args.models === "number") {
|
||||
modelLimit = args.models
|
||||
}
|
||||
|
||||
displayStats(stats, args.tools, modelLimit)
|
||||
displayStats(stats, args.tools)
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -143,7 +121,6 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
|
||||
},
|
||||
},
|
||||
toolUsage: {},
|
||||
modelUsage: {},
|
||||
dateRange: {
|
||||
earliest: Date.now(),
|
||||
latest: Date.now(),
|
||||
@@ -177,43 +154,17 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
|
||||
let sessionCost = 0
|
||||
let sessionTokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }
|
||||
let sessionToolUsage: Record<string, number> = {}
|
||||
let sessionModelUsage: Record<
|
||||
string,
|
||||
{
|
||||
messages: number
|
||||
tokens: {
|
||||
input: number
|
||||
output: number
|
||||
}
|
||||
cost: number
|
||||
}
|
||||
> = {}
|
||||
|
||||
for (const message of messages) {
|
||||
if (message.info.role === "assistant") {
|
||||
sessionCost += message.info.cost || 0
|
||||
|
||||
const modelKey = `${message.info.providerID}/${message.info.modelID}`
|
||||
if (!sessionModelUsage[modelKey]) {
|
||||
sessionModelUsage[modelKey] = {
|
||||
messages: 0,
|
||||
tokens: { input: 0, output: 0 },
|
||||
cost: 0,
|
||||
}
|
||||
}
|
||||
sessionModelUsage[modelKey].messages++
|
||||
sessionModelUsage[modelKey].cost += message.info.cost || 0
|
||||
|
||||
if (message.info.tokens) {
|
||||
sessionTokens.input += message.info.tokens.input || 0
|
||||
sessionTokens.output += message.info.tokens.output || 0
|
||||
sessionTokens.reasoning += message.info.tokens.reasoning || 0
|
||||
sessionTokens.cache.read += message.info.tokens.cache?.read || 0
|
||||
sessionTokens.cache.write += message.info.tokens.cache?.write || 0
|
||||
|
||||
sessionModelUsage[modelKey].tokens.input += message.info.tokens.input || 0
|
||||
sessionModelUsage[modelKey].tokens.output +=
|
||||
(message.info.tokens.output || 0) + (message.info.tokens.reasoning || 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +181,6 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
|
||||
sessionTokens,
|
||||
sessionTotalTokens: sessionTokens.input + sessionTokens.output + sessionTokens.reasoning,
|
||||
sessionToolUsage,
|
||||
sessionModelUsage,
|
||||
earliestTime: session.time.created,
|
||||
latestTime: session.time.updated,
|
||||
}
|
||||
@@ -254,20 +204,6 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
|
||||
for (const [tool, count] of Object.entries(result.sessionToolUsage)) {
|
||||
stats.toolUsage[tool] = (stats.toolUsage[tool] || 0) + count
|
||||
}
|
||||
|
||||
for (const [model, usage] of Object.entries(result.sessionModelUsage)) {
|
||||
if (!stats.modelUsage[model]) {
|
||||
stats.modelUsage[model] = {
|
||||
messages: 0,
|
||||
tokens: { input: 0, output: 0 },
|
||||
cost: 0,
|
||||
}
|
||||
}
|
||||
stats.modelUsage[model].messages += usage.messages
|
||||
stats.modelUsage[model].tokens.input += usage.tokens.input
|
||||
stats.modelUsage[model].tokens.output += usage.tokens.output
|
||||
stats.modelUsage[model].cost += usage.cost
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,7 +228,7 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
|
||||
return stats
|
||||
}
|
||||
|
||||
export function displayStats(stats: SessionStats, toolLimit?: number, modelLimit?: number) {
|
||||
export function displayStats(stats: SessionStats, toolLimit?: number) {
|
||||
const width = 56
|
||||
|
||||
function renderRow(label: string, value: string): string {
|
||||
@@ -331,29 +267,6 @@ export function displayStats(stats: SessionStats, toolLimit?: number, modelLimit
|
||||
console.log("└────────────────────────────────────────────────────────┘")
|
||||
console.log()
|
||||
|
||||
// Model Usage section
|
||||
if (modelLimit !== undefined && Object.keys(stats.modelUsage).length > 0) {
|
||||
const sortedModels = Object.entries(stats.modelUsage).sort(([, a], [, b]) => b.messages - a.messages)
|
||||
const modelsToDisplay = modelLimit === Infinity ? sortedModels : sortedModels.slice(0, modelLimit)
|
||||
|
||||
console.log("┌────────────────────────────────────────────────────────┐")
|
||||
console.log("│ MODEL USAGE │")
|
||||
console.log("├────────────────────────────────────────────────────────┤")
|
||||
|
||||
for (const [model, usage] of modelsToDisplay) {
|
||||
console.log(`│ ${model.padEnd(54)} │`)
|
||||
console.log(renderRow(" Messages", usage.messages.toLocaleString()))
|
||||
console.log(renderRow(" Input Tokens", formatNumber(usage.tokens.input)))
|
||||
console.log(renderRow(" Output Tokens", formatNumber(usage.tokens.output)))
|
||||
console.log(renderRow(" Cost", `$${usage.cost.toFixed(4)}`))
|
||||
console.log("├────────────────────────────────────────────────────────┤")
|
||||
}
|
||||
// Remove last separator and add bottom border
|
||||
process.stdout.write("\x1B[1A") // Move up one line
|
||||
console.log("└────────────────────────────────────────────────────────┘")
|
||||
}
|
||||
console.log()
|
||||
|
||||
// Tool Usage section
|
||||
if (Object.keys(stats.toolUsage).length > 0) {
|
||||
const sortedTools = Object.entries(stats.toolUsage).sort(([, a], [, b]) => b - a)
|
||||
|
||||
@@ -259,7 +259,7 @@ export function Autocomplete(props: {
|
||||
const s = session()
|
||||
for (const command of sync.data.command) {
|
||||
results.push({
|
||||
display: "/" + command.name + (command.mcp ? " (MCP)" : ""),
|
||||
display: "/" + command.name,
|
||||
description: command.description,
|
||||
onSelect: () => {
|
||||
const newText = "/" + command.name + " "
|
||||
|
||||
@@ -28,7 +28,7 @@ export const TIPS = [
|
||||
"Press {highlight}Ctrl+C{/highlight} when typing to clear the input field.",
|
||||
"Press {highlight}Escape{/highlight} to stop the AI mid-response.",
|
||||
"Switch to {highlight}Plan{/highlight} agent to get suggestions without making actual changes.",
|
||||
"Use {highlight}@<agent-name>{/highlight} in prompts to invoke specialized subagents.",
|
||||
"Use {highlight}@agent-name{/highlight} in prompts to invoke specialized subagents.",
|
||||
"Press {highlight}Ctrl+X Right/Left{/highlight} to cycle through parent and child sessions.",
|
||||
"Create {highlight}opencode.json{/highlight} in project root for project-specific settings.",
|
||||
"Place settings in {highlight}~/.config/opencode/opencode.json{/highlight} for global config.",
|
||||
|
||||
@@ -253,7 +253,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
const next = favorites[index]
|
||||
if (!next) return
|
||||
setModelStore("model", agent.current().name, { ...next })
|
||||
const uniq = uniqueBy([next, ...modelStore.recent], (x) => `${x.providerID}/${x.modelID}`)
|
||||
const uniq = uniqueBy([next, ...modelStore.recent], (x) => x.providerID + x.modelID)
|
||||
if (uniq.length > 10) uniq.pop()
|
||||
setModelStore(
|
||||
"recent",
|
||||
@@ -273,7 +273,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
}
|
||||
setModelStore("model", agent.current().name, model)
|
||||
if (options?.recent) {
|
||||
const uniq = uniqueBy([model, ...modelStore.recent], (x) => `${x.providerID}/${x.modelID}`)
|
||||
const uniq = uniqueBy([model, ...modelStore.recent], (x) => x.providerID + x.modelID)
|
||||
if (uniq.length > 10) uniq.pop()
|
||||
setModelStore(
|
||||
"recent",
|
||||
|
||||
@@ -1716,8 +1716,8 @@ ToolRegistry.register<typeof TaskTool>({
|
||||
</box>
|
||||
</Show>
|
||||
<text fg={theme.text}>
|
||||
{keybind.print("session_child_cycle")}
|
||||
<span style={{ fg: theme.textMuted }}> view subagents</span>
|
||||
{keybind.print("session_child_cycle")}, {keybind.print("session_child_cycle_reverse")}
|
||||
<span style={{ fg: theme.textMuted }}> to navigate between subagent sessions</span>
|
||||
</text>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -17,12 +17,6 @@ const options = {
|
||||
describe: "enable mDNS service discovery (defaults hostname to 0.0.0.0)",
|
||||
default: false,
|
||||
},
|
||||
cors: {
|
||||
type: "string" as const,
|
||||
array: true,
|
||||
describe: "additional domains to allow for CORS",
|
||||
default: [] as string[],
|
||||
},
|
||||
}
|
||||
|
||||
export type NetworkOptions = InferredOptionTypes<typeof options>
|
||||
@@ -36,7 +30,6 @@ export async function resolveNetworkOptions(args: NetworkOptions) {
|
||||
const portExplicitlySet = process.argv.includes("--port")
|
||||
const hostnameExplicitlySet = process.argv.includes("--hostname")
|
||||
const mdnsExplicitlySet = process.argv.includes("--mdns")
|
||||
const corsExplicitlySet = process.argv.includes("--cors")
|
||||
|
||||
const mdns = mdnsExplicitlySet ? args.mdns : (config?.server?.mdns ?? args.mdns)
|
||||
const port = portExplicitlySet ? args.port : (config?.server?.port ?? args.port)
|
||||
@@ -45,9 +38,6 @@ export async function resolveNetworkOptions(args: NetworkOptions) {
|
||||
: mdns && !config?.server?.hostname
|
||||
? "0.0.0.0"
|
||||
: (config?.server?.hostname ?? args.hostname)
|
||||
const configCors = config?.server?.cors ?? []
|
||||
const argsCors = Array.isArray(args.cors) ? args.cors : args.cors ? [args.cors] : []
|
||||
const cors = [...configCors, ...argsCors]
|
||||
|
||||
return { hostname, port, mdns, cors }
|
||||
return { hostname, port, mdns }
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Bus } from "@/bus"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import z from "zod"
|
||||
import { Config } from "../config/config"
|
||||
@@ -5,7 +6,6 @@ import { Instance } from "../project/instance"
|
||||
import { Identifier } from "../id/id"
|
||||
import PROMPT_INITIALIZE from "./template/initialize.txt"
|
||||
import PROMPT_REVIEW from "./template/review.txt"
|
||||
import { MCP } from "../mcp"
|
||||
|
||||
export namespace Command {
|
||||
export const Event = {
|
||||
@@ -26,29 +26,13 @@ export namespace Command {
|
||||
description: z.string().optional(),
|
||||
agent: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
mcp: z.boolean().optional(),
|
||||
// workaround for zod not supporting async functions natively so we use getters
|
||||
// https://zod.dev/v4/changelog?id=zfunction
|
||||
template: z.promise(z.string()).or(z.string()),
|
||||
template: z.string(),
|
||||
subtask: z.boolean().optional(),
|
||||
hints: z.array(z.string()),
|
||||
})
|
||||
.meta({
|
||||
ref: "Command",
|
||||
})
|
||||
|
||||
// for some reason zod is inferring `string` for z.promise(z.string()).or(z.string()) so we have to manually override it
|
||||
export type Info = Omit<z.infer<typeof Info>, "template"> & { template: Promise<string> | string }
|
||||
|
||||
export function hints(template: string): string[] {
|
||||
const result: string[] = []
|
||||
const numbered = template.match(/\$\d+/g)
|
||||
if (numbered) {
|
||||
for (const match of [...new Set(numbered)].sort()) result.push(match)
|
||||
}
|
||||
if (template.includes("$ARGUMENTS")) result.push("$ARGUMENTS")
|
||||
return result
|
||||
}
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
export const Default = {
|
||||
INIT: "init",
|
||||
@@ -62,19 +46,13 @@ export namespace Command {
|
||||
[Default.INIT]: {
|
||||
name: Default.INIT,
|
||||
description: "create/update AGENTS.md",
|
||||
get template() {
|
||||
return PROMPT_INITIALIZE.replace("${path}", Instance.worktree)
|
||||
},
|
||||
hints: hints(PROMPT_INITIALIZE),
|
||||
template: PROMPT_INITIALIZE.replace("${path}", Instance.worktree),
|
||||
},
|
||||
[Default.REVIEW]: {
|
||||
name: Default.REVIEW,
|
||||
description: "review changes [commit|branch|pr], defaults to uncommitted",
|
||||
get template() {
|
||||
return PROMPT_REVIEW.replace("${path}", Instance.worktree)
|
||||
},
|
||||
template: PROMPT_REVIEW.replace("${path}", Instance.worktree),
|
||||
subtask: true,
|
||||
hints: hints(PROMPT_REVIEW),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -84,37 +62,8 @@ export namespace Command {
|
||||
agent: command.agent,
|
||||
model: command.model,
|
||||
description: command.description,
|
||||
get template() {
|
||||
return command.template
|
||||
},
|
||||
template: command.template,
|
||||
subtask: command.subtask,
|
||||
hints: hints(command.template),
|
||||
}
|
||||
}
|
||||
for (const [name, prompt] of Object.entries(await MCP.prompts())) {
|
||||
result[name] = {
|
||||
name,
|
||||
mcp: true,
|
||||
description: prompt.description,
|
||||
get template() {
|
||||
// since a getter can't be async we need to manually return a promise here
|
||||
return new Promise<string>(async (resolve, reject) => {
|
||||
const template = await MCP.getPrompt(
|
||||
prompt.client,
|
||||
prompt.name,
|
||||
prompt.arguments
|
||||
? // substitute each argument with $1, $2, etc.
|
||||
Object.fromEntries(prompt.arguments?.map((argument, i) => [argument.name, `$${i + 1}`]))
|
||||
: {},
|
||||
).catch(reject)
|
||||
resolve(
|
||||
template?.messages
|
||||
.map((message) => (message.content.type === "text" ? message.content.text : ""))
|
||||
.join("\n") || "",
|
||||
)
|
||||
})
|
||||
},
|
||||
hints: prompt.arguments?.map((_, i) => `$${i + 1}`) ?? [],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,6 +92,8 @@ export namespace Config {
|
||||
|
||||
const promises: Promise<void>[] = []
|
||||
for (const dir of unique(directories)) {
|
||||
await assertValid(dir)
|
||||
|
||||
if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) {
|
||||
for (const file of ["opencode.jsonc", "opencode.json"]) {
|
||||
log.debug(`loading config from ${path.join(dir, file)}`)
|
||||
@@ -153,6 +155,23 @@ export namespace Config {
|
||||
}
|
||||
})
|
||||
|
||||
const INVALID_DIRS = new Bun.Glob(`{${["agents", "commands", "plugins", "tools", "skills"].join(",")}}/`)
|
||||
async function assertValid(dir: string) {
|
||||
const invalid = await Array.fromAsync(
|
||||
INVALID_DIRS.scan({
|
||||
onlyFiles: false,
|
||||
cwd: dir,
|
||||
}),
|
||||
)
|
||||
for (const item of invalid) {
|
||||
throw new ConfigDirectoryTypoError({
|
||||
path: dir,
|
||||
dir: item,
|
||||
suggestion: item.substring(0, item.length - 1),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function installDependencies(dir: string) {
|
||||
if (Installation.isLocal()) return
|
||||
|
||||
@@ -178,7 +197,7 @@ export namespace Config {
|
||||
await BunProc.run(["install"], { cwd: dir }).catch(() => {})
|
||||
}
|
||||
|
||||
const COMMAND_GLOB = new Bun.Glob("{command,commands}/**/*.md")
|
||||
const COMMAND_GLOB = new Bun.Glob("command/**/*.md")
|
||||
async function loadCommand(dir: string) {
|
||||
const result: Record<string, Command> = {}
|
||||
for await (const item of COMMAND_GLOB.scan({
|
||||
@@ -216,7 +235,7 @@ export namespace Config {
|
||||
return result
|
||||
}
|
||||
|
||||
const AGENT_GLOB = new Bun.Glob("{agent,agents}/**/*.md")
|
||||
const AGENT_GLOB = new Bun.Glob("agent/**/*.md")
|
||||
async function loadAgent(dir: string) {
|
||||
const result: Record<string, Agent> = {}
|
||||
|
||||
@@ -259,7 +278,7 @@ export namespace Config {
|
||||
return result
|
||||
}
|
||||
|
||||
const MODE_GLOB = new Bun.Glob("{mode,modes}/*.md")
|
||||
const MODE_GLOB = new Bun.Glob("mode/*.md")
|
||||
async function loadMode(dir: string) {
|
||||
const result: Record<string, Agent> = {}
|
||||
for await (const item of MODE_GLOB.scan({
|
||||
@@ -288,7 +307,7 @@ export namespace Config {
|
||||
return result
|
||||
}
|
||||
|
||||
const PLUGIN_GLOB = new Bun.Glob("{plugin,plugins}/*.{ts,js}")
|
||||
const PLUGIN_GLOB = new Bun.Glob("plugin/*.{ts,js}")
|
||||
async function loadPlugin(dir: string) {
|
||||
const plugins: string[] = []
|
||||
|
||||
@@ -586,7 +605,6 @@ export namespace Config {
|
||||
port: z.number().int().positive().optional().describe("Port to listen on"),
|
||||
hostname: z.string().optional().describe("Hostname to listen on"),
|
||||
mdns: z.boolean().optional().describe("Enable mDNS service discovery"),
|
||||
cors: z.array(z.string()).optional().describe("Additional domains to allow for CORS"),
|
||||
})
|
||||
.strict()
|
||||
.meta({
|
||||
@@ -845,12 +863,6 @@ export namespace Config {
|
||||
.optional()
|
||||
.describe("Tools that should only be available to primary agents."),
|
||||
continue_loop_on_deny: z.boolean().optional().describe("Continue the agent loop when a tool call is denied"),
|
||||
mcp_timeout: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.describe("Timeout in milliseconds for model context protocol (MCP) requests"),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
@@ -5,6 +5,8 @@ import fs from "fs/promises"
|
||||
import z from "zod"
|
||||
import { NamedError } from "@opencode-ai/util/error"
|
||||
import { lazy } from "../util/lazy"
|
||||
import { $ } from "bun"
|
||||
|
||||
import { ZipReader, BlobReader, BlobWriter } from "@zip.js/zip.js"
|
||||
import { Log } from "@/util/log"
|
||||
|
||||
@@ -366,7 +368,7 @@ export namespace Ripgrep {
|
||||
}
|
||||
|
||||
export async function search(input: { cwd: string; pattern: string; glob?: string[]; limit?: number }) {
|
||||
const args = [await filepath(), "--json", "--hidden", "--glob=!.git/*"]
|
||||
const args = [`${await filepath()}`, "--json", "--hidden", "--glob='!.git/*'"]
|
||||
|
||||
if (input.glob) {
|
||||
for (const g of input.glob) {
|
||||
@@ -381,21 +383,15 @@ export namespace Ripgrep {
|
||||
args.push("--")
|
||||
args.push(input.pattern)
|
||||
|
||||
const proc = Bun.spawn(args, {
|
||||
cwd: input.cwd,
|
||||
stdout: "pipe",
|
||||
stderr: "ignore",
|
||||
})
|
||||
|
||||
const output = await Bun.readableStreamToText(proc.stdout)
|
||||
await proc.exited
|
||||
|
||||
if (proc.exitCode !== 0) {
|
||||
const command = args.join(" ")
|
||||
const result = await $`${{ raw: command }}`.cwd(input.cwd).quiet().nothrow()
|
||||
if (result.exitCode !== 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Handle both Unix (\n) and Windows (\r\n) line endings
|
||||
const lines = output.trim().split(/\r?\n/).filter(Boolean)
|
||||
const lines = result.text().trim().split(/\r?\n/).filter(Boolean)
|
||||
// Parse JSON lines from ripgrep output
|
||||
|
||||
return lines
|
||||
.map((line) => JSON.parse(line))
|
||||
|
||||
@@ -331,12 +331,3 @@ export const nixfmt: Info = {
|
||||
return Bun.which("nixfmt") !== null
|
||||
},
|
||||
}
|
||||
|
||||
export const rustfmt: Info = {
|
||||
name: "rustfmt",
|
||||
command: ["rustfmt", "$FILE"],
|
||||
extensions: [".rs"],
|
||||
async enabled() {
|
||||
return Bun.which("rustfmt") !== null
|
||||
},
|
||||
}
|
||||
|
||||
@@ -98,9 +98,6 @@ export namespace LSPClient {
|
||||
},
|
||||
workspace: {
|
||||
configuration: true,
|
||||
didChangeWatchedFiles: {
|
||||
dynamicRegistration: true,
|
||||
},
|
||||
},
|
||||
textDocument: {
|
||||
synchronization: {
|
||||
@@ -154,16 +151,6 @@ export namespace LSPClient {
|
||||
|
||||
const version = files[input.path]
|
||||
if (version !== undefined) {
|
||||
log.info("workspace/didChangeWatchedFiles", input)
|
||||
await connection.sendNotification("workspace/didChangeWatchedFiles", {
|
||||
changes: [
|
||||
{
|
||||
uri: pathToFileURL(input.path).href,
|
||||
type: 2, // Changed
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const next = version + 1
|
||||
files[input.path] = next
|
||||
log.info("textDocument/didChange", {
|
||||
@@ -180,16 +167,6 @@ export namespace LSPClient {
|
||||
return
|
||||
}
|
||||
|
||||
log.info("workspace/didChangeWatchedFiles", input)
|
||||
await connection.sendNotification("workspace/didChangeWatchedFiles", {
|
||||
changes: [
|
||||
{
|
||||
uri: pathToFileURL(input.path).href,
|
||||
type: 1, // Created
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
log.info("textDocument/didOpen", input)
|
||||
diagnostics.delete(input.path)
|
||||
await connection.sendNotification("textDocument/didOpen", {
|
||||
|
||||
@@ -4,11 +4,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
|
||||
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
||||
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"
|
||||
import {
|
||||
CallToolResultSchema,
|
||||
type Tool as MCPToolDef,
|
||||
ToolListChangedNotificationSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js"
|
||||
import { type Tool as MCPToolDef, ToolListChangedNotificationSchema } from "@modelcontextprotocol/sdk/types.js"
|
||||
import { Config } from "../config/config"
|
||||
import { Log } from "../util/log"
|
||||
import { NamedError } from "@opencode-ai/util/error"
|
||||
@@ -97,7 +93,7 @@ export namespace MCP {
|
||||
}
|
||||
|
||||
// Convert MCP tool definition to AI SDK Tool type
|
||||
async function convertMcpTool(mcpTool: MCPToolDef, client: MCPClient): Promise<Tool> {
|
||||
function convertMcpTool(mcpTool: MCPToolDef, client: MCPClient): Tool {
|
||||
const inputSchema = mcpTool.inputSchema
|
||||
|
||||
// Spread first, then override type to ensure it's always "object"
|
||||
@@ -107,23 +103,15 @@ export namespace MCP {
|
||||
properties: (inputSchema.properties ?? {}) as JSONSchema7["properties"],
|
||||
additionalProperties: false,
|
||||
}
|
||||
const config = await Config.get()
|
||||
|
||||
return dynamicTool({
|
||||
description: mcpTool.description ?? "",
|
||||
inputSchema: jsonSchema(schema),
|
||||
execute: async (args: unknown) => {
|
||||
return client.callTool(
|
||||
{
|
||||
name: mcpTool.name,
|
||||
arguments: args as Record<string, unknown>,
|
||||
},
|
||||
CallToolResultSchema,
|
||||
{
|
||||
resetTimeoutOnProgress: true,
|
||||
timeout: config.experimental?.mcp_timeout,
|
||||
},
|
||||
)
|
||||
return client.callTool({
|
||||
name: mcpTool.name,
|
||||
arguments: args as Record<string, unknown>,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -132,9 +120,6 @@ export namespace MCP {
|
||||
type TransportWithAuth = StreamableHTTPClientTransport | SSEClientTransport
|
||||
const pendingOAuthTransports = new Map<string, TransportWithAuth>()
|
||||
|
||||
// Prompt cache types
|
||||
type PromptInfo = Awaited<ReturnType<MCPClient["listPrompts"]>>["prompts"][number]
|
||||
|
||||
const state = Instance.state(
|
||||
async () => {
|
||||
const cfg = await Config.get()
|
||||
@@ -179,29 +164,6 @@ export namespace MCP {
|
||||
},
|
||||
)
|
||||
|
||||
// Helper function to fetch prompts for a specific client
|
||||
async function fetchPromptsForClient(clientName: string, client: Client) {
|
||||
const prompts = await client.listPrompts().catch((e) => {
|
||||
log.error("failed to get prompts", { clientName, error: e.message })
|
||||
return undefined
|
||||
})
|
||||
|
||||
if (!prompts) {
|
||||
return
|
||||
}
|
||||
|
||||
const commands: Record<string, PromptInfo & { client: string }> = {}
|
||||
|
||||
for (const prompt of prompts.prompts) {
|
||||
const sanitizedClientName = clientName.replace(/[^a-zA-Z0-9_-]/g, "_")
|
||||
const sanitizedPromptName = prompt.name.replace(/[^a-zA-Z0-9_-]/g, "_")
|
||||
const key = sanitizedClientName + ":" + sanitizedPromptName
|
||||
|
||||
commands[key] = { ...prompt, client: clientName }
|
||||
}
|
||||
return commands
|
||||
}
|
||||
|
||||
export async function add(name: string, mcp: Config.Mcp) {
|
||||
const s = await state()
|
||||
const result = await create(name, mcp)
|
||||
@@ -512,61 +474,12 @@ export namespace MCP {
|
||||
for (const mcpTool of toolsResult.tools) {
|
||||
const sanitizedClientName = clientName.replace(/[^a-zA-Z0-9_-]/g, "_")
|
||||
const sanitizedToolName = mcpTool.name.replace(/[^a-zA-Z0-9_-]/g, "_")
|
||||
result[sanitizedClientName + "_" + sanitizedToolName] = await convertMcpTool(mcpTool, client)
|
||||
result[sanitizedClientName + "_" + sanitizedToolName] = convertMcpTool(mcpTool, client)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export async function prompts() {
|
||||
const s = await state()
|
||||
const clientsSnapshot = await clients()
|
||||
|
||||
const prompts = Object.fromEntries<PromptInfo & { client: string }>(
|
||||
(
|
||||
await Promise.all(
|
||||
Object.entries(clientsSnapshot).map(async ([clientName, client]) => {
|
||||
if (s.status[clientName]?.status !== "connected") {
|
||||
return []
|
||||
}
|
||||
|
||||
return Object.entries((await fetchPromptsForClient(clientName, client)) ?? {})
|
||||
}),
|
||||
)
|
||||
).flat(),
|
||||
)
|
||||
|
||||
return prompts
|
||||
}
|
||||
|
||||
export async function getPrompt(clientName: string, name: string, args?: Record<string, string>) {
|
||||
const clientsSnapshot = await clients()
|
||||
const client = clientsSnapshot[clientName]
|
||||
|
||||
if (!client) {
|
||||
log.warn("client not found for prompt", {
|
||||
clientName,
|
||||
})
|
||||
return undefined
|
||||
}
|
||||
|
||||
const result = await client
|
||||
.getPrompt({
|
||||
name: name,
|
||||
arguments: args,
|
||||
})
|
||||
.catch((e) => {
|
||||
log.error("failed to get prompt from MCP server", {
|
||||
clientName,
|
||||
promptName: name,
|
||||
error: e.message,
|
||||
})
|
||||
return undefined
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Start OAuth authentication flow for an MCP server.
|
||||
* Returns the authorization URL that should be opened in a browser.
|
||||
|
||||
@@ -34,7 +34,6 @@ import { createCohere } from "@ai-sdk/cohere"
|
||||
import { createGateway } from "@ai-sdk/gateway"
|
||||
import { createTogetherAI } from "@ai-sdk/togetherai"
|
||||
import { createPerplexity } from "@ai-sdk/perplexity"
|
||||
import { createVercel } from "@ai-sdk/vercel"
|
||||
import { ProviderTransform } from "./transform"
|
||||
|
||||
export namespace Provider {
|
||||
@@ -59,7 +58,6 @@ export namespace Provider {
|
||||
"@ai-sdk/gateway": createGateway,
|
||||
"@ai-sdk/togetherai": createTogetherAI,
|
||||
"@ai-sdk/perplexity": createPerplexity,
|
||||
"@ai-sdk/vercel": createVercel,
|
||||
// @ts-ignore (TODO: kill this code so we dont have to maintain it)
|
||||
"@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,
|
||||
}
|
||||
@@ -1018,7 +1016,6 @@ export namespace Provider {
|
||||
"claude-haiku-4.5",
|
||||
"3-5-haiku",
|
||||
"3.5-haiku",
|
||||
"gemini-3-flash",
|
||||
"gemini-2.5-flash",
|
||||
"gpt-5-nano",
|
||||
]
|
||||
|
||||
@@ -3,7 +3,6 @@ import { unique } from "remeda"
|
||||
import type { JSONSchema } from "zod/v4/core"
|
||||
import type { Provider } from "./provider"
|
||||
import type { ModelsDev } from "./models"
|
||||
import { iife } from "@/util/iife"
|
||||
|
||||
type Modality = NonNullable<ModelsDev.Model["modalities"]>["input"][number]
|
||||
|
||||
@@ -230,6 +229,7 @@ export namespace ProviderTransform {
|
||||
const id = model.id.toLowerCase()
|
||||
if (id.includes("qwen")) return 1
|
||||
if (id.includes("minimax-m2")) {
|
||||
if (id.includes("m2.1")) return 0.9
|
||||
return 0.95
|
||||
}
|
||||
if (id.includes("gemini")) return 0.95
|
||||
@@ -238,10 +238,7 @@ export namespace ProviderTransform {
|
||||
|
||||
export function topK(model: Provider.Model) {
|
||||
const id = model.id.toLowerCase()
|
||||
if (id.includes("minimax-m2")) {
|
||||
if (id.includes("m2.1")) return 40
|
||||
return 20
|
||||
}
|
||||
if (id.includes("minimax-m2")) return 20
|
||||
if (id.includes("gemini")) return 64
|
||||
return undefined
|
||||
}
|
||||
@@ -295,17 +292,13 @@ export namespace ProviderTransform {
|
||||
case "@ai-sdk/openai":
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/openai
|
||||
if (id === "gpt-5-pro") return {}
|
||||
const openaiEfforts = iife(() => {
|
||||
if (model.id.includes("codex")) return WIDELY_SUPPORTED_EFFORTS
|
||||
const arr = ["minimal", ...WIDELY_SUPPORTED_EFFORTS]
|
||||
if (model.release_date >= "2025-11-13") {
|
||||
arr.unshift("none")
|
||||
}
|
||||
if (model.release_date >= "2025-12-04") {
|
||||
arr.push("xhigh")
|
||||
}
|
||||
return arr
|
||||
})
|
||||
const openaiEfforts = ["minimal", ...WIDELY_SUPPORTED_EFFORTS]
|
||||
if (model.release_date >= "2025-11-13") {
|
||||
openaiEfforts.unshift("none")
|
||||
}
|
||||
if (model.release_date >= "2025-12-04") {
|
||||
openaiEfforts.push("xhigh")
|
||||
}
|
||||
return Object.fromEntries(
|
||||
openaiEfforts.map((effort) => [
|
||||
effort,
|
||||
|
||||
@@ -45,6 +45,7 @@ import { Snapshot } from "@/snapshot"
|
||||
import { SessionSummary } from "@/session/summary"
|
||||
import { SessionStatus } from "@/session/status"
|
||||
import { upgradeWebSocket, websocket } from "hono/bun"
|
||||
import type { BunWebSocketData } from "hono/bun"
|
||||
import { errors } from "./error"
|
||||
import { Pty } from "@/pty"
|
||||
import { Installation } from "@/installation"
|
||||
@@ -57,7 +58,6 @@ export namespace Server {
|
||||
const log = Log.create({ service: "server" })
|
||||
|
||||
let _url: URL | undefined
|
||||
let _corsWhitelist: string[] = []
|
||||
|
||||
export function url(): URL {
|
||||
return _url ?? new URL("http://localhost:4096")
|
||||
@@ -111,16 +111,12 @@ export namespace Server {
|
||||
|
||||
if (input.startsWith("http://localhost:")) return input
|
||||
if (input.startsWith("http://127.0.0.1:")) return input
|
||||
if (input === "tauri://localhost" || input === "http://tauri.localhost") return input
|
||||
if (input === "tauri://localhost") return input
|
||||
|
||||
// *.opencode.ai (https only, adjust if needed)
|
||||
if (/^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/.test(input)) {
|
||||
return input
|
||||
}
|
||||
if (_corsWhitelist.includes(input)) {
|
||||
return input
|
||||
}
|
||||
|
||||
return
|
||||
},
|
||||
}),
|
||||
@@ -2680,9 +2676,7 @@ export namespace Server {
|
||||
return result
|
||||
}
|
||||
|
||||
export function listen(opts: { port: number; hostname: string; mdns?: boolean; cors?: string[] }) {
|
||||
_corsWhitelist = opts.cors ?? []
|
||||
|
||||
export function listen(opts: { port: number; hostname: string; mdns?: boolean }) {
|
||||
const args = {
|
||||
hostname: opts.hostname,
|
||||
idleTimeout: 0,
|
||||
@@ -2708,7 +2702,7 @@ export namespace Server {
|
||||
opts.hostname !== "localhost" &&
|
||||
opts.hostname !== "::1"
|
||||
if (shouldPublishMDNS) {
|
||||
MDNS.publish(server.port!, `opencode-${server.port!}`)
|
||||
MDNS.publish(server.port!)
|
||||
} else if (opts.mdns) {
|
||||
log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish")
|
||||
}
|
||||
|
||||
@@ -1290,9 +1290,7 @@ export namespace SessionPrompt {
|
||||
const raw = input.arguments.match(argsRegex) ?? []
|
||||
const args = raw.map((arg) => arg.replace(quoteTrimRegex, ""))
|
||||
|
||||
const templateCommand = await command.template
|
||||
|
||||
const placeholders = templateCommand.match(placeholderRegex) ?? []
|
||||
const placeholders = command.template.match(placeholderRegex) ?? []
|
||||
let last = 0
|
||||
for (const item of placeholders) {
|
||||
const value = Number(item.slice(1))
|
||||
@@ -1300,7 +1298,7 @@ export namespace SessionPrompt {
|
||||
}
|
||||
|
||||
// Let the final placeholder swallow any extra arguments so prompts read naturally
|
||||
const withArgs = templateCommand.replaceAll(placeholderRegex, (_, index) => {
|
||||
const withArgs = command.template.replaceAll(placeholderRegex, (_, index) => {
|
||||
const position = Number(index)
|
||||
const argIndex = position - 1
|
||||
if (argIndex >= args.length) return ""
|
||||
|
||||
@@ -35,7 +35,7 @@ export namespace Skill {
|
||||
}),
|
||||
)
|
||||
|
||||
const OPENCODE_SKILL_GLOB = new Bun.Glob("{skill,skills}/**/SKILL.md")
|
||||
const OPENCODE_SKILL_GLOB = new Bun.Glob("skill/**/SKILL.md")
|
||||
const CLAUDE_SKILL_GLOB = new Bun.Glob("skills/**/SKILL.md")
|
||||
|
||||
export const state = Instance.state(async () => {
|
||||
|
||||
@@ -1,331 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import path from "path"
|
||||
import { Ripgrep } from "../../src/file/ripgrep"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
describe("Ripgrep.tree", () => {
|
||||
test("generates tree for flat directory", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, "a.ts"), "content")
|
||||
await Bun.write(path.join(dir, "b.ts"), "content")
|
||||
await Bun.write(path.join(dir, "c.ts"), "content")
|
||||
},
|
||||
})
|
||||
|
||||
const result = await Ripgrep.tree({ cwd: tmp.path })
|
||||
const lines = result.split("\n")
|
||||
expect(lines).toContain("a.ts")
|
||||
expect(lines).toContain("b.ts")
|
||||
expect(lines).toContain("c.ts")
|
||||
})
|
||||
|
||||
test("generates tree with nested directories", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, "src", "index.ts"), "content")
|
||||
await Bun.write(path.join(dir, "src", "utils", "helper.ts"), "content")
|
||||
await Bun.write(path.join(dir, "README.md"), "content")
|
||||
},
|
||||
})
|
||||
|
||||
const result = await Ripgrep.tree({ cwd: tmp.path })
|
||||
expect(result).toContain("src/")
|
||||
expect(result).toContain("index.ts")
|
||||
expect(result).toContain("utils/")
|
||||
expect(result).toContain("helper.ts")
|
||||
expect(result).toContain("README.md")
|
||||
})
|
||||
|
||||
test("sorts directories before files", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, "aaa.txt"), "content")
|
||||
await Bun.write(path.join(dir, "zzz", "file.txt"), "content")
|
||||
},
|
||||
})
|
||||
|
||||
const result = await Ripgrep.tree({ cwd: tmp.path })
|
||||
const lines = result.split("\n")
|
||||
const dirIndex = lines.findIndex((l) => l.includes("zzz/"))
|
||||
const fileIndex = lines.findIndex((l) => l.includes("aaa.txt"))
|
||||
expect(dirIndex).toBeLessThan(fileIndex)
|
||||
})
|
||||
|
||||
test("sorts alphabetically within same type", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, "c.txt"), "content")
|
||||
await Bun.write(path.join(dir, "a.txt"), "content")
|
||||
await Bun.write(path.join(dir, "b.txt"), "content")
|
||||
},
|
||||
})
|
||||
|
||||
const result = await Ripgrep.tree({ cwd: tmp.path })
|
||||
const lines = result.split("\n").filter(Boolean)
|
||||
const aIndex = lines.findIndex((l) => l.includes("a.txt"))
|
||||
const bIndex = lines.findIndex((l) => l.includes("b.txt"))
|
||||
const cIndex = lines.findIndex((l) => l.includes("c.txt"))
|
||||
expect(aIndex).toBeLessThan(bIndex)
|
||||
expect(bIndex).toBeLessThan(cIndex)
|
||||
})
|
||||
|
||||
test("respects limit parameter", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
for (let i = 0; i < 100; i++) {
|
||||
await Bun.write(path.join(dir, `file${i.toString().padStart(3, "0")}.txt`), "content")
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const result = await Ripgrep.tree({ cwd: tmp.path, limit: 10 })
|
||||
expect(result).toContain("[")
|
||||
expect(result).toContain("truncated]")
|
||||
})
|
||||
|
||||
test("excludes .opencode directory", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, "src", "index.ts"), "content")
|
||||
await Bun.write(path.join(dir, ".opencode", "config.json"), "content")
|
||||
},
|
||||
})
|
||||
|
||||
const result = await Ripgrep.tree({ cwd: tmp.path })
|
||||
expect(result).not.toContain(".opencode")
|
||||
expect(result).toContain("src/")
|
||||
})
|
||||
|
||||
test("handles empty directory", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
const result = await Ripgrep.tree({ cwd: tmp.path })
|
||||
expect(result).toBe("")
|
||||
})
|
||||
|
||||
test("indents nested items correctly", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, "a", "b", "c.txt"), "content")
|
||||
},
|
||||
})
|
||||
|
||||
const result = await Ripgrep.tree({ cwd: tmp.path })
|
||||
const lines = result.split("\n")
|
||||
const aLine = lines.find((l) => l.includes("a/"))
|
||||
const bLine = lines.find((l) => l.includes("b/"))
|
||||
const cLine = lines.find((l) => l.includes("c.txt"))
|
||||
|
||||
expect(aLine).toBe("a/")
|
||||
expect(bLine).toBe("\tb/")
|
||||
expect(cLine).toBe("\t\tc.txt")
|
||||
})
|
||||
|
||||
test("default limit is 50", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
for (let i = 0; i < 60; i++) {
|
||||
await Bun.write(path.join(dir, `file${i.toString().padStart(3, "0")}.txt`), "content")
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const result = await Ripgrep.tree({ cwd: tmp.path })
|
||||
expect(result).toContain("truncated]")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Ripgrep.files", () => {
|
||||
test("lists files in directory", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, "file1.ts"), "content")
|
||||
await Bun.write(path.join(dir, "file2.ts"), "content")
|
||||
},
|
||||
})
|
||||
|
||||
const files = await Array.fromAsync(Ripgrep.files({ cwd: tmp.path }))
|
||||
expect(files).toContain("file1.ts")
|
||||
expect(files).toContain("file2.ts")
|
||||
})
|
||||
|
||||
test("respects glob filter", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, "file1.ts"), "content")
|
||||
await Bun.write(path.join(dir, "file2.js"), "content")
|
||||
},
|
||||
})
|
||||
|
||||
const files = await Array.fromAsync(Ripgrep.files({ cwd: tmp.path, glob: ["*.ts"] }))
|
||||
expect(files).toContain("file1.ts")
|
||||
expect(files).not.toContain("file2.js")
|
||||
})
|
||||
|
||||
test("includes hidden files by default", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, ".hidden"), "content")
|
||||
await Bun.write(path.join(dir, "visible"), "content")
|
||||
},
|
||||
})
|
||||
|
||||
const files = await Array.fromAsync(Ripgrep.files({ cwd: tmp.path }))
|
||||
expect(files).toContain(".hidden")
|
||||
expect(files).toContain("visible")
|
||||
})
|
||||
|
||||
test("respects maxDepth option", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, "top.txt"), "content")
|
||||
await Bun.write(path.join(dir, "level1", "mid.txt"), "content")
|
||||
await Bun.write(path.join(dir, "level1", "level2", "deep.txt"), "content")
|
||||
},
|
||||
})
|
||||
|
||||
const files = await Array.fromAsync(Ripgrep.files({ cwd: tmp.path, maxDepth: 1 }))
|
||||
expect(files).toContain("top.txt")
|
||||
expect(files).not.toContain(path.join("level1", "mid.txt"))
|
||||
})
|
||||
|
||||
test("throws for non-existent directory", async () => {
|
||||
const nonexistent = "/tmp/nonexistent-dir-" + Math.random().toString(36).slice(2)
|
||||
expect(Array.fromAsync(Ripgrep.files({ cwd: nonexistent }))).rejects.toThrow()
|
||||
})
|
||||
|
||||
test("excludes .git directory by default", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, "src.ts"), "content")
|
||||
},
|
||||
})
|
||||
|
||||
const files = await Array.fromAsync(Ripgrep.files({ cwd: tmp.path }))
|
||||
const gitFiles = files.filter((f) => f.includes(".git"))
|
||||
expect(gitFiles.length).toBe(0)
|
||||
expect(files).toContain("src.ts")
|
||||
})
|
||||
|
||||
test("respects exclude glob pattern", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, "keep.ts"), "content")
|
||||
await Bun.write(path.join(dir, "ignore.test.ts"), "content")
|
||||
},
|
||||
})
|
||||
|
||||
const files = await Array.fromAsync(Ripgrep.files({ cwd: tmp.path, glob: ["!*.test.ts"] }))
|
||||
expect(files).toContain("keep.ts")
|
||||
expect(files).not.toContain("ignore.test.ts")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Ripgrep.search", () => {
|
||||
test("finds matches in files", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, "test.ts"), "function hello() { return 'world' }")
|
||||
},
|
||||
})
|
||||
|
||||
const results = await Ripgrep.search({ cwd: tmp.path, pattern: "hello" })
|
||||
expect(results.length).toBeGreaterThan(0)
|
||||
expect(results[0].path.text).toBe("test.ts")
|
||||
expect(results[0].lines.text).toContain("hello")
|
||||
})
|
||||
|
||||
test("returns empty array for no matches", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, "test.ts"), "function hello() {}")
|
||||
},
|
||||
})
|
||||
|
||||
const results = await Ripgrep.search({ cwd: tmp.path, pattern: "nonexistentpattern123" })
|
||||
expect(results).toEqual([])
|
||||
})
|
||||
|
||||
test("respects limit parameter", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
const content = "match\nmatch\nmatch\nmatch\nmatch"
|
||||
await Bun.write(path.join(dir, "test.txt"), content)
|
||||
},
|
||||
})
|
||||
|
||||
const results = await Ripgrep.search({ cwd: tmp.path, pattern: "match", limit: 2 })
|
||||
expect(results.length).toBe(2)
|
||||
})
|
||||
|
||||
test("includes line numbers", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, "test.txt"), "line1\nline2\nmatch here\nline4")
|
||||
},
|
||||
})
|
||||
|
||||
const results = await Ripgrep.search({ cwd: tmp.path, pattern: "match" })
|
||||
expect(results[0].line_number).toBe(3)
|
||||
})
|
||||
|
||||
test("includes submatches", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, "test.txt"), "hello world")
|
||||
},
|
||||
})
|
||||
|
||||
const results = await Ripgrep.search({ cwd: tmp.path, pattern: "world" })
|
||||
expect(results[0].submatches.length).toBeGreaterThan(0)
|
||||
expect(results[0].submatches[0].match.text).toBe("world")
|
||||
})
|
||||
|
||||
test("respects glob filter", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, "include.ts"), "searchterm")
|
||||
await Bun.write(path.join(dir, "exclude.js"), "searchterm")
|
||||
},
|
||||
})
|
||||
|
||||
const results = await Ripgrep.search({ cwd: tmp.path, pattern: "searchterm", glob: ["*.ts"] })
|
||||
expect(results.length).toBe(1)
|
||||
expect(results[0].path.text).toBe("include.ts")
|
||||
})
|
||||
|
||||
test("respects exclude glob filter", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, "include.ts"), "searchterm")
|
||||
await Bun.write(path.join(dir, "node_modules", "exclude.ts"), "searchterm")
|
||||
},
|
||||
})
|
||||
|
||||
const results = await Ripgrep.search({ cwd: tmp.path, pattern: "searchterm", glob: ["!node_modules/**"] })
|
||||
expect(results.length).toBe(1)
|
||||
expect(results[0].path.text).toBe("include.ts")
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
||||
@@ -1177,10 +1177,6 @@ export type ServerConfig = {
|
||||
* Enable mDNS service discovery
|
||||
*/
|
||||
mdns?: boolean
|
||||
/**
|
||||
* Additional domains to allow for CORS
|
||||
*/
|
||||
cors?: Array<string>
|
||||
}
|
||||
|
||||
export type AgentConfig = {
|
||||
@@ -1655,10 +1651,6 @@ export type Config = {
|
||||
* Continue the agent loop when a tool call is denied
|
||||
*/
|
||||
continue_loop_on_deny?: boolean
|
||||
/**
|
||||
* Timeout in milliseconds for model context protocol (MCP) requests
|
||||
*/
|
||||
mcp_timeout?: number
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1733,10 +1725,8 @@ export type Command = {
|
||||
description?: string
|
||||
agent?: string
|
||||
model?: string
|
||||
mcp?: boolean
|
||||
template: string
|
||||
subtask?: boolean
|
||||
hints: Array<string>
|
||||
}
|
||||
|
||||
export type Model = {
|
||||
|
||||
+13
-66
@@ -3209,19 +3209,6 @@
|
||||
}
|
||||
},
|
||||
"required": ["npm"]
|
||||
},
|
||||
"variants": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -7793,13 +7780,6 @@
|
||||
"mdns": {
|
||||
"description": "Enable mDNS service discovery",
|
||||
"type": "boolean"
|
||||
},
|
||||
"cors": {
|
||||
"description": "Additional domains to allow for CORS",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -8081,23 +8061,6 @@
|
||||
}
|
||||
},
|
||||
"required": ["npm"]
|
||||
},
|
||||
"variants": {
|
||||
"description": "Variant-specific configuration",
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"disabled": {
|
||||
"description": "Disable this variant for the model",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8774,12 +8737,6 @@
|
||||
"continue_loop_on_deny": {
|
||||
"description": "Continue the agent loop when a tool call is denied",
|
||||
"type": "boolean"
|
||||
},
|
||||
"mcp_timeout": {
|
||||
"description": "Timeout in milliseconds for model context protocol (MCP) requests",
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8982,30 +8939,24 @@
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"mcp": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"template": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
"type": "string"
|
||||
},
|
||||
"subtask": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hints": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "template", "hints"]
|
||||
"required": ["name", "template"]
|
||||
},
|
||||
"Variant": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"disabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["disabled"],
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"Model": {
|
||||
"type": "object",
|
||||
@@ -9203,11 +9154,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {}
|
||||
"$ref": "#/components/schemas/Variant"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun run src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./*": "./src/components/*.tsx",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type FileContents, File, FileOptions, LineAnnotation } from "@pierre/diffs"
|
||||
import { ComponentProps, createEffect, createMemo, splitProps } from "solid-js"
|
||||
import { createDefaultOptions, styleVariables } from "../pierre"
|
||||
import { getWorkerPool } from "../pierre/worker"
|
||||
import { workerPool } from "../pierre/worker"
|
||||
|
||||
export type CodeProps<T = {}> = FileOptions<T> & {
|
||||
file: FileContents
|
||||
@@ -21,7 +21,7 @@ export function Code<T>(props: CodeProps<T>) {
|
||||
...createDefaultOptions<T>("unified"),
|
||||
...others,
|
||||
},
|
||||
getWorkerPool("unified"),
|
||||
workerPool,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export function Diff<T>(props: SSRDiffProps<T>) {
|
||||
let container!: HTMLDivElement
|
||||
let fileDiffRef!: HTMLElement
|
||||
const [local, others] = splitProps(props, ["before", "after", "class", "classList", "annotations"])
|
||||
const workerPool = useWorkerPool(props.diffStyle)
|
||||
const workerPool = useWorkerPool()
|
||||
|
||||
let fileDiffInstance: FileDiff<T> | undefined
|
||||
const cleanupFunctions: Array<() => void> = []
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FileDiff } from "@pierre/diffs"
|
||||
import { createEffect, createMemo, onCleanup, splitProps } from "solid-js"
|
||||
import { createDefaultOptions, type DiffProps, styleVariables } from "../pierre"
|
||||
import { getWorkerPool } from "../pierre/worker"
|
||||
import { workerPool } from "../pierre/worker"
|
||||
|
||||
// interface ThreadMetadata {
|
||||
// threadId: string
|
||||
@@ -20,23 +20,26 @@ export function Diff<T>(props: DiffProps<T>) {
|
||||
...createDefaultOptions(props.diffStyle),
|
||||
...others,
|
||||
},
|
||||
getWorkerPool(props.diffStyle),
|
||||
workerPool,
|
||||
),
|
||||
)
|
||||
|
||||
const cleanupFunctions: Array<() => void> = []
|
||||
|
||||
createEffect(() => {
|
||||
const diff = fileDiff()
|
||||
container.innerHTML = ""
|
||||
diff.render({
|
||||
fileDiff().render({
|
||||
oldFile: local.before,
|
||||
newFile: local.after,
|
||||
lineAnnotations: local.annotations,
|
||||
containerWrapper: container,
|
||||
})
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
diff.cleanUp()
|
||||
})
|
||||
onCleanup(() => {
|
||||
// Clean up FileDiff event handlers and dispose SolidJS components
|
||||
fileDiff()?.cleanUp()
|
||||
cleanupFunctions.forEach((dispose) => dispose())
|
||||
})
|
||||
|
||||
return <div data-component="diff" style={styleVariables} ref={container} />
|
||||
|
||||
@@ -57,7 +57,6 @@ const icons = {
|
||||
share: `<path d="M10.0013 12.0846L10.0013 3.33464M13.7513 6.66797L10.0013 2.91797L6.2513 6.66797M17.0846 10.418V17.0846H2.91797V10.418" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
download: `<path d="M13.9583 10.6257L10 14.584L6.04167 10.6257M10 2.08398V13.959M16.25 17.9173H3.75" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
menu: `<path d="M2.5 5H17.5M2.5 10H17.5M2.5 15H17.5" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
server: `<rect x="3.35547" y="1.92969" width="13.2857" height="16.1429" stroke="currentColor"/><rect x="3.35547" y="11.9297" width="13.2857" height="6.14286" stroke="currentColor"/><rect x="12.8555" y="14.2852" width="1.42857" height="1.42857" fill="currentColor"/><rect x="10" y="14.2852" width="1.42857" height="1.42857" fill="currentColor"/>`,
|
||||
}
|
||||
|
||||
export interface IconProps extends ComponentProps<"svg"> {
|
||||
|
||||
@@ -78,9 +78,10 @@
|
||||
[data-slot="user-message-text"] {
|
||||
white-space: pre-wrap;
|
||||
overflow: hidden;
|
||||
background: var(--surface-base);
|
||||
padding: 8px 12px;
|
||||
background: var(--surface-inset-base);
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
border: 0.5px solid var(--border-weak-base);
|
||||
}
|
||||
|
||||
.text-text-strong {
|
||||
|
||||
@@ -874,6 +874,7 @@ ToolRegistry.register({
|
||||
return (
|
||||
<BasicTool
|
||||
{...props}
|
||||
defaultOpen
|
||||
icon="code-lines"
|
||||
trigger={
|
||||
<div data-component="edit-trigger">
|
||||
@@ -925,6 +926,7 @@ ToolRegistry.register({
|
||||
return (
|
||||
<BasicTool
|
||||
{...props}
|
||||
defaultOpen
|
||||
icon="code-lines"
|
||||
trigger={
|
||||
<div data-component="write-trigger">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
all: unset;
|
||||
background-color: var(--surface-base);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-xs-border);
|
||||
box-shadow: inset 0 0 0 1px var(--border-weak-base);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
@@ -23,7 +23,10 @@
|
||||
[data-slot="radio-group-indicator"] {
|
||||
background: var(--button-secondary-base);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-xs-border);
|
||||
box-shadow:
|
||||
var(--shadow-xs),
|
||||
inset 0 0 0 var(--indicator-focus-width, 0px) var(--border-selected),
|
||||
inset 0 0 0 1px var(--border-base);
|
||||
content: "";
|
||||
opacity: var(--indicator-opacity, 1);
|
||||
position: absolute;
|
||||
@@ -112,7 +115,7 @@
|
||||
/* Focus state */
|
||||
[data-slot="radio-group-wrapper"]:has([data-slot="radio-group-item-input"]:focus-visible)
|
||||
[data-slot="radio-group-indicator"] {
|
||||
box-shadow: var(--shadow-xs-border-focus);
|
||||
--indicator-focus-width: 2px;
|
||||
}
|
||||
|
||||
/* Hide indicator when nothing is checked */
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Accordion } from "./accordion"
|
||||
import { Button } from "./button"
|
||||
import { RadioGroup } from "./radio-group"
|
||||
import { DiffChanges } from "./diff-changes"
|
||||
import { FileIcon } from "./file-icon"
|
||||
import { Icon } from "./icon"
|
||||
@@ -14,12 +13,8 @@ import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
|
||||
import { Dynamic } from "solid-js/web"
|
||||
import { checksum } from "@opencode-ai/util/encode"
|
||||
|
||||
export type SessionReviewDiffStyle = "unified" | "split"
|
||||
|
||||
export interface SessionReviewProps {
|
||||
split?: boolean
|
||||
diffStyle?: SessionReviewDiffStyle
|
||||
onDiffStyleChange?: (diffStyle: SessionReviewDiffStyle) => void
|
||||
class?: string
|
||||
classList?: Record<string, boolean | undefined>
|
||||
classes?: { root?: string; header?: string; container?: string }
|
||||
@@ -33,8 +28,6 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
open: props.diffs.length > 10 ? [] : props.diffs.map((d) => d.file),
|
||||
})
|
||||
|
||||
const diffStyle = () => props.diffStyle ?? (props.split ? "split" : "unified")
|
||||
|
||||
const handleChange = (open: string[]) => {
|
||||
setStore("open", open)
|
||||
}
|
||||
@@ -67,15 +60,6 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
>
|
||||
<div data-slot="session-review-title">Session changes</div>
|
||||
<div data-slot="session-review-actions">
|
||||
<Show when={props.onDiffStyleChange}>
|
||||
<RadioGroup
|
||||
options={["unified", "split"] as const}
|
||||
current={diffStyle()}
|
||||
value={(style) => style}
|
||||
label={(style) => (style === "unified" ? "Unified" : "Split")}
|
||||
onSelect={(style) => style && props.onDiffStyleChange?.(style)}
|
||||
/>
|
||||
</Show>
|
||||
<Button size="normal" icon="chevron-grabber-vertical" onClick={handleExpandOrCollapseAll}>
|
||||
<Switch>
|
||||
<Match when={store.open.length > 0}>Collapse all</Match>
|
||||
@@ -118,7 +102,7 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||
<Dynamic
|
||||
component={diffComponent}
|
||||
preloadedDiff={diff.preloaded}
|
||||
diffStyle={diffStyle()}
|
||||
diffStyle={props.split ? "split" : "unified"}
|
||||
before={{
|
||||
name: diff.file!,
|
||||
contents: diff.before!,
|
||||
|
||||
@@ -78,7 +78,6 @@ function AssistantMessageItem(props: {
|
||||
message: AssistantMessage
|
||||
responsePartId: string | undefined
|
||||
hideResponsePart: boolean
|
||||
hideReasoning: boolean
|
||||
}) {
|
||||
const data = useData()
|
||||
const emptyParts: PartType[] = []
|
||||
@@ -93,12 +92,7 @@ function AssistantMessageItem(props: {
|
||||
})
|
||||
|
||||
const filteredParts = createMemo(() => {
|
||||
let parts = msgParts()
|
||||
|
||||
if (props.hideReasoning) {
|
||||
parts = parts.filter((part) => part?.type !== "reasoning")
|
||||
}
|
||||
|
||||
const parts = msgParts()
|
||||
if (!props.hideResponsePart) return parts
|
||||
|
||||
const responsePartId = props.responsePartId
|
||||
@@ -562,7 +556,6 @@ export function SessionTurn(
|
||||
message={assistantMessage}
|
||||
responsePartId={responsePartId()}
|
||||
hideResponsePart={hideResponsePart()}
|
||||
hideReasoning={!working()}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
import type { WorkerPoolManager } from "@pierre/diffs/worker"
|
||||
import { createSimpleContext } from "./helper"
|
||||
|
||||
export type WorkerPools = {
|
||||
unified: WorkerPoolManager | undefined
|
||||
split: WorkerPoolManager | undefined
|
||||
}
|
||||
|
||||
const ctx = createSimpleContext<WorkerPools, { pools: WorkerPools }>({
|
||||
const ctx = createSimpleContext<WorkerPoolManager | undefined, { pool: WorkerPoolManager | undefined }>({
|
||||
name: "WorkerPool",
|
||||
init: (props) => props.pools,
|
||||
init: (props) => props.pool,
|
||||
})
|
||||
|
||||
export const WorkerPoolProvider = ctx.provider
|
||||
|
||||
export function useWorkerPool(diffStyle: "unified" | "split" | undefined) {
|
||||
const pools = ctx.use()
|
||||
if (diffStyle === "split") return pools.split
|
||||
return pools.unified
|
||||
}
|
||||
export const useWorkerPool = ctx.use
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { WorkerPoolManager } from "@pierre/diffs/worker"
|
||||
import { getOrCreateWorkerPoolSingleton, WorkerPoolManager } from "@pierre/diffs/worker"
|
||||
import ShikiWorkerUrl from "@pierre/diffs/worker/worker.js?worker&url"
|
||||
|
||||
export type WorkerPoolStyle = "unified" | "split"
|
||||
|
||||
export function workerFactory(): Worker {
|
||||
return new Worker(ShikiWorkerUrl, { type: "module" })
|
||||
}
|
||||
|
||||
function createPool(lineDiffType: "none" | "word-alt") {
|
||||
const pool = new WorkerPoolManager(
|
||||
{
|
||||
export const workerPool: WorkerPoolManager | undefined = (() => {
|
||||
if (typeof window === "undefined") {
|
||||
return undefined
|
||||
}
|
||||
return getOrCreateWorkerPoolSingleton({
|
||||
poolOptions: {
|
||||
workerFactory,
|
||||
// poolSize defaults to 8. More workers = more parallelism but
|
||||
// also more memory. Too many can actually slow things down.
|
||||
@@ -18,34 +19,10 @@ function createPool(lineDiffType: "none" | "word-alt") {
|
||||
// boot up time for workers
|
||||
poolSize: 2,
|
||||
},
|
||||
{
|
||||
highlighterOptions: {
|
||||
theme: "OpenCode",
|
||||
lineDiffType,
|
||||
// Optionally preload languages to avoid lazy-loading delays
|
||||
// langs: ["typescript", "javascript", "css", "html"],
|
||||
},
|
||||
)
|
||||
|
||||
pool.initialize()
|
||||
return pool
|
||||
}
|
||||
|
||||
let unified: WorkerPoolManager | undefined
|
||||
let split: WorkerPoolManager | undefined
|
||||
|
||||
export function getWorkerPool(style: WorkerPoolStyle | undefined): WorkerPoolManager | undefined {
|
||||
if (typeof window === "undefined") return
|
||||
|
||||
if (style === "split") {
|
||||
if (!split) split = createPool("word-alt")
|
||||
return split
|
||||
}
|
||||
|
||||
if (!unified) unified = createPool("none")
|
||||
return unified
|
||||
}
|
||||
|
||||
export function getWorkerPools() {
|
||||
return {
|
||||
unified: getWorkerPool("unified"),
|
||||
split: getWorkerPool("split"),
|
||||
}
|
||||
}
|
||||
})
|
||||
})()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/web",
|
||||
"type": "module",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
||||
@@ -362,12 +362,11 @@ This starts an HTTP server that provides API access to opencode functionality wi
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Description |
|
||||
| ------------ | ------------------------------------------ |
|
||||
| `--port` | Port to listen on |
|
||||
| `--hostname` | Hostname to listen on |
|
||||
| `--mdns` | Enable mDNS discovery |
|
||||
| `--cors` | Additional browser origin(s) to allow CORS |
|
||||
| Flag | Description |
|
||||
| ------------ | --------------------- |
|
||||
| `--port` | Port to listen on |
|
||||
| `--hostname` | Hostname to listen on |
|
||||
| `--mdns` | Enable mDNS discovery |
|
||||
|
||||
---
|
||||
|
||||
@@ -408,12 +407,11 @@ opencode stats
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Description |
|
||||
| ----------- | --------------------------------------------------------------------------- |
|
||||
| `--days` | Show stats for the last N days (all time) |
|
||||
| `--tools` | Number of tools to show (all) |
|
||||
| `--models` | Show model usage breakdown (hidden by default). Pass a number to show top N |
|
||||
| `--project` | Filter by project (all projects, empty string: current project) |
|
||||
| Flag | Description |
|
||||
| ----------- | --------------------------------------------------------------- |
|
||||
| `--days` | Show stats for the last N days (all time) |
|
||||
| `--tools` | Number of tools to show (all) |
|
||||
| `--project` | Filter by project (all projects, empty string: current project) |
|
||||
|
||||
---
|
||||
|
||||
@@ -458,12 +456,11 @@ This starts an HTTP server and opens a web browser to access OpenCode through a
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Description |
|
||||
| ------------ | ------------------------------------------ |
|
||||
| `--port` | Port to listen on |
|
||||
| `--hostname` | Hostname to listen on |
|
||||
| `--mdns` | Enable mDNS discovery |
|
||||
| `--cors` | Additional browser origin(s) to allow CORS |
|
||||
| Flag | Description |
|
||||
| ------------ | --------------------- |
|
||||
| `--port` | Port to listen on |
|
||||
| `--hostname` | Hostname to listen on |
|
||||
| `--mdns` | Enable mDNS discovery |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -132,8 +132,7 @@ You can configure server settings for the `opencode serve` and `opencode web` co
|
||||
"server": {
|
||||
"port": 4096,
|
||||
"hostname": "0.0.0.0",
|
||||
"mdns": true,
|
||||
"cors": ["http://localhost:5173"]
|
||||
"mdns": true
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -143,7 +142,6 @@ Available options:
|
||||
- `port` - Port to listen on.
|
||||
- `hostname` - Hostname to listen on. When `mdns` is enabled and no hostname is set, defaults to `0.0.0.0`.
|
||||
- `mdns` - Enable mDNS service discovery. This allows other devices on the network to discover your OpenCode server.
|
||||
- `cors` - Additional origins to allow for CORS when using the HTTP server from a browser-based client. Values must be full origins (scheme + host + optional port), eg `https://app.example.com`.
|
||||
|
||||
[Learn more about the server here](/docs/server).
|
||||
|
||||
|
||||
@@ -15,29 +15,28 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw
|
||||
|
||||
## Plugins
|
||||
|
||||
| Name | Description |
|
||||
| -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
||||
| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Automatically inject Helicone session headers for request grouping |
|
||||
| [opencode-skills](https://github.com/malhashemi/opencode-skills) | Manage and organize OpenCode skills and capabilities |
|
||||
| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Auto-inject TypeScript/Svelte types into file reads with lookup tools |
|
||||
| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Use your ChatGPT Plus/Pro subscription instead of API credits |
|
||||
| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Use your existing Gemini plan instead of API billing |
|
||||
| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Use Antigravity's free models instead of API billing |
|
||||
| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Google Antigravity OAuth Plugin, with support for Google Search, and more robust API handling |
|
||||
| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimize token usage by pruning obsolete tool outputs |
|
||||
| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Add native websearch support for supported providers with Google grounded style |
|
||||
| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Enables AI agents to run background processes in a PTY, send interactive input to them. |
|
||||
| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Instructions for non-interactive shell commands - prevents hangs from TTY-dependent operations |
|
||||
| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Track OpenCode usage with Wakatime |
|
||||
| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Clean up markdown tables produced by LLMs |
|
||||
| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 10x faster code editing with Morph Fast Apply API and lazy edit markers |
|
||||
| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Background agents, pre-built LSP/AST/MCP tools, curated agents, Claude Code compatible |
|
||||
| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Desktop notifications and sound alerts for OpenCode sessions |
|
||||
| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Desktop notifications and sound alerts for permission, completion, and error events |
|
||||
| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | AI-powered automatic Zellij session naming based on OpenCode context |
|
||||
| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | Allow OpenCode agents to lazy load prompts on demand with skill discovery and injection |
|
||||
| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Persistent memory across sessions using Supermemory |
|
||||
| [@plannotator/opencode](https://github.com/backnotprop/plannotator/tree/main/apps/opencode-plugin) | Interactive plan review with visual annotation and private/offline sharing |
|
||||
| Name | Description |
|
||||
| ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
||||
| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Automatically inject Helicone session headers for request grouping |
|
||||
| [opencode-skills](https://github.com/malhashemi/opencode-skills) | Manage and organize OpenCode skills and capabilities |
|
||||
| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Auto-inject TypeScript/Svelte types into file reads with lookup tools |
|
||||
| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Use your ChatGPT Plus/Pro subscription instead of API credits |
|
||||
| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Use your existing Gemini plan instead of API billing |
|
||||
| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Use Antigravity's free models instead of API billing |
|
||||
| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-google-antigravity-auth) | Google Antigravity OAuth Plugin, with support for Google Search, and more robust API handling |
|
||||
| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimize token usage by pruning obsolete tool outputs |
|
||||
| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Add native websearch support for supported providers with Google grounded style |
|
||||
| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Enables AI agents to run background processes in a PTY, send interactive input to them. |
|
||||
| [opencode-shell-strategy](https://github.com/JRedeker/opencode-shell-strategy) | Instructions for non-interactive shell commands - prevents hangs from TTY-dependent operations |
|
||||
| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Track OpenCode usage with Wakatime |
|
||||
| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Clean up markdown tables produced by LLMs |
|
||||
| [opencode-morph-fast-apply](https://github.com/JRedeker/opencode-morph-fast-apply) | 10x faster code editing with Morph Fast Apply API and lazy edit markers |
|
||||
| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Background agents, pre-built LSP/AST/MCP tools, curated agents, Claude Code compatible |
|
||||
| [opencode-notificator](https://github.com/panta82/opencode-notificator) | Desktop notifications and sound alerts for OpenCode sessions |
|
||||
| [opencode-notifier](https://github.com/mohak34/opencode-notifier) | Desktop notifications and sound alerts for permission, completion, and error events |
|
||||
| [opencode-zellij-namer](https://github.com/24601/opencode-zellij-namer) | AI-powered automatic Zellij session naming based on OpenCode context |
|
||||
| [opencode-skillful](https://github.com/zenobi-us/opencode-skillful) | Allow OpenCode agents to lazy load prompts on demand with skill discovery and injection |
|
||||
| [opencode-supermemory](https://github.com/supermemoryai/opencode-supermemory) | Persistent memory across sessions using Supermemory |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ OpenCode comes with several built-in formatters for popular languages and framew
|
||||
| clang-format | .c, .cpp, .h, .hpp, .ino, and [more](https://clang.llvm.org/docs/ClangFormat.html) | `.clang-format` config file |
|
||||
| ktlint | .kt, .kts | `ktlint` command available |
|
||||
| ruff | .py, .pyi | `ruff` command available with config |
|
||||
| rustfmt | .rs | `rustfmt` command available |
|
||||
| uv | .py, .pyi | `uv` command available |
|
||||
| rubocop | .rb, .rake, .gemspec, .ru | `rubocop` command available |
|
||||
| standardrb | .rb, .rake, .gemspec, .ru | `standardrb` command available |
|
||||
|
||||
@@ -43,7 +43,6 @@ OpenCode has a list of keybinds that you can customize through the OpenCode conf
|
||||
"model_list": "<leader>m",
|
||||
"model_cycle_recent": "f2",
|
||||
"model_cycle_recent_reverse": "shift+f2",
|
||||
"variant_cycle": "ctrl+t",
|
||||
"command_list": "ctrl+p",
|
||||
"agent_list": "<leader>a",
|
||||
"agent_cycle": "tab",
|
||||
|
||||
@@ -35,13 +35,15 @@ Consider using one of the models we recommend.
|
||||
|
||||
However, there are only a few of them that are good at both generating code and tool calling.
|
||||
|
||||
Here are several models that work well with OpenCode, in no particular order. (This is not an exhaustive list nor is it necessarily up to date):
|
||||
Here are several models that work well with OpenCode, in no particular order. (This is not an exhaustive list):
|
||||
|
||||
- GPT 5.2
|
||||
- GPT 5.1
|
||||
- GPT 5.1 Codex
|
||||
- Claude Opus 4.5
|
||||
- Claude Sonnet 4.5
|
||||
- Minimax M2.1
|
||||
- Claude Haiku 4.5
|
||||
- Kimi K2
|
||||
- GLM 4.6
|
||||
- Qwen3 Coder
|
||||
- Gemini 3 Pro
|
||||
|
||||
---
|
||||
@@ -105,26 +107,30 @@ The built-in provider and model names can be found on [Models.dev](https://model
|
||||
|
||||
You can also configure these options for any agents that you are using. The agent config overrides any global options here. [Learn more](/docs/agents/#additional).
|
||||
|
||||
You can also define custom variants that extend built-in ones. Variants let you configure different settings for the same model without creating duplicate entries:
|
||||
You can also define custom models that extend built-in ones and can optionally use specific options by referring to their id:
|
||||
|
||||
```jsonc title="opencode.jsonc" {6-21}
|
||||
```jsonc title="opencode.jsonc" {6-20}
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": {
|
||||
"opencode": {
|
||||
"models": {
|
||||
"gpt-5": {
|
||||
"variants": {
|
||||
"high": {
|
||||
"reasoningEffort": "high",
|
||||
"textVerbosity": "low",
|
||||
"reasoningSummary": "auto",
|
||||
},
|
||||
"low": {
|
||||
"reasoningEffort": "low",
|
||||
"textVerbosity": "low",
|
||||
"reasoningSummary": "auto",
|
||||
},
|
||||
"gpt-5-high": {
|
||||
"id": "gpt-5",
|
||||
"name": "MyGPT5 (High Reasoning)",
|
||||
"options": {
|
||||
"reasoningEffort": "high",
|
||||
"textVerbosity": "low",
|
||||
"reasoningSummary": "auto",
|
||||
},
|
||||
},
|
||||
"gpt-5-low": {
|
||||
"id": "gpt-5",
|
||||
"name": "MyGPT5 (Low Reasoning)",
|
||||
"options": {
|
||||
"reasoningEffort": "low",
|
||||
"textVerbosity": "low",
|
||||
"reasoningSummary": "auto",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -135,72 +141,6 @@ You can also define custom variants that extend built-in ones. Variants let you
|
||||
|
||||
---
|
||||
|
||||
## Variants
|
||||
|
||||
Many models support multiple variants with different configurations. OpenCode ships with built-in default variants for popular providers.
|
||||
|
||||
### Built-in variants
|
||||
|
||||
OpenCode ships with default variants for many providers:
|
||||
|
||||
**Anthropic**:
|
||||
|
||||
- `high` - High thinking budget (default)
|
||||
- `max` - Maximum thinking budget
|
||||
|
||||
**OpenAI**:
|
||||
|
||||
Varies by model but roughly:
|
||||
|
||||
- `none` - No reasoning
|
||||
- `minimal` - Minimal reasoning effort
|
||||
- `low` - Low reasoning effort
|
||||
- `medium` - Medium reasoning effort
|
||||
- `high` - High reasoning effort
|
||||
- `xhigh` - Extra high reasoning effort
|
||||
|
||||
**Google**:
|
||||
|
||||
- `low` - Lower effort/token budget
|
||||
- `high` - Higher effort/token budget
|
||||
|
||||
:::tip
|
||||
This list is not comprehensive. Many other providers have built-in defaults too.
|
||||
:::
|
||||
|
||||
### Custom variants
|
||||
|
||||
You can override existing variants or add your own:
|
||||
|
||||
```jsonc title="opencode.jsonc" {7-18}
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": {
|
||||
"openai": {
|
||||
"models": {
|
||||
"gpt-5": {
|
||||
"variants": {
|
||||
"thinking": {
|
||||
"reasoningEffort": "high",
|
||||
"textVerbosity": "low",
|
||||
},
|
||||
"fast": {
|
||||
"disabled": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Cycle variants
|
||||
|
||||
Use the keybind `variant_cycle` to quickly switch between variants. [Learn more](/docs/keybinds).
|
||||
|
||||
---
|
||||
|
||||
## Loading models
|
||||
|
||||
When OpenCode starts up, it checks for models in the following priority order:
|
||||
|
||||
@@ -13,23 +13,16 @@ The `opencode serve` command runs a headless HTTP server that exposes an OpenAPI
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
opencode serve [--port <number>] [--hostname <string>] [--cors <origin>]
|
||||
opencode serve [--port <number>] [--hostname <string>]
|
||||
```
|
||||
|
||||
#### Options
|
||||
|
||||
| Flag | Description | Default |
|
||||
| ------------ | ----------------------------------- | ----------- |
|
||||
| `--port` | Port to listen on | `4096` |
|
||||
| `--hostname` | Hostname to listen on | `127.0.0.1` |
|
||||
| `--mdns` | Enable mDNS discovery | `false` |
|
||||
| `--cors` | Additional browser origins to allow | `[]` |
|
||||
|
||||
`--cors` can be passed multiple times:
|
||||
|
||||
```bash
|
||||
opencode serve --cors http://localhost:5173 --cors https://app.example.com
|
||||
```
|
||||
| Flag | Description | Default |
|
||||
| ------------ | --------------------- | ----------- |
|
||||
| `--port` | Port to listen on | `4096` |
|
||||
| `--hostname` | Hostname to listen on | `127.0.0.1` |
|
||||
| `--mdns` | Enable mDNS discovery | `false` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ export async function getCommits(from: string, to: string): Promise<Commit[]> {
|
||||
const areas = new Set<string>()
|
||||
|
||||
for (const file of files.split("\n").filter(Boolean)) {
|
||||
if (file.startsWith("packages/opencode/src/cli/cmd/")) areas.add("tui")
|
||||
if (file.startsWith("packages/opencode/src/cli/cmd/tui/")) areas.add("tui")
|
||||
else if (file.startsWith("packages/opencode/")) areas.add("core")
|
||||
else if (file.startsWith("packages/desktop/src-tauri/")) areas.add("tauri")
|
||||
else if (file.startsWith("packages/desktop/")) areas.add("app")
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "opencode",
|
||||
"displayName": "opencode",
|
||||
"description": "opencode for VS Code",
|
||||
"version": "1.0.220",
|
||||
"version": "1.0.217",
|
||||
"publisher": "sst-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Reference in New Issue
Block a user