mirror of
https://github.com/anomalyco/opencode.git
synced 2026-03-17 20:24:23 +00:00
Compare commits
2 Commits
no-project
...
production
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dadddc9c8c | ||
|
|
6708c3f6cf |
@@ -19,42 +19,3 @@ test("server picker dialog opens from home", async ({ page }) => {
|
||||
await expect(dialog).toBeVisible()
|
||||
await expect(dialog.getByRole("textbox").first()).toBeVisible()
|
||||
})
|
||||
|
||||
test("home hides desktop history and sidebar controls", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1400, height: 900 })
|
||||
await page.goto("/")
|
||||
|
||||
await expect(page.getByRole("button", { name: "Toggle sidebar" })).toHaveCount(0)
|
||||
await expect(page.getByRole("button", { name: "Go back" })).toHaveCount(0)
|
||||
await expect(page.getByRole("button", { name: "Go forward" })).toHaveCount(0)
|
||||
await expect(page.getByRole("button", { name: "Toggle menu" })).toHaveCount(0)
|
||||
})
|
||||
|
||||
test("home keeps the mobile menu available", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 430, height: 900 })
|
||||
await page.goto("/")
|
||||
|
||||
const toggle = page.getByRole("button", { name: "Toggle menu" }).first()
|
||||
await expect(toggle).toBeVisible()
|
||||
await toggle.click()
|
||||
|
||||
const nav = page.locator('[data-component="sidebar-nav-mobile"]')
|
||||
await expect(nav).toBeVisible()
|
||||
await expect.poll(async () => (await nav.boundingBox())?.width ?? 0).toBeLessThan(120)
|
||||
await expect(nav.getByRole("button", { name: "Settings" })).toBeVisible()
|
||||
await expect(nav.getByRole("button", { name: "Help" })).toBeVisible()
|
||||
|
||||
await page.setViewportSize({ width: 1400, height: 900 })
|
||||
await expect(nav).toBeHidden()
|
||||
|
||||
await page.setViewportSize({ width: 430, height: 900 })
|
||||
await expect(toggle).toBeVisible()
|
||||
await expect(toggle).toHaveAttribute("aria-expanded", "false")
|
||||
await expect(nav).toHaveClass(/-translate-x-full/)
|
||||
|
||||
await toggle.click()
|
||||
await expect(nav).toBeVisible()
|
||||
|
||||
await nav.getByRole("button", { name: "Settings" }).click()
|
||||
await expect(page.getByRole("dialog")).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -58,7 +58,6 @@ export function Titlebar() {
|
||||
})
|
||||
|
||||
const path = () => `${location.pathname}${location.search}${location.hash}`
|
||||
const home = createMemo(() => !params.dir)
|
||||
const creating = createMemo(() => {
|
||||
if (!params.dir) return false
|
||||
if (params.id) return false
|
||||
@@ -199,93 +198,91 @@ export function Titlebar() {
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={!home()}>
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
<TooltipKeybind
|
||||
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
|
||||
placement="bottom"
|
||||
title={language.t("command.sidebar.toggle")}
|
||||
keybind={command.keybind("sidebar.toggle")}
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
<TooltipKeybind
|
||||
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
|
||||
placement="bottom"
|
||||
title={language.t("command.sidebar.toggle")}
|
||||
keybind={command.keybind("sidebar.toggle")}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="group/sidebar-toggle titlebar-icon w-8 h-6 p-0 box-border"
|
||||
onClick={layout.sidebar.toggle}
|
||||
aria-label={language.t("command.sidebar.toggle")}
|
||||
aria-expanded={layout.sidebar.opened()}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="group/sidebar-toggle titlebar-icon w-8 h-6 p-0 box-border"
|
||||
onClick={layout.sidebar.toggle}
|
||||
aria-label={language.t("command.sidebar.toggle")}
|
||||
aria-expanded={layout.sidebar.opened()}
|
||||
>
|
||||
<Icon size="small" name={layout.sidebar.opened() ? "sidebar-active" : "sidebar"} />
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
<div class="hidden xl:flex items-center shrink-0">
|
||||
<Show when={params.dir}>
|
||||
<div
|
||||
class="flex items-center shrink-0 w-8 mr-1"
|
||||
aria-hidden={layout.sidebar.opened() ? "true" : undefined}
|
||||
>
|
||||
<div
|
||||
class="transition-opacity"
|
||||
classList={{
|
||||
"opacity-100 duration-120 ease-out": !layout.sidebar.opened(),
|
||||
"opacity-0 duration-120 ease-in delay-0 pointer-events-none": layout.sidebar.opened(),
|
||||
}}
|
||||
>
|
||||
<TooltipKeybind
|
||||
placement="bottom"
|
||||
title={language.t("command.session.new")}
|
||||
keybind={command.keybind("session.new")}
|
||||
openDelay={2000}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon={creating() ? "new-session-active" : "new-session"}
|
||||
class="titlebar-icon w-8 h-6 p-0 box-border"
|
||||
disabled={layout.sidebar.opened()}
|
||||
tabIndex={layout.sidebar.opened() ? -1 : undefined}
|
||||
onClick={() => {
|
||||
if (!params.dir) return
|
||||
navigate(`/${params.dir}/session`)
|
||||
}}
|
||||
aria-label={language.t("command.session.new")}
|
||||
aria-current={creating() ? "page" : undefined}
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<Icon size="small" name={layout.sidebar.opened() ? "sidebar-active" : "sidebar"} />
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
<div class="hidden xl:flex items-center shrink-0">
|
||||
<Show when={params.dir}>
|
||||
<div
|
||||
class="flex items-center gap-0 transition-transform"
|
||||
classList={{
|
||||
"translate-x-0": !layout.sidebar.opened(),
|
||||
"-translate-x-[36px]": layout.sidebar.opened(),
|
||||
"duration-180 ease-out": !layout.sidebar.opened(),
|
||||
"duration-180 ease-in": layout.sidebar.opened(),
|
||||
}}
|
||||
class="flex items-center shrink-0 w-8 mr-1"
|
||||
aria-hidden={layout.sidebar.opened() ? "true" : undefined}
|
||||
>
|
||||
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon="chevron-left"
|
||||
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||
disabled={!canBack()}
|
||||
onClick={back}
|
||||
aria-label={language.t("common.goBack")}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon="chevron-right"
|
||||
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||
disabled={!canForward()}
|
||||
onClick={forward}
|
||||
aria-label={language.t("common.goForward")}
|
||||
/>
|
||||
</Tooltip>
|
||||
<div
|
||||
class="transition-opacity"
|
||||
classList={{
|
||||
"opacity-100 duration-120 ease-out": !layout.sidebar.opened(),
|
||||
"opacity-0 duration-120 ease-in delay-0 pointer-events-none": layout.sidebar.opened(),
|
||||
}}
|
||||
>
|
||||
<TooltipKeybind
|
||||
placement="bottom"
|
||||
title={language.t("command.session.new")}
|
||||
keybind={command.keybind("session.new")}
|
||||
openDelay={2000}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon={creating() ? "new-session-active" : "new-session"}
|
||||
class="titlebar-icon w-8 h-6 p-0 box-border"
|
||||
disabled={layout.sidebar.opened()}
|
||||
tabIndex={layout.sidebar.opened() ? -1 : undefined}
|
||||
onClick={() => {
|
||||
if (!params.dir) return
|
||||
navigate(`/${params.dir}/session`)
|
||||
}}
|
||||
aria-label={language.t("command.session.new")}
|
||||
aria-current={creating() ? "page" : undefined}
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<div
|
||||
class="flex items-center gap-0 transition-transform"
|
||||
classList={{
|
||||
"translate-x-0": !layout.sidebar.opened(),
|
||||
"-translate-x-[36px]": layout.sidebar.opened(),
|
||||
"duration-180 ease-out": !layout.sidebar.opened(),
|
||||
"duration-180 ease-in": layout.sidebar.opened(),
|
||||
}}
|
||||
>
|
||||
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon="chevron-left"
|
||||
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||
disabled={!canBack()}
|
||||
onClick={back}
|
||||
aria-label={language.t("common.goBack")}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon="chevron-right"
|
||||
class="titlebar-icon w-6 h-6 p-0 box-border"
|
||||
disabled={!canForward()}
|
||||
onClick={forward}
|
||||
aria-label={language.t("common.goForward")}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -200,20 +200,13 @@ export default function Layout(props: ParentProps) {
|
||||
|
||||
onMount(() => {
|
||||
const stop = () => setState("sizing", false)
|
||||
const sync = () => {
|
||||
if (!window.matchMedia("(min-width: 1280px)").matches) return
|
||||
layout.mobileSidebar.hide()
|
||||
}
|
||||
window.addEventListener("pointerup", stop)
|
||||
window.addEventListener("pointercancel", stop)
|
||||
window.addEventListener("blur", stop)
|
||||
window.addEventListener("resize", sync)
|
||||
sync()
|
||||
onCleanup(() => {
|
||||
window.removeEventListener("pointerup", stop)
|
||||
window.removeEventListener("pointercancel", stop)
|
||||
window.removeEventListener("blur", stop)
|
||||
window.removeEventListener("resize", sync)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2249,7 +2242,6 @@ export default function Layout(props: ParentProps) {
|
||||
<SidebarContent
|
||||
mobile={mobile}
|
||||
opened={() => layout.sidebar.opened()}
|
||||
hasPanel={() => !!currentProject()}
|
||||
aimMove={aim.move}
|
||||
projects={projects}
|
||||
renderProject={(project) => (
|
||||
@@ -2348,9 +2340,7 @@ export default function Layout(props: ParentProps) {
|
||||
aria-label={language.t("sidebar.nav.projectsAndSessions")}
|
||||
data-component="sidebar-nav-mobile"
|
||||
classList={{
|
||||
"@container fixed top-10 bottom-0 left-0 z-50 overflow-hidden border-r border-border-weaker-base bg-background-base transition-transform duration-200 ease-out": true,
|
||||
"w-16": !currentProject(),
|
||||
"w-full max-w-[400px]": !!currentProject(),
|
||||
"@container fixed top-10 bottom-0 left-0 z-50 w-full max-w-[400px] overflow-hidden border-r border-border-weaker-base bg-background-base transition-transform duration-200 ease-out": true,
|
||||
"translate-x-0": layout.mobileSidebar.opened(),
|
||||
"-translate-x-full": !layout.mobileSidebar.opened(),
|
||||
}}
|
||||
|
||||
@@ -15,7 +15,6 @@ import { type LocalProject } from "@/context/layout"
|
||||
export const SidebarContent = (props: {
|
||||
mobile?: boolean
|
||||
opened: Accessor<boolean>
|
||||
hasPanel?: Accessor<boolean>
|
||||
aimMove: (event: MouseEvent) => void
|
||||
projects: Accessor<LocalProject[]>
|
||||
renderProject: (project: LocalProject) => JSX.Element
|
||||
@@ -34,7 +33,6 @@ export const SidebarContent = (props: {
|
||||
renderPanel: () => JSX.Element
|
||||
}): JSX.Element => {
|
||||
const expanded = createMemo(() => !!props.mobile || props.opened())
|
||||
const hasPanel = createMemo(() => props.hasPanel?.() ?? true)
|
||||
const placement = () => (props.mobile ? "bottom" : "right")
|
||||
let panel: HTMLDivElement | undefined
|
||||
|
||||
@@ -113,17 +111,15 @@ export const SidebarContent = (props: {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={hasPanel()}>
|
||||
<div
|
||||
ref={(el) => {
|
||||
panel = el
|
||||
}}
|
||||
classList={{ "flex-1 flex h-full min-h-0 min-w-0 overflow-hidden": true, "pointer-events-none": !expanded() }}
|
||||
aria-hidden={!expanded()}
|
||||
>
|
||||
{props.renderPanel()}
|
||||
</div>
|
||||
</Show>
|
||||
<div
|
||||
ref={(el) => {
|
||||
panel = el
|
||||
}}
|
||||
classList={{ "flex-1 flex h-full min-h-0 min-w-0 overflow-hidden": true, "pointer-events-none": !expanded() }}
|
||||
aria-hidden={!expanded()}
|
||||
>
|
||||
{props.renderPanel()}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -366,9 +366,11 @@ The model ID in your OpenCode config uses the format `provider/model-id`. For ex
|
||||
|
||||
---
|
||||
|
||||
### Tools
|
||||
### Tools (deprecated)
|
||||
|
||||
Control which tools are available in this agent with the `tools` config. You can enable or disable specific tools by setting them to `true` or `false`.
|
||||
`tools` is **deprecated**. Prefer the agent's [`permission`](#permissions) field for new configs, updates and more fine-grained control.
|
||||
|
||||
Allows you to control which tools are available in this agent. You can enable or disable specific tools by setting them to `true` or `false`. In an agent's `tools` config, `true` is equivalent to `{"*": "allow"}` permission and `false` is equivalent to `{"*": "deny"}` permission.
|
||||
|
||||
```json title="opencode.json" {3-6,9-12}
|
||||
{
|
||||
@@ -392,7 +394,7 @@ Control which tools are available in this agent with the `tools` config. You can
|
||||
The agent-specific config overrides the global config.
|
||||
:::
|
||||
|
||||
You can also use wildcards to control multiple tools at once. For example, to disable all tools from an MCP server:
|
||||
You can also use wildcards in legacy `tools` entries to control multiple tools at once. For example, to disable all tools from an MCP server:
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
|
||||
@@ -86,7 +86,6 @@ You can also access our models through the following API endpoints.
|
||||
| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||
| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
|
||||
| Gemini 3 Pro | gemini-3-pro | `https://opencode.ai/zen/v1/models/gemini-3-pro` | `@ai-sdk/google` |
|
||||
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiniMax M2.5 Free | minimax-m2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -95,8 +94,6 @@ You can also access our models through the following API endpoints.
|
||||
| GLM 4.7 | glm-4.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| GLM 4.6 | glm-4.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2 Thinking | kimi-k2-thinking | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2 | kimi-k2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Qwen3 Coder 480B | qwen3-coder | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| MiMo V2 Flash Free | mimo-v2-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -134,8 +131,6 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**.
|
||||
| GLM 4.7 | $0.60 | $2.20 | $0.10 | - |
|
||||
| GLM 4.6 | $0.60 | $2.20 | $0.10 | - |
|
||||
| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - |
|
||||
| Kimi K2 Thinking | $0.40 | $2.50 | - | - |
|
||||
| Kimi K2 | $0.40 | $2.50 | - | - |
|
||||
| Qwen3 Coder 480B | $0.45 | $1.50 | - | - |
|
||||
| Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||
@@ -149,8 +144,6 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**.
|
||||
| Claude Haiku 3.5 | $0.80 | $4.00 | $0.08 | $1.00 |
|
||||
| Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
|
||||
| Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
|
||||
| Gemini 3 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
|
||||
| Gemini 3 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
|
||||
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
|
||||
| GPT 5.4 Pro | $30.00 | $180.00 | $30.00 | - |
|
||||
| GPT 5.4 | $2.50 | $15.00 | $0.25 | - |
|
||||
@@ -206,12 +199,13 @@ charging you more than $20 if your balance goes below $5.
|
||||
|
||||
| Model | Deprecation date |
|
||||
| ---------------- | ---------------- |
|
||||
| Qwen3 Coder 480B | Feb 6, 2026 |
|
||||
| Kimi K2 Thinking | March 6, 2026 |
|
||||
| Kimi K2 | March 6, 2026 |
|
||||
| MiniMax M2.1 | March 15, 2026 |
|
||||
| GLM 4.7 | March 15, 2026 |
|
||||
| GLM 4.6 | March 15, 2026 |
|
||||
| Gemini 3 Pro | March 9, 2026 |
|
||||
| Kimi K2 Thinking | March 6, 2026 |
|
||||
| Kimi K2 | March 6, 2026 |
|
||||
| Qwen3 Coder 480B | Feb 6, 2026 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user