mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-27 21:51:49 -04:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35b8117d8d | |||
| 20eefe49ec | |||
| 05259d5429 | |||
| 284e97068c | |||
| 970c38257c | |||
| c39ad38424 | |||
| 91508f6c88 | |||
| 1274408b89 | |||
| 7ae1be5bf6 | |||
| 40e4d730ca | |||
| 8dc539b4ae | |||
| 8e8d38c3cd | |||
| a9bebd3653 | |||
| 78f5764319 | |||
| 55b0211690 | |||
| 4f62295e44 | |||
| e1c41941a9 | |||
| 0b4edfc64c | |||
| 4469cecc4e | |||
| b06768aa8e | |||
| bc2d3df05f | |||
| f77f5a343e | |||
| 985ee1e2ec | |||
| 83bee1e776 | |||
| 124714ca3a |
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"dependencies": {
|
||||
"@corvu/drawer": "catalog:",
|
||||
"@dnd-kit/abstract": "0.5.0",
|
||||
@@ -96,7 +96,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@opencode-ai/cli",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"bin": {
|
||||
"lildax": "./bin/lildax.cjs",
|
||||
},
|
||||
@@ -144,7 +144,7 @@
|
||||
},
|
||||
"packages/codemode": {
|
||||
"name": "@opencode-ai/codemode",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"dependencies": {
|
||||
"acorn": "8.15.0",
|
||||
"effect": "catalog:",
|
||||
@@ -158,7 +158,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -194,7 +194,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -221,7 +221,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "3.0.82",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
@@ -243,7 +243,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -267,7 +267,7 @@
|
||||
},
|
||||
"packages/console/support": {
|
||||
"name": "@opencode-ai/console-support",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@opencode-ai/console-core": "workspace:*",
|
||||
@@ -287,7 +287,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -381,7 +381,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"dependencies": {
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"effect": "catalog:",
|
||||
@@ -435,7 +435,7 @@
|
||||
},
|
||||
"packages/effect-drizzle-sqlite": {
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -449,7 +449,7 @@
|
||||
},
|
||||
"packages/effect-sqlite-node": {
|
||||
"name": "@opencode-ai/effect-sqlite-node",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
},
|
||||
@@ -461,7 +461,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"dependencies": {
|
||||
"@hono/standard-validator": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -493,7 +493,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -509,7 +509,7 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "4.0.0-beta.83",
|
||||
"@effect/platform-node-shared": "4.0.0-beta.83",
|
||||
@@ -540,7 +540,7 @@
|
||||
},
|
||||
"packages/llm": {
|
||||
"name": "@opencode-ai/llm",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"dependencies": {
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@smithy/eventstream-codec": "4.2.14",
|
||||
@@ -559,7 +559,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -690,7 +690,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "3.0.8",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -766,7 +766,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -781,7 +781,7 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@opencode-ai/server",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/protocol": "workspace:*",
|
||||
@@ -796,7 +796,7 @@
|
||||
},
|
||||
"packages/session-ui": {
|
||||
"name": "@opencode-ai/session-ui",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz",
|
||||
@@ -841,7 +841,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -854,7 +854,7 @@
|
||||
},
|
||||
"packages/stats/app": {
|
||||
"name": "@opencode-ai/stats-app",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -888,7 +888,7 @@
|
||||
},
|
||||
"packages/stats/core": {
|
||||
"name": "@opencode-ai/stats-core",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-athena": "3.933.0",
|
||||
"@planetscale/database": "1.19.0",
|
||||
@@ -907,7 +907,7 @@
|
||||
},
|
||||
"packages/stats/server": {
|
||||
"name": "@opencode-ai/stats-server",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-firehose": "3.933.0",
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -949,7 +949,7 @@
|
||||
},
|
||||
"packages/tui": {
|
||||
"name": "@opencode-ai/tui",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
@@ -976,7 +976,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@pierre/diffs": "catalog:",
|
||||
@@ -1027,7 +1027,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
Switch,
|
||||
} from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { useQueryClient } from "@tanstack/solid-query"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { Link } from "@/components/link"
|
||||
import { useServerSDK } from "@/context/server-sdk"
|
||||
@@ -38,7 +37,6 @@ import { useSettings } from "@/context/settings"
|
||||
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
||||
import { CustomProviderForm } from "./dialog-custom-provider"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
import { pathKey } from "@/utils/path-key"
|
||||
|
||||
const CUSTOM_ID = "_custom"
|
||||
type ConnectMethod = Extract<IntegrationMethod, { type: "key" | "oauth" }>
|
||||
@@ -386,7 +384,6 @@ function ProviderConnection(props: {
|
||||
const dialog = useDialog()
|
||||
const serverSync = useServerSync()
|
||||
const serverSDK = useServerSDK()
|
||||
const queryClient = useQueryClient()
|
||||
const params = useParams()
|
||||
const language = useLanguage()
|
||||
const settings = useSettings()
|
||||
@@ -707,9 +704,8 @@ function ProviderConnection(props: {
|
||||
})
|
||||
|
||||
async function complete() {
|
||||
const value = directory()
|
||||
await queryClient
|
||||
.refetchQueries(serverSync().queryOptions.providers(value ? pathKey(value) : null))
|
||||
await serverSync()
|
||||
.refreshProviders()
|
||||
.catch(() => undefined)
|
||||
dialog.close()
|
||||
showToast({
|
||||
|
||||
@@ -88,7 +88,7 @@ export const DialogSelectModelUnpaidV2: Component<{ model?: ModelState }> = (pro
|
||||
class="w-full"
|
||||
placement="right-start"
|
||||
gutter={6}
|
||||
openDelay={0}
|
||||
delay="intent"
|
||||
contentStyle={{ "font-family": "var(--v2-font-family-sans)" }}
|
||||
value={
|
||||
<ModelTooltip
|
||||
|
||||
@@ -452,7 +452,7 @@ export function ModelSelectorPopoverV2(props: {
|
||||
class="w-full"
|
||||
placement="right-start"
|
||||
gutter={6}
|
||||
openDelay={0}
|
||||
delay="intent"
|
||||
value={
|
||||
<ModelTooltip
|
||||
model={item}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
|
||||
import { Icon } from "@opencode-ai/ui/v2/icon"
|
||||
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
|
||||
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||
import type { Prompt, ReferenceInfo } from "@opencode-ai/sdk/v2/client"
|
||||
import type { ReferenceInfo } from "@opencode-ai/sdk/v2/client"
|
||||
import { createEffect, createMemo, on, Show } from "solid-js"
|
||||
import { ModelSelectorPopoverV2 } from "@/components/dialog-select-model"
|
||||
import { DialogSelectModelUnpaidV2 } from "@/components/dialog-select-model-unpaid-v2"
|
||||
@@ -37,11 +37,9 @@ export type PromptInputV2ComposerProps = {
|
||||
class?: string
|
||||
controller: PromptInputV2ComposerController
|
||||
borderUnderlay?: boolean
|
||||
edit?: PromptInputProps["edit"]
|
||||
onEditLoaded?: PromptInputProps["onEditLoaded"]
|
||||
}
|
||||
|
||||
export type PromptInputV2ControllerProps = Omit<PromptInputProps, "class" | "edit" | "onEditLoaded" | "submission">
|
||||
export type PromptInputV2ControllerProps = Omit<PromptInputProps, "class" | "submission">
|
||||
export type PromptInputV2ComposerController = PromptInputV2Interaction & {
|
||||
readonly model: PromptInputProps["controls"]["model"]
|
||||
}
|
||||
@@ -51,9 +49,6 @@ export function PromptInputV2Composer(props: PromptInputV2ComposerProps) {
|
||||
const command = useCommand()
|
||||
const language = useLanguage()
|
||||
|
||||
useCommands(props)
|
||||
useEditHandler(props)
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-3">
|
||||
<PromptInputV2
|
||||
@@ -82,70 +77,6 @@ export function PromptInputV2Composer(props: PromptInputV2ComposerProps) {
|
||||
)
|
||||
}
|
||||
|
||||
const useEditHandler = (props: PromptInputV2ComposerProps) => {
|
||||
const prompt = usePrompt()
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => props.edit?.id,
|
||||
(id) => {
|
||||
const edit = props.edit
|
||||
if (!id || !edit) return
|
||||
prompt.context.items().forEach((item) => prompt.context.remove(item.key))
|
||||
edit.context.forEach((item) =>
|
||||
prompt.context.add({
|
||||
type: item.type,
|
||||
path: item.path,
|
||||
selection: item.selection,
|
||||
comment: item.comment,
|
||||
commentID: item.commentID,
|
||||
commentOrigin: item.commentOrigin,
|
||||
preview: item.preview,
|
||||
}),
|
||||
)
|
||||
props.controller.dispatch({ type: "mode.normal" })
|
||||
props.controller.resetHistory()
|
||||
prompt.set(edit.prompt, promptLength(edit.prompt))
|
||||
props.controller.restoreFocus()
|
||||
props.onEditLoaded?.()
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const useCommands = (props: PromptInputV2ComposerProps) => {
|
||||
const command = useCommand()
|
||||
const language = useLanguage()
|
||||
|
||||
command.register("prompt-input", () => [
|
||||
{
|
||||
id: "file.attach",
|
||||
title: language.t("prompt.action.attachFile"),
|
||||
category: language.t("command.category.file"),
|
||||
keybind: "mod+u",
|
||||
disabled: props.controller.state.mode !== "normal",
|
||||
onSelect: () => props.controller.attach(),
|
||||
},
|
||||
{
|
||||
id: "prompt.mode.shell",
|
||||
title: language.t("command.prompt.mode.shell"),
|
||||
category: language.t("command.category.session"),
|
||||
keybind: "mod+shift+x",
|
||||
disabled: props.controller.state.mode === "shell",
|
||||
onSelect: () => props.controller.dispatch({ type: "mode.shell" }),
|
||||
},
|
||||
{
|
||||
id: "prompt.mode.normal",
|
||||
title: language.t("command.prompt.mode.normal"),
|
||||
category: language.t("command.category.session"),
|
||||
keybind: "mod+shift+e",
|
||||
disabled: props.controller.state.mode === "normal",
|
||||
onSelect: () => props.controller.dispatch({ type: "mode.normal" }),
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
export function usePromptInputV2Controller(props: PromptInputV2ControllerProps): PromptInputV2ComposerController {
|
||||
const sdk = useSDK()
|
||||
const sync = useSync()
|
||||
@@ -472,6 +403,62 @@ export function usePromptInputV2Controller(props: PromptInputV2ControllerProps):
|
||||
},
|
||||
})
|
||||
Object.defineProperty(controller, "model", { get: () => props.controls.model })
|
||||
|
||||
command.register("prompt-input", () => [
|
||||
{
|
||||
id: "file.attach",
|
||||
title: language.t("prompt.action.attachFile"),
|
||||
category: language.t("command.category.file"),
|
||||
keybind: "mod+u",
|
||||
disabled: controller.state.mode !== "normal",
|
||||
onSelect: () => controller.attach(),
|
||||
},
|
||||
{
|
||||
id: "prompt.mode.shell",
|
||||
title: language.t("command.prompt.mode.shell"),
|
||||
category: language.t("command.category.session"),
|
||||
keybind: "mod+shift+x",
|
||||
disabled: controller.state.mode === "shell",
|
||||
onSelect: () => controller.dispatch({ type: "mode.shell" }),
|
||||
},
|
||||
{
|
||||
id: "prompt.mode.normal",
|
||||
title: language.t("command.prompt.mode.normal"),
|
||||
category: language.t("command.category.session"),
|
||||
keybind: "mod+shift+e",
|
||||
disabled: controller.state.mode === "normal",
|
||||
onSelect: () => controller.dispatch({ type: "mode.normal" }),
|
||||
},
|
||||
])
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => props.edit?.id,
|
||||
(id) => {
|
||||
const edit = props.edit
|
||||
if (!id || !edit) return
|
||||
prompt.context.items().forEach((item) => prompt.context.remove(item.key))
|
||||
edit.context.forEach((item) =>
|
||||
prompt.context.add({
|
||||
type: item.type,
|
||||
path: item.path,
|
||||
selection: item.selection,
|
||||
comment: item.comment,
|
||||
commentID: item.commentID,
|
||||
commentOrigin: item.commentOrigin,
|
||||
preview: item.preview,
|
||||
}),
|
||||
)
|
||||
controller.dispatch({ type: "mode.normal" })
|
||||
controller.resetHistory()
|
||||
prompt.set(edit.prompt, promptLength(edit.prompt))
|
||||
controller.restoreFocus()
|
||||
props.onEditLoaded?.()
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
return controller as PromptInputV2ComposerController
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ export const PromptContextItems: Component<ContextItemsProps> = (props) => {
|
||||
</span>
|
||||
}
|
||||
placement="top"
|
||||
openDelay={800}
|
||||
{...(!props.newLayoutDesigns ? { openDelay: 800 } : {})}
|
||||
>
|
||||
<div
|
||||
classList={{
|
||||
|
||||
@@ -52,12 +52,7 @@ export const PromptImageAttachments: Component<PromptImageAttachmentsProps> = (p
|
||||
<For each={props.comments ?? []}>
|
||||
{(item) => (
|
||||
<div class="relative group shrink-0">
|
||||
<TooltipV2
|
||||
value={item.comment}
|
||||
placement="top"
|
||||
openDelay={800}
|
||||
contentClass="max-w-[300px] break-words"
|
||||
>
|
||||
<TooltipV2 value={item.comment} placement="top" contentClass="max-w-[300px] break-words">
|
||||
<CommentCardV2
|
||||
comment={item.comment ?? ""}
|
||||
path={item.path}
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { hasNonBlockingServiceIssue, serverStatusDotClass } from "./status-popover-indicator"
|
||||
import {
|
||||
hasNonBlockingServiceIssue,
|
||||
hasServiceNeedingAttention,
|
||||
serverStatusDotClass,
|
||||
} from "./status-popover-indicator"
|
||||
|
||||
describe("serverStatusDotClass", () => {
|
||||
test("uses the success token while the server and services are healthy", () => {
|
||||
expect(serverStatusDotClass({ ready: true, serverHealth: true, issue: false })).toBe("bg-icon-success-base")
|
||||
})
|
||||
|
||||
test("uses the session attention token when a service needs attention", () => {
|
||||
expect(serverStatusDotClass({ ready: true, serverHealth: true, attention: true, issue: true })).toBe(
|
||||
"bg-v2-background-bg-accent",
|
||||
)
|
||||
})
|
||||
|
||||
test("uses the warning token for non-blocking issues while the server is online", () => {
|
||||
expect(serverStatusDotClass({ ready: true, serverHealth: true, issue: true })).toBe("bg-icon-warning-base")
|
||||
})
|
||||
@@ -34,3 +44,15 @@ describe("hasNonBlockingServiceIssue", () => {
|
||||
expect(hasNonBlockingServiceIssue({ mcp: [], lsp: ["connected"] })).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("hasServiceNeedingAttention", () => {
|
||||
test("detects MCP states that need user attention", () => {
|
||||
expect(hasServiceNeedingAttention({ mcp: ["needs_auth"] })).toBe(true)
|
||||
expect(hasServiceNeedingAttention({ mcp: ["needs_client_registration"] })).toBe(true)
|
||||
})
|
||||
|
||||
test("ignores states that do not need user attention", () => {
|
||||
expect(hasServiceNeedingAttention({ mcp: ["failed"] })).toBe(false)
|
||||
expect(hasServiceNeedingAttention({ mcp: ["connected", "pending", "disabled"] })).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { LspStatus } from "@opencode-ai/sdk/v2/client"
|
||||
import type { McpServer } from "@opencode-ai/client/promise"
|
||||
|
||||
export function hasServiceNeedingAttention(input: { mcp: Array<McpServer["status"]["status"]> }) {
|
||||
return input.mcp.some((status) => status === "needs_auth" || status === "needs_client_registration")
|
||||
}
|
||||
|
||||
export function hasNonBlockingServiceIssue(input: {
|
||||
mcp: Array<McpServer["status"]["status"]>
|
||||
lsp: Array<LspStatus["status"]>
|
||||
@@ -11,9 +15,15 @@ export function hasNonBlockingServiceIssue(input: {
|
||||
)
|
||||
}
|
||||
|
||||
export function serverStatusDotClass(input: { ready: boolean; serverHealth: boolean | undefined; issue: boolean }) {
|
||||
export function serverStatusDotClass(input: {
|
||||
ready: boolean
|
||||
serverHealth: boolean | undefined
|
||||
attention?: boolean
|
||||
issue: boolean
|
||||
}) {
|
||||
if (input.serverHealth === false) return "bg-icon-critical-base"
|
||||
if (!input.ready || input.serverHealth === undefined) return "bg-border-weak-base"
|
||||
if (input.attention) return "bg-v2-background-bg-accent"
|
||||
if (input.issue) return "bg-icon-warning-base"
|
||||
if (input.serverHealth === true) return "bg-icon-success-base"
|
||||
return "bg-border-weak-base"
|
||||
|
||||
@@ -9,7 +9,11 @@ import { ServerConnection, useServer } from "@/context/server"
|
||||
import { useServerSDK } from "@/context/server-sdk"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useGlobal } from "@/context/global"
|
||||
import { hasNonBlockingServiceIssue, serverStatusDotClass } from "./status-popover-indicator"
|
||||
import {
|
||||
hasNonBlockingServiceIssue,
|
||||
hasServiceNeedingAttention,
|
||||
serverStatusDotClass,
|
||||
} from "./status-popover-indicator"
|
||||
|
||||
const Body = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverBody })))
|
||||
const ServerBody = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverServerBody })))
|
||||
@@ -22,6 +26,11 @@ export function StatusPopover() {
|
||||
const [shown, setShown] = createSignal(false)
|
||||
const serverHealth = () => global.servers.health[server.key]?.healthy
|
||||
const ready = createMemo(() => serverHealth() === false || (sync().data.mcp_ready && sync().data.lsp_ready))
|
||||
const attention = createMemo(() =>
|
||||
hasServiceNeedingAttention({
|
||||
mcp: Object.values(sync().data.mcp ?? {}).map((item) => item.status),
|
||||
}),
|
||||
)
|
||||
const issue = createMemo(() =>
|
||||
hasNonBlockingServiceIssue({
|
||||
mcp: Object.values(sync().data.mcp ?? {}).map((item) => item.status),
|
||||
@@ -49,6 +58,7 @@ export function StatusPopover() {
|
||||
class={`absolute -top-px -right-px size-1.5 rounded-full ${serverStatusDotClass({
|
||||
ready: ready(),
|
||||
serverHealth: serverHealth(),
|
||||
attention: attention(),
|
||||
issue: issue(),
|
||||
})}`}
|
||||
/>
|
||||
@@ -85,6 +95,11 @@ function DirectoryStatusPopover() {
|
||||
const [shown, setShown] = createSignal(false)
|
||||
const serverHealth = () => global.servers.health[ServerConnection.key(server().server)]?.healthy
|
||||
const ready = createMemo(() => serverHealth() === false || (sync().data.mcp_ready && sync().data.lsp_ready))
|
||||
const attention = createMemo(() =>
|
||||
hasServiceNeedingAttention({
|
||||
mcp: Object.values(sync().data.mcp ?? {}).map((item) => item.status),
|
||||
}),
|
||||
)
|
||||
const issue = createMemo(() =>
|
||||
hasNonBlockingServiceIssue({
|
||||
mcp: Object.values(sync().data.mcp ?? {}).map((item) => item.status),
|
||||
@@ -95,6 +110,7 @@ function DirectoryStatusPopover() {
|
||||
shown: shown(),
|
||||
ready: ready(),
|
||||
serverHealth: serverHealth(),
|
||||
attention: attention(),
|
||||
issue: issue(),
|
||||
label: language.t("status.popover.trigger"),
|
||||
onOpenChange: setShown,
|
||||
@@ -118,6 +134,7 @@ function ServerStatusPopover() {
|
||||
shown: shown(),
|
||||
ready: serverHealth() !== undefined,
|
||||
serverHealth: serverHealth(),
|
||||
attention: false,
|
||||
issue: false,
|
||||
label: language.t("status.popover.trigger"),
|
||||
onOpenChange: setShown,
|
||||
@@ -135,6 +152,7 @@ type StatusPopoverState = {
|
||||
shown: boolean
|
||||
ready: boolean
|
||||
serverHealth: boolean | undefined
|
||||
attention: boolean
|
||||
issue: boolean
|
||||
label: string
|
||||
onOpenChange: (value: boolean) => void
|
||||
|
||||
@@ -12,6 +12,6 @@ export function forwardTabRef(ref: Ref<HTMLDivElement> | undefined, element: HTM
|
||||
if (typeof ref === "function") ref(element)
|
||||
}
|
||||
|
||||
export function canOpenTabRename(dragging: boolean | undefined, editing: boolean, committing: boolean) {
|
||||
return !dragging && !editing && !committing
|
||||
export function canOpenTabRename(dragging: boolean | undefined, editing: boolean, pending: boolean) {
|
||||
return !dragging && !editing && !pending
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { createEffect, createMemo, createSignal, onCleanup, Show, type Ref } from "solid-js"
|
||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
import { createMutation } from "@tanstack/solid-query"
|
||||
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
|
||||
import { useGlobal } from "@/context/global"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { ServerConnection, serverName } from "@/context/server"
|
||||
import { displayName, projectForSession } from "@/pages/layout/helpers"
|
||||
import { SessionTabAvatar } from "@/pages/layout/session-tab-avatar"
|
||||
import { showToast } from "@/utils/toast"
|
||||
import type { Session } from "@opencode-ai/sdk/v2"
|
||||
import { canOpenTabRename, forwardTabRef } from "./titlebar-tab-gesture"
|
||||
import { TabPreviewPopover } from "./titlebar-tab-popover"
|
||||
@@ -23,8 +22,7 @@ export function TabNavItem(props: {
|
||||
server: ServerConnection.Key
|
||||
session: () => Session | undefined
|
||||
fallbackTitle?: string
|
||||
onTitleChange?: (title: string) => void
|
||||
onTitleChangeFailed?: (title: string) => void
|
||||
onRename: (title: string) => Promise<void>
|
||||
onClose: () => void
|
||||
onNavigate: () => void
|
||||
active?: boolean
|
||||
@@ -34,13 +32,12 @@ export function TabNavItem(props: {
|
||||
pressed?: boolean
|
||||
hidden?: boolean
|
||||
}) {
|
||||
const language = useLanguage()
|
||||
const [editing, setEditing] = createSignal(false)
|
||||
const [titleOverflowing, setTitleOverflowing] = createSignal(false)
|
||||
let tabRoot!: HTMLDivElement
|
||||
let titleEl!: HTMLSpanElement
|
||||
let committing = false
|
||||
let measureFrame: number | undefined
|
||||
const rename = createMutation(() => ({ mutationFn: props.onRename }))
|
||||
|
||||
const closeTab = (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
@@ -116,40 +113,20 @@ export function TabNavItem(props: {
|
||||
selection?.addRange(range)
|
||||
}
|
||||
|
||||
const rename = async (title: string) => {
|
||||
const ctx = serverCtx()
|
||||
const session = props.session()
|
||||
if (!ctx || !session) return
|
||||
await ctx.sdk.api.session.rename({ sessionID: session.id, title })
|
||||
}
|
||||
|
||||
const closeRename = async (save: boolean) => {
|
||||
if (committing || !editing()) return
|
||||
committing = true
|
||||
if (rename.isPending || !editing()) return
|
||||
|
||||
const original = props.session()?.title ?? ""
|
||||
const next = (titleEl.textContent ?? "").trim()
|
||||
|
||||
titleEl.scrollLeft = 0
|
||||
if (save && next && next !== original) props.onTitleChange?.(next)
|
||||
setEditing(false)
|
||||
|
||||
if (!save || !next || next === original) {
|
||||
committing = false
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await rename(next)
|
||||
} catch (err) {
|
||||
props.onTitleChangeFailed?.(original)
|
||||
showToast({
|
||||
title: language.t("common.requestFailed"),
|
||||
description: err instanceof Error ? err.message : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
committing = false
|
||||
await rename.mutateAsync(next)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
@@ -163,7 +140,7 @@ export function TabNavItem(props: {
|
||||
const openRename = (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (!canOpenTabRename(props.dragging, editing(), committing)) return
|
||||
if (!canOpenTabRename(props.dragging, editing(), rename.isPending)) return
|
||||
const session = props.session()
|
||||
if (!session) return
|
||||
titleEl.textContent = session.title
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useCommand } from "@/context/command"
|
||||
import { useTabs } from "@/context/tabs"
|
||||
import { createTabPromptState } from "@/context/prompt"
|
||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
import { showToast } from "@/utils/toast"
|
||||
import { canStartTabDrag, isTabCloseTarget } from "./titlebar-tab-gesture"
|
||||
|
||||
function SessionTabSlot(props: {
|
||||
@@ -52,6 +53,25 @@ function SessionTabSlot(props: {
|
||||
const missingSession = createMemo(() => !!props.serverCtx() && !loadedSession.loading && !session())
|
||||
let prefetched = false
|
||||
|
||||
const rename = async (title: string) => {
|
||||
const value = session()
|
||||
const ctx = props.serverCtx()
|
||||
if (!value || !ctx) return
|
||||
|
||||
ctx.sync.session.remember({ ...value, title })
|
||||
try {
|
||||
await ctx.sdk.api.session.rename({ sessionID: value.id, title })
|
||||
} catch (err) {
|
||||
const current = session()
|
||||
const currentCtx = props.serverCtx()
|
||||
if (current && currentCtx) currentCtx.sync.session.remember({ ...current, title: value.title })
|
||||
showToast({
|
||||
title: language.t("common.requestFailed"),
|
||||
description: err instanceof Error ? err.message : undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
const ctx = props.serverCtx()
|
||||
const value = session()
|
||||
@@ -99,16 +119,7 @@ function SessionTabSlot(props: {
|
||||
server={props.tab.server}
|
||||
session={session}
|
||||
fallbackTitle={persisted()?.title ?? (missingSession() ? language.t("session.tab.unknown") : undefined)}
|
||||
onTitleChange={(title) => {
|
||||
const value = session()
|
||||
const ctx = props.serverCtx()
|
||||
if (value && ctx) ctx.sync.session.remember({ ...value, title })
|
||||
}}
|
||||
onTitleChangeFailed={(title) => {
|
||||
const value = session()
|
||||
const ctx = props.serverCtx()
|
||||
if (value && ctx) ctx.sync.session.remember({ ...value, title })
|
||||
}}
|
||||
onRename={rename}
|
||||
onNavigate={() => props.onNavigate(ref)}
|
||||
onClose={props.onClose}
|
||||
active={props.active()}
|
||||
|
||||
@@ -344,16 +344,7 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
|
||||
title: "",
|
||||
keybind: `mod+option+ArrowLeft,ctrl+shift+tab`,
|
||||
hidden: true,
|
||||
onSelect: () => {
|
||||
let index = tabsStore.findIndex((tab) => tab === currentTab())
|
||||
if (index === -1) return
|
||||
|
||||
index -= 1
|
||||
if (index === -1) index = tabsStore.length - 1
|
||||
|
||||
const next = tabsStore[index]
|
||||
if (next) tabs.select(next)
|
||||
},
|
||||
onSelect: tabs.previous,
|
||||
},
|
||||
{
|
||||
id: `tab.next`,
|
||||
@@ -361,16 +352,7 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
|
||||
title: "",
|
||||
keybind: `mod+option+ArrowRight,ctrl+tab`,
|
||||
hidden: true,
|
||||
onSelect: () => {
|
||||
let index = tabsStore.findIndex((tab) => tab === currentTab())
|
||||
if (index === -1) return
|
||||
|
||||
index += 1
|
||||
if (index === tabsStore.length) index = 0
|
||||
|
||||
const next = tabsStore[index]
|
||||
if (next) tabs.select(next)
|
||||
},
|
||||
onSelect: tabs.next,
|
||||
},
|
||||
].filter((v) => v !== undefined)
|
||||
})
|
||||
|
||||
@@ -103,7 +103,6 @@ describe("normalizeProviderList", () => {
|
||||
)
|
||||
|
||||
expect(result.connected).toEqual(["openai"])
|
||||
expect(result.defaultModel).toEqual({ providerID: "openai", modelID: "gpt-5" })
|
||||
expect(result.default).toEqual({ openai: "gpt-5" })
|
||||
expect(result.all.get("openai")?.models["gpt-old"]).toBeUndefined()
|
||||
expect(result.all.get("openai")?.models["gpt-5"]).toMatchObject({
|
||||
@@ -114,10 +113,6 @@ describe("normalizeProviderList", () => {
|
||||
variants: { high: {} },
|
||||
})
|
||||
})
|
||||
|
||||
test("preserves an empty current default", () => {
|
||||
expect(normalizeProviderList([] as ProviderListOutput["data"], [], null).defaultModel).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("directoryKey", () => {
|
||||
|
||||
@@ -139,7 +139,6 @@ export function normalizeProviderList(
|
||||
return {
|
||||
all,
|
||||
connected: providers.map((provider) => provider.id),
|
||||
defaultModel: defaultModel ? { providerID: defaultModel.providerID, modelID: defaultModel.id } : null,
|
||||
default: Object.fromEntries(
|
||||
providers.flatMap((provider) => {
|
||||
const model =
|
||||
|
||||
@@ -6,7 +6,6 @@ import { createStore } from "solid-js/store"
|
||||
import { useModels } from "@/context/models"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { useProviders } from "@/hooks/use-providers"
|
||||
import { resolveDefaultModel } from "@/hooks/provider-catalog"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { hasCustomAgent, resolveAgent } from "./local-agent"
|
||||
import { cycleModelVariant, getConfiguredAgentVariant, resolveModelVariant } from "./model-variant"
|
||||
@@ -150,8 +149,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
})
|
||||
|
||||
const configuredModel = () => {
|
||||
const model = resolveDefaultModel(providers.defaultModel(), sync().data.config.model)
|
||||
if (!model) return
|
||||
const configured = sync().data.config.model
|
||||
if (!configured) return
|
||||
const [providerID, modelID] = configured.split("/")
|
||||
const model = { providerID, modelID }
|
||||
if (validModel(model)) return model
|
||||
}
|
||||
|
||||
|
||||
@@ -290,6 +290,10 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const homeSessions = createHomeSessionIndexCache(queryClient, ServerConnection.key(serverSDK.server))
|
||||
const refreshProviders = () =>
|
||||
queryClient.refetchQueries({
|
||||
predicate: (query) => query.queryKey[0] === serverSDK.scope && query.queryKey[2] === "providers",
|
||||
})
|
||||
|
||||
let bootedAt = 0
|
||||
let bootingRoot = false
|
||||
@@ -537,6 +541,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
||||
homeSessions.apply(event)
|
||||
}
|
||||
homeSessions.refresh(event.type)
|
||||
if (eventType === "integration.connection.updated") void refreshProviders()
|
||||
|
||||
if (directory === "global") {
|
||||
if (eventType === "server.connected" && activeSessionsQuery.data === undefined && !activeSessionsQuery.isFetching)
|
||||
@@ -678,6 +683,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
||||
peek: children.peek,
|
||||
disableMcp: children.disableMcp,
|
||||
queryOptions: queryOptionsApi,
|
||||
refreshProviders,
|
||||
// bootstrap,
|
||||
updateConfig: updateConfigMutation.mutateAsync,
|
||||
project: projectApi,
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { nextTab, previousTab, rememberTab, type TabHistory } from "./tab-history"
|
||||
|
||||
function history(): TabHistory {
|
||||
return { stack: [], index: -1 }
|
||||
}
|
||||
|
||||
describe("tab history", () => {
|
||||
test("moves backward and forward through selected tabs", () => {
|
||||
const selected = ["a", "b", "c"].reduce(rememberTab, history())
|
||||
const available = new Set(selected.stack)
|
||||
|
||||
const previous = previousTab(selected, available)
|
||||
expect(previous?.key).toBe("b")
|
||||
|
||||
const first = previousTab(previous!.state, available)
|
||||
expect(first?.key).toBe("a")
|
||||
|
||||
const next = nextTab(first!.state, available)
|
||||
expect(next?.key).toBe("b")
|
||||
})
|
||||
|
||||
test("replaces forward history after a new selection", () => {
|
||||
const selected = ["a", "b", "c"].reduce(rememberTab, history())
|
||||
const previous = previousTab(selected, new Set(selected.stack))
|
||||
const next = rememberTab(previous!.state, "d")
|
||||
|
||||
expect(next).toEqual({ stack: ["a", "b", "d"], index: 2 })
|
||||
expect(nextTab(next, new Set(next.stack))).toBeUndefined()
|
||||
})
|
||||
|
||||
test("skips tabs that are no longer open", () => {
|
||||
const selected = ["a", "b", "c"].reduce(rememberTab, history())
|
||||
|
||||
expect(previousTab(selected, new Set(["a", "c"]))?.key).toBe("a")
|
||||
})
|
||||
|
||||
test("skips a repeated current tab after closing the previous selection", () => {
|
||||
const selected = ["a", "b", "c", "b"].reduce(rememberTab, history())
|
||||
|
||||
expect(previousTab(selected, new Set(["a", "b"]))?.key).toBe("a")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,28 @@
|
||||
const MAX_TAB_HISTORY = 100
|
||||
|
||||
export type TabHistory = {
|
||||
stack: string[]
|
||||
index: number
|
||||
}
|
||||
|
||||
export function rememberTab(state: TabHistory, key: string): TabHistory {
|
||||
if (state.stack[state.index] === key) return state
|
||||
const stack = state.stack.slice(0, state.index + 1).concat(key).slice(-MAX_TAB_HISTORY)
|
||||
return { stack, index: stack.length - 1 }
|
||||
}
|
||||
|
||||
export function previousTab(state: TabHistory, available: Set<string>) {
|
||||
return move(state, -1, available)
|
||||
}
|
||||
|
||||
export function nextTab(state: TabHistory, available: Set<string>) {
|
||||
return move(state, 1, available)
|
||||
}
|
||||
|
||||
function move(state: TabHistory, offset: -1 | 1, available: Set<string>) {
|
||||
const current = state.stack[state.index]
|
||||
for (let index = state.index + offset; index >= 0 && index < state.stack.length; index += offset) {
|
||||
const key = state.stack[index]
|
||||
if (key && key !== current && available.has(key)) return { state: { ...state, index }, key }
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { sessionHref } from "@/utils/session-route"
|
||||
import { createTabMemory } from "./tab-memory"
|
||||
import { nextTabAfterClose, pushClosedTab, removeClosedTabs, takeClosedTab, type ClosedTab } from "./closed-tabs"
|
||||
import { createDraftPromptSession, type PromptModel } from "./prompt-state"
|
||||
import { nextTab, previousTab, rememberTab, type TabHistory } from "./tab-history"
|
||||
|
||||
export type SessionTab = {
|
||||
type: "session"
|
||||
@@ -79,6 +80,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
||||
const memory = createTabMemory(getOwner())
|
||||
|
||||
const closing = new Set<string>()
|
||||
let history: TabHistory = { stack: [], index: -1 }
|
||||
let recentWrite = 0
|
||||
let recentValue: string | undefined
|
||||
|
||||
@@ -150,10 +152,21 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
||||
|
||||
const navigateTab = (tab: Tab) => {
|
||||
const href = tabHref(tab)
|
||||
history = rememberTab(history, tabKey(tab))
|
||||
setRecentKey(tabKey(tab))
|
||||
navigate(href)
|
||||
}
|
||||
|
||||
const moveHistory = (direction: "previous" | "next") => {
|
||||
const available = new Set(store.map(tabKey))
|
||||
const result = direction === "previous" ? previousTab(history, available) : nextTab(history, available)
|
||||
if (!result) return
|
||||
const tab = store.find((item) => tabKey(item) === result.key)
|
||||
if (!tab) return
|
||||
history = result.state
|
||||
navigateTab(tab)
|
||||
}
|
||||
|
||||
const removeTab = (index: number) => {
|
||||
const tab = store[index]
|
||||
if (!tab) return
|
||||
@@ -359,8 +372,11 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
|
||||
select: navigateTab,
|
||||
remember(tab: Tab) {
|
||||
const key = tabKey(tab)
|
||||
history = rememberTab(history, key)
|
||||
if (recentKey() !== key) setRecentKey(key)
|
||||
},
|
||||
previous: () => moveHistory("previous"),
|
||||
next: () => moveHistory("next"),
|
||||
toggleHome(input: { home: boolean; current?: Tab }) {
|
||||
if (input.home) {
|
||||
const tab = store.find((tab) => tabKey(tab) === recentKey())
|
||||
|
||||
@@ -2,6 +2,17 @@ import { describe, expect, test } from "bun:test"
|
||||
import { DESKTOP_MENU } from "./desktop-menu"
|
||||
|
||||
describe("desktop menu", () => {
|
||||
test("navigates between tabs", () => {
|
||||
const items = DESKTOP_MENU.flatMap((menu) => menu.items ?? []).filter(
|
||||
(item) => item.type === "item" && (item.label === "Previous Tab" || item.label === "Next Tab"),
|
||||
)
|
||||
|
||||
expect(items).toEqual([
|
||||
{ type: "item", label: "Previous Tab", command: "tab.prev", accelerator: { macos: "Option+Up" } },
|
||||
{ type: "item", label: "Next Tab", command: "tab.next", accelerator: { macos: "Option+Down" } },
|
||||
])
|
||||
})
|
||||
|
||||
test("exports logs through the desktop command registry", () => {
|
||||
const items = DESKTOP_MENU.flatMap((menu) => menu.items ?? []).filter(
|
||||
(item) => item.type === "item" && item.label === "Export Logs...",
|
||||
|
||||
@@ -168,8 +168,8 @@ export const DESKTOP_MENU: DesktopMenu[] = [
|
||||
{ type: "item", label: "Back", command: "common.goBack", accelerator: { macos: "Cmd+[" } },
|
||||
{ type: "item", label: "Forward", command: "common.goForward", accelerator: { macos: "Cmd+]" } },
|
||||
{ type: "separator" },
|
||||
{ type: "item", label: "Previous Session", command: "session.previous", accelerator: { macos: "Option+Up" } },
|
||||
{ type: "item", label: "Next Session", command: "session.next", accelerator: { macos: "Option+Down" } },
|
||||
{ type: "item", label: "Previous Tab", command: "tab.prev", accelerator: { macos: "Option+Up" } },
|
||||
{ type: "item", label: "Next Tab", command: "tab.next", accelerator: { macos: "Option+Down" } },
|
||||
{ type: "separator" },
|
||||
{
|
||||
type: "item",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
|
||||
import { resolveDefaultModel, selectProviderCatalog } from "./provider-catalog"
|
||||
import { selectProviderCatalog } from "./provider-catalog"
|
||||
|
||||
const catalog = (id: string): NormalizedProviderListResponse => ({
|
||||
all: new Map([[id, { id, name: id, source: "api", env: [], options: {}, models: {} }]]),
|
||||
@@ -57,21 +57,3 @@ test("falls back to the global catalog for route consumers", () => {
|
||||
}),
|
||||
).toBe(global)
|
||||
})
|
||||
|
||||
test("uses the current server default model", () => {
|
||||
expect(resolveDefaultModel({ providerID: "openai", modelID: "gpt-5" }, "anthropic/claude")).toEqual({
|
||||
providerID: "openai",
|
||||
modelID: "gpt-5",
|
||||
})
|
||||
})
|
||||
|
||||
test("does not use legacy config when the current server has no default", () => {
|
||||
expect(resolveDefaultModel(null, "anthropic/claude")).toBeUndefined()
|
||||
})
|
||||
|
||||
test("uses config for legacy servers", () => {
|
||||
expect(resolveDefaultModel(undefined, "anthropic/claude")).toEqual({
|
||||
providerID: "anthropic",
|
||||
modelID: "claude",
|
||||
})
|
||||
})
|
||||
|
||||
@@ -25,13 +25,3 @@ export function selectProviderCatalog(input: ProviderCatalogInput) {
|
||||
if (input.explicit) return emptyProviderCatalog
|
||||
return input.global
|
||||
}
|
||||
|
||||
export function resolveDefaultModel(
|
||||
current: NormalizedProviderListResponse["defaultModel"],
|
||||
legacy: string | undefined,
|
||||
) {
|
||||
if (current !== undefined) return current ?? undefined
|
||||
if (!legacy) return undefined
|
||||
const [providerID, modelID] = legacy.split("/")
|
||||
return { providerID, modelID }
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ export function useProviders(directory: Accessor<string | undefined>) {
|
||||
return {
|
||||
all: () => providers().all,
|
||||
default: () => providers().default,
|
||||
defaultModel: () => providers().defaultModel,
|
||||
popular: () =>
|
||||
pipe(
|
||||
providers().all,
|
||||
|
||||
@@ -360,25 +360,26 @@ function HomeProjectSlot(
|
||||
index: () => number
|
||||
},
|
||||
) {
|
||||
const project = createMemo(() => props.items.find((item) => item.worktree === props.worktree))
|
||||
const initial = props.items.find((item) => item.worktree === props.worktree)
|
||||
if (!initial) return
|
||||
const project = createMemo<LocalProject>(
|
||||
(previous) => props.items.find((item) => item.worktree === props.worktree) ?? previous,
|
||||
initial,
|
||||
)
|
||||
|
||||
return (
|
||||
<Show when={project()}>
|
||||
{(item) => (
|
||||
<HomeProjectRow
|
||||
{...props}
|
||||
project={item()}
|
||||
server={props.server}
|
||||
index={props.index}
|
||||
serverSelected={props.selection().server === ServerConnection.key(props.server)}
|
||||
selected={
|
||||
props.selection().server === ServerConnection.key(props.server) &&
|
||||
props.selection().directory === props.worktree
|
||||
}
|
||||
unseen={props.unseenCount(props.server, item())}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
<HomeProjectRow
|
||||
{...props}
|
||||
project={project()}
|
||||
server={props.server}
|
||||
index={props.index}
|
||||
serverSelected={props.selection().server === ServerConnection.key(props.server)}
|
||||
selected={
|
||||
props.selection().server === ServerConnection.key(props.server) &&
|
||||
props.selection().directory === props.worktree
|
||||
}
|
||||
unseen={props.unseenCount(props.server, project())}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,14 @@ export default function NewSessionPage() {
|
||||
keybind: "ctrl+l",
|
||||
onSelect: () => promptInputV2Controller.restoreFocus(),
|
||||
},
|
||||
{
|
||||
id: "project.select",
|
||||
title: language.t("session.new.project.search"),
|
||||
category: language.t("command.category.project"),
|
||||
keybind: "mod+shift+o",
|
||||
disabled: projectController.empty(),
|
||||
onSelect: () => projectController.setOpen(true),
|
||||
},
|
||||
])
|
||||
|
||||
createEffect(() => {
|
||||
@@ -263,7 +271,6 @@ function ProviderTip(props: { ready: () => boolean; connected: () => boolean; op
|
||||
<TooltipV2
|
||||
class="hover-reveal absolute left-full top-0 flex h-6 w-7 items-center justify-end delay-0 duration-0 group-hover/provider-tip:delay-[250ms] group-hover/provider-tip:duration-150 group-hover/provider-tip:opacity-100 focus-within:delay-0 focus-within:duration-0 focus-within:opacity-100"
|
||||
placement="top"
|
||||
openDelay={1000}
|
||||
value={language.t("common.dismiss")}
|
||||
>
|
||||
<button
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
batch,
|
||||
ErrorBoundary,
|
||||
onCleanup,
|
||||
Suspense,
|
||||
Show,
|
||||
Match,
|
||||
Switch,
|
||||
@@ -2217,6 +2218,10 @@ export default function Page() {
|
||||
comments.clear()
|
||||
resumeScroll()
|
||||
},
|
||||
get edit() {
|
||||
return editingFollowup()
|
||||
},
|
||||
onEditLoaded: clearFollowupEdit,
|
||||
shouldQueue: queueEnabled,
|
||||
onQueue: queueFollowup,
|
||||
onAbort: () => {
|
||||
@@ -2225,14 +2230,7 @@ export default function Page() {
|
||||
setFollowup("paused", id, true)
|
||||
},
|
||||
})
|
||||
return (
|
||||
<PromptInputV2Composer
|
||||
controller={controller}
|
||||
borderUnderlay
|
||||
edit={editingFollowup()}
|
||||
onEditLoaded={clearFollowupEdit}
|
||||
/>
|
||||
)
|
||||
return <PromptInputV2Composer controller={controller} borderUnderlay />
|
||||
}}
|
||||
</Show>
|
||||
}
|
||||
@@ -2300,49 +2298,53 @@ export default function Page() {
|
||||
</div>
|
||||
|
||||
<Show when={!newSessionDesign() && desktopSidePanelOpen()}>
|
||||
<SessionSidePanel
|
||||
canReview={canReview}
|
||||
diffs={reviewDiffs}
|
||||
diffsReady={reviewReady}
|
||||
empty={reviewEmptyText}
|
||||
hasReview={hasReview}
|
||||
reviewHasFocusableContent={hasReview}
|
||||
reviewCount={reviewCount}
|
||||
reviewPanel={reviewPanel}
|
||||
activeDiff={activeReviewFile()}
|
||||
focusReviewDiff={focusReviewDiff}
|
||||
reviewSnap={ui.reviewSnap}
|
||||
size={size}
|
||||
/>
|
||||
<Suspense>
|
||||
<SessionSidePanel
|
||||
canReview={canReview}
|
||||
diffs={reviewDiffs}
|
||||
diffsReady={reviewReady}
|
||||
empty={reviewEmptyText}
|
||||
hasReview={hasReview}
|
||||
reviewHasFocusableContent={hasReview}
|
||||
reviewCount={reviewCount}
|
||||
reviewPanel={reviewPanel}
|
||||
activeDiff={activeReviewFile()}
|
||||
focusReviewDiff={focusReviewDiff}
|
||||
reviewSnap={ui.reviewSnap}
|
||||
size={size}
|
||||
/>
|
||||
</Suspense>
|
||||
</Show>
|
||||
<Show when={newSessionDesign()}>
|
||||
<Show when={isDesktop() ? desktopV2PanelLayout().visible : terminalOpen()}>
|
||||
<div class="min-w-0 h-full flex flex-1 flex-col">
|
||||
<Show when={isDesktop() && (desktopV2ReviewOpen() || desktopFileTreeOpen())}>
|
||||
<div class="min-h-0 flex-1">
|
||||
<SessionSidePanel
|
||||
canReview={canReview}
|
||||
diffs={reviewDiffs}
|
||||
diffsReady={reviewReady}
|
||||
empty={reviewEmptyText}
|
||||
hasReview={hasReview}
|
||||
reviewHasFocusableContent={() => hasReview() || reviewV2State.sidebarOpened()}
|
||||
reviewCount={reviewCount}
|
||||
reviewPanel={reviewPanelV2}
|
||||
reviewSidebarToggle={(disabled) => (
|
||||
<SessionReviewV2SidebarToggle
|
||||
opened={reviewV2State.sidebarOpened()}
|
||||
disabled={disabled}
|
||||
onToggle={reviewV2State.toggleSidebar}
|
||||
/>
|
||||
)}
|
||||
fileBrowserState={reviewV2State}
|
||||
activeDiff={activeReviewFile()}
|
||||
focusReviewDiff={focusReviewDiff}
|
||||
reviewSnap={ui.reviewSnap}
|
||||
size={size}
|
||||
stacked={desktopV2PanelLayout().stacked}
|
||||
/>
|
||||
<Suspense>
|
||||
<SessionSidePanel
|
||||
canReview={canReview}
|
||||
diffs={reviewDiffs}
|
||||
diffsReady={reviewReady}
|
||||
empty={reviewEmptyText}
|
||||
hasReview={hasReview}
|
||||
reviewHasFocusableContent={() => hasReview() || reviewV2State.sidebarOpened()}
|
||||
reviewCount={reviewCount}
|
||||
reviewPanel={reviewPanelV2}
|
||||
reviewSidebarToggle={(disabled) => (
|
||||
<SessionReviewV2SidebarToggle
|
||||
opened={reviewV2State.sidebarOpened()}
|
||||
disabled={disabled}
|
||||
onToggle={reviewV2State.toggleSidebar}
|
||||
/>
|
||||
)}
|
||||
fileBrowserState={reviewV2State}
|
||||
activeDiff={activeReviewFile()}
|
||||
focusReviewDiff={focusReviewDiff}
|
||||
reviewSnap={ui.reviewSnap}
|
||||
size={size}
|
||||
stacked={desktopV2PanelLayout().stacked}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={desktopV2PanelLayout().stacked}>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { usePrompt } from "@/context/prompt"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useProviders } from "@/hooks/use-providers"
|
||||
import { resolveDefaultModel } from "@/hooks/provider-catalog"
|
||||
|
||||
export function createPromptModelSelection(input: { agent: () => { model?: ModelKey; variant?: string } | undefined }) {
|
||||
const sdk = useSDK()
|
||||
@@ -22,8 +21,10 @@ export function createPromptModelSelection(input: { agent: () => { model?: Model
|
||||
}
|
||||
|
||||
const configured = () => {
|
||||
const model = resolveDefaultModel(providers.defaultModel(), sync().data.config.model)
|
||||
if (!model) return
|
||||
const value = sync().data.config.model
|
||||
if (!value) return
|
||||
const [providerID, modelID] = value.split("/")
|
||||
const model = { providerID, modelID }
|
||||
if (valid(model)) return model
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/cli",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/codemode",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"description": "Effect-native confined code execution over schema-described tools",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -376,7 +376,6 @@
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
@media (max-width: 40rem) {
|
||||
position: static;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-support",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"name": "@opencode-ai/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -112,7 +112,7 @@ Every field is optional.
|
||||
"type": "local",
|
||||
"command": ["npx", "-y", "@playwright/mcp"],
|
||||
"enabled": true,
|
||||
"env": {}
|
||||
"environment": {}
|
||||
},
|
||||
"remote-thing": {
|
||||
"type": "remote",
|
||||
@@ -371,7 +371,7 @@ Special object-shaped (not callbacks): `tool: { my_tool: { ... } }`,
|
||||
"type": "local",
|
||||
"command": ["npx", "-y", "@playwright/mcp"],
|
||||
"enabled": true,
|
||||
"env": { "BROWSER": "chromium" }
|
||||
"environment": { "BROWSER": "chromium" }
|
||||
},
|
||||
"github": {
|
||||
"type": "remote",
|
||||
@@ -384,7 +384,8 @@ Special object-shaped (not callbacks): `tool: { my_tool: { ... } }`,
|
||||
}
|
||||
```
|
||||
|
||||
`command` is an array of strings. `type` is required. Use `enabled: false` to
|
||||
`command` is an array of strings. `environment` sets environment variables for
|
||||
a local MCP server. `type` is required. Use `enabled: false` to
|
||||
disable a server inherited from a parent config. String values such as header
|
||||
tokens support `{env:VAR}` interpolation (and `{file:path}`); the shell-style
|
||||
`${VAR}` is not substituted.
|
||||
|
||||
@@ -24,6 +24,8 @@ for (const channel of channels) {
|
||||
expect(config.extraMetadata?.desktopName).toBe(`${channel.appId}.desktop`)
|
||||
expect(config.linux?.executableName).toBe(channel.appId)
|
||||
expect(config.linux?.desktop?.entry?.StartupWMClass).toBe(channel.appId)
|
||||
expect(config.deb?.fpm).toContainEqual(expect.stringContaining(`/usr/share/metainfo/${channel.appId}.metainfo.xml`))
|
||||
expect(config.rpm?.fpm).toContainEqual(expect.stringContaining(`/usr/share/metainfo/${channel.appId}.metainfo.xml`))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -37,8 +39,16 @@ test("keeps a hidden prod launcher for old Linux pins", async () => {
|
||||
if (previous === undefined) delete process.env.OPENCODE_CHANNEL
|
||||
else process.env.OPENCODE_CHANNEL = previous
|
||||
|
||||
expect(config.deb?.fpm?.[0]).toEndWith(`${legacyDesktopEntry}=/usr/share/applications/opencode-desktop.desktop`)
|
||||
expect(config.rpm?.fpm?.[0]).toEndWith(`${legacyDesktopEntry}=/usr/share/applications/opencode-desktop.desktop`)
|
||||
expect(
|
||||
config.deb?.fpm?.some((entry) =>
|
||||
entry.endsWith("opencode-desktop.desktop=/usr/share/applications/opencode-desktop.desktop"),
|
||||
),
|
||||
).toBe(true)
|
||||
expect(
|
||||
config.rpm?.fpm?.some((entry) =>
|
||||
entry.endsWith("opencode-desktop.desktop=/usr/share/applications/opencode-desktop.desktop"),
|
||||
),
|
||||
).toBe(true)
|
||||
|
||||
const desktop = await Bun.file(legacyDesktopEntry).text()
|
||||
expect(desktop).toContain("Exec=/opt/OpenCode/ai.opencode.desktop %U")
|
||||
|
||||
@@ -15,6 +15,9 @@ const signScript = path.join(rootDir, "script", "sign-windows.ps1")
|
||||
const legacyDesktopEntry = path.join(packageDir, "resources", "linux", "opencode-desktop.desktop")
|
||||
const legacyDesktopEntryFpm = `${legacyDesktopEntry}=/usr/share/applications/opencode-desktop.desktop`
|
||||
|
||||
const metainfoFpm = (appId: string) =>
|
||||
`${path.join(packageDir, "resources", `${appId}.metainfo.xml`)}=/usr/share/metainfo/${appId}.metainfo.xml`
|
||||
|
||||
async function signWindows(configuration: { path: string }) {
|
||||
if (process.platform !== "win32") return
|
||||
if (process.env.GITHUB_ACTIONS !== "true") return
|
||||
@@ -116,7 +119,8 @@ function getConfig() {
|
||||
...base,
|
||||
appId,
|
||||
productName: "OpenCode Dev",
|
||||
rpm: { packageName: "opencode-dev" },
|
||||
deb: { fpm: [metainfoFpm(appId)] },
|
||||
rpm: { packageName: "opencode-dev", fpm: [metainfoFpm(appId)] },
|
||||
}
|
||||
}
|
||||
case "beta": {
|
||||
@@ -126,7 +130,8 @@ function getConfig() {
|
||||
productName: "OpenCode Beta",
|
||||
protocols: { name: "OpenCode Beta", schemes: ["opencode"] },
|
||||
publish: { provider: "github", owner: "anomalyco", repo: "opencode-beta", channel: "latest" },
|
||||
rpm: { packageName: "opencode-beta" },
|
||||
deb: { fpm: [metainfoFpm(appId)] },
|
||||
rpm: { packageName: "opencode-beta", fpm: [metainfoFpm(appId)] },
|
||||
}
|
||||
}
|
||||
case "prod": {
|
||||
@@ -136,8 +141,8 @@ function getConfig() {
|
||||
productName: "OpenCode",
|
||||
protocols: { name: "OpenCode", schemes: ["opencode"] },
|
||||
publish: { provider: "github", owner: "anomalyco", repo: "opencode", channel: "latest" },
|
||||
deb: { fpm: [legacyDesktopEntryFpm] },
|
||||
rpm: { packageName: "opencode", fpm: [legacyDesktopEntryFpm] },
|
||||
deb: { fpm: [metainfoFpm(appId), legacyDesktopEntryFpm] },
|
||||
rpm: { packageName: "opencode", fpm: [metainfoFpm(appId), legacyDesktopEntryFpm] },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
|
||||
@@ -214,7 +214,6 @@ function createSidecarEnv(): Record<string, string> {
|
||||
)
|
||||
delete env.DEBUG
|
||||
if (process.platform === "linux") delete env.LD_PRELOAD
|
||||
if (!app.isPackaged) env.OPENCODE_DISABLE_CHANNEL_DB = "1"
|
||||
return env
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"name": "@opencode-ai/effect-sqlite-node",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"description": "Record and replay Effect HTTP client traffic with deterministic cassettes",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"name": "@opencode-ai/llm",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/server",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/session-ui",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -5,10 +5,6 @@ import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
|
||||
|
||||
export type NormalizedProviderListResponse = {
|
||||
all: Map<string, Provider>
|
||||
defaultModel?: {
|
||||
providerID: string
|
||||
modelID: string
|
||||
} | null
|
||||
default: {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ export function CommentCardV2(props: {
|
||||
return (
|
||||
<TooltipV2
|
||||
placement="top"
|
||||
openDelay={1000}
|
||||
value={props.title ?? props.comment}
|
||||
disabled={!props.tooltip || !truncated()}
|
||||
class={props.wide ? "w-full" : undefined}
|
||||
|
||||
@@ -385,12 +385,7 @@ export function PromptInputV2Attachments(props: {
|
||||
<For each={props.comments ?? []}>
|
||||
{(comment) => (
|
||||
<div class="relative group shrink-0">
|
||||
<TooltipV2
|
||||
value={comment.comment}
|
||||
placement="top"
|
||||
openDelay={800}
|
||||
contentClass="max-w-[300px] break-words"
|
||||
>
|
||||
<TooltipV2 value={comment.comment} placement="top" contentClass="max-w-[300px] break-words">
|
||||
<CommentCardV2
|
||||
comment={comment.comment ?? ""}
|
||||
path={comment.path}
|
||||
|
||||
@@ -214,7 +214,6 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
|
||||
</Show>
|
||||
<div class="flex items-center">
|
||||
<TooltipV2
|
||||
openDelay={2000}
|
||||
inactive={!prev()}
|
||||
value={
|
||||
<>
|
||||
@@ -234,7 +233,6 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
|
||||
/>
|
||||
</TooltipV2>
|
||||
<TooltipV2
|
||||
openDelay={2000}
|
||||
inactive={!next()}
|
||||
value={
|
||||
<>
|
||||
@@ -268,12 +266,12 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
|
||||
class="session-review-v2-segmented-control session-review-v2-segmented-control--icon"
|
||||
aria-label={i18n.t("ui.sessionReviewV2.expandMode")}
|
||||
>
|
||||
<TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.showAllLines")}>
|
||||
<TooltipV2 value={i18n.t("ui.sessionReviewV2.showAllLines")}>
|
||||
<SegmentedControlItemV2 value="expand" aria-label={i18n.t("ui.sessionReviewV2.showAllLines")}>
|
||||
<Icon name="expand" />
|
||||
</SegmentedControlItemV2>
|
||||
</TooltipV2>
|
||||
<TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.hideNonDiffLines")}>
|
||||
<TooltipV2 value={i18n.t("ui.sessionReviewV2.hideNonDiffLines")}>
|
||||
<SegmentedControlItemV2 value="collapse" aria-label={i18n.t("ui.sessionReviewV2.hideNonDiffLines")}>
|
||||
<Icon name="collapse" />
|
||||
</SegmentedControlItemV2>
|
||||
@@ -289,12 +287,12 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
|
||||
class="session-review-v2-segmented-control session-review-v2-segmented-control--icon"
|
||||
aria-label={i18n.t("ui.sessionReviewV2.diffView")}
|
||||
>
|
||||
<TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.unifiedDiff")}>
|
||||
<TooltipV2 value={i18n.t("ui.sessionReviewV2.unifiedDiff")}>
|
||||
<SegmentedControlItemV2 value="unified" aria-label={i18n.t("ui.sessionReviewV2.unifiedDiff")}>
|
||||
<Icon name="unified" />
|
||||
</SegmentedControlItemV2>
|
||||
</TooltipV2>
|
||||
<TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.splitDiff")}>
|
||||
<TooltipV2 value={i18n.t("ui.sessionReviewV2.splitDiff")}>
|
||||
<SegmentedControlItemV2 value="split" aria-label={i18n.t("ui.sessionReviewV2.splitDiff")}>
|
||||
<Icon name="split" />
|
||||
</SegmentedControlItemV2>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/stats-app",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/stats-core",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/stats-server",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/tui",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -104,7 +104,6 @@ const appGlobalBindingCommands = [
|
||||
] as const
|
||||
|
||||
const appBindingCommands = [
|
||||
"command.palette.show",
|
||||
"model.list",
|
||||
"model.cycle_recent",
|
||||
"model.cycle_recent_reverse",
|
||||
@@ -963,6 +962,11 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
|
||||
commands: appCommands(),
|
||||
}))
|
||||
|
||||
useBindings(() => ({
|
||||
enabled: () => dialog.stack.length === 0,
|
||||
bindings: tuiConfig.keybinds.get(COMMAND_PALETTE_COMMAND),
|
||||
}))
|
||||
|
||||
useBindings(() => ({
|
||||
mode: OPENCODE_BASE_MODE,
|
||||
bindings: tuiConfig.keybinds.gather("app", appBindingCommands),
|
||||
|
||||
@@ -5,7 +5,13 @@ import { testRender, useRenderer } from "@opentui/solid"
|
||||
import { expect, test } from "bun:test"
|
||||
import { onCleanup } from "solid-js"
|
||||
import { TuiKeybind } from "../src/config/keybind"
|
||||
import { getOpencodeModeStack, OPENCODE_BASE_MODE, OpencodeKeymapProvider, registerOpencodeKeymap } from "../src/keymap"
|
||||
import {
|
||||
COMMAND_PALETTE_COMMAND,
|
||||
getOpencodeModeStack,
|
||||
OPENCODE_BASE_MODE,
|
||||
OpencodeKeymapProvider,
|
||||
registerOpencodeKeymap,
|
||||
} from "../src/keymap"
|
||||
|
||||
function createResolvedKeymapConfig(input: TuiKeybind.KeybindOverrides = {}) {
|
||||
const keybinds = TuiKeybind.parse(input)
|
||||
@@ -73,12 +79,14 @@ test("mode-less bindings stay active when opencode mode changes", async () => {
|
||||
const offKeymap = registerOpencodeKeymap(keymap, renderer, config)
|
||||
const offGlobal = keymap.registerLayer({
|
||||
commands: [
|
||||
{ name: COMMAND_PALETTE_COMMAND, run() {} },
|
||||
{ name: "session.list", run() {} },
|
||||
{ name: "session.new", run() {} },
|
||||
{ name: "session.page.up", run() {} },
|
||||
{ name: "session.first", run() {} },
|
||||
],
|
||||
bindings: config.keybinds.gather("test.global", [
|
||||
COMMAND_PALETTE_COMMAND,
|
||||
"session.list",
|
||||
"session.new",
|
||||
"session.page.up",
|
||||
@@ -95,7 +103,14 @@ test("mode-less bindings stay active when opencode mode changes", async () => {
|
||||
Array.from(
|
||||
keymap.getCommandBindings({
|
||||
visibility: "active",
|
||||
commands: ["session.list", "session.new", "session.page.up", "session.first", "model.list"],
|
||||
commands: [
|
||||
COMMAND_PALETTE_COMMAND,
|
||||
"session.list",
|
||||
"session.new",
|
||||
"session.page.up",
|
||||
"session.first",
|
||||
"model.list",
|
||||
],
|
||||
}),
|
||||
([command, bindings]) => [command, bindings.length],
|
||||
),
|
||||
@@ -125,9 +140,24 @@ test("mode-less bindings stay active when opencode mode changes", async () => {
|
||||
const app = await testRender(() => <Harness />)
|
||||
try {
|
||||
expect(counts).toEqual({
|
||||
base: { "session.list": 1, "session.new": 1, "session.page.up": 2, "session.first": 2, "model.list": 1 },
|
||||
question: { "session.list": 1, "session.new": 1, "session.page.up": 2, "session.first": 2, "model.list": 0 },
|
||||
base: {
|
||||
[COMMAND_PALETTE_COMMAND]: 1,
|
||||
"session.list": 1,
|
||||
"session.new": 1,
|
||||
"session.page.up": 2,
|
||||
"session.first": 2,
|
||||
"model.list": 1,
|
||||
},
|
||||
question: {
|
||||
[COMMAND_PALETTE_COMMAND]: 1,
|
||||
"session.list": 1,
|
||||
"session.new": 1,
|
||||
"session.page.up": 2,
|
||||
"session.first": 2,
|
||||
"model.list": 0,
|
||||
},
|
||||
autocomplete: {
|
||||
[COMMAND_PALETTE_COMMAND]: 1,
|
||||
"session.list": 1,
|
||||
"session.new": 1,
|
||||
"session.page.up": 2,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
border: none;
|
||||
background:
|
||||
linear-gradient(180deg, var(--v2-alpha-light-0) 0%, var(--v2-alpha-light-20) 100%),
|
||||
var(--v2-background-bg-layer-03);
|
||||
var(--v2-background-bg-layer-04);
|
||||
box-shadow: var(--v2-elevation-switch-off);
|
||||
transition:
|
||||
background 90ms ease-out,
|
||||
@@ -90,7 +90,7 @@
|
||||
background:
|
||||
linear-gradient(0deg, var(--v2-overlay-simple-overlay-hover), var(--v2-overlay-simple-overlay-hover)),
|
||||
linear-gradient(180deg, var(--v2-alpha-light-0) 0%, var(--v2-alpha-light-20) 100%),
|
||||
var(--v2-background-bg-layer-03);
|
||||
var(--v2-background-bg-layer-04);
|
||||
}
|
||||
|
||||
&:hover:not([data-disabled], [data-readonly]) [data-slot="switch-thumb"] {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
const OPEN_DELAY = 1_000
|
||||
|
||||
// Kobalte warms every tooltip globally. Keep intent previews isolated so opening
|
||||
// a model picker never inherits warm state from an unrelated tooltip.
|
||||
let warm = false
|
||||
let reset: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
export function openTooltipIntent(open: () => void) {
|
||||
clearTimeout(reset)
|
||||
reset = undefined
|
||||
if (warm) {
|
||||
open()
|
||||
return
|
||||
}
|
||||
const timer = setTimeout(() => {
|
||||
warm = true
|
||||
open()
|
||||
}, OPEN_DELAY)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
|
||||
export function closeTooltipIntent() {
|
||||
clearTimeout(reset)
|
||||
// Adjacent triggers enter before the next task; leaving the group does not.
|
||||
reset = setTimeout(() => {
|
||||
warm = false
|
||||
reset = undefined
|
||||
})
|
||||
}
|
||||
|
||||
export function resetTooltipIntent() {
|
||||
clearTimeout(reset)
|
||||
reset = undefined
|
||||
warm = false
|
||||
}
|
||||
@@ -2,19 +2,22 @@ import { Tooltip as KobalteTooltip } from "@kobalte/core/tooltip"
|
||||
import { createEffect, Match, onCleanup, splitProps, Switch, type JSX } from "solid-js"
|
||||
import type { ComponentProps } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { closeTooltipIntent, openTooltipIntent, resetTooltipIntent } from "./tooltip-intent"
|
||||
import "./tooltip-v2.css"
|
||||
|
||||
export interface TooltipV2Props extends ComponentProps<typeof KobalteTooltip> {
|
||||
export interface TooltipV2Props extends Omit<ComponentProps<typeof KobalteTooltip>, "openDelay"> {
|
||||
value: JSX.Element
|
||||
class?: string
|
||||
contentClass?: string
|
||||
contentStyle?: JSX.CSSProperties
|
||||
inactive?: boolean
|
||||
delay?: "standard" | "intent"
|
||||
forceOpen?: boolean
|
||||
}
|
||||
|
||||
export function TooltipV2(props: TooltipV2Props) {
|
||||
let ref: HTMLDivElement | undefined
|
||||
let cancelIntent: (() => void) | undefined
|
||||
const [state, setState] = createStore({
|
||||
open: false,
|
||||
block: false,
|
||||
@@ -26,19 +29,37 @@ export function TooltipV2(props: TooltipV2Props) {
|
||||
"contentClass",
|
||||
"contentStyle",
|
||||
"inactive",
|
||||
"delay",
|
||||
"forceOpen",
|
||||
"ignoreSafeArea",
|
||||
"value",
|
||||
])
|
||||
|
||||
const close = () => setState("open", false)
|
||||
|
||||
const inside = () => {
|
||||
const active = document.activeElement
|
||||
if (!ref || !active) return false
|
||||
return ref.contains(active)
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
cancelIntent?.()
|
||||
cancelIntent = undefined
|
||||
if (local.delay === "intent") closeTooltipIntent()
|
||||
setState("open", false)
|
||||
}
|
||||
|
||||
const show = () => {
|
||||
if (local.delay !== "intent" || inside()) {
|
||||
setState("open", true)
|
||||
return
|
||||
}
|
||||
if (cancelIntent) return
|
||||
cancelIntent = openTooltipIntent(() => {
|
||||
cancelIntent = undefined
|
||||
setState("open", true)
|
||||
})
|
||||
}
|
||||
|
||||
const drop = (expand = state.expand) => {
|
||||
if (expand) return
|
||||
if (ref?.matches(":hover")) return
|
||||
@@ -80,6 +101,11 @@ export function TooltipV2(props: TooltipV2Props) {
|
||||
onCleanup(() => obs.disconnect())
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
cancelIntent?.()
|
||||
if (local.delay === "intent") resetTooltipIntent()
|
||||
})
|
||||
|
||||
let justClickedTrigger = false
|
||||
|
||||
return (
|
||||
@@ -88,8 +114,8 @@ export function TooltipV2(props: TooltipV2Props) {
|
||||
<Match when={true}>
|
||||
<KobalteTooltip
|
||||
gutter={4}
|
||||
openDelay={400}
|
||||
skipDelayDuration={300}
|
||||
openDelay={local.delay === "intent" ? 0 : 400}
|
||||
skipDelayDuration={local.delay === "intent" ? 0 : 300}
|
||||
{...others}
|
||||
closeDelay={0}
|
||||
ignoreSafeArea={local.ignoreSafeArea ?? true}
|
||||
@@ -101,7 +127,11 @@ export function TooltipV2(props: TooltipV2Props) {
|
||||
justClickedTrigger = false
|
||||
return
|
||||
}
|
||||
setState("open", open)
|
||||
if (open) {
|
||||
show()
|
||||
return
|
||||
}
|
||||
close()
|
||||
}}
|
||||
>
|
||||
<KobalteTooltip.Trigger
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@opencode-ai/web",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
||||
@@ -107,6 +107,7 @@ OpenCode Zen هي بوابة AI تتيح لك الوصول إلى هذه الن
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K3 | kimi-k3 | `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.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -149,6 +150,7 @@ https://opencode.ai/zen/v1/models
|
||||
| GLM 5.1 | $1.40 | $4.40 | $0.26 | - |
|
||||
| GLM 5 | $1.00 | $3.20 | $0.20 | - |
|
||||
| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - |
|
||||
| Kimi K3 | $3.00 | $15.00 | $0.30 | - |
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - |
|
||||
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 |
|
||||
|
||||
@@ -112,6 +112,7 @@ Našim modelima možete pristupiti i preko sljedećih API endpointa.
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K3 | kimi-k3 | `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.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -156,6 +157,7 @@ Podržavamo pay-as-you-go model. Ispod su cijene **po 1M tokena**.
|
||||
| GLM 5.1 | $1.40 | $4.40 | $0.26 | - |
|
||||
| GLM 5 | $1.00 | $3.20 | $0.20 | - |
|
||||
| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - |
|
||||
| Kimi K3 | $3.00 | $15.00 | $0.30 | - |
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - |
|
||||
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 |
|
||||
|
||||
@@ -112,6 +112,7 @@ Du kan også få adgang til vores modeller gennem følgende API-endpoints.
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K3 | kimi-k3 | `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.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -156,6 +157,7 @@ Vi understøtter en pay-as-you-go-model. Nedenfor er priserne **pr. 1M tokens**.
|
||||
| GLM 5.1 | $1.40 | $4.40 | $0.26 | - |
|
||||
| GLM 5 | $1.00 | $3.20 | $0.20 | - |
|
||||
| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - |
|
||||
| Kimi K3 | $3.00 | $15.00 | $0.30 | - |
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - |
|
||||
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 |
|
||||
|
||||
@@ -103,6 +103,7 @@ Du kannst auch über die folgenden API-Endpunkte auf unsere Modelle zugreifen.
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K3 | kimi-k3 | `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.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -145,6 +146,7 @@ Wir unterstützen ein Pay-as-you-go-Modell. Unten findest du die Preise **pro 1M
|
||||
| GLM 5.1 | $1.40 | $4.40 | $0.26 | - |
|
||||
| GLM 5 | $1.00 | $3.20 | $0.20 | - |
|
||||
| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - |
|
||||
| Kimi K3 | $3.00 | $15.00 | $0.30 | - |
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - |
|
||||
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 |
|
||||
|
||||
@@ -112,6 +112,7 @@ También puedes acceder a nuestros modelos a través de los siguientes endpoints
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K3 | kimi-k3 | `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.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -156,6 +157,7 @@ Admitimos un modelo de pago por uso. A continuación se muestran los precios **p
|
||||
| GLM 5.1 | $1.40 | $4.40 | $0.26 | - |
|
||||
| GLM 5 | $1.00 | $3.20 | $0.20 | - |
|
||||
| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - |
|
||||
| Kimi K3 | $3.00 | $15.00 | $0.30 | - |
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - |
|
||||
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 |
|
||||
|
||||
@@ -103,6 +103,7 @@ Vous pouvez également accéder à nos modèles via les points de terminaison AP
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K3 | kimi-k3 | `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.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -145,6 +146,7 @@ Nous prenons en charge un modèle de paiement à l'utilisation. Vous trouverez c
|
||||
| GLM 5.1 | $1.40 | $4.40 | $0.26 | - |
|
||||
| GLM 5 | $1.00 | $3.20 | $0.20 | - |
|
||||
| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - |
|
||||
| Kimi K3 | $3.00 | $15.00 | $0.30 | - |
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - |
|
||||
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 |
|
||||
|
||||
@@ -112,6 +112,7 @@ Puoi anche accedere ai nostri modelli tramite i seguenti endpoint API.
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K3 | kimi-k3 | `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.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -156,6 +157,7 @@ Supportiamo un modello pay-as-you-go. Qui sotto trovi i prezzi **per 1M token**.
|
||||
| GLM 5.1 | $1.40 | $4.40 | $0.26 | - |
|
||||
| GLM 5 | $1.00 | $3.20 | $0.20 | - |
|
||||
| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - |
|
||||
| Kimi K3 | $3.00 | $15.00 | $0.30 | - |
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - |
|
||||
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 |
|
||||
|
||||
@@ -103,6 +103,7 @@ OpenCode Zen は、OpenCode のほかのプロバイダーと同じように動
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K3 | kimi-k3 | `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.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -145,6 +146,7 @@ https://opencode.ai/zen/v1/models
|
||||
| GLM 5.1 | $1.40 | $4.40 | $0.26 | - |
|
||||
| GLM 5 | $1.00 | $3.20 | $0.20 | - |
|
||||
| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - |
|
||||
| Kimi K3 | $3.00 | $15.00 | $0.30 | - |
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - |
|
||||
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 |
|
||||
|
||||
@@ -103,6 +103,7 @@ OpenCode Zen은 OpenCode의 다른 provider와 똑같이 작동합니다.
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K3 | kimi-k3 | `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.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -145,6 +146,7 @@ https://opencode.ai/zen/v1/models
|
||||
| GLM 5.1 | $1.40 | $4.40 | $0.26 | - |
|
||||
| GLM 5 | $1.00 | $3.20 | $0.20 | - |
|
||||
| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - |
|
||||
| Kimi K3 | $3.00 | $15.00 | $0.30 | - |
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - |
|
||||
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 |
|
||||
|
||||
@@ -112,6 +112,7 @@ Du kan også få tilgang til modellene våre gjennom følgende API-endepunkter.
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K3 | kimi-k3 | `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.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -156,6 +157,7 @@ Vi støtter en pay-as-you-go-modell. Nedenfor er prisene **per 1M tokens**.
|
||||
| GLM 5.1 | $1.40 | $4.40 | $0.26 | - |
|
||||
| GLM 5 | $1.00 | $3.20 | $0.20 | - |
|
||||
| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - |
|
||||
| Kimi K3 | $3.00 | $15.00 | $0.30 | - |
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - |
|
||||
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 |
|
||||
|
||||
@@ -112,6 +112,7 @@ Możesz też uzyskać dostęp do naszych modeli przez poniższe endpointy API.
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K3 | kimi-k3 | `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.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -156,6 +157,7 @@ Obsługujemy model pay-as-you-go. Poniżej znajdują się ceny **za 1M tokenów*
|
||||
| GLM 5.1 | $1.40 | $4.40 | $0.26 | - |
|
||||
| GLM 5 | $1.00 | $3.20 | $0.20 | - |
|
||||
| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - |
|
||||
| Kimi K3 | $3.00 | $15.00 | $0.30 | - |
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - |
|
||||
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 |
|
||||
|
||||
@@ -103,6 +103,7 @@ Você também pode acessar nossos modelos pelos seguintes endpoints de API.
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K3 | kimi-k3 | `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.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -145,6 +146,7 @@ Oferecemos um modelo pay-as-you-go. Abaixo estão os preços **por 1M tokens**.
|
||||
| GLM 5.1 | $1.40 | $4.40 | $0.26 | - |
|
||||
| GLM 5 | $1.00 | $3.20 | $0.20 | - |
|
||||
| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - |
|
||||
| Kimi K3 | $3.00 | $15.00 | $0.30 | - |
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - |
|
||||
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 |
|
||||
|
||||
@@ -112,6 +112,7 @@ OpenCode Zen работает как любой другой провайдер
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K3 | kimi-k3 | `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.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -156,6 +157,7 @@ https://opencode.ai/zen/v1/models
|
||||
| GLM 5.1 | $1.40 | $4.40 | $0.26 | - |
|
||||
| GLM 5 | $1.00 | $3.20 | $0.20 | - |
|
||||
| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - |
|
||||
| Kimi K3 | $3.00 | $15.00 | $0.30 | - |
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - |
|
||||
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 |
|
||||
|
||||
@@ -105,6 +105,7 @@ OpenCode Zen ทำงานเหมือน provider อื่น ๆ ใน
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K3 | kimi-k3 | `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.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -147,6 +148,7 @@ https://opencode.ai/zen/v1/models
|
||||
| GLM 5.1 | $1.40 | $4.40 | $0.26 | - |
|
||||
| GLM 5 | $1.00 | $3.20 | $0.20 | - |
|
||||
| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - |
|
||||
| Kimi K3 | $3.00 | $15.00 | $0.30 | - |
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - |
|
||||
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 |
|
||||
|
||||
@@ -103,6 +103,7 @@ Modellerimize aşağıdaki API uç noktaları aracılığıyla da erişebilirsin
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K3 | kimi-k3 | `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.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -145,6 +146,7 @@ Kullandıkça öde modelini destekliyoruz. Aşağıda **1M token başına** fiya
|
||||
| GLM 5.1 | $1.40 | $4.40 | $0.26 | - |
|
||||
| GLM 5 | $1.00 | $3.20 | $0.20 | - |
|
||||
| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - |
|
||||
| Kimi K3 | $3.00 | $15.00 | $0.30 | - |
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - |
|
||||
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 |
|
||||
|
||||
@@ -112,6 +112,7 @@ You can also access our models through the following API endpoints.
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K3 | kimi-k3 | `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.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -156,6 +157,7 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**.
|
||||
| GLM 5.1 | $1.40 | $4.40 | $0.26 | - |
|
||||
| GLM 5 | $1.00 | $3.20 | $0.20 | - |
|
||||
| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - |
|
||||
| Kimi K3 | $3.00 | $15.00 | $0.30 | - |
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - |
|
||||
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 |
|
||||
|
||||
@@ -103,6 +103,7 @@ OpenCode Zen 的工作方式与 OpenCode 中的任何其他提供商相同。
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K3 | kimi-k3 | `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.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -145,6 +146,7 @@ https://opencode.ai/zen/v1/models
|
||||
| GLM 5.1 | $1.40 | $4.40 | $0.26 | - |
|
||||
| GLM 5 | $1.00 | $3.20 | $0.20 | - |
|
||||
| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - |
|
||||
| Kimi K3 | $3.00 | $15.00 | $0.30 | - |
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - |
|
||||
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 |
|
||||
|
||||
@@ -107,6 +107,7 @@ OpenCode Zen 的運作方式和 OpenCode 中的其他供應商一樣。
|
||||
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Kimi K3 | kimi-k3 | `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.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||
@@ -150,6 +151,7 @@ https://opencode.ai/zen/v1/models
|
||||
| GLM 5.1 | $1.40 | $4.40 | $0.26 | - |
|
||||
| GLM 5 | $1.00 | $3.20 | $0.20 | - |
|
||||
| Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - |
|
||||
| Kimi K3 | $3.00 | $15.00 | $0.30 | - |
|
||||
| Kimi K2.6 | $0.95 | $4.00 | $0.16 | - |
|
||||
| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - |
|
||||
| Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "opencode",
|
||||
"displayName": "opencode",
|
||||
"description": "opencode for VS Code",
|
||||
"version": "1.18.6",
|
||||
"version": "1.18.7",
|
||||
"publisher": "sst-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Reference in New Issue
Block a user