mirror of
https://github.com/anomalyco/opencode.git
synced 2026-03-03 05:03:52 +00:00
Compare commits
1 Commits
cli-auth-c
...
sidebar-fa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d20698401b |
@@ -1 +0,0 @@
|
||||
Fixed typecheck error by reverting key name from 'session.new.worktree.startup' back to 'session.new.workspace.startup' in packages/console/app/src/i18n/tr.ts.
|
||||
@@ -1 +0,0 @@
|
||||
Applied minor linguistic polishes to Turkish translations in packages/console/app/src/i18n/tr.ts. PR created at https://github.com/anomalyco/opencode/pull/15468
|
||||
@@ -111,7 +111,3 @@ const table = sqliteTable("session", {
|
||||
- Avoid mocks as much as possible
|
||||
- Test actual implementation, do not duplicate logic into tests
|
||||
- Tests cannot run from repo root (guard: `do-not-run-tests-from-root`); run from package dirs like `packages/opencode`.
|
||||
|
||||
## Type Checking
|
||||
|
||||
- Always run `bun typecheck` from package directories (e.g., `packages/opencode`), never `tsc` directly.
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
<a href="README.ja.md">日本語</a> |
|
||||
<a href="README.pl.md">Polski</a> |
|
||||
<a href="README.ru.md">Русский</a> |
|
||||
<a href="README.bs.md">Bosanski</a> |
|
||||
<a href="README.ar.md">العربية</a> |
|
||||
<a href="README.no.md">Norsk</a> |
|
||||
<a href="README.br.md">Português (Brasil)</a> |
|
||||
|
||||
6
bun.lock
6
bun.lock
@@ -545,7 +545,7 @@
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@pierre/diffs": "1.1.0-beta.18",
|
||||
"@pierre/diffs": "1.1.0-beta.13",
|
||||
"@playwright/test": "1.51.0",
|
||||
"@solid-primitives/storage": "4.3.3",
|
||||
"@solidjs/meta": "0.29.4",
|
||||
@@ -1469,9 +1469,7 @@
|
||||
|
||||
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
|
||||
|
||||
"@pierre/diffs": ["@pierre/diffs@1.1.0-beta.18", "", { "dependencies": { "@pierre/theme": "0.0.22", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-7ZF3YD9fxdbYsPnltz5cUqHacN7ztp8RX/fJLxwv8wIEORpP4+7dHz1h/qx3o4EW2xUrIhmbM8ImywLasB787Q=="],
|
||||
|
||||
"@pierre/theme": ["@pierre/theme@0.0.22", "", {}, "sha512-ePUIdQRNGjrveELTU7fY89Xa7YGHHEy5Po5jQy/18lm32eRn96+tnYJEtFooGdffrx55KBUtOXfvVy/7LDFFhA=="],
|
||||
"@pierre/diffs": ["@pierre/diffs@1.1.0-beta.13", "", { "dependencies": { "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-D35rxDu5V7XHX5aVGU6PF12GhscL+I+9QYgxK/i3h0d2XSirAxDdVNm49aYwlOhgmdvL0NbS1IHxPswVB5yJvw=="],
|
||||
|
||||
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-2XLuizbG90QDUQL+1M90XxfVZxjkIQ1cFYS46nnVO7g=",
|
||||
"aarch64-linux": "sha256-hlckiGAtbpAlwgcE7KgzKKRq9T2FEOSq3Q1MhuHfZ2c=",
|
||||
"aarch64-darwin": "sha256-V/8Kay+5bDb/BSVgBQhSMwzmRmkNGl3U0HFMVbVcMak=",
|
||||
"x86_64-darwin": "sha256-duLDF88Q/hXK5jwBy4dVxMSiTTS0R4obp9MlTuOF/Pw="
|
||||
"x86_64-linux": "sha256-dZoLhWe4smBsOF7WczMySLXSAB1YRO1vfhiOCL1rBf0=",
|
||||
"aarch64-linux": "sha256-J7nIz1xuVZEHun5WRZkYRySz29B0A8g5g0RRxnIWTYU=",
|
||||
"aarch64-darwin": "sha256-R2PuhX+EjUBuLE8MF0G0fcUwNaU+5n6V6uVeK89ulzw=",
|
||||
"x86_64-darwin": "sha256-Bvzfz9TsTpYriZNLSLgpNcNb+BgtkgpjoWqdOtF2IBg="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
"@cloudflare/workers-types": "4.20251008.0",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@pierre/diffs": "1.1.0-beta.18",
|
||||
"@pierre/diffs": "1.1.0-beta.13",
|
||||
"@solid-primitives/storage": "4.3.3",
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"diff": "8.0.2",
|
||||
|
||||
@@ -145,7 +145,6 @@ try {
|
||||
Object.assign(process.env, serverEnv)
|
||||
process.env.AGENT = "1"
|
||||
process.env.OPENCODE = "1"
|
||||
process.env.OPENCODE_PID = String(process.pid)
|
||||
|
||||
const log = await import("../../opencode/src/util/log")
|
||||
const install = await import("../../opencode/src/installation")
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import type { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { List, type ListRef } from "@opencode-ai/ui/list"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||
@@ -446,7 +447,7 @@ export function DialogConnectProvider(props: { provider: string }) {
|
||||
>
|
||||
<div class="flex flex-col gap-6 px-2.5 pb-3">
|
||||
<div class="px-2.5 flex gap-4 items-center">
|
||||
<ProviderIcon id={props.provider} class="size-5 shrink-0 icon-strong-base" />
|
||||
<ProviderIcon id={props.provider as IconName} class="size-5 shrink-0 icon-strong-base" />
|
||||
<div class="text-16-medium text-text-strong">
|
||||
<Switch>
|
||||
<Match when={props.provider === "anthropic" && method()?.label?.toLowerCase().includes("max")}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import type { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { List, type ListRef } from "@opencode-ai/ui/list"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { Tag } from "@opencode-ai/ui/tag"
|
||||
@@ -94,7 +95,7 @@ export const DialogSelectModelUnpaid: Component = () => {
|
||||
>
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center gap-x-3">
|
||||
<ProviderIcon data-slot="list-item-extra-icon" id={i.id} />
|
||||
<ProviderIcon data-slot="list-item-extra-icon" id={i.id as IconName} />
|
||||
<span>{i.name}</span>
|
||||
<Show when={i.id === "opencode"}>
|
||||
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.opencode.tagline")}</div>
|
||||
|
||||
@@ -5,12 +5,18 @@ import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { Tag } from "@opencode-ai/ui/tag"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { iconNames, type IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { DialogConnectProvider } from "./dialog-connect-provider"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { DialogCustomProvider } from "./dialog-custom-provider"
|
||||
|
||||
const CUSTOM_ID = "_custom"
|
||||
|
||||
function icon(id: string): IconName {
|
||||
if (iconNames.includes(id as IconName)) return id as IconName
|
||||
return "synthetic"
|
||||
}
|
||||
|
||||
export const DialogSelectProvider: Component = () => {
|
||||
const dialog = useDialog()
|
||||
const providers = useProviders()
|
||||
@@ -63,7 +69,7 @@ export const DialogSelectProvider: Component = () => {
|
||||
>
|
||||
{(i) => (
|
||||
<div class="px-1.25 w-full flex items-center gap-x-3">
|
||||
<ProviderIcon data-slot="list-item-extra-icon" id={i.id} />
|
||||
<ProviderIcon data-slot="list-item-extra-icon" id={icon(i.id)} />
|
||||
<span>{i.name}</span>
|
||||
<Show when={i.id === "opencode"}>
|
||||
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.opencode.tagline")}</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ import { Button } from "@opencode-ai/ui/button"
|
||||
import { DockShellForm, DockTray } from "@opencode-ai/ui/dock-surface"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import type { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Select } from "@opencode-ai/ui/select"
|
||||
@@ -1397,7 +1398,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
>
|
||||
<Show when={local.model.current()?.provider?.id}>
|
||||
<ProviderIcon
|
||||
id={local.model.current()!.provider.id}
|
||||
id={local.model.current()!.provider.id as IconName}
|
||||
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
||||
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
||||
/>
|
||||
@@ -1427,7 +1428,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
>
|
||||
<Show when={local.model.current()?.provider?.id}>
|
||||
<ProviderIcon
|
||||
id={local.model.current()!.provider.id}
|
||||
id={local.model.current()!.provider.id as IconName}
|
||||
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
||||
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
||||
/>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Switch } from "@opencode-ai/ui/switch"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import type { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { type Component, For, Show } from "solid-js"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useModels } from "@/context/models"
|
||||
@@ -97,7 +98,7 @@ export const SettingsModels: Component = () => {
|
||||
{(group) => (
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex items-center gap-2 pb-2">
|
||||
<ProviderIcon id={group.category} class="size-5 shrink-0 icon-strong-base" />
|
||||
<ProviderIcon id={group.category as IconName} class="size-5 shrink-0 icon-strong-base" />
|
||||
<span class="text-14-medium text-text-strong">{group.items[0].provider.name}</span>
|
||||
</div>
|
||||
<div class="bg-surface-raised-base px-4 rounded-lg">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { Tag } from "@opencode-ai/ui/tag"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { iconNames, type IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
||||
import { createMemo, type Component, For, Show } from "solid-js"
|
||||
import { useLanguage } from "@/context/language"
|
||||
@@ -32,6 +33,11 @@ export const SettingsProviders: Component = () => {
|
||||
const globalSync = useGlobalSync()
|
||||
const providers = useProviders()
|
||||
|
||||
const icon = (id: string): IconName => {
|
||||
if (iconNames.includes(id as IconName)) return id as IconName
|
||||
return "synthetic"
|
||||
}
|
||||
|
||||
const connected = createMemo(() => {
|
||||
return providers
|
||||
.connected()
|
||||
@@ -148,7 +154,7 @@ export const SettingsProviders: Component = () => {
|
||||
{(item) => (
|
||||
<div class="group flex flex-wrap items-center justify-between gap-4 min-h-16 py-3 border-b border-border-weak-base last:border-none">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<ProviderIcon id={item.id} class="size-5 shrink-0 icon-strong-base" />
|
||||
<ProviderIcon id={icon(item.id)} class="size-5 shrink-0 icon-strong-base" />
|
||||
<span class="text-14-medium text-text-strong truncate">{item.name}</span>
|
||||
<Tag>{type(item)}</Tag>
|
||||
</div>
|
||||
@@ -179,7 +185,7 @@ export const SettingsProviders: Component = () => {
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 min-h-16 py-3 border-b border-border-weak-base last:border-none">
|
||||
<div class="flex flex-col min-w-0">
|
||||
<div class="flex items-center gap-x-3">
|
||||
<ProviderIcon id={item.id} class="size-5 shrink-0 icon-strong-base" />
|
||||
<ProviderIcon id={icon(item.id)} class="size-5 shrink-0 icon-strong-base" />
|
||||
<span class="text-14-medium text-text-strong">{item.name}</span>
|
||||
<Show when={item.id === "opencode"}>
|
||||
<span class="text-14-regular text-text-weak">
|
||||
@@ -222,7 +228,7 @@ export const SettingsProviders: Component = () => {
|
||||
>
|
||||
<div class="flex flex-col min-w-0">
|
||||
<div class="flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||
<ProviderIcon id="synthetic" class="size-5 shrink-0 icon-strong-base" />
|
||||
<ProviderIcon id={icon("synthetic")} class="size-5 shrink-0 icon-strong-base" />
|
||||
<span class="text-14-medium text-text-strong">{language.t("provider.custom.title")}</span>
|
||||
<Tag>{language.t("settings.providers.tag.custom")}</Tag>
|
||||
</div>
|
||||
|
||||
@@ -43,11 +43,12 @@ type OptimisticRemoveInput = {
|
||||
|
||||
export function applyOptimisticAdd(draft: OptimisticStore, input: OptimisticAddInput) {
|
||||
const messages = draft.message[input.sessionID]
|
||||
if (!messages) {
|
||||
draft.message[input.sessionID] = [input.message]
|
||||
}
|
||||
if (messages) {
|
||||
const result = Binary.search(messages, input.message.id, (m) => m.id)
|
||||
messages.splice(result.index, 0, input.message)
|
||||
} else {
|
||||
draft.message[input.sessionID] = [input.message]
|
||||
}
|
||||
draft.part[input.message.id] = sortParts(input.parts)
|
||||
}
|
||||
@@ -104,7 +105,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
return globalSync.child(directory)
|
||||
}
|
||||
const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/")
|
||||
const messagePageSize = 200
|
||||
const messagePageSize = 400
|
||||
const inflight = new Map<string, Promise<void>>()
|
||||
const inflightDiff = new Map<string, Promise<void>>()
|
||||
const inflightTodo = new Map<string, Promise<void>>()
|
||||
@@ -121,12 +122,20 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
return undefined
|
||||
}
|
||||
|
||||
const limitFor = (count: number) => {
|
||||
if (count <= messagePageSize) return messagePageSize
|
||||
return Math.ceil(count / messagePageSize) * messagePageSize
|
||||
}
|
||||
|
||||
const fetchMessages = async (input: { client: typeof sdk.client; sessionID: string; limit: number }) => {
|
||||
const messages = await retry(() =>
|
||||
input.client.session.messages({ sessionID: input.sessionID, limit: input.limit }),
|
||||
)
|
||||
const items = (messages.data ?? []).filter((x) => !!x?.info?.id)
|
||||
const session = items.map((x) => x.info).sort((a, b) => cmp(a.id, b.id))
|
||||
const session = items
|
||||
.map((x) => x.info)
|
||||
.filter((m) => !!m?.id)
|
||||
.sort((a, b) => cmp(a.id, b.id))
|
||||
const part = items.map((message) => ({ id: message.info.id, part: sortParts(message.parts) }))
|
||||
return {
|
||||
session,
|
||||
@@ -150,8 +159,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
.then((next) => {
|
||||
batch(() => {
|
||||
input.setStore("message", input.sessionID, reconcile(next.session, { key: "id" }))
|
||||
for (const p of next.part) {
|
||||
input.setStore("part", p.id, p.part)
|
||||
for (const message of next.part) {
|
||||
input.setStore("part", message.id, reconcile(message.part, { key: "id" }))
|
||||
}
|
||||
setMeta("limit", key, input.limit)
|
||||
setMeta("complete", key, next.complete)
|
||||
@@ -220,9 +229,17 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
const client = sdk.client
|
||||
const [store, setStore] = globalSync.child(directory)
|
||||
const key = keyFor(directory, sessionID)
|
||||
const hasSession = Binary.search(store.session, sessionID, (s) => s.id).found
|
||||
const hasSession = (() => {
|
||||
const match = Binary.search(store.session, sessionID, (s) => s.id)
|
||||
return match.found
|
||||
})()
|
||||
|
||||
const limit = meta.limit[key] ?? messagePageSize
|
||||
const hasMessages = store.message[sessionID] !== undefined
|
||||
const hydrated = meta.limit[key] !== undefined
|
||||
if (hasSession && hasMessages && hydrated) return
|
||||
|
||||
const count = store.message[sessionID]?.length ?? 0
|
||||
const limit = hydrated ? (meta.limit[key] ?? messagePageSize) : limitFor(count)
|
||||
|
||||
const sessionReq = hasSession
|
||||
? Promise.resolve()
|
||||
@@ -242,13 +259,16 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
)
|
||||
})
|
||||
|
||||
const messagesReq = loadMessages({
|
||||
directory,
|
||||
client,
|
||||
setStore,
|
||||
sessionID,
|
||||
limit,
|
||||
})
|
||||
const messagesReq =
|
||||
hasMessages && hydrated
|
||||
? Promise.resolve()
|
||||
: loadMessages({
|
||||
directory,
|
||||
client,
|
||||
setStore,
|
||||
sessionID,
|
||||
limit,
|
||||
})
|
||||
|
||||
return runInflight(inflight, key, () => Promise.all([sessionReq, messagesReq]).then(() => {}))
|
||||
},
|
||||
@@ -270,14 +290,14 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
const client = sdk.client
|
||||
const [store, setStore] = globalSync.child(directory)
|
||||
const existing = store.todo[sessionID]
|
||||
const cached = globalSync.data.session_todo[sessionID]
|
||||
if (existing !== undefined) {
|
||||
if (cached === undefined) {
|
||||
if (globalSync.data.session_todo[sessionID] === undefined) {
|
||||
globalSync.todo.set(sessionID, existing)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const cached = globalSync.data.session_todo[sessionID]
|
||||
if (cached !== undefined) {
|
||||
setStore("todo", sessionID, reconcile(cached, { key: "id" }))
|
||||
}
|
||||
@@ -304,12 +324,11 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
const key = keyFor(sdk.directory, sessionID)
|
||||
return meta.loading[key] ?? false
|
||||
},
|
||||
async loadMore(sessionID: string, count?: number) {
|
||||
async loadMore(sessionID: string, count = messagePageSize) {
|
||||
const directory = sdk.directory
|
||||
const client = sdk.client
|
||||
const [, setStore] = globalSync.child(directory)
|
||||
const key = keyFor(directory, sessionID)
|
||||
const step = count ?? messagePageSize
|
||||
if (meta.loading[key]) return
|
||||
if (meta.complete[key]) return
|
||||
|
||||
@@ -319,7 +338,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
client,
|
||||
setStore,
|
||||
sessionID,
|
||||
limit: currentLimit + step,
|
||||
limit: currentLimit + count,
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
@@ -119,7 +119,7 @@ export const dict = {
|
||||
"dialog.model.manage.description": "Model seçicide hangi modellerin görüneceğini özelleştirin.",
|
||||
"dialog.model.manage.provider.toggle": "Tüm {{provider}} modellerini aç/kapat",
|
||||
|
||||
"dialog.model.unpaid.freeModels.title": "OpenCode'un sunduğu ücretsiz modeller",
|
||||
"dialog.model.unpaid.freeModels.title": "OpenCode tarafından sunulan ücretsiz modeller",
|
||||
"dialog.model.unpaid.addMore.title": "Popüler sağlayıcılardan daha fazla model ekleyin",
|
||||
|
||||
"dialog.provider.viewAll": "Daha fazla sağlayıcı göster",
|
||||
@@ -195,7 +195,7 @@ export const dict = {
|
||||
"provider.custom.error.baseURL.required": "Temel URL gerekli",
|
||||
"provider.custom.error.baseURL.format": "http:// veya https:// ile başlamalı",
|
||||
"provider.custom.error.required": "Gerekli",
|
||||
"provider.custom.error.duplicate": "Çakışma",
|
||||
"provider.custom.error.duplicate": "Tekrar",
|
||||
|
||||
"provider.disconnect.toast.disconnected.title": "{{provider}} bağlantısı kesildi",
|
||||
"provider.disconnect.toast.disconnected.description": "{{provider}} modelleri artık kullanılabilir değil.",
|
||||
@@ -252,7 +252,7 @@ export const dict = {
|
||||
"prompt.example.10": "API dokümantasyonu oluştur",
|
||||
"prompt.example.11": "Veritabanı sorgularını optimize et",
|
||||
"prompt.example.12": "Girdi doğrulama ekle",
|
||||
"prompt.example.13": "... için yeni bir bileşen oluştur",
|
||||
"prompt.example.13": "İçin yeni bir bileşen oluştur...",
|
||||
"prompt.example.14": "Bu projeyi nasıl dağıtabilirim?",
|
||||
"prompt.example.15": "Kodumu en iyi uygulamalar için incele",
|
||||
"prompt.example.16": "Bu fonksiyona hata yönetimi ekle",
|
||||
@@ -263,13 +263,13 @@ export const dict = {
|
||||
"prompt.example.21": "Bir göç betiği yazmama yardım et",
|
||||
"prompt.example.22": "Bu uç nokta için önbellekleme uygula",
|
||||
"prompt.example.23": "Bu listeye sayfalama ekle",
|
||||
"prompt.example.24": "... için bir CLI komutu oluştur",
|
||||
"prompt.example.24": "İçin bir CLI komutu oluştur...",
|
||||
"prompt.example.25": "Ortam değişkenleri burada nasıl çalışıyor?",
|
||||
|
||||
"prompt.popover.emptyResults": "Eşleşen sonuç yok",
|
||||
"prompt.popover.emptyCommands": "Eşleşen komut yok",
|
||||
"prompt.dropzone.label": "Görsel veya PDF'leri buraya bırakın",
|
||||
"prompt.dropzone.file.label": "Dosyayı referans göstermek için bırakın",
|
||||
"prompt.dropzone.file.label": "@bahsetmek için dosyayı bırakın",
|
||||
"prompt.slash.badge.custom": "özel",
|
||||
"prompt.slash.badge.skill": "beceri",
|
||||
"prompt.slash.badge.mcp": "mcp",
|
||||
@@ -343,7 +343,7 @@ export const dict = {
|
||||
"dialog.project.edit.icon.recommended": "Önerilen: 128x128px",
|
||||
"dialog.project.edit.color": "Renk",
|
||||
"dialog.project.edit.color.select": "{{color}} rengini seç",
|
||||
"dialog.project.edit.worktree.startup": "Çalışma ağacı başlatma betiği",
|
||||
"dialog.project.edit.worktree.startup": "Çalışma alanı başlatma betiği",
|
||||
"dialog.project.edit.worktree.startup.description": "Yeni bir çalışma alanı (worktree) oluşturduktan sonra çalışır.",
|
||||
"dialog.project.edit.worktree.startup.placeholder": "örneğin bun install",
|
||||
|
||||
@@ -454,7 +454,7 @@ export const dict = {
|
||||
"error.page.version": "Sürüm: {{version}}",
|
||||
|
||||
"error.dev.rootNotFound":
|
||||
"Kök eleman bulunamadı. index.html dosyanıza eklemeyi unuttunuz mu? Ya da ID özelliği yanlış mı yazıldı?",
|
||||
"Kök eleman bulunamadı. index.html dosyanıza eklemeyi unuttunuz mu? Ya da id özelliği yanlış mı yazıldı?",
|
||||
|
||||
"error.globalSync.connectFailed": "Sunucuya bağlanılamadı. `{{url}}` adresinde çalışan bir sunucu var mı?",
|
||||
"directory.error.invalidUrl": "URL'de geçersiz dizin.",
|
||||
|
||||
@@ -1851,45 +1851,34 @@ export default function Layout(props: ParentProps) {
|
||||
when={workspacesEnabled()}
|
||||
fallback={
|
||||
<>
|
||||
<div class="shrink-0 py-4 px-3">
|
||||
<TooltipKeybind
|
||||
title={language.t("command.session.new")}
|
||||
keybind={command.keybind("session.new")}
|
||||
placement="top"
|
||||
>
|
||||
<Button
|
||||
size="large"
|
||||
icon="plus-small"
|
||||
class="w-full"
|
||||
onClick={() => navigateWithSidebarReset(`/${base64Encode(p().worktree)}/session`)}
|
||||
>
|
||||
{language.t("command.session.new")}
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
<div class="flex-1 min-h-0">
|
||||
<LocalWorkspace
|
||||
ctx={workspaceSidebarCtx}
|
||||
project={p()}
|
||||
sortNow={sortNow}
|
||||
mobile={panelProps.mobile}
|
||||
header={
|
||||
<TooltipKeybind
|
||||
title={language.t("command.session.new")}
|
||||
keybind={command.keybind("session.new")}
|
||||
placement="top"
|
||||
>
|
||||
<Button
|
||||
size="large"
|
||||
icon="plus-small"
|
||||
class="w-full"
|
||||
onClick={() => navigateWithSidebarReset(`/${base64Encode(p().worktree)}/session`)}
|
||||
>
|
||||
{language.t("command.session.new")}
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<>
|
||||
<div class="shrink-0 py-4 px-3">
|
||||
<TooltipKeybind
|
||||
title={language.t("workspace.new")}
|
||||
keybind={command.keybind("workspace.new")}
|
||||
placement="top"
|
||||
>
|
||||
<Button size="large" icon="plus-small" class="w-full" onClick={() => createWorkspace(p())}>
|
||||
{language.t("workspace.new")}
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
<div class="relative flex-1 min-h-0">
|
||||
<DragDropProvider
|
||||
onDragStart={handleWorkspaceDragStart}
|
||||
@@ -1903,21 +1892,41 @@ export default function Layout(props: ParentProps) {
|
||||
ref={(el) => {
|
||||
if (!panelProps.mobile) scrollContainerRef = el
|
||||
}}
|
||||
class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar [overflow-anchor:none]"
|
||||
class="size-full flex flex-col overflow-y-auto no-scrollbar [overflow-anchor:none]"
|
||||
>
|
||||
<SortableProvider ids={workspaces()}>
|
||||
<For each={workspaces()}>
|
||||
{(directory) => (
|
||||
<SortableWorkspace
|
||||
ctx={workspaceSidebarCtx}
|
||||
directory={directory}
|
||||
project={p()}
|
||||
sortNow={sortNow}
|
||||
mobile={panelProps.mobile}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</SortableProvider>
|
||||
<div class="sticky top-0 z-20 pointer-events-none bg-[linear-gradient(to_bottom,var(--background-stronger)_calc(100%_-_24px),transparent)] pt-4 pb-6 px-3">
|
||||
<div class="pointer-events-auto">
|
||||
<TooltipKeybind
|
||||
title={language.t("workspace.new")}
|
||||
keybind={command.keybind("workspace.new")}
|
||||
placement="top"
|
||||
>
|
||||
<Button
|
||||
size="large"
|
||||
icon="plus-small"
|
||||
class="w-full"
|
||||
onClick={() => createWorkspace(p())}
|
||||
>
|
||||
{language.t("workspace.new")}
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 py-2">
|
||||
<SortableProvider ids={workspaces()}>
|
||||
<For each={workspaces()}>
|
||||
{(directory) => (
|
||||
<SortableWorkspace
|
||||
ctx={workspaceSidebarCtx}
|
||||
directory={directory}
|
||||
project={p()}
|
||||
sortNow={sortNow}
|
||||
mobile={panelProps.mobile}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</SortableProvider>
|
||||
</div>
|
||||
</div>
|
||||
<DragOverlay>
|
||||
<WorkspaceDragOverlay
|
||||
|
||||
@@ -283,7 +283,7 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
||||
return (
|
||||
<div
|
||||
data-session-id={props.session.id}
|
||||
class="group/session relative w-full rounded-md cursor-default transition-colors pl-2 pr-3
|
||||
class="group/session relative w-full rounded-md cursor-default transition-colors pl-2 pr-3 scroll-mt-24
|
||||
hover:bg-surface-raised-base-hover [&:has(:focus-visible)]:bg-surface-raised-base-hover has-[[data-expanded]]:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active"
|
||||
>
|
||||
<Show
|
||||
|
||||
@@ -467,6 +467,7 @@ export const LocalWorkspace = (props: {
|
||||
project: LocalProject
|
||||
sortNow: Accessor<number>
|
||||
mobile?: boolean
|
||||
header?: JSX.Element
|
||||
}): JSX.Element => {
|
||||
const globalSync = useGlobalSync()
|
||||
const language = useLanguage()
|
||||
@@ -488,9 +489,14 @@ export const LocalWorkspace = (props: {
|
||||
return (
|
||||
<div
|
||||
ref={(el) => props.ctx.setScrollContainerRef(el, props.mobile)}
|
||||
class="size-full flex flex-col py-2 overflow-y-auto no-scrollbar [overflow-anchor:none]"
|
||||
class="size-full flex flex-col overflow-y-auto no-scrollbar [overflow-anchor:none]"
|
||||
>
|
||||
<nav class="flex flex-col gap-1 px-2">
|
||||
<Show when={props.header}>
|
||||
<div class="sticky top-0 z-20 pointer-events-none bg-[linear-gradient(to_bottom,var(--background-stronger)_calc(100%_-_24px),transparent)] pt-4 pb-6 px-3">
|
||||
<div class="pointer-events-auto">{props.header}</div>
|
||||
</div>
|
||||
</Show>
|
||||
<nav class="flex flex-col gap-1 px-2 pb-2" classList={{ "pt-2": !props.header }}>
|
||||
<Show when={loading()}>
|
||||
<SessionSkeleton />
|
||||
</Show>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { onCleanup, Show, Match, Switch, createMemo, createEffect, on, onMount, untrack } from "solid-js"
|
||||
import { onCleanup, Show, Match, Switch, createMemo, createEffect, on, onMount } from "solid-js"
|
||||
import { createMediaQuery } from "@solid-primitives/media"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
import { useLocal } from "@/context/local"
|
||||
@@ -32,215 +32,6 @@ import { SessionMobileTabs } from "@/pages/session/session-mobile-tabs"
|
||||
import { SessionSidePanel } from "@/pages/session/session-side-panel"
|
||||
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
|
||||
|
||||
const emptyUserMessages: UserMessage[] = []
|
||||
|
||||
type SessionHistoryWindowInput = {
|
||||
sessionID: () => string | undefined
|
||||
messagesReady: () => boolean
|
||||
visibleUserMessages: () => UserMessage[]
|
||||
historyMore: () => boolean
|
||||
historyLoading: () => boolean
|
||||
loadMore: (sessionID: string) => Promise<void>
|
||||
userScrolled: () => boolean
|
||||
scroller: () => HTMLDivElement | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Maintains the rendered history window for a session timeline.
|
||||
*
|
||||
* It keeps initial paint bounded to recent turns, reveals cached turns in
|
||||
* small batches while scrolling upward, and prefetches older history near top.
|
||||
*/
|
||||
function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
||||
const turnInit = 10
|
||||
const turnBatch = 8
|
||||
const turnScrollThreshold = 200
|
||||
const turnPrefetchBuffer = 16
|
||||
const prefetchCooldownMs = 400
|
||||
const prefetchNoGrowthLimit = 2
|
||||
|
||||
const [state, setState] = createStore({
|
||||
turnID: undefined as string | undefined,
|
||||
turnStart: 0,
|
||||
prefetchUntil: 0,
|
||||
prefetchNoGrowth: 0,
|
||||
})
|
||||
|
||||
const initialTurnStart = (len: number) => (len > turnInit ? len - turnInit : 0)
|
||||
|
||||
const turnStart = createMemo(() => {
|
||||
const id = input.sessionID()
|
||||
const len = input.visibleUserMessages().length
|
||||
if (!id || len <= 0) return 0
|
||||
if (state.turnID !== id) return initialTurnStart(len)
|
||||
if (state.turnStart <= 0) return 0
|
||||
if (state.turnStart >= len) return initialTurnStart(len)
|
||||
return state.turnStart
|
||||
})
|
||||
|
||||
const setTurnStart = (start: number) => {
|
||||
const id = input.sessionID()
|
||||
const next = start > 0 ? start : 0
|
||||
if (!id) {
|
||||
setState({ turnID: undefined, turnStart: next })
|
||||
return
|
||||
}
|
||||
setState({ turnID: id, turnStart: next })
|
||||
}
|
||||
|
||||
const renderedUserMessages = createMemo(
|
||||
() => {
|
||||
const msgs = input.visibleUserMessages()
|
||||
const start = turnStart()
|
||||
if (start <= 0) return msgs
|
||||
return msgs.slice(start)
|
||||
},
|
||||
emptyUserMessages,
|
||||
{
|
||||
equals: same,
|
||||
},
|
||||
)
|
||||
|
||||
const preserveScroll = (fn: () => void) => {
|
||||
const el = input.scroller()
|
||||
if (!el) {
|
||||
fn()
|
||||
return
|
||||
}
|
||||
const beforeTop = el.scrollTop
|
||||
const beforeHeight = el.scrollHeight
|
||||
fn()
|
||||
requestAnimationFrame(() => {
|
||||
const delta = el.scrollHeight - beforeHeight
|
||||
if (!delta) return
|
||||
el.scrollTop = beforeTop + delta
|
||||
})
|
||||
}
|
||||
|
||||
const backfillTurns = () => {
|
||||
const start = turnStart()
|
||||
if (start <= 0) return
|
||||
|
||||
const next = start - turnBatch
|
||||
const nextStart = next > 0 ? next : 0
|
||||
|
||||
preserveScroll(() => setTurnStart(nextStart))
|
||||
}
|
||||
|
||||
/** Button path: reveal all cached turns, fetch older history, reveal one batch. */
|
||||
const loadAndReveal = async () => {
|
||||
const id = input.sessionID()
|
||||
if (!id) return
|
||||
|
||||
const start = turnStart()
|
||||
const beforeVisible = input.visibleUserMessages().length
|
||||
|
||||
if (start > 0) setTurnStart(0)
|
||||
|
||||
if (!input.historyMore() || input.historyLoading()) return
|
||||
|
||||
await input.loadMore(id)
|
||||
if (input.sessionID() !== id) return
|
||||
|
||||
const afterVisible = input.visibleUserMessages().length
|
||||
const growth = afterVisible - beforeVisible
|
||||
if (state.prefetchNoGrowth) setState("prefetchNoGrowth", 0)
|
||||
if (growth <= 0) return
|
||||
if (turnStart() !== 0) return
|
||||
|
||||
const target = Math.min(afterVisible, Math.max(beforeVisible, renderedUserMessages().length) + turnBatch)
|
||||
const nextStart = Math.max(0, afterVisible - target)
|
||||
preserveScroll(() => setTurnStart(nextStart))
|
||||
}
|
||||
|
||||
/** Scroll/prefetch path: fetch older history from server. */
|
||||
const fetchOlderMessages = async (opts?: { prefetch?: boolean }) => {
|
||||
const id = input.sessionID()
|
||||
if (!id) return
|
||||
if (!input.historyMore() || input.historyLoading()) return
|
||||
|
||||
if (opts?.prefetch) {
|
||||
const now = Date.now()
|
||||
if (state.prefetchUntil > now) return
|
||||
if (state.prefetchNoGrowth >= prefetchNoGrowthLimit) return
|
||||
setState("prefetchUntil", now + prefetchCooldownMs)
|
||||
}
|
||||
|
||||
const start = turnStart()
|
||||
const beforeVisible = input.visibleUserMessages().length
|
||||
const beforeRendered = start <= 0 ? beforeVisible : renderedUserMessages().length
|
||||
|
||||
await input.loadMore(id)
|
||||
if (input.sessionID() !== id) return
|
||||
|
||||
const afterVisible = input.visibleUserMessages().length
|
||||
const growth = afterVisible - beforeVisible
|
||||
|
||||
if (opts?.prefetch) {
|
||||
setState("prefetchNoGrowth", growth > 0 ? 0 : state.prefetchNoGrowth + 1)
|
||||
} else if (growth > 0 && state.prefetchNoGrowth) {
|
||||
setState("prefetchNoGrowth", 0)
|
||||
}
|
||||
|
||||
if (growth <= 0) return
|
||||
if (turnStart() !== start) return
|
||||
|
||||
const reveal = !opts?.prefetch
|
||||
const currentRendered = renderedUserMessages().length
|
||||
const base = Math.max(beforeRendered, currentRendered)
|
||||
const target = reveal ? Math.min(afterVisible, base + turnBatch) : base
|
||||
const nextStart = Math.max(0, afterVisible - target)
|
||||
preserveScroll(() => setTurnStart(nextStart))
|
||||
}
|
||||
|
||||
const onScrollerScroll = () => {
|
||||
if (!input.userScrolled()) return
|
||||
const el = input.scroller()
|
||||
if (!el) return
|
||||
if (el.scrollTop >= turnScrollThreshold) return
|
||||
|
||||
const start = turnStart()
|
||||
if (start > 0) {
|
||||
if (start <= turnPrefetchBuffer) {
|
||||
void fetchOlderMessages({ prefetch: true })
|
||||
}
|
||||
backfillTurns()
|
||||
return
|
||||
}
|
||||
|
||||
void fetchOlderMessages()
|
||||
}
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
input.sessionID,
|
||||
() => {
|
||||
setState({ prefetchUntil: 0, prefetchNoGrowth: 0 })
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => [input.sessionID(), input.messagesReady()] as const,
|
||||
([id, ready]) => {
|
||||
if (!id || !ready) return
|
||||
setTurnStart(initialTurnStart(input.visibleUserMessages().length))
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
return {
|
||||
turnStart,
|
||||
setTurnStart,
|
||||
renderedUserMessages,
|
||||
loadAndReveal,
|
||||
onScrollerScroll,
|
||||
}
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const layout = useLayout()
|
||||
const local = useLocal()
|
||||
@@ -387,6 +178,7 @@ export default function Page() {
|
||||
return sync.session.history.loading(id)
|
||||
})
|
||||
|
||||
const emptyUserMessages: UserMessage[] = []
|
||||
const userMessages = createMemo(
|
||||
() => messages().filter((m) => m.role === "user") as UserMessage[],
|
||||
emptyUserMessages,
|
||||
@@ -419,6 +211,7 @@ export default function Page() {
|
||||
|
||||
const [store, setStore] = createStore({
|
||||
messageId: undefined as string | undefined,
|
||||
turnStart: 0,
|
||||
mobileTab: "session" as "session" | "changes",
|
||||
changes: "session" as "session" | "turn",
|
||||
newSessionWorktree: "main",
|
||||
@@ -427,6 +220,20 @@ export default function Page() {
|
||||
const turnDiffs = createMemo(() => lastUserMessage()?.summary?.diffs ?? [])
|
||||
const reviewDiffs = createMemo(() => (store.changes === "session" ? diffs() : turnDiffs()))
|
||||
|
||||
const renderedUserMessages = createMemo(
|
||||
() => {
|
||||
const msgs = visibleUserMessages()
|
||||
const start = store.turnStart
|
||||
if (start <= 0) return msgs
|
||||
if (start >= msgs.length) return emptyUserMessages
|
||||
return msgs.slice(start)
|
||||
},
|
||||
emptyUserMessages,
|
||||
{
|
||||
equals: same,
|
||||
},
|
||||
)
|
||||
|
||||
const newSessionWorktree = createMemo(() => {
|
||||
if (store.newSessionWorktree === "create") return "create"
|
||||
const project = sync.project
|
||||
@@ -495,15 +302,13 @@ export default function Page() {
|
||||
|
||||
const hasScrollGesture = () => Date.now() - ui.scrollGesture < scrollGestureWindowMs
|
||||
|
||||
createEffect(
|
||||
on([() => sdk.directory, () => params.id] as const, ([, id]) => {
|
||||
if (!id) return
|
||||
untrack(() => {
|
||||
void sync.session.sync(id)
|
||||
void sync.session.todo(id)
|
||||
})
|
||||
}),
|
||||
)
|
||||
createEffect(() => {
|
||||
sdk.directory
|
||||
const id = params.id
|
||||
if (!id) return
|
||||
void sync.session.sync(id)
|
||||
void sync.session.todo(id)
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
@@ -1089,16 +894,88 @@ export default function Page() {
|
||||
},
|
||||
)
|
||||
|
||||
const historyWindow = createSessionHistoryWindow({
|
||||
sessionID: () => params.id,
|
||||
messagesReady,
|
||||
visibleUserMessages,
|
||||
historyMore,
|
||||
historyLoading,
|
||||
loadMore: (sessionID) => sync.session.history.loadMore(sessionID),
|
||||
userScrolled: autoScroll.userScrolled,
|
||||
scroller: () => scroller,
|
||||
})
|
||||
const turnInit = 20
|
||||
const turnBatch = 20
|
||||
let turnHandle: number | undefined
|
||||
let turnIdle = false
|
||||
|
||||
function cancelTurnBackfill() {
|
||||
const handle = turnHandle
|
||||
if (handle === undefined) return
|
||||
turnHandle = undefined
|
||||
|
||||
if (turnIdle && window.cancelIdleCallback) {
|
||||
window.cancelIdleCallback(handle)
|
||||
return
|
||||
}
|
||||
|
||||
clearTimeout(handle)
|
||||
}
|
||||
|
||||
function scheduleTurnBackfill() {
|
||||
if (turnHandle !== undefined) return
|
||||
if (store.turnStart <= 0) return
|
||||
|
||||
if (window.requestIdleCallback) {
|
||||
turnIdle = true
|
||||
turnHandle = window.requestIdleCallback(() => {
|
||||
turnHandle = undefined
|
||||
backfillTurns()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
turnIdle = false
|
||||
turnHandle = window.setTimeout(() => {
|
||||
turnHandle = undefined
|
||||
backfillTurns()
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function backfillTurns() {
|
||||
const start = store.turnStart
|
||||
if (start <= 0) return
|
||||
|
||||
const next = start - turnBatch
|
||||
const nextStart = next > 0 ? next : 0
|
||||
|
||||
const el = scroller
|
||||
if (!el) {
|
||||
setStore("turnStart", nextStart)
|
||||
scheduleTurnBackfill()
|
||||
return
|
||||
}
|
||||
|
||||
const beforeTop = el.scrollTop
|
||||
const beforeHeight = el.scrollHeight
|
||||
|
||||
setStore("turnStart", nextStart)
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const delta = el.scrollHeight - beforeHeight
|
||||
if (!delta) return
|
||||
el.scrollTop = beforeTop + delta
|
||||
})
|
||||
|
||||
scheduleTurnBackfill()
|
||||
}
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => [params.id, messagesReady()] as const,
|
||||
([id, ready]) => {
|
||||
cancelTurnBackfill()
|
||||
setStore("turnStart", 0)
|
||||
if (!id || !ready) return
|
||||
|
||||
const len = visibleUserMessages().length
|
||||
const start = len > turnInit ? len - turnInit : 0
|
||||
setStore("turnStart", start)
|
||||
scheduleTurnBackfill()
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
createResizeObserver(
|
||||
() => promptDock,
|
||||
@@ -1125,12 +1002,13 @@ export default function Page() {
|
||||
sessionID: () => params.id,
|
||||
messagesReady,
|
||||
visibleUserMessages,
|
||||
turnStart: historyWindow.turnStart,
|
||||
turnStart: () => store.turnStart,
|
||||
currentMessageId: () => store.messageId,
|
||||
pendingMessage: () => ui.pendingMessage,
|
||||
setPendingMessage: (value) => setUi("pendingMessage", value),
|
||||
setActiveMessage,
|
||||
setTurnStart: historyWindow.setTurnStart,
|
||||
setTurnStart: (value) => setStore("turnStart", value),
|
||||
scheduleTurnBackfill,
|
||||
autoScroll,
|
||||
scroller: () => scroller,
|
||||
anchor,
|
||||
@@ -1143,6 +1021,7 @@ export default function Page() {
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
cancelTurnBackfill()
|
||||
document.removeEventListener("keydown", handleKeyDown)
|
||||
scrollSpy.destroy()
|
||||
if (scrollStateFrame !== undefined) cancelAnimationFrame(scrollStateFrame)
|
||||
@@ -1197,7 +1076,6 @@ export default function Page() {
|
||||
hasScrollGesture={hasScrollGesture}
|
||||
isDesktop={isDesktop()}
|
||||
onScrollSpyScroll={scrollSpy.onScroll}
|
||||
onTurnBackfillScroll={historyWindow.onScrollerScroll}
|
||||
onAutoScrollInteraction={autoScroll.handleInteraction}
|
||||
centered={centered()}
|
||||
setContentRef={(el) => {
|
||||
@@ -1207,16 +1085,21 @@ export default function Page() {
|
||||
const root = scroller
|
||||
if (root) scheduleScrollState(root)
|
||||
}}
|
||||
turnStart={historyWindow.turnStart()}
|
||||
turnStart={store.turnStart}
|
||||
onRenderEarlier={() => setStore("turnStart", 0)}
|
||||
historyMore={historyMore()}
|
||||
historyLoading={historyLoading()}
|
||||
onLoadEarlier={() => {
|
||||
void historyWindow.loadAndReveal()
|
||||
const id = params.id
|
||||
if (!id) return
|
||||
setStore("turnStart", 0)
|
||||
sync.session.history.loadMore(id)
|
||||
}}
|
||||
renderedUserMessages={historyWindow.renderedUserMessages()}
|
||||
renderedUserMessages={renderedUserMessages()}
|
||||
anchor={anchor}
|
||||
onRegisterMessage={scrollSpy.register}
|
||||
onUnregisterMessage={scrollSpy.unregister}
|
||||
lastUserMessageID={lastUserMessage()?.id}
|
||||
/>
|
||||
</Show>
|
||||
</Match>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { For, createEffect, createMemo, on, onCleanup, Show, startTransition, type JSX } from "solid-js"
|
||||
import { For, createEffect, createMemo, on, onCleanup, Show, type JSX } from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { useNavigate, useParams } from "@solidjs/router"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
@@ -81,103 +81,6 @@ const markBoundaryGesture = (input: {
|
||||
}
|
||||
}
|
||||
|
||||
type StageConfig = {
|
||||
init: number
|
||||
batch: number
|
||||
}
|
||||
|
||||
type TimelineStageInput = {
|
||||
sessionKey: () => string
|
||||
turnStart: () => number
|
||||
messages: () => UserMessage[]
|
||||
config: StageConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* Defer-mounts small timeline windows so revealing older turns does not
|
||||
* block first paint with a large DOM mount.
|
||||
*
|
||||
* Once staging completes for a session it never re-stages — backfill and
|
||||
* new messages render immediately.
|
||||
*/
|
||||
function createTimelineStaging(input: TimelineStageInput) {
|
||||
const [state, setState] = createStore({
|
||||
activeSession: "",
|
||||
completedSession: "",
|
||||
count: 0,
|
||||
})
|
||||
|
||||
const stagedCount = createMemo(() => {
|
||||
const total = input.messages().length
|
||||
if (input.turnStart() <= 0) return total
|
||||
if (state.completedSession === input.sessionKey()) return total
|
||||
const init = Math.min(total, input.config.init)
|
||||
if (state.count <= init) return init
|
||||
if (state.count >= total) return total
|
||||
return state.count
|
||||
})
|
||||
|
||||
const stagedUserMessages = createMemo(() => {
|
||||
const list = input.messages()
|
||||
const count = stagedCount()
|
||||
if (count >= list.length) return list
|
||||
return list.slice(Math.max(0, list.length - count))
|
||||
})
|
||||
|
||||
let frame: number | undefined
|
||||
const cancel = () => {
|
||||
if (frame === undefined) return
|
||||
cancelAnimationFrame(frame)
|
||||
frame = undefined
|
||||
}
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => [input.sessionKey(), input.turnStart() > 0, input.messages().length] as const,
|
||||
([sessionKey, isWindowed, total]) => {
|
||||
cancel()
|
||||
const shouldStage =
|
||||
isWindowed &&
|
||||
total > input.config.init &&
|
||||
state.completedSession !== sessionKey &&
|
||||
state.activeSession !== sessionKey
|
||||
if (!shouldStage) {
|
||||
setState({ activeSession: "", count: total })
|
||||
return
|
||||
}
|
||||
|
||||
let count = Math.min(total, input.config.init)
|
||||
setState({ activeSession: sessionKey, count })
|
||||
|
||||
const step = () => {
|
||||
if (input.sessionKey() !== sessionKey) {
|
||||
frame = undefined
|
||||
return
|
||||
}
|
||||
const currentTotal = input.messages().length
|
||||
count = Math.min(currentTotal, count + input.config.batch)
|
||||
startTransition(() => setState("count", count))
|
||||
if (count >= currentTotal) {
|
||||
setState({ completedSession: sessionKey, activeSession: "" })
|
||||
frame = undefined
|
||||
return
|
||||
}
|
||||
frame = requestAnimationFrame(step)
|
||||
}
|
||||
frame = requestAnimationFrame(step)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
const isStaging = createMemo(() => {
|
||||
const key = input.sessionKey()
|
||||
return state.activeSession === key && state.completedSession !== key
|
||||
})
|
||||
|
||||
onCleanup(cancel)
|
||||
return { messages: stagedUserMessages, isStaging }
|
||||
}
|
||||
|
||||
export function MessageTimeline(props: {
|
||||
mobileChanges: boolean
|
||||
mobileFallback: JSX.Element
|
||||
@@ -190,11 +93,11 @@ export function MessageTimeline(props: {
|
||||
hasScrollGesture: () => boolean
|
||||
isDesktop: boolean
|
||||
onScrollSpyScroll: () => void
|
||||
onTurnBackfillScroll: () => void
|
||||
onAutoScrollInteraction: (event: MouseEvent) => void
|
||||
centered: boolean
|
||||
setContentRef: (el: HTMLDivElement) => void
|
||||
turnStart: number
|
||||
onRenderEarlier: () => void
|
||||
historyMore: boolean
|
||||
historyLoading: boolean
|
||||
onLoadEarlier: () => void
|
||||
@@ -202,6 +105,7 @@ export function MessageTimeline(props: {
|
||||
anchor: (id: string) => string
|
||||
onRegisterMessage: (el: HTMLDivElement, id: string) => void
|
||||
onUnregisterMessage: (id: string) => void
|
||||
lastUserMessageID?: string
|
||||
}) {
|
||||
let touchGesture: number | undefined
|
||||
|
||||
@@ -223,13 +127,6 @@ export function MessageTimeline(props: {
|
||||
const titleValue = createMemo(() => info()?.title)
|
||||
const parentID = createMemo(() => info()?.parentID)
|
||||
const showHeader = createMemo(() => !!(titleValue() || parentID()))
|
||||
const stageCfg = { init: 1, batch: 3 }
|
||||
const staging = createTimelineStaging({
|
||||
sessionKey,
|
||||
turnStart: () => props.turnStart,
|
||||
messages: () => props.renderedUserMessages,
|
||||
config: stageCfg,
|
||||
})
|
||||
|
||||
const [title, setTitle] = createStore({
|
||||
draft: "",
|
||||
@@ -446,10 +343,8 @@ export function MessageTimeline(props: {
|
||||
<div
|
||||
class="absolute left-1/2 -translate-x-1/2 bottom-6 z-[60] pointer-events-none transition-all duration-200 ease-out"
|
||||
classList={{
|
||||
"opacity-100 translate-y-0 scale-100":
|
||||
props.scroll.overflow && !props.scroll.bottom && !staging.isStaging(),
|
||||
"opacity-0 translate-y-2 scale-95 pointer-events-none":
|
||||
!props.scroll.overflow || props.scroll.bottom || staging.isStaging(),
|
||||
"opacity-100 translate-y-0 scale-100": props.scroll.overflow && !props.scroll.bottom,
|
||||
"opacity-0 translate-y-2 scale-95 pointer-events-none": !props.scroll.overflow || props.scroll.bottom,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
@@ -498,7 +393,6 @@ export function MessageTimeline(props: {
|
||||
}}
|
||||
onScroll={(e) => {
|
||||
props.onScheduleScrollState(e.currentTarget)
|
||||
props.onTurnBackfillScroll()
|
||||
if (!props.hasScrollGesture()) return
|
||||
props.onAutoScrollHandleScroll()
|
||||
props.onMarkScrollGesture(e.currentTarget)
|
||||
@@ -636,7 +530,14 @@ export function MessageTimeline(props: {
|
||||
"mt-0": !props.centered,
|
||||
}}
|
||||
>
|
||||
<Show when={props.turnStart > 0 || props.historyMore}>
|
||||
<Show when={props.turnStart > 0}>
|
||||
<div class="w-full flex justify-center">
|
||||
<Button variant="ghost" size="large" class="text-12-medium opacity-50" onClick={props.onRenderEarlier}>
|
||||
{language.t("session.messages.renderEarlier")}
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={props.historyMore}>
|
||||
<div class="w-full flex justify-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -651,10 +552,9 @@ export function MessageTimeline(props: {
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
<For each={staging.messages()}>
|
||||
<For each={props.renderedUserMessages}>
|
||||
{(message) => {
|
||||
const comments = createMemo(() => messageComments(sync.data.part[message.id] ?? []))
|
||||
const commentCount = createMemo(() => comments().length)
|
||||
return (
|
||||
<div
|
||||
id={props.anchor(message.id)}
|
||||
@@ -667,9 +567,8 @@ export function MessageTimeline(props: {
|
||||
"min-w-0 w-full max-w-full": true,
|
||||
"md:max-w-200 2xl:max-w-[1000px]": props.centered,
|
||||
}}
|
||||
style={{ "content-visibility": "auto", "contain-intrinsic-size": "auto 500px" }}
|
||||
>
|
||||
<Show when={commentCount() > 0}>
|
||||
<Show when={comments().length > 0}>
|
||||
<div class="w-full px-4 md:px-5 pb-2">
|
||||
<div class="ml-auto max-w-[82%] overflow-x-auto no-scrollbar">
|
||||
<div class="flex w-max min-w-full justify-end gap-2">
|
||||
@@ -702,6 +601,7 @@ export function MessageTimeline(props: {
|
||||
<SessionTurn
|
||||
sessionID={sessionID() ?? ""}
|
||||
messageID={message.id}
|
||||
lastUserMessageID={props.lastUserMessageID}
|
||||
showReasoningSummaries={settings.general.showReasoningSummaries()}
|
||||
shellToolDefaultOpen={settings.general.shellToolPartsExpanded()}
|
||||
editToolDefaultOpen={settings.general.editToolPartsExpanded()}
|
||||
|
||||
@@ -19,6 +19,7 @@ export const useSessionHashScroll = (input: {
|
||||
setPendingMessage: (value: string | undefined) => void
|
||||
setActiveMessage: (message: UserMessage | undefined) => void
|
||||
setTurnStart: (value: number) => void
|
||||
scheduleTurnBackfill: () => void
|
||||
autoScroll: { pause: () => void; forceScrollToBottom: () => void }
|
||||
scroller: () => HTMLDivElement | undefined
|
||||
anchor: (id: string) => string
|
||||
@@ -57,6 +58,7 @@ export const useSessionHashScroll = (input: {
|
||||
const index = messageIndex().get(message.id) ?? -1
|
||||
if (index !== -1 && index < input.turnStart()) {
|
||||
input.setTurnStart(index)
|
||||
input.scheduleTurnBackfill()
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const el = document.getElementById(input.anchor(message.id))
|
||||
|
||||
@@ -7,21 +7,9 @@ import { Font } from "@opencode-ai/ui/font"
|
||||
import "@ibm/plex/css/ibm-plex.css"
|
||||
import "./app.css"
|
||||
import { LanguageProvider } from "~/context/language"
|
||||
import { I18nProvider, useI18n } from "~/context/i18n"
|
||||
import { I18nProvider } from "~/context/i18n"
|
||||
import { strip } from "~/lib/language"
|
||||
|
||||
function AppMeta() {
|
||||
const i18n = useI18n()
|
||||
return (
|
||||
<>
|
||||
<Title>opencode</Title>
|
||||
<Meta name="description" content={i18n.t("app.meta.description")} />
|
||||
<Favicon />
|
||||
<Font />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Router
|
||||
@@ -31,7 +19,10 @@ export default function App() {
|
||||
<LanguageProvider>
|
||||
<I18nProvider>
|
||||
<MetaProvider>
|
||||
<AppMeta />
|
||||
<Title>opencode</Title>
|
||||
<Meta name="description" content="OpenCode - The open source coding agent." />
|
||||
<Favicon />
|
||||
<Font />
|
||||
<Suspense>{props.children}</Suspense>
|
||||
</MetaProvider>
|
||||
</I18nProvider>
|
||||
|
||||
@@ -124,8 +124,8 @@ export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) {
|
||||
<section data-component="top">
|
||||
<div onContextMenu={handleLogoContextMenu}>
|
||||
<A href={language.route("/")}>
|
||||
<img data-slot="logo light" src={logoLight} alt={i18n.t("nav.logoAlt")} width="189" height="34" />
|
||||
<img data-slot="logo dark" src={logoDark} alt={i18n.t("nav.logoAlt")} width="189" height="34" />
|
||||
<img data-slot="logo light" src={logoLight} alt="OpenCode" width="189" height="34" />
|
||||
<img data-slot="logo dark" src={logoDark} alt="OpenCode" width="189" height="34" />
|
||||
</A>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ export const dict = {
|
||||
"nav.home": "الرئيسية",
|
||||
"nav.openMenu": "فتح القائمة",
|
||||
"nav.getStartedFree": "ابدأ مجانا",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "نسخ الشعار كـ SVG",
|
||||
"nav.context.copyWordmark": "نسخ اسم العلامة كـ SVG",
|
||||
@@ -43,13 +42,9 @@ export const dict = {
|
||||
"notFound.docs": "الوثائق",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "شعار opencode الفاتح",
|
||||
"notFound.logoDarkAlt": "شعار opencode الداكن",
|
||||
|
||||
"user.logout": "تسجيل الخروج",
|
||||
|
||||
"auth.callback.error.codeMissing": "لم يتم العثور على رمز التفويض.",
|
||||
|
||||
"workspace.select": "اختر مساحة العمل",
|
||||
"workspace.createNew": "+ إنشاء مساحة عمل جديدة",
|
||||
"workspace.modal.title": "إنشاء مساحة عمل جديدة",
|
||||
@@ -81,8 +76,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "يجب أن يكون مبلغ الشحن ${{amount}} على الأقل",
|
||||
"error.reloadTriggerMin": "يجب أن يكون حد الرصيد ${{amount}} على الأقل",
|
||||
|
||||
"app.meta.description": "OpenCode - وكيل البرمجة مفتوح المصدر.",
|
||||
|
||||
"home.title": "OpenCode | وكيل برمجة بالذكاء الاصطناعي مفتوح المصدر",
|
||||
|
||||
"temp.title": "opencode | وكيل برمجة بالذكاء الاصطناعي مبني للطرفية",
|
||||
@@ -98,8 +91,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": "، بما في ذلك النماذج المحلية",
|
||||
"temp.screenshot.caption": "واجهة OpenCode الطرفية مع سمة tokyonight",
|
||||
"temp.screenshot.alt": "واجهة OpenCode الطرفية بسمة tokyonight",
|
||||
"temp.logoLightAlt": "شعار opencode الفاتح",
|
||||
"temp.logoDarkAlt": "شعار opencode الداكن",
|
||||
|
||||
"home.banner.badge": "جديد",
|
||||
"home.banner.text": "تطبيق سطح المكتب متاح بنسخة تجريبية",
|
||||
@@ -247,24 +238,6 @@ export const dict = {
|
||||
"تتم استضافة جميع نماذج Zen في الولايات المتحدة. يتبع المزودون سياسة عدم الاحتفاظ بالبيانات ولا يستخدمون بياناتك لتدريب النماذج، مع",
|
||||
"zen.privacy.exceptionsLink": "الاستثناءات التالية",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "تم تجاوز حد الطلبات. يرجى المحاولة مرة أخرى لاحقًا.",
|
||||
"zen.api.error.modelNotSupported": "النموذج {{model}} غير مدعوم",
|
||||
"zen.api.error.modelFormatNotSupported": "النموذج {{model}} غير مدعوم للتنسيق {{format}}",
|
||||
"zen.api.error.noProviderAvailable": "لا يوجد مزود متاح",
|
||||
"zen.api.error.providerNotSupported": "المزود {{provider}} غير مدعوم",
|
||||
"zen.api.error.missingApiKey": "مفتاح API مفقود.",
|
||||
"zen.api.error.invalidApiKey": "مفتاح API غير صالح.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "تم تجاوز حصة الاشتراك. أعد المحاولة خلال {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"تم تجاوز حصة الاشتراك. يمكنك الاستمرار في استخدام النماذج المجانية.",
|
||||
"zen.api.error.noPaymentMethod": "لا توجد طريقة دفع. أضف طريقة دفع هنا: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "رصيد غير كاف. إدارة فواتيرك هنا: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"وصلت مساحة العمل الخاصة بك إلى حد الإنفاق الشهري البالغ ${{amount}}. إدارة حدودك هنا: {{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached":
|
||||
"لقد وصلت إلى حد الإنفاق الشهري البالغ ${{amount}}. إدارة حدودك هنا: {{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "النموذج معطل",
|
||||
|
||||
"black.meta.title": "OpenCode Black | الوصول إلى أفضل نماذج البرمجة في العالم",
|
||||
"black.meta.description": "احصل على وصول إلى Claude، GPT، Gemini والمزيد مع خطط اشتراك OpenCode Black.",
|
||||
"black.hero.title": "الوصول إلى أفضل نماذج البرمجة في العالم",
|
||||
@@ -473,7 +446,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "يرجى تحديث طريقة الدفع والمحاولة مرة أخرى.",
|
||||
"workspace.reload.retrying": "جارٍ إعادة المحاولة...",
|
||||
"workspace.reload.retry": "أعد المحاولة",
|
||||
"workspace.reload.error.paymentFailed": "فشلت عملية الدفع.",
|
||||
|
||||
"workspace.payments.title": "سجل المدفوعات",
|
||||
"workspace.payments.subtitle": "معاملات الدفع الأخيرة.",
|
||||
@@ -591,10 +563,6 @@ export const dict = {
|
||||
"enterprise.form.send": "إرسال",
|
||||
"enterprise.form.sending": "جارٍ الإرسال...",
|
||||
"enterprise.form.success": "تم إرسال الرسالة، سنتواصل معك قريبًا.",
|
||||
"enterprise.form.success.submitted": "تم إرسال النموذج بنجاح.",
|
||||
"enterprise.form.error.allFieldsRequired": "جميع الحقول مطلوبة.",
|
||||
"enterprise.form.error.invalidEmailFormat": "تنسيق البريد الإلكتروني غير صالح.",
|
||||
"enterprise.form.error.internalServer": "خطأ داخلي في الخادم.",
|
||||
"enterprise.faq.title": "الأسئلة الشائعة",
|
||||
"enterprise.faq.q1": "ما هو OpenCode Enterprise؟",
|
||||
"enterprise.faq.a1":
|
||||
@@ -627,7 +595,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "الوكيل",
|
||||
"bench.list.table.model": "النموذج",
|
||||
"bench.list.table.score": "الدرجة",
|
||||
"bench.submission.error.allFieldsRequired": "جميع الحقول مطلوبة.",
|
||||
|
||||
"bench.detail.title": "المعيار - {{task}}",
|
||||
"bench.detail.notFound": "المهمة غير موجودة",
|
||||
|
||||
@@ -15,7 +15,6 @@ export const dict = {
|
||||
"nav.home": "Início",
|
||||
"nav.openMenu": "Abrir menu",
|
||||
"nav.getStartedFree": "Começar grátis",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "Copiar logo como SVG",
|
||||
"nav.context.copyWordmark": "Copiar marca como SVG",
|
||||
@@ -43,13 +42,9 @@ export const dict = {
|
||||
"notFound.docs": "Documentação",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "logo opencode claro",
|
||||
"notFound.logoDarkAlt": "logo opencode escuro",
|
||||
|
||||
"user.logout": "Sair",
|
||||
|
||||
"auth.callback.error.codeMissing": "Nenhum código de autorização encontrado.",
|
||||
|
||||
"workspace.select": "Selecionar workspace",
|
||||
"workspace.createNew": "+ Criar novo workspace",
|
||||
"workspace.modal.title": "Criar novo workspace",
|
||||
@@ -81,8 +76,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "O valor de recarga deve ser de pelo menos ${{amount}}",
|
||||
"error.reloadTriggerMin": "O gatilho de saldo deve ser de pelo menos ${{amount}}",
|
||||
|
||||
"app.meta.description": "OpenCode - O agente de codificação de código aberto.",
|
||||
|
||||
"home.title": "OpenCode | O agente de codificação de código aberto com IA",
|
||||
|
||||
"temp.title": "opencode | Agente de codificação com IA feito para o terminal",
|
||||
@@ -98,8 +91,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": ", incluindo modelos locais",
|
||||
"temp.screenshot.caption": "OpenCode TUI com o tema tokyonight",
|
||||
"temp.screenshot.alt": "OpenCode TUI com tema tokyonight",
|
||||
"temp.logoLightAlt": "logo opencode claro",
|
||||
"temp.logoDarkAlt": "logo opencode escuro",
|
||||
|
||||
"home.banner.badge": "Novo",
|
||||
"home.banner.text": "App desktop disponível em beta",
|
||||
@@ -251,24 +242,6 @@ export const dict = {
|
||||
"Todos os modelos Zen são hospedados nos EUA. Os provedores seguem uma política de retenção zero e não usam seus dados para treinamento de modelo, com as",
|
||||
"zen.privacy.exceptionsLink": "seguintes exceções",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Limite de taxa excedido. Por favor, tente novamente mais tarde.",
|
||||
"zen.api.error.modelNotSupported": "Modelo {{model}} não suportado",
|
||||
"zen.api.error.modelFormatNotSupported": "Modelo {{model}} não suportado para o formato {{format}}",
|
||||
"zen.api.error.noProviderAvailable": "Nenhum provedor disponível",
|
||||
"zen.api.error.providerNotSupported": "Provedor {{provider}} não suportado",
|
||||
"zen.api.error.missingApiKey": "Chave de API ausente.",
|
||||
"zen.api.error.invalidApiKey": "Chave de API inválida.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Cota de assinatura excedida. Tente novamente em {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Cota de assinatura excedida. Você pode continuar usando modelos gratuitos.",
|
||||
"zen.api.error.noPaymentMethod": "Nenhuma forma de pagamento. Adicione uma forma de pagamento aqui: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Saldo insuficiente. Gerencie seu faturamento aqui: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"Seu workspace atingiu o limite de gastos mensais de ${{amount}}. Gerencie seus limites aqui: {{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached":
|
||||
"Você atingiu seu limite de gastos mensais de ${{amount}}. Gerencie seus limites aqui: {{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "O modelo está desabilitado",
|
||||
|
||||
"black.meta.title": "OpenCode Black | Acesse os melhores modelos de codificação do mundo",
|
||||
"black.meta.description": "Tenha acesso ao Claude, GPT, Gemini e mais com os planos de assinatura OpenCode Black.",
|
||||
"black.hero.title": "Acesse os melhores modelos de codificação do mundo",
|
||||
@@ -478,7 +451,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "Por favor, atualize sua forma de pagamento e tente novamente.",
|
||||
"workspace.reload.retrying": "Tentando novamente...",
|
||||
"workspace.reload.retry": "Tentar novamente",
|
||||
"workspace.reload.error.paymentFailed": "Pagamento falhou.",
|
||||
|
||||
"workspace.payments.title": "Histórico de Pagamentos",
|
||||
"workspace.payments.subtitle": "Transações de pagamento recentes.",
|
||||
@@ -599,10 +571,6 @@ export const dict = {
|
||||
"enterprise.form.send": "Enviar",
|
||||
"enterprise.form.sending": "Enviando...",
|
||||
"enterprise.form.success": "Mensagem enviada, entraremos em contato em breve.",
|
||||
"enterprise.form.success.submitted": "Formulário enviado com sucesso.",
|
||||
"enterprise.form.error.allFieldsRequired": "Todos os campos são obrigatórios.",
|
||||
"enterprise.form.error.invalidEmailFormat": "Formato de e-mail inválido.",
|
||||
"enterprise.form.error.internalServer": "Erro interno do servidor.",
|
||||
"enterprise.faq.title": "FAQ",
|
||||
"enterprise.faq.q1": "O que é OpenCode Enterprise?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -635,7 +603,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "Agente",
|
||||
"bench.list.table.model": "Modelo",
|
||||
"bench.list.table.score": "Pontuação",
|
||||
"bench.submission.error.allFieldsRequired": "Todos os campos são obrigatórios.",
|
||||
|
||||
"bench.detail.title": "Benchmark - {{task}}",
|
||||
"bench.detail.notFound": "Tarefa não encontrada",
|
||||
|
||||
@@ -15,7 +15,6 @@ export const dict = {
|
||||
"nav.home": "Hjem",
|
||||
"nav.openMenu": "Åbn menu",
|
||||
"nav.getStartedFree": "Kom i gang gratis",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "Kopier logo som SVG",
|
||||
"nav.context.copyWordmark": "Kopier wordmark som SVG",
|
||||
@@ -43,13 +42,9 @@ export const dict = {
|
||||
"notFound.docs": "Dokumentation",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "opencode logo light",
|
||||
"notFound.logoDarkAlt": "opencode logo dark",
|
||||
|
||||
"user.logout": "Log ud",
|
||||
|
||||
"auth.callback.error.codeMissing": "Ingen autorisationskode fundet.",
|
||||
|
||||
"workspace.select": "Vælg workspace",
|
||||
"workspace.createNew": "+ Opret nyt workspace",
|
||||
"workspace.modal.title": "Opret nyt workspace",
|
||||
@@ -81,8 +76,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "Genopfyldningsbeløb skal være mindst ${{amount}}",
|
||||
"error.reloadTriggerMin": "Saldogrænse skal være mindst ${{amount}}",
|
||||
|
||||
"app.meta.description": "OpenCode - Den open source kodningsagent.",
|
||||
|
||||
"home.title": "OpenCode | Den open source AI-kodningsagent",
|
||||
|
||||
"temp.title": "opencode | AI-kodningsagent bygget til terminalen",
|
||||
@@ -98,8 +91,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": ", inklusive lokale modeller",
|
||||
"temp.screenshot.caption": "opencode TUI med tokyonight-temaet",
|
||||
"temp.screenshot.alt": "opencode TUI med tokyonight-temaet",
|
||||
"temp.logoLightAlt": "opencode logo light",
|
||||
"temp.logoDarkAlt": "opencode logo dark",
|
||||
|
||||
"home.banner.badge": "Ny",
|
||||
"home.banner.text": "Desktop-app tilgængelig i beta",
|
||||
@@ -249,24 +240,6 @@ export const dict = {
|
||||
"Alle Zen-modeller er hostet i USA. Udbydere følger en nulopbevaringspolitik og bruger ikke dine data til modeltræning med",
|
||||
"zen.privacy.exceptionsLink": "følgende undtagelser",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Hastighedsgrænse overskredet. Prøv venligst igen senere.",
|
||||
"zen.api.error.modelNotSupported": "Model {{model}} understøttes ikke",
|
||||
"zen.api.error.modelFormatNotSupported": "Model {{model}} understøttes ikke for format {{format}}",
|
||||
"zen.api.error.noProviderAvailable": "Ingen udbyder tilgængelig",
|
||||
"zen.api.error.providerNotSupported": "Udbyder {{provider}} understøttes ikke",
|
||||
"zen.api.error.missingApiKey": "Manglende API-nøgle.",
|
||||
"zen.api.error.invalidApiKey": "Ugyldig API-nøgle.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Abonnementskvote overskredet. Prøv igen om {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Abonnementskvote overskredet. Du kan fortsætte med at bruge gratis modeller.",
|
||||
"zen.api.error.noPaymentMethod": "Ingen betalingsmetode. Tilføj en betalingsmetode her: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Utilstrækkelig saldo. Administrer din fakturering her: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"Dit workspace har nået sin månedlige forbrugsgrænse på ${{amount}}. Administrer dine grænser her: {{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached":
|
||||
"Du har nået din månedlige forbrugsgrænse på ${{amount}}. Administrer dine grænser her: {{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "Modellen er deaktiveret",
|
||||
|
||||
"black.meta.title": "OpenCode Black | Få adgang til verdens bedste kodningsmodeller",
|
||||
"black.meta.description": "Få adgang til Claude, GPT, Gemini og mere med OpenCode Black-abonnementer.",
|
||||
"black.hero.title": "Få adgang til verdens bedste kodningsmodeller",
|
||||
@@ -476,7 +449,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "Opdater din betalingsmetode, og prøv igen.",
|
||||
"workspace.reload.retrying": "Prøver igen...",
|
||||
"workspace.reload.retry": "Prøv igen",
|
||||
"workspace.reload.error.paymentFailed": "Betaling mislykkedes.",
|
||||
|
||||
"workspace.payments.title": "Betalingshistorik",
|
||||
"workspace.payments.subtitle": "Seneste betalingstransaktioner.",
|
||||
@@ -595,10 +567,6 @@ export const dict = {
|
||||
"enterprise.form.send": "Send",
|
||||
"enterprise.form.sending": "Sender...",
|
||||
"enterprise.form.success": "Besked sendt, vi vender tilbage snart.",
|
||||
"enterprise.form.success.submitted": "Formular indsendt med succes.",
|
||||
"enterprise.form.error.allFieldsRequired": "Alle felter er påkrævet.",
|
||||
"enterprise.form.error.invalidEmailFormat": "Ugyldigt e-mailformat.",
|
||||
"enterprise.form.error.internalServer": "Intern serverfejl.",
|
||||
"enterprise.faq.title": "FAQ",
|
||||
"enterprise.faq.q1": "Hvad er OpenCode Enterprise?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -631,7 +599,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "Agent",
|
||||
"bench.list.table.model": "Model",
|
||||
"bench.list.table.score": "Score",
|
||||
"bench.submission.error.allFieldsRequired": "Alle felter er påkrævet.",
|
||||
|
||||
"bench.detail.title": "Benchmark - {{task}}",
|
||||
"bench.detail.notFound": "Opgave ikke fundet",
|
||||
|
||||
@@ -15,7 +15,6 @@ export const dict = {
|
||||
"nav.home": "Startseite",
|
||||
"nav.openMenu": "Menü öffnen",
|
||||
"nav.getStartedFree": "Kostenlos starten",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "Logo als SVG kopieren",
|
||||
"nav.context.copyWordmark": "Wortmarke als SVG kopieren",
|
||||
@@ -43,13 +42,9 @@ export const dict = {
|
||||
"notFound.docs": "Dokumentation",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "OpenCode Logo hell",
|
||||
"notFound.logoDarkAlt": "OpenCode Logo dunkel",
|
||||
|
||||
"user.logout": "Abmelden",
|
||||
|
||||
"auth.callback.error.codeMissing": "Kein Autorisierungscode gefunden.",
|
||||
|
||||
"workspace.select": "Workspace auswählen",
|
||||
"workspace.createNew": "+ Neuen Workspace erstellen",
|
||||
"workspace.modal.title": "Neuen Workspace erstellen",
|
||||
@@ -81,8 +76,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "Aufladebetrag muss mindestens ${{amount}} betragen",
|
||||
"error.reloadTriggerMin": "Guthaben-Auslöser muss mindestens ${{amount}} betragen",
|
||||
|
||||
"app.meta.description": "OpenCode - Der Open-Source Coding-Agent.",
|
||||
|
||||
"home.title": "OpenCode | Der Open-Source AI-Coding-Agent",
|
||||
|
||||
"temp.title": "OpenCode | Für das Terminal gebauter AI-Coding-Agent",
|
||||
@@ -98,8 +91,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": ", einschließlich lokaler Modelle",
|
||||
"temp.screenshot.caption": "OpenCode TUI mit dem Tokyonight-Theme",
|
||||
"temp.screenshot.alt": "OpenCode TUI mit Tokyonight-Theme",
|
||||
"temp.logoLightAlt": "OpenCode Logo hell",
|
||||
"temp.logoDarkAlt": "OpenCode Logo dunkel",
|
||||
|
||||
"home.banner.badge": "Neu",
|
||||
"home.banner.text": "Desktop-App in der Beta verfügbar",
|
||||
@@ -251,24 +242,6 @@ export const dict = {
|
||||
"Alle Zen-Modelle werden in den USA gehostet. Anbieter folgen einer Zero-Retention-Policy und nutzen deine Daten nicht für Modelltraining, mit den",
|
||||
"zen.privacy.exceptionsLink": "folgenden Ausnahmen",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Ratenlimit überschritten. Bitte versuche es später erneut.",
|
||||
"zen.api.error.modelNotSupported": "Modell {{model}} wird nicht unterstützt",
|
||||
"zen.api.error.modelFormatNotSupported": "Modell {{model}} wird für das Format {{format}} nicht unterstützt",
|
||||
"zen.api.error.noProviderAvailable": "Kein Anbieter verfügbar",
|
||||
"zen.api.error.providerNotSupported": "Anbieter {{provider}} wird nicht unterstützt",
|
||||
"zen.api.error.missingApiKey": "Fehlender API-Key.",
|
||||
"zen.api.error.invalidApiKey": "Ungültiger API-Key.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Abonnement-Quote überschritten. Erneuter Versuch in {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Abonnement-Quote überschritten. Du kannst weiterhin kostenlose Modelle nutzen.",
|
||||
"zen.api.error.noPaymentMethod": "Keine Zahlungsmethode. Füge hier eine Zahlungsmethode hinzu: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Unzureichendes Guthaben. Verwalte deine Abrechnung hier: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"Dein Workspace hat sein monatliches Ausgabenlimit von ${{amount}} erreicht. Verwalte deine Limits hier: {{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached":
|
||||
"Du hast dein monatliches Ausgabenlimit von ${{amount}} erreicht. Verwalte deine Limits hier: {{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "Modell ist deaktiviert",
|
||||
|
||||
"black.meta.title": "OpenCode Black | Zugriff auf die weltweit besten Coding-Modelle",
|
||||
"black.meta.description": "Erhalte Zugriff auf Claude, GPT, Gemini und mehr mit OpenCode Black Abos.",
|
||||
"black.hero.title": "Zugriff auf die weltweit besten Coding-Modelle",
|
||||
@@ -478,7 +451,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "Bitte aktualisiere deine Zahlungsmethode und versuche es erneut.",
|
||||
"workspace.reload.retrying": "Versuche erneut...",
|
||||
"workspace.reload.retry": "Erneut versuchen",
|
||||
"workspace.reload.error.paymentFailed": "Zahlung fehlgeschlagen.",
|
||||
|
||||
"workspace.payments.title": "Zahlungshistorie",
|
||||
"workspace.payments.subtitle": "Kürzliche Zahlungstransaktionen.",
|
||||
@@ -599,10 +571,6 @@ export const dict = {
|
||||
"enterprise.form.send": "Senden",
|
||||
"enterprise.form.sending": "Sende...",
|
||||
"enterprise.form.success": "Nachricht gesendet, wir melden uns bald.",
|
||||
"enterprise.form.success.submitted": "Formular erfolgreich gesendet.",
|
||||
"enterprise.form.error.allFieldsRequired": "Alle Felder sind erforderlich.",
|
||||
"enterprise.form.error.invalidEmailFormat": "Ungültiges E-Mail-Format.",
|
||||
"enterprise.form.error.internalServer": "Interner Serverfehler.",
|
||||
"enterprise.faq.title": "FAQ",
|
||||
"enterprise.faq.q1": "Was ist OpenCode Enterprise?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -635,7 +603,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "Agent",
|
||||
"bench.list.table.model": "Modell",
|
||||
"bench.list.table.score": "Score",
|
||||
"bench.submission.error.allFieldsRequired": "Alle Felder sind erforderlich.",
|
||||
|
||||
"bench.detail.title": "Benchmark - {{task}}",
|
||||
"bench.detail.notFound": "Task nicht gefunden",
|
||||
|
||||
@@ -11,7 +11,6 @@ export const dict = {
|
||||
"nav.home": "Home",
|
||||
"nav.openMenu": "Open menu",
|
||||
"nav.getStartedFree": "Get started for free",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "Copy logo as SVG",
|
||||
"nav.context.copyWordmark": "Copy wordmark as SVG",
|
||||
@@ -39,13 +38,9 @@ export const dict = {
|
||||
"notFound.docs": "Docs",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "opencode logo light",
|
||||
"notFound.logoDarkAlt": "opencode logo dark",
|
||||
|
||||
"user.logout": "Logout",
|
||||
|
||||
"auth.callback.error.codeMissing": "No authorization code found.",
|
||||
|
||||
"workspace.select": "Select workspace",
|
||||
"workspace.createNew": "+ Create New Workspace",
|
||||
"workspace.modal.title": "Create New Workspace",
|
||||
@@ -77,8 +72,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "Reload amount must be at least ${{amount}}",
|
||||
"error.reloadTriggerMin": "Balance trigger must be at least ${{amount}}",
|
||||
|
||||
"app.meta.description": "OpenCode - The open source coding agent.",
|
||||
|
||||
"home.title": "OpenCode | The open source AI coding agent",
|
||||
|
||||
"temp.title": "opencode | AI coding agent built for the terminal",
|
||||
@@ -94,8 +87,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": ", including local models",
|
||||
"temp.screenshot.caption": "opencode TUI with the tokyonight theme",
|
||||
"temp.screenshot.alt": "opencode TUI with tokyonight theme",
|
||||
"temp.logoLightAlt": "opencode logo light",
|
||||
"temp.logoDarkAlt": "opencode logo dark",
|
||||
|
||||
"home.banner.badge": "New",
|
||||
"home.banner.text": "Desktop app available in beta",
|
||||
@@ -243,24 +234,6 @@ export const dict = {
|
||||
"All Zen models are hosted in the US. Providers follow a zero-retention policy and do not use your data for model training, with the",
|
||||
"zen.privacy.exceptionsLink": "following exceptions",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Rate limit exceeded. Please try again later.",
|
||||
"zen.api.error.modelNotSupported": "Model {{model}} not supported",
|
||||
"zen.api.error.modelFormatNotSupported": "Model {{model}} not supported for format {{format}}",
|
||||
"zen.api.error.noProviderAvailable": "No provider available",
|
||||
"zen.api.error.providerNotSupported": "Provider {{provider}} not supported",
|
||||
"zen.api.error.missingApiKey": "Missing API key.",
|
||||
"zen.api.error.invalidApiKey": "Invalid API key.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Subscription quota exceeded. Retry in {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Subscription quota exceeded. You can continue using free models.",
|
||||
"zen.api.error.noPaymentMethod": "No payment method. Add a payment method here: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Insufficient balance. Manage your billing here: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"Your workspace has reached its monthly spending limit of ${{amount}}. Manage your limits here: {{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached":
|
||||
"You have reached your monthly spending limit of ${{amount}}. Manage your limits here: {{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "Model is disabled",
|
||||
|
||||
"black.meta.title": "OpenCode Black | Access all the world's best coding models",
|
||||
"black.meta.description": "Get access to Claude, GPT, Gemini and more with OpenCode Black subscription plans.",
|
||||
"black.hero.title": "Access all the world's best coding models",
|
||||
@@ -470,7 +443,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "Please update your payment method and try again.",
|
||||
"workspace.reload.retrying": "Retrying...",
|
||||
"workspace.reload.retry": "Retry",
|
||||
"workspace.reload.error.paymentFailed": "Payment failed.",
|
||||
|
||||
"workspace.payments.title": "Payments History",
|
||||
"workspace.payments.subtitle": "Recent payment transactions.",
|
||||
@@ -589,10 +561,6 @@ export const dict = {
|
||||
"enterprise.form.send": "Send",
|
||||
"enterprise.form.sending": "Sending...",
|
||||
"enterprise.form.success": "Message sent, we'll be in touch soon.",
|
||||
"enterprise.form.success.submitted": "Form submitted successfully.",
|
||||
"enterprise.form.error.allFieldsRequired": "All fields are required.",
|
||||
"enterprise.form.error.invalidEmailFormat": "Invalid email format.",
|
||||
"enterprise.form.error.internalServer": "Internal server error.",
|
||||
"enterprise.faq.title": "FAQ",
|
||||
"enterprise.faq.q1": "What is OpenCode Enterprise?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -625,7 +593,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "Agent",
|
||||
"bench.list.table.model": "Model",
|
||||
"bench.list.table.score": "Score",
|
||||
"bench.submission.error.allFieldsRequired": "All fields are required.",
|
||||
|
||||
"bench.detail.title": "Benchmark - {{task}}",
|
||||
"bench.detail.notFound": "Task not found",
|
||||
|
||||
@@ -15,7 +15,6 @@ export const dict = {
|
||||
"nav.home": "Inicio",
|
||||
"nav.openMenu": "Abrir menú",
|
||||
"nav.getStartedFree": "Empezar gratis",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "Copiar logo como SVG",
|
||||
"nav.context.copyWordmark": "Copiar marca como SVG",
|
||||
@@ -43,13 +42,9 @@ export const dict = {
|
||||
"notFound.docs": "Documentación",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "opencode logo claro",
|
||||
"notFound.logoDarkAlt": "opencode logo oscuro",
|
||||
|
||||
"user.logout": "Cerrar sesión",
|
||||
|
||||
"auth.callback.error.codeMissing": "No se encontró código de autorización.",
|
||||
|
||||
"workspace.select": "Seleccionar espacio de trabajo",
|
||||
"workspace.createNew": "+ Crear nuevo espacio de trabajo",
|
||||
"workspace.modal.title": "Crear nuevo espacio de trabajo",
|
||||
@@ -81,8 +76,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "La cantidad de recarga debe ser al menos ${{amount}}",
|
||||
"error.reloadTriggerMin": "El disparador de saldo debe ser al menos ${{amount}}",
|
||||
|
||||
"app.meta.description": "OpenCode - El agente de codificación de código abierto.",
|
||||
|
||||
"home.title": "OpenCode | El agente de codificación IA de código abierto",
|
||||
|
||||
"temp.title": "opencode | Agente de codificación IA creado para la terminal",
|
||||
@@ -98,8 +91,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": ", incluyendo modelos locales",
|
||||
"temp.screenshot.caption": "opencode TUI con el tema tokyonight",
|
||||
"temp.screenshot.alt": "opencode TUI con tema tokyonight",
|
||||
"temp.logoLightAlt": "logo de opencode claro",
|
||||
"temp.logoDarkAlt": "logo de opencode oscuro",
|
||||
|
||||
"home.banner.badge": "Nuevo",
|
||||
"home.banner.text": "Aplicación de escritorio disponible en beta",
|
||||
@@ -252,24 +243,6 @@ export const dict = {
|
||||
"Todos los modelos Zen están alojados en EE. UU. Los proveedores siguen una política de cero retención y no usan tus datos para entrenamiento de modelos, con las",
|
||||
"zen.privacy.exceptionsLink": "siguientes excepciones",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Límite de tasa excedido. Por favor, inténtalo de nuevo más tarde.",
|
||||
"zen.api.error.modelNotSupported": "Modelo {{model}} no soportado",
|
||||
"zen.api.error.modelFormatNotSupported": "Modelo {{model}} no soportado para el formato {{format}}",
|
||||
"zen.api.error.noProviderAvailable": "Ningún proveedor disponible",
|
||||
"zen.api.error.providerNotSupported": "Proveedor {{provider}} no soportado",
|
||||
"zen.api.error.missingApiKey": "Falta la clave API.",
|
||||
"zen.api.error.invalidApiKey": "Clave API inválida.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Cuota de suscripción excedida. Reintenta en {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Cuota de suscripción excedida. Puedes continuar usando modelos gratuitos.",
|
||||
"zen.api.error.noPaymentMethod": "Sin método de pago. Añade un método de pago aquí: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Saldo insuficiente. Gestiona tu facturación aquí: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"Tu espacio de trabajo ha alcanzado su límite de gasto mensual de ${{amount}}. Gestiona tus límites aquí: {{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached":
|
||||
"Has alcanzado tu límite de gasto mensual de ${{amount}}. Gestiona tus límites aquí: {{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "El modelo está deshabilitado",
|
||||
|
||||
"black.meta.title": "OpenCode Black | Accede a los mejores modelos de codificación del mundo",
|
||||
"black.meta.description": "Obtén acceso a Claude, GPT, Gemini y más con los planes de suscripción de OpenCode Black.",
|
||||
"black.hero.title": "Accede a los mejores modelos de codificación del mundo",
|
||||
@@ -479,7 +452,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "Por favor actualiza tu método de pago e intenta de nuevo.",
|
||||
"workspace.reload.retrying": "Reintentando...",
|
||||
"workspace.reload.retry": "Reintentar",
|
||||
"workspace.reload.error.paymentFailed": "El pago falló.",
|
||||
|
||||
"workspace.payments.title": "Historial de Pagos",
|
||||
"workspace.payments.subtitle": "Transacciones de pago recientes.",
|
||||
@@ -599,10 +571,6 @@ export const dict = {
|
||||
"enterprise.form.send": "Enviar",
|
||||
"enterprise.form.sending": "Enviando...",
|
||||
"enterprise.form.success": "Mensaje enviado, estaremos en contacto pronto.",
|
||||
"enterprise.form.success.submitted": "Formulario enviado con éxito.",
|
||||
"enterprise.form.error.allFieldsRequired": "Todos los campos son obligatorios.",
|
||||
"enterprise.form.error.invalidEmailFormat": "Formato de correo inválido.",
|
||||
"enterprise.form.error.internalServer": "Error interno del servidor.",
|
||||
"enterprise.faq.title": "FAQ",
|
||||
"enterprise.faq.q1": "¿Qué es OpenCode Enterprise?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -635,7 +603,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "Agente",
|
||||
"bench.list.table.model": "Modelo",
|
||||
"bench.list.table.score": "Puntuación",
|
||||
"bench.submission.error.allFieldsRequired": "Todos los campos son obligatorios.",
|
||||
|
||||
"bench.detail.title": "Benchmark - {{task}}",
|
||||
"bench.detail.notFound": "Tarea no encontrada",
|
||||
|
||||
@@ -3,7 +3,6 @@ import { dict as en } from "./en"
|
||||
|
||||
export const dict = {
|
||||
...en,
|
||||
"app.meta.description": "OpenCode - L'agent de code open source.",
|
||||
"nav.github": "GitHub",
|
||||
"nav.docs": "Documentation",
|
||||
"nav.changelog": "Changelog",
|
||||
@@ -16,7 +15,6 @@ export const dict = {
|
||||
"nav.home": "Accueil",
|
||||
"nav.openMenu": "Ouvrir le menu",
|
||||
"nav.getStartedFree": "Commencer gratuitement",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "Copier le logo en SVG",
|
||||
"nav.context.copyWordmark": "Copier le logotype en SVG",
|
||||
@@ -44,8 +42,6 @@ export const dict = {
|
||||
"notFound.docs": "Documentation",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "opencode logo light",
|
||||
"notFound.logoDarkAlt": "opencode logo dark",
|
||||
|
||||
"user.logout": "Se déconnecter",
|
||||
|
||||
@@ -79,7 +75,6 @@ export const dict = {
|
||||
"error.modelRequired": "Le modèle est requis",
|
||||
"error.reloadAmountMin": "Le montant de recharge doit être d'au moins {{amount}} $",
|
||||
"error.reloadTriggerMin": "Le seuil de déclenchement doit être d'au moins {{amount}} $",
|
||||
"auth.callback.error.codeMissing": "Aucun code d'autorisation trouvé.",
|
||||
|
||||
"home.title": "OpenCode | L'agent de code IA open source",
|
||||
|
||||
@@ -96,8 +91,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": ", y compris les modèles locaux",
|
||||
"temp.screenshot.caption": "OpenCode TUI avec le thème tokyonight",
|
||||
"temp.screenshot.alt": "OpenCode TUI avec le thème tokyonight",
|
||||
"temp.logoLightAlt": "opencode logo light",
|
||||
"temp.logoDarkAlt": "opencode logo dark",
|
||||
|
||||
"home.banner.badge": "Nouveau",
|
||||
"home.banner.text": "Application desktop disponible en bêta",
|
||||
@@ -253,24 +246,6 @@ export const dict = {
|
||||
"Tous les modèles Zen sont hébergés aux États-Unis. Les fournisseurs suivent une politique de rétention zéro et n'utilisent pas vos données pour l'entraînement des modèles, avec les",
|
||||
"zen.privacy.exceptionsLink": "exceptions suivantes",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Limite de débit dépassée. Veuillez réessayer plus tard.",
|
||||
"zen.api.error.modelNotSupported": "Modèle {{model}} non pris en charge",
|
||||
"zen.api.error.modelFormatNotSupported": "Modèle {{model}} non pris en charge pour le format {{format}}",
|
||||
"zen.api.error.noProviderAvailable": "Aucun fournisseur disponible",
|
||||
"zen.api.error.providerNotSupported": "Fournisseur {{provider}} non pris en charge",
|
||||
"zen.api.error.missingApiKey": "Clé API manquante.",
|
||||
"zen.api.error.invalidApiKey": "Clé API invalide.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Quota d'abonnement dépassé. Réessayez dans {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Quota d'abonnement dépassé. Vous pouvez continuer à utiliser les modèles gratuits.",
|
||||
"zen.api.error.noPaymentMethod": "Aucune méthode de paiement. Ajoutez une méthode de paiement ici : {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Solde insuffisant. Gérez votre facturation ici : {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"Votre espace de travail a atteint sa limite de dépense mensuelle de {{amount}} $. Gérez vos limites ici : {{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached":
|
||||
"Vous avez atteint votre limite de dépense mensuelle de {{amount}} $. Gérez vos limites ici : {{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "Le modèle est désactivé",
|
||||
|
||||
"black.meta.title": "OpenCode Black | Accédez aux meilleurs modèles de code au monde",
|
||||
"black.meta.description": "Accédez à Claude, GPT, Gemini et plus avec les forfaits d'abonnement OpenCode Black.",
|
||||
"black.hero.title": "Accédez aux meilleurs modèles de code au monde",
|
||||
@@ -482,7 +457,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "Veuillez mettre à jour votre méthode de paiement et réessayer.",
|
||||
"workspace.reload.retrying": "Nouvelle tentative...",
|
||||
"workspace.reload.retry": "Réessayer",
|
||||
"workspace.reload.error.paymentFailed": "Échec du paiement.",
|
||||
|
||||
"workspace.payments.title": "Historique des paiements",
|
||||
"workspace.payments.subtitle": "Transactions de paiement récentes.",
|
||||
@@ -607,10 +581,6 @@ export const dict = {
|
||||
"enterprise.form.send": "Envoyer",
|
||||
"enterprise.form.sending": "Envoi...",
|
||||
"enterprise.form.success": "Message envoyé, nous vous contacterons bientôt.",
|
||||
"enterprise.form.success.submitted": "Formulaire soumis avec succès.",
|
||||
"enterprise.form.error.allFieldsRequired": "Tous les champs sont requis.",
|
||||
"enterprise.form.error.invalidEmailFormat": "Format d'e-mail invalide.",
|
||||
"enterprise.form.error.internalServer": "Erreur interne du serveur.",
|
||||
"enterprise.faq.title": "FAQ",
|
||||
"enterprise.faq.q1": "Qu'est-ce que OpenCode Enterprise ?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -670,5 +640,4 @@ export const dict = {
|
||||
"bench.detail.table.duration": "Durée",
|
||||
"bench.detail.run.title": "Exécution {{n}}",
|
||||
"bench.detail.rawJson": "JSON brut",
|
||||
"bench.submission.error.allFieldsRequired": "Tous les champs sont requis.",
|
||||
} satisfies Dict
|
||||
|
||||
@@ -15,7 +15,6 @@ export const dict = {
|
||||
"nav.home": "Home",
|
||||
"nav.openMenu": "Apri menu",
|
||||
"nav.getStartedFree": "Inizia gratis",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "Copia il logo come SVG",
|
||||
"nav.context.copyWordmark": "Copia il wordmark come SVG",
|
||||
@@ -43,13 +42,9 @@ export const dict = {
|
||||
"notFound.docs": "Documentazione",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "logo chiaro di opencode",
|
||||
"notFound.logoDarkAlt": "logo scuro di opencode",
|
||||
|
||||
"user.logout": "Esci",
|
||||
|
||||
"auth.callback.error.codeMissing": "Nessun codice di autorizzazione trovato.",
|
||||
|
||||
"workspace.select": "Seleziona workspace",
|
||||
"workspace.createNew": "+ Crea nuovo workspace",
|
||||
"workspace.modal.title": "Crea nuovo workspace",
|
||||
@@ -81,8 +76,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "L'importo della ricarica deve essere almeno ${{amount}}",
|
||||
"error.reloadTriggerMin": "La soglia del saldo deve essere almeno ${{amount}}",
|
||||
|
||||
"app.meta.description": "OpenCode - L'agente di programmazione open source.",
|
||||
|
||||
"home.title": "OpenCode | L'agente di coding IA open source",
|
||||
|
||||
"temp.title": "opencode | Agente di coding IA costruito per il terminale",
|
||||
@@ -98,8 +91,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": ", inclusi modelli locali",
|
||||
"temp.screenshot.caption": "OpenCode TUI con il tema tokyonight",
|
||||
"temp.screenshot.alt": "OpenCode TUI con tema tokyonight",
|
||||
"temp.logoLightAlt": "logo chiaro di opencode",
|
||||
"temp.logoDarkAlt": "logo scuro di opencode",
|
||||
|
||||
"home.banner.badge": "Nuovo",
|
||||
"home.banner.text": "App desktop disponibile in beta",
|
||||
@@ -249,24 +240,6 @@ export const dict = {
|
||||
"Tutti i modelli Zen sono ospitati negli Stati Uniti. I provider seguono una policy di zero-retention e non usano i tuoi dati per l'addestramento dei modelli, con le",
|
||||
"zen.privacy.exceptionsLink": "seguenti eccezioni",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Limite di richieste superato. Riprova più tardi.",
|
||||
"zen.api.error.modelNotSupported": "Modello {{model}} non supportato",
|
||||
"zen.api.error.modelFormatNotSupported": "Modello {{model}} non supportato per il formato {{format}}",
|
||||
"zen.api.error.noProviderAvailable": "Nessun provider disponibile",
|
||||
"zen.api.error.providerNotSupported": "Provider {{provider}} non supportato",
|
||||
"zen.api.error.missingApiKey": "Chiave API mancante.",
|
||||
"zen.api.error.invalidApiKey": "Chiave API non valida.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Quota dell'abbonamento superata. Riprova tra {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Quota dell'abbonamento superata. Puoi continuare a utilizzare modelli gratuiti.",
|
||||
"zen.api.error.noPaymentMethod": "Nessun metodo di pagamento. Aggiungi un metodo di pagamento qui: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Saldo insufficiente. Gestisci la tua fatturazione qui: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"La tua area di lavoro ha raggiunto il limite di spesa mensile di ${{amount}}. Gestisci i tuoi limiti qui: {{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached":
|
||||
"Hai raggiunto il tuo limite di spesa mensile di ${{amount}}. Gestisci i tuoi limiti qui: {{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "Il modello è disabilitato",
|
||||
|
||||
"black.meta.title": "OpenCode Black | Accedi ai migliori modelli di coding al mondo",
|
||||
"black.meta.description":
|
||||
"Ottieni l'accesso a Claude, GPT, Gemini e altri con i piani di abbonamento OpenCode Black.",
|
||||
@@ -478,7 +451,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "Aggiorna il tuo metodo di pagamento e riprova.",
|
||||
"workspace.reload.retrying": "Riprovo...",
|
||||
"workspace.reload.retry": "Riprova",
|
||||
"workspace.reload.error.paymentFailed": "Pagamento fallito.",
|
||||
|
||||
"workspace.payments.title": "Cronologia Pagamenti",
|
||||
"workspace.payments.subtitle": "Transazioni di pagamento recenti.",
|
||||
@@ -597,10 +569,6 @@ export const dict = {
|
||||
"enterprise.form.send": "Invia",
|
||||
"enterprise.form.sending": "Invio...",
|
||||
"enterprise.form.success": "Messaggio inviato, ti contatteremo presto.",
|
||||
"enterprise.form.success.submitted": "Modulo inviato con successo.",
|
||||
"enterprise.form.error.allFieldsRequired": "Tutti i campi sono obbligatori.",
|
||||
"enterprise.form.error.invalidEmailFormat": "Formato email non valido.",
|
||||
"enterprise.form.error.internalServer": "Errore interno del server.",
|
||||
"enterprise.faq.title": "FAQ",
|
||||
"enterprise.faq.q1": "Cos'è OpenCode Enterprise?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -633,7 +601,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "Agente",
|
||||
"bench.list.table.model": "Modello",
|
||||
"bench.list.table.score": "Punteggio",
|
||||
"bench.submission.error.allFieldsRequired": "Tutti i campi sono obbligatori.",
|
||||
|
||||
"bench.detail.title": "Benchmark - {{task}}",
|
||||
"bench.detail.notFound": "Task non trovato",
|
||||
|
||||
@@ -15,7 +15,6 @@ export const dict = {
|
||||
"nav.home": "ホーム",
|
||||
"nav.openMenu": "メニューを開く",
|
||||
"nav.getStartedFree": "無料ではじめる",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "ロゴをSVGでコピー",
|
||||
"nav.context.copyWordmark": "ワードマークをSVGでコピー",
|
||||
@@ -43,13 +42,9 @@ export const dict = {
|
||||
"notFound.docs": "ドキュメント",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "opencodeのロゴ(ライト)",
|
||||
"notFound.logoDarkAlt": "opencodeのロゴ(ダーク)",
|
||||
|
||||
"user.logout": "ログアウト",
|
||||
|
||||
"auth.callback.error.codeMissing": "認証コードが見つかりません。",
|
||||
|
||||
"workspace.select": "ワークスペースを選択",
|
||||
"workspace.createNew": "+ 新しいワークスペースを作成",
|
||||
"workspace.modal.title": "新しいワークスペースを作成",
|
||||
@@ -81,8 +76,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "リロード額は少なくとも ${{amount}} である必要があります",
|
||||
"error.reloadTriggerMin": "残高トリガーは少なくとも ${{amount}} である必要があります",
|
||||
|
||||
"app.meta.description": "OpenCode - オープンソースのコーディングエージェント。",
|
||||
|
||||
"home.title": "OpenCode | オープンソースのAIコーディングエージェント",
|
||||
|
||||
"temp.title": "OpenCode | ターミナル向けに構築されたAIコーディングエージェント",
|
||||
@@ -98,8 +91,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": "を通じて75以上のLLMプロバイダーをサポート",
|
||||
"temp.screenshot.caption": "tokyonight テーマを使用した OpenCode TUI",
|
||||
"temp.screenshot.alt": "tokyonight テーマの OpenCode TUI",
|
||||
"temp.logoLightAlt": "opencodeのロゴ(ライト)",
|
||||
"temp.logoDarkAlt": "opencodeのロゴ(ダーク)",
|
||||
|
||||
"home.banner.badge": "新着",
|
||||
"home.banner.text": "デスクトップアプリのベータ版が利用可能",
|
||||
@@ -248,25 +239,6 @@ export const dict = {
|
||||
"すべてのZenモデルは米国でホストされています。プロバイダーはゼロ保持ポリシーに従い、モデルのトレーニングにデータを使用しません(",
|
||||
"zen.privacy.exceptionsLink": "以下の例外",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "レート制限を超えました。後でもう一度お試しください。",
|
||||
"zen.api.error.modelNotSupported": "モデル {{model}} はサポートされていません",
|
||||
"zen.api.error.modelFormatNotSupported": "フォーマット {{format}} ではモデル {{model}} はサポートされていません",
|
||||
"zen.api.error.noProviderAvailable": "利用可能なプロバイダーがありません",
|
||||
"zen.api.error.providerNotSupported": "プロバイダー {{provider}} はサポートされていません",
|
||||
"zen.api.error.missingApiKey": "APIキーがありません。",
|
||||
"zen.api.error.invalidApiKey": "無効なAPIキーです。",
|
||||
"zen.api.error.subscriptionQuotaExceeded":
|
||||
"サブスクリプションの制限を超えました。{{retryIn}} 後に再試行してください。",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"サブスクリプションの制限を超えました。無料モデルは引き続きご利用いただけます。",
|
||||
"zen.api.error.noPaymentMethod": "お支払い方法がありません。こちらからお支払い方法を追加してください: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "残高が不足しています。こちらから請求を管理してください: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"ワークスペースが月額の利用上限 ${{amount}} に達しました。こちらから上限を管理してください: {{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached":
|
||||
"月額の利用上限 ${{amount}} に達しました。こちらから上限を管理してください: {{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "モデルが無効です",
|
||||
|
||||
"black.meta.title": "OpenCode Black | 世界最高峰のコーディングモデルすべてにアクセス",
|
||||
"black.meta.description": "OpenCode Black サブスクリプションプランで、Claude、GPT、Gemini などにアクセス。",
|
||||
"black.hero.title": "世界最高峰のコーディングモデルすべてにアクセス",
|
||||
@@ -476,7 +448,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "支払い方法を更新して、もう一度お試しください。",
|
||||
"workspace.reload.retrying": "再試行中...",
|
||||
"workspace.reload.retry": "再試行",
|
||||
"workspace.reload.error.paymentFailed": "支払いに失敗しました。",
|
||||
|
||||
"workspace.payments.title": "支払い履歴",
|
||||
"workspace.payments.subtitle": "最近の支払い取引。",
|
||||
@@ -597,10 +568,6 @@ export const dict = {
|
||||
"enterprise.form.send": "送信",
|
||||
"enterprise.form.sending": "送信中...",
|
||||
"enterprise.form.success": "送信しました。まもなくご連絡いたします。",
|
||||
"enterprise.form.success.submitted": "フォームが正常に送信されました。",
|
||||
"enterprise.form.error.allFieldsRequired": "すべての項目は必須です。",
|
||||
"enterprise.form.error.invalidEmailFormat": "無効なメール形式です。",
|
||||
"enterprise.form.error.internalServer": "内部サーバーエラー。",
|
||||
"enterprise.faq.title": "FAQ",
|
||||
"enterprise.faq.q1": "OpenCode Enterpriseとは?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -633,7 +600,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "エージェント",
|
||||
"bench.list.table.model": "モデル",
|
||||
"bench.list.table.score": "スコア",
|
||||
"bench.submission.error.allFieldsRequired": "すべての項目は必須です。",
|
||||
|
||||
"bench.detail.title": "ベンチマーク - {{task}}",
|
||||
"bench.detail.notFound": "タスクが見つかりません",
|
||||
|
||||
@@ -15,7 +15,6 @@ export const dict = {
|
||||
"nav.home": "홈",
|
||||
"nav.openMenu": "메뉴 열기",
|
||||
"nav.getStartedFree": "무료로 시작하기",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "로고를 SVG로 복사",
|
||||
"nav.context.copyWordmark": "워드마크를 SVG로 복사",
|
||||
@@ -43,13 +42,9 @@ export const dict = {
|
||||
"notFound.docs": "문서",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "opencode 밝은 로고",
|
||||
"notFound.logoDarkAlt": "opencode 어두운 로고",
|
||||
|
||||
"user.logout": "로그아웃",
|
||||
|
||||
"auth.callback.error.codeMissing": "인증 코드를 찾을 수 없습니다.",
|
||||
|
||||
"workspace.select": "워크스페이스 선택",
|
||||
"workspace.createNew": "+ 새 워크스페이스 만들기",
|
||||
"workspace.modal.title": "새 워크스페이스 만들기",
|
||||
@@ -81,8 +76,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "충전 금액은 최소 ${{amount}}이어야 합니다",
|
||||
"error.reloadTriggerMin": "잔액 트리거는 최소 ${{amount}}이어야 합니다",
|
||||
|
||||
"app.meta.description": "OpenCode - 오픈 소스 코딩 에이전트.",
|
||||
|
||||
"home.title": "OpenCode | 오픈 소스 AI 코딩 에이전트",
|
||||
|
||||
"temp.title": "OpenCode | 터미널을 위해 만들어진 AI 코딩 에이전트",
|
||||
@@ -98,8 +91,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": "를 통해 75개 이상의 LLM 제공자 지원",
|
||||
"temp.screenshot.caption": "tokyonight 테마가 적용된 OpenCode TUI",
|
||||
"temp.screenshot.alt": "tokyonight 테마가 적용된 OpenCode TUI",
|
||||
"temp.logoLightAlt": "opencode 밝은 로고",
|
||||
"temp.logoDarkAlt": "opencode 어두운 로고",
|
||||
|
||||
"home.banner.badge": "신규",
|
||||
"home.banner.text": "데스크톱 앱 베타 버전 출시",
|
||||
@@ -245,24 +236,6 @@ export const dict = {
|
||||
"모든 Zen 모델은 미국에서 호스팅됩니다. 제공자들은 데이터 보존 금지 정책을 따르며 모델 학습에 데이터를 사용하지 않습니다. 단,",
|
||||
"zen.privacy.exceptionsLink": "다음 예외",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "속도 제한을 초과했습니다. 나중에 다시 시도해 주세요.",
|
||||
"zen.api.error.modelNotSupported": "{{model}} 모델은 지원되지 않습니다",
|
||||
"zen.api.error.modelFormatNotSupported": "{{model}} 모델은 {{format}} 형식에 대해 지원되지 않습니다",
|
||||
"zen.api.error.noProviderAvailable": "사용 가능한 제공자가 없습니다",
|
||||
"zen.api.error.providerNotSupported": "{{provider}} 제공자는 지원되지 않습니다",
|
||||
"zen.api.error.missingApiKey": "API 키가 누락되었습니다.",
|
||||
"zen.api.error.invalidApiKey": "유효하지 않은 API 키입니다.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "구독 할당량을 초과했습니다. {{retryIn}} 후 다시 시도해 주세요.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"구독 할당량을 초과했습니다. 무료 모델은 계속 사용할 수 있습니다.",
|
||||
"zen.api.error.noPaymentMethod": "결제 수단이 없습니다. 결제 수단을 추가하세요: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "잔액이 부족합니다. 결제 관리를 여기서 하세요: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"워크스페이스의 월간 지출 한도인 ${{amount}}에 도달했습니다. 한도 관리를 여기서 하세요: {{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached":
|
||||
"월간 지출 한도인 ${{amount}}에 도달했습니다. 한도 관리를 여기서 하세요: {{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "모델이 비활성화되었습니다",
|
||||
|
||||
"black.meta.title": "OpenCode Black | 세계 최고의 코딩 모델에 액세스하세요",
|
||||
"black.meta.description": "OpenCode Black 구독 플랜으로 Claude, GPT, Gemini 등에 액세스하세요.",
|
||||
"black.hero.title": "세계 최고의 코딩 모델에 액세스하세요",
|
||||
@@ -472,7 +445,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "결제 수단을 업데이트하고 다시 시도해 주세요.",
|
||||
"workspace.reload.retrying": "재시도 중...",
|
||||
"workspace.reload.retry": "재시도",
|
||||
"workspace.reload.error.paymentFailed": "결제에 실패했습니다.",
|
||||
|
||||
"workspace.payments.title": "결제 내역",
|
||||
"workspace.payments.subtitle": "최근 결제 거래 내역입니다.",
|
||||
@@ -590,10 +562,6 @@ export const dict = {
|
||||
"enterprise.form.send": "전송",
|
||||
"enterprise.form.sending": "전송 중...",
|
||||
"enterprise.form.success": "메시지가 전송되었습니다. 곧 연락드리겠습니다.",
|
||||
"enterprise.form.success.submitted": "양식이 성공적으로 제출되었습니다.",
|
||||
"enterprise.form.error.allFieldsRequired": "모든 필드는 필수 항목입니다.",
|
||||
"enterprise.form.error.invalidEmailFormat": "유효하지 않은 이메일 형식입니다.",
|
||||
"enterprise.form.error.internalServer": "내부 서버 오류입니다.",
|
||||
"enterprise.faq.title": "FAQ",
|
||||
"enterprise.faq.q1": "OpenCode 엔터프라이즈란 무엇인가요?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -626,7 +594,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "에이전트",
|
||||
"bench.list.table.model": "모델",
|
||||
"bench.list.table.score": "점수",
|
||||
"bench.submission.error.allFieldsRequired": "모든 필드는 필수 항목입니다.",
|
||||
|
||||
"bench.detail.title": "벤치마크 - {{task}}",
|
||||
"bench.detail.notFound": "태스크를 찾을 수 없음",
|
||||
|
||||
@@ -15,7 +15,6 @@ export const dict = {
|
||||
"nav.home": "Hjem",
|
||||
"nav.openMenu": "Åpne meny",
|
||||
"nav.getStartedFree": "Kom i gang gratis",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "Kopier logo som SVG",
|
||||
"nav.context.copyWordmark": "Kopier wordmark som SVG",
|
||||
@@ -43,13 +42,9 @@ export const dict = {
|
||||
"notFound.docs": "Dokumentasjon",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "opencode logo lys",
|
||||
"notFound.logoDarkAlt": "opencode logo mørk",
|
||||
|
||||
"user.logout": "Logg ut",
|
||||
|
||||
"auth.callback.error.codeMissing": "Ingen autorisasjonskode funnet.",
|
||||
|
||||
"workspace.select": "Velg arbeidsområde",
|
||||
"workspace.createNew": "+ Opprett nytt arbeidsområde",
|
||||
"workspace.modal.title": "Opprett nytt arbeidsområde",
|
||||
@@ -81,8 +76,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "Påfyllingsbeløp må være minst ${{amount}}",
|
||||
"error.reloadTriggerMin": "Saldo-trigger må være minst ${{amount}}",
|
||||
|
||||
"app.meta.description": "OpenCode - Den åpne kildekode kodingsagenten.",
|
||||
|
||||
"home.title": "OpenCode | Den åpne kildekode AI-kodingsagenten",
|
||||
|
||||
"temp.title": "opencode | AI-kodingsagent bygget for terminalen",
|
||||
@@ -98,8 +91,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": ", inkludert lokale modeller",
|
||||
"temp.screenshot.caption": "opencode TUI med tokyonight-tema",
|
||||
"temp.screenshot.alt": "opencode TUI med tokyonight-tema",
|
||||
"temp.logoLightAlt": "opencode logo lys",
|
||||
"temp.logoDarkAlt": "opencode logo mørk",
|
||||
|
||||
"home.banner.badge": "Ny",
|
||||
"home.banner.text": "Desktop-app tilgjengelig i beta",
|
||||
@@ -249,24 +240,6 @@ export const dict = {
|
||||
"Alle Zen-modeller hostes i USA. Leverandører følger en policy om null oppbevaring og bruker ikke dataene dine til modelltrening, med",
|
||||
"zen.privacy.exceptionsLink": "følgende unntak",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Rate limit overskredet. Vennligst prøv igjen senere.",
|
||||
"zen.api.error.modelNotSupported": "Modell {{model}} støttes ikke",
|
||||
"zen.api.error.modelFormatNotSupported": "Modell {{model}} støttes ikke for format {{format}}",
|
||||
"zen.api.error.noProviderAvailable": "Ingen leverandør tilgjengelig",
|
||||
"zen.api.error.providerNotSupported": "Leverandør {{provider}} støttes ikke",
|
||||
"zen.api.error.missingApiKey": "Mangler API-nøkkel.",
|
||||
"zen.api.error.invalidApiKey": "Ugyldig API-nøkkel.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Abonnementskvote overskredet. Prøv igjen om {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Abonnementskvote overskredet. Du kan fortsette å bruke gratis modeller.",
|
||||
"zen.api.error.noPaymentMethod": "Ingen betalingsmetode. Legg til en betalingsmetode her: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Utilstrekkelig saldo. Administrer faktureringen din her: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"Arbeidsområdet ditt har nådd sin månedlige utgiftsgrense på ${{amount}}. Administrer grensene dine her: {{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached":
|
||||
"Du har nådd din månedlige utgiftsgrense på ${{amount}}. Administrer grensene dine her: {{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "Modellen er deaktivert",
|
||||
|
||||
"black.meta.title": "OpenCode Black | Få tilgang til verdens beste kodemodeller",
|
||||
"black.meta.description": "Få tilgang til Claude, GPT, Gemini og mer med OpenCode Black-abonnementer.",
|
||||
"black.hero.title": "Få tilgang til verdens beste kodemodeller",
|
||||
@@ -476,7 +449,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "Vennligst oppdater betalingsmetoden din og prøv på nytt.",
|
||||
"workspace.reload.retrying": "Prøver på nytt...",
|
||||
"workspace.reload.retry": "Prøv på nytt",
|
||||
"workspace.reload.error.paymentFailed": "Betaling mislyktes.",
|
||||
|
||||
"workspace.payments.title": "Betalingshistorikk",
|
||||
"workspace.payments.subtitle": "Nylige betalingstransaksjoner.",
|
||||
@@ -595,10 +567,6 @@ export const dict = {
|
||||
"enterprise.form.send": "Send",
|
||||
"enterprise.form.sending": "Sender...",
|
||||
"enterprise.form.success": "Melding sendt, vi tar kontakt snart.",
|
||||
"enterprise.form.success.submitted": "Skjemaet ble sendt inn.",
|
||||
"enterprise.form.error.allFieldsRequired": "Alle felt er obligatoriske.",
|
||||
"enterprise.form.error.invalidEmailFormat": "Ugyldig e-postformat.",
|
||||
"enterprise.form.error.internalServer": "Intern serverfeil.",
|
||||
"enterprise.faq.title": "FAQ",
|
||||
"enterprise.faq.q1": "Hva er OpenCode Enterprise?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -631,7 +599,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "Agent",
|
||||
"bench.list.table.model": "Modell",
|
||||
"bench.list.table.score": "Poengsum",
|
||||
"bench.submission.error.allFieldsRequired": "Alle felt er obligatoriske.",
|
||||
|
||||
"bench.detail.title": "Benchmark - {{task}}",
|
||||
"bench.detail.notFound": "Oppgave ikke funnet",
|
||||
|
||||
@@ -14,7 +14,6 @@ export const dict = {
|
||||
"nav.home": "Strona główna",
|
||||
"nav.openMenu": "Otwórz menu",
|
||||
"nav.getStartedFree": "Zacznij za darmo",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "Skopiuj logo jako SVG",
|
||||
"nav.context.copyWordmark": "Skopiuj logotyp jako SVG",
|
||||
@@ -42,13 +41,9 @@ export const dict = {
|
||||
"notFound.docs": "Dokumentacja",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "jasne logo opencode",
|
||||
"notFound.logoDarkAlt": "ciemne logo opencode",
|
||||
|
||||
"user.logout": "Wyloguj się",
|
||||
|
||||
"auth.callback.error.codeMissing": "Nie znaleziono kodu autoryzacji.",
|
||||
|
||||
"workspace.select": "Wybierz obszar roboczy",
|
||||
"workspace.createNew": "+ Utwórz nowy obszar roboczy",
|
||||
"workspace.modal.title": "Utwórz nowy obszar roboczy",
|
||||
@@ -80,8 +75,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "Kwota doładowania musi wynosić co najmniej ${{amount}}",
|
||||
"error.reloadTriggerMin": "Próg salda musi wynosić co najmniej ${{amount}}",
|
||||
|
||||
"app.meta.description": "OpenCode - Otwartoźródłowy agent programistyczny.",
|
||||
|
||||
"home.title": "OpenCode | Open source'owy agent AI do kodowania",
|
||||
|
||||
"temp.title": "opencode | Agent AI do kodowania zbudowany dla terminala",
|
||||
@@ -97,8 +90,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": ", w tym modele lokalne",
|
||||
"temp.screenshot.caption": "OpenCode TUI z motywem tokyonight",
|
||||
"temp.screenshot.alt": "OpenCode TUI z motywem tokyonight",
|
||||
"temp.logoLightAlt": "jasne logo opencode",
|
||||
"temp.logoDarkAlt": "ciemne logo opencode",
|
||||
|
||||
"home.banner.badge": "Nowość",
|
||||
"home.banner.text": "Aplikacja desktopowa dostępna w wersji beta",
|
||||
@@ -250,24 +241,6 @@ export const dict = {
|
||||
"Wszystkie modele Zen są hostowane w USA. Dostawcy stosują politykę zerowej retencji i nie wykorzystują Twoich danych do trenowania modeli, z",
|
||||
"zen.privacy.exceptionsLink": "następującymi wyjątkami",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Przekroczono limit zapytań. Spróbuj ponownie później.",
|
||||
"zen.api.error.modelNotSupported": "Model {{model}} nie jest obsługiwany",
|
||||
"zen.api.error.modelFormatNotSupported": "Model {{model}} nie jest obsługiwany dla formatu {{format}}",
|
||||
"zen.api.error.noProviderAvailable": "Brak dostępnego dostawcy",
|
||||
"zen.api.error.providerNotSupported": "Dostawca {{provider}} nie jest obsługiwany",
|
||||
"zen.api.error.missingApiKey": "Brak klucza API.",
|
||||
"zen.api.error.invalidApiKey": "Nieprawidłowy klucz API.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Przekroczono limit subskrypcji. Spróbuj ponownie za {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Przekroczono limit subskrypcji. Możesz kontynuować korzystanie z darmowych modeli.",
|
||||
"zen.api.error.noPaymentMethod": "Brak metody płatności. Dodaj metodę płatności tutaj: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Niewystarczające saldo. Zarządzaj swoimi płatnościami tutaj: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"Twoja przestrzeń robocza osiągnęła miesięczny limit wydatków w wysokości ${{amount}}. Zarządzaj swoimi limitami tutaj: {{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached":
|
||||
"Osiągnąłeś swój miesięczny limit wydatków w wysokości ${{amount}}. Zarządzaj swoimi limitami tutaj: {{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "Model jest wyłączony",
|
||||
|
||||
"black.meta.title": "OpenCode Black | Dostęp do najlepszych na świecie modeli kodujących",
|
||||
"black.meta.description": "Uzyskaj dostęp do Claude, GPT, Gemini i innych dzięki planom subskrypcji OpenCode Black.",
|
||||
"black.hero.title": "Dostęp do najlepszych na świecie modeli kodujących",
|
||||
@@ -477,7 +450,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "Zaktualizuj metodę płatności i spróbuj ponownie.",
|
||||
"workspace.reload.retrying": "Ponawianie...",
|
||||
"workspace.reload.retry": "Spróbuj ponownie",
|
||||
"workspace.reload.error.paymentFailed": "Płatność nie powiodła się.",
|
||||
|
||||
"workspace.payments.title": "Historia płatności",
|
||||
"workspace.payments.subtitle": "Ostatnie transakcje płatnicze.",
|
||||
@@ -598,10 +570,6 @@ export const dict = {
|
||||
"enterprise.form.send": "Wyślij",
|
||||
"enterprise.form.sending": "Wysyłanie...",
|
||||
"enterprise.form.success": "Wiadomość wysłana, skontaktujemy się wkrótce.",
|
||||
"enterprise.form.success.submitted": "Formularz został pomyślnie wysłany.",
|
||||
"enterprise.form.error.allFieldsRequired": "Wszystkie pola są wymagane.",
|
||||
"enterprise.form.error.invalidEmailFormat": "Nieprawidłowy format adresu e-mail.",
|
||||
"enterprise.form.error.internalServer": "Wewnętrzny błąd serwera.",
|
||||
"enterprise.faq.title": "FAQ",
|
||||
"enterprise.faq.q1": "Czym jest OpenCode Enterprise?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -634,7 +602,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "Agent",
|
||||
"bench.list.table.model": "Model",
|
||||
"bench.list.table.score": "Wynik",
|
||||
"bench.submission.error.allFieldsRequired": "Wszystkie pola są wymagane.",
|
||||
|
||||
"bench.detail.title": "Benchmark - {{task}}",
|
||||
"bench.detail.notFound": "Nie znaleziono zadania",
|
||||
|
||||
@@ -15,7 +15,6 @@ export const dict = {
|
||||
"nav.home": "Главная",
|
||||
"nav.openMenu": "Открыть меню",
|
||||
"nav.getStartedFree": "Начать бесплатно",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "Скопировать логотип как SVG",
|
||||
"nav.context.copyWordmark": "Скопировать название как SVG",
|
||||
@@ -43,13 +42,9 @@ export const dict = {
|
||||
"notFound.docs": "Документация",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "светлый логотип opencode",
|
||||
"notFound.logoDarkAlt": "темный логотип opencode",
|
||||
|
||||
"user.logout": "Выйти",
|
||||
|
||||
"auth.callback.error.codeMissing": "Код авторизации не найден.",
|
||||
|
||||
"workspace.select": "Выбрать рабочее пространство",
|
||||
"workspace.createNew": "+ Создать рабочее пространство",
|
||||
"workspace.modal.title": "Создать рабочее пространство",
|
||||
@@ -81,8 +76,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "Сумма пополнения должна быть не менее ${{amount}}",
|
||||
"error.reloadTriggerMin": "Порог баланса должен быть не менее ${{amount}}",
|
||||
|
||||
"app.meta.description": "OpenCode - AI-агент с открытым кодом для программирования.",
|
||||
|
||||
"home.title": "OpenCode | AI-агент с открытым кодом для программирования",
|
||||
|
||||
"temp.title": "opencode | AI-агент для программирования в терминале",
|
||||
@@ -98,8 +91,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": ", включая локальные модели",
|
||||
"temp.screenshot.caption": "OpenCode TUI с темой tokyonight",
|
||||
"temp.screenshot.alt": "OpenCode TUI с темой tokyonight",
|
||||
"temp.logoLightAlt": "светлый логотип opencode",
|
||||
"temp.logoDarkAlt": "темный логотип opencode",
|
||||
|
||||
"home.banner.badge": "Новое",
|
||||
"home.banner.text": "Доступно десктопное приложение (бета)",
|
||||
@@ -253,24 +244,6 @@ export const dict = {
|
||||
"Все модели Zen размещены в США. Провайдеры следуют политике нулевого хранения и не используют ваши данные для обучения моделей, за",
|
||||
"zen.privacy.exceptionsLink": "следующими исключениями",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Превышен лимит запросов. Пожалуйста, попробуйте позже.",
|
||||
"zen.api.error.modelNotSupported": "Модель {{model}} не поддерживается",
|
||||
"zen.api.error.modelFormatNotSupported": "Модель {{model}} не поддерживается для формата {{format}}",
|
||||
"zen.api.error.noProviderAvailable": "Нет доступных провайдеров",
|
||||
"zen.api.error.providerNotSupported": "Провайдер {{provider}} не поддерживается",
|
||||
"zen.api.error.missingApiKey": "Отсутствует API ключ.",
|
||||
"zen.api.error.invalidApiKey": "Неверный API ключ.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Квота подписки превышена. Повторите попытку через {{retryIn}}.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Квота подписки превышена. Вы можете продолжить использовать бесплатные модели.",
|
||||
"zen.api.error.noPaymentMethod": "Нет способа оплаты. Добавьте способ оплаты здесь: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Недостаточно средств. Управляйте оплатой здесь: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"Ваше рабочее пространство достигло ежемесячного лимита расходов в ${{amount}}. Управляйте лимитами здесь: {{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached":
|
||||
"Вы достигли ежемесячного лимита расходов в ${{amount}}. Управляйте лимитами здесь: {{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "Модель отключена",
|
||||
|
||||
"black.meta.title": "OpenCode Black | Доступ к лучшим моделям для кодинга в мире",
|
||||
"black.meta.description": "Получите доступ к Claude, GPT, Gemini и другим моделям с подпиской OpenCode Black.",
|
||||
"black.hero.title": "Доступ к лучшим моделям для кодинга в мире",
|
||||
@@ -482,7 +455,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "Пожалуйста, обновите способ оплаты и попробуйте снова.",
|
||||
"workspace.reload.retrying": "Повторная попытка...",
|
||||
"workspace.reload.retry": "Повторить",
|
||||
"workspace.reload.error.paymentFailed": "Ошибка оплаты.",
|
||||
|
||||
"workspace.payments.title": "История платежей",
|
||||
"workspace.payments.subtitle": "Недавние транзакции.",
|
||||
@@ -602,10 +574,6 @@ export const dict = {
|
||||
"enterprise.form.send": "Отправить",
|
||||
"enterprise.form.sending": "Отправка...",
|
||||
"enterprise.form.success": "Сообщение отправлено, мы скоро свяжемся с вами.",
|
||||
"enterprise.form.success.submitted": "Форма успешно отправлена.",
|
||||
"enterprise.form.error.allFieldsRequired": "Все поля обязательны.",
|
||||
"enterprise.form.error.invalidEmailFormat": "Неверный формат email.",
|
||||
"enterprise.form.error.internalServer": "Внутренняя ошибка сервера.",
|
||||
"enterprise.faq.title": "FAQ",
|
||||
"enterprise.faq.q1": "Что такое OpenCode Enterprise?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -638,7 +606,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "Агент",
|
||||
"bench.list.table.model": "Модель",
|
||||
"bench.list.table.score": "Оценка",
|
||||
"bench.submission.error.allFieldsRequired": "Все поля обязательны.",
|
||||
|
||||
"bench.detail.title": "Бенчмарк - {{task}}",
|
||||
"bench.detail.notFound": "Задача не найдена",
|
||||
|
||||
@@ -15,7 +15,6 @@ export const dict = {
|
||||
"nav.home": "หน้าหลัก",
|
||||
"nav.openMenu": "เปิดเมนู",
|
||||
"nav.getStartedFree": "เริ่มต้นฟรี",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "คัดลอกโลโก้เป็น SVG",
|
||||
"nav.context.copyWordmark": "คัดลอกตัวอักษรแบรนด์เป็น SVG",
|
||||
@@ -43,13 +42,9 @@ export const dict = {
|
||||
"notFound.docs": "เอกสาร",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "โลโก้ opencode แบบสว่าง",
|
||||
"notFound.logoDarkAlt": "โลโก้ opencode แบบมืด",
|
||||
|
||||
"user.logout": "ออกจากระบบ",
|
||||
|
||||
"auth.callback.error.codeMissing": "ไม่พบ authorization code",
|
||||
|
||||
"workspace.select": "เลือก Workspace",
|
||||
"workspace.createNew": "+ สร้าง Workspace ใหม่",
|
||||
"workspace.modal.title": "สร้าง Workspace ใหม่",
|
||||
@@ -81,8 +76,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "จำนวนเงินที่โหลดซ้ำต้องมีอย่างน้อย ${{amount}}",
|
||||
"error.reloadTriggerMin": "ยอดคงเหลือที่กระตุ้นต้องมีอย่างน้อย ${{amount}}",
|
||||
|
||||
"app.meta.description": "OpenCode - เอเจนต์เขียนโค้ดแบบโอเพนซอร์ส",
|
||||
|
||||
"home.title": "OpenCode | เอเจนต์เขียนโค้ดด้วย AI แบบโอเพนซอร์ส",
|
||||
|
||||
"temp.title": "OpenCode | เอเจนต์เขียนโค้ด AI ที่สร้างมาเพื่อเทอร์มินัล",
|
||||
@@ -98,8 +91,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": "รวมถึงโมเดล Local",
|
||||
"temp.screenshot.caption": "OpenCode TUI พร้อมธีม tokyonight",
|
||||
"temp.screenshot.alt": "OpenCode TUI พร้อมธีม tokyonight",
|
||||
"temp.logoLightAlt": "โลโก้ opencode แบบสว่าง",
|
||||
"temp.logoDarkAlt": "โลโก้ opencode แบบมืด",
|
||||
|
||||
"home.banner.badge": "ใหม่",
|
||||
"home.banner.text": "แอปเดสก์ท็อปพร้อมใช้งานในเวอร์ชันเบต้า",
|
||||
@@ -248,24 +239,6 @@ export const dict = {
|
||||
"โมเดล Zen ทั้งหมดโฮสต์ในสหรัฐอเมริกา ผู้ให้บริการปฏิบัติตามนโยบายไม่เก็บรักษาข้อมูล (zero-retention policy) และไม่ใช้ข้อมูลของคุณสำหรับการฝึกโมเดล โดยมี",
|
||||
"zen.privacy.exceptionsLink": "ข้อยกเว้นดังนี้",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "เกินขีดจำกัดอัตราการใช้งาน กรุณาลองใหม่ในภายหลัง",
|
||||
"zen.api.error.modelNotSupported": "ไม่รองรับโมเดล {{model}}",
|
||||
"zen.api.error.modelFormatNotSupported": "ไม่รองรับโมเดล {{model}} สำหรับรูปแบบ {{format}}",
|
||||
"zen.api.error.noProviderAvailable": "ไม่มีผู้ให้บริการที่พร้อมใช้งาน",
|
||||
"zen.api.error.providerNotSupported": "ไม่รองรับผู้ให้บริการ {{provider}}",
|
||||
"zen.api.error.missingApiKey": "ไม่มี API key",
|
||||
"zen.api.error.invalidApiKey": "API key ไม่ถูกต้อง",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "โควต้าการสมัครสมาชิกเกินขีดจำกัด ลองใหม่ในอีก {{retryIn}}",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"โควต้าการสมัครสมาชิกเกินขีดจำกัด คุณสามารถดำเนินการต่อโดยใช้โมเดลฟรี",
|
||||
"zen.api.error.noPaymentMethod": "ไม่มีวิธีการชำระเงิน เพิ่มวิธีการชำระเงินที่นี่: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "ยอดเงินคงเหลือไม่เพียงพอ จัดการการเรียกเก็บเงินของคุณที่นี่: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"Workspace ของคุณถึงขีดจำกัดการใช้จ่ายรายเดือนที่ ${{amount}} แล้ว จัดการขีดจำกัดของคุณที่นี่: {{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached":
|
||||
"คุณถึงขีดจำกัดการใช้จ่ายรายเดือนที่ ${{amount}} แล้ว จัดการขีดจำกัดของคุณที่นี่: {{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "โมเดลถูกปิดใช้งาน",
|
||||
|
||||
"black.meta.title": "OpenCode Black | เข้าถึงโมเดลเขียนโค้ดที่ดีที่สุดในโลก",
|
||||
"black.meta.description": "เข้าถึง Claude, GPT, Gemini และอื่นๆ ด้วยแผนสมาชิก OpenCode Black",
|
||||
"black.hero.title": "เข้าถึงโมเดลเขียนโค้ดที่ดีที่สุดในโลก",
|
||||
@@ -475,7 +448,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "โปรดอัปเดตวิธีการชำระเงินของคุณแล้วลองอีกครั้ง",
|
||||
"workspace.reload.retrying": "กำลังลองอีกครั้ง...",
|
||||
"workspace.reload.retry": "ลองอีกครั้ง",
|
||||
"workspace.reload.error.paymentFailed": "การชำระเงินล้มเหลว",
|
||||
|
||||
"workspace.payments.title": "ประวัติการชำระเงิน",
|
||||
"workspace.payments.subtitle": "รายการธุรกรรมการชำระเงินล่าสุด",
|
||||
@@ -594,10 +566,6 @@ export const dict = {
|
||||
"enterprise.form.send": "ส่ง",
|
||||
"enterprise.form.sending": "กำลังส่ง...",
|
||||
"enterprise.form.success": "ส่งข้อความแล้ว เราจะติดต่อกลับเร็วๆ นี้",
|
||||
"enterprise.form.success.submitted": "ส่งแบบฟอร์มสำเร็จแล้ว",
|
||||
"enterprise.form.error.allFieldsRequired": "จำเป็นต้องกรอกทุกช่อง",
|
||||
"enterprise.form.error.invalidEmailFormat": "รูปแบบอีเมลไม่ถูกต้อง",
|
||||
"enterprise.form.error.internalServer": "เกิดข้อผิดพลาดภายในเซิร์ฟเวอร์",
|
||||
"enterprise.faq.title": "คำถามที่พบบ่อย",
|
||||
"enterprise.faq.q1": "OpenCode Enterprise คืออะไร?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -630,7 +598,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "เอเจนต์",
|
||||
"bench.list.table.model": "โมเดล",
|
||||
"bench.list.table.score": "คะแนน",
|
||||
"bench.submission.error.allFieldsRequired": "จำเป็นต้องกรอกทุกช่อง",
|
||||
|
||||
"bench.detail.title": "Benchmark - {{task}}",
|
||||
"bench.detail.notFound": "ไม่พบงาน",
|
||||
|
||||
@@ -15,7 +15,6 @@ export const dict = {
|
||||
"nav.home": "Ana sayfa",
|
||||
"nav.openMenu": "Menüyü aç",
|
||||
"nav.getStartedFree": "Ücretsiz başla",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "Logoyu SVG olarak kopyala",
|
||||
"nav.context.copyWordmark": "Wordmark'ı SVG olarak kopyala",
|
||||
@@ -43,13 +42,9 @@ export const dict = {
|
||||
"notFound.docs": "Dokümantasyon",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "opencode açık logo",
|
||||
"notFound.logoDarkAlt": "opencode koyu logo",
|
||||
|
||||
"user.logout": "Çıkış",
|
||||
|
||||
"auth.callback.error.codeMissing": "Yetkilendirme kodu bulunamadı.",
|
||||
|
||||
"workspace.select": "Çalışma alanı seç",
|
||||
"workspace.createNew": "+ Yeni çalışma alanı oluştur",
|
||||
"workspace.modal.title": "Yeni çalışma alanı oluştur",
|
||||
@@ -81,8 +76,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "Yükleme tutarı en az ${{amount}} olmalıdır",
|
||||
"error.reloadTriggerMin": "Bakiye tetikleyicisi en az ${{amount}} olmalıdır",
|
||||
|
||||
"app.meta.description": "OpenCode - Açık kaynaklı kodlama ajanı.",
|
||||
|
||||
"home.title": "OpenCode | Açık kaynaklı yapay zeka kodlama ajanı",
|
||||
|
||||
"temp.title": "opencode | Terminal için geliştirilmiş yapay zeka kodlama ajanı",
|
||||
@@ -98,8 +91,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": " üzerinden destekler",
|
||||
"temp.screenshot.caption": "opencode TUI ve tokyonight teması",
|
||||
"temp.screenshot.alt": "tokyonight temalı opencode TUI",
|
||||
"temp.logoLightAlt": "opencode açık logo",
|
||||
"temp.logoDarkAlt": "opencode koyu logo",
|
||||
|
||||
"home.banner.badge": "Yeni",
|
||||
"home.banner.text": "Masaüstü uygulaması beta olarak kullanılabilir",
|
||||
@@ -251,24 +242,6 @@ export const dict = {
|
||||
"Tüm Zen modelleri ABD'de barındırılmaktadır. Sağlayıcılar sıfır saklama politikası izler ve verilerinizi model eğitimi için kullanmaz; şu",
|
||||
"zen.privacy.exceptionsLink": "aşağıdaki istisnalar",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "İstek limiti aşıldı. Lütfen daha sonra tekrar deneyin.",
|
||||
"zen.api.error.modelNotSupported": "{{model}} modeli desteklenmiyor",
|
||||
"zen.api.error.modelFormatNotSupported": "{{model}} modeli {{format}} formatı için desteklenmiyor",
|
||||
"zen.api.error.noProviderAvailable": "Kullanılabilir sağlayıcı yok",
|
||||
"zen.api.error.providerNotSupported": "{{provider}} sağlayıcısı desteklenmiyor",
|
||||
"zen.api.error.missingApiKey": "API anahtarı eksik.",
|
||||
"zen.api.error.invalidApiKey": "Geçersiz API anahtarı.",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "Abonelik kotası aşıldı. {{retryIn}} içinde tekrar deneyin.",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
|
||||
"Abonelik kotası aşıldı. Ücretsiz modelleri kullanmaya devam edebilirsiniz.",
|
||||
"zen.api.error.noPaymentMethod": "Ödeme yöntemi bulunamadı. Buradan bir ödeme yöntemi ekleyin: {{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "Yetersiz bakiye. Faturalandırmanızı buradan yönetin: {{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"Çalışma alanınız aylık ${{amount}} harcama limitine ulaştı. Limitlerinizi buradan yönetin: {{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached":
|
||||
"Aylık ${{amount}} harcama limitinize ulaştınız. Limitlerinizi buradan yönetin: {{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "Model devre dışı",
|
||||
|
||||
"black.meta.title": "OpenCode Black | Dünyanın en iyi kodlama modellerine erişin",
|
||||
"black.meta.description": "OpenCode Black abonelik planlarıyla Claude, GPT, Gemini ve daha fazlasına erişin.",
|
||||
"black.hero.title": "Dünyanın en iyi kodlama modellerine erişin",
|
||||
@@ -478,7 +451,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "Lütfen ödeme yönteminizi güncelleyin ve tekrar deneyin.",
|
||||
"workspace.reload.retrying": "Yeniden deneniyor...",
|
||||
"workspace.reload.retry": "Yeniden dene",
|
||||
"workspace.reload.error.paymentFailed": "Ödeme başarısız.",
|
||||
|
||||
"workspace.payments.title": "Ödeme Geçmişi",
|
||||
"workspace.payments.subtitle": "Son ödeme işlemleri.",
|
||||
@@ -599,10 +571,6 @@ export const dict = {
|
||||
"enterprise.form.send": "Gönder",
|
||||
"enterprise.form.sending": "Gönderiliyor...",
|
||||
"enterprise.form.success": "Mesaj gönderildi, yakında size dönüş yapacağız.",
|
||||
"enterprise.form.success.submitted": "Form başarıyla gönderildi.",
|
||||
"enterprise.form.error.allFieldsRequired": "Tüm alanlar gereklidir.",
|
||||
"enterprise.form.error.invalidEmailFormat": "Geçersiz e-posta formatı.",
|
||||
"enterprise.form.error.internalServer": "İç sunucu hatası.",
|
||||
"enterprise.faq.title": "SSS",
|
||||
"enterprise.faq.q1": "OpenCode Enterprise nedir?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -635,7 +603,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "Ajan",
|
||||
"bench.list.table.model": "Model",
|
||||
"bench.list.table.score": "Puan",
|
||||
"bench.submission.error.allFieldsRequired": "Tüm alanlar gereklidir.",
|
||||
|
||||
"bench.detail.title": "Benchmark - {{task}}",
|
||||
"bench.detail.notFound": "Görev bulunamadı",
|
||||
|
||||
@@ -15,7 +15,6 @@ export const dict = {
|
||||
"nav.home": "首页",
|
||||
"nav.openMenu": "打开菜单",
|
||||
"nav.getStartedFree": "免费开始",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "复制 Logo (SVG)",
|
||||
"nav.context.copyWordmark": "复制商标 (SVG)",
|
||||
@@ -43,13 +42,9 @@ export const dict = {
|
||||
"notFound.docs": "文档",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "opencode logo 亮色",
|
||||
"notFound.logoDarkAlt": "opencode logo 暗色",
|
||||
|
||||
"user.logout": "退出登录",
|
||||
|
||||
"auth.callback.error.codeMissing": "未找到授权码。",
|
||||
|
||||
"workspace.select": "选择工作区",
|
||||
"workspace.createNew": "+ 新建工作区",
|
||||
"workspace.modal.title": "新建工作区",
|
||||
@@ -81,8 +76,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "充值金额必须至少为 ${{amount}}",
|
||||
"error.reloadTriggerMin": "余额触发阈值必须至少为 ${{amount}}",
|
||||
|
||||
"app.meta.description": "OpenCode - 开源编程代理。",
|
||||
|
||||
"home.title": "OpenCode | 开源 AI 编程代理",
|
||||
|
||||
"temp.title": "OpenCode | 专为终端打造的 AI 编程代理",
|
||||
@@ -98,8 +91,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": ",包括本地模型",
|
||||
"temp.screenshot.caption": "使用 Tokyonight 主题的 OpenCode TUI",
|
||||
"temp.screenshot.alt": "使用 Tokyonight 主题的 OpenCode TUI",
|
||||
"temp.logoLightAlt": "opencode logo 亮色",
|
||||
"temp.logoDarkAlt": "opencode logo 暗色",
|
||||
|
||||
"home.banner.badge": "新",
|
||||
"home.banner.text": "桌面应用 Beta 版现已推出",
|
||||
@@ -238,22 +229,6 @@ export const dict = {
|
||||
"zen.privacy.beforeExceptions": "所有 Zen 模型均托管在美国。提供商遵循零留存政策,不使用您的数据进行模型训练,",
|
||||
"zen.privacy.exceptionsLink": "以下例外情况除外",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "超出速率限制。请稍后重试。",
|
||||
"zen.api.error.modelNotSupported": "不支持模型 {{model}}",
|
||||
"zen.api.error.modelFormatNotSupported": "格式 {{format}} 不支持模型 {{model}}",
|
||||
"zen.api.error.noProviderAvailable": "没有可用的提供商",
|
||||
"zen.api.error.providerNotSupported": "不支持提供商 {{provider}}",
|
||||
"zen.api.error.missingApiKey": "缺少 API 密钥。",
|
||||
"zen.api.error.invalidApiKey": "无效的 API 密钥。",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "超出订阅配额。请在 {{retryIn}} 后重试。",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels": "超出订阅配额。您可以继续使用免费模型。",
|
||||
"zen.api.error.noPaymentMethod": "没有付款方式。请在此处添加付款方式:{{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "余额不足。请在此处管理您的计费:{{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"您的工作区已达到每月支出限额 ${{amount}}。请在此处管理您的限额:{{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached": "您已达到每月支出限额 ${{amount}}。请在此处管理您的限额:{{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "模型已禁用",
|
||||
|
||||
"black.meta.title": "OpenCode Black | 访问全球顶尖编程模型",
|
||||
"black.meta.description": "通过 OpenCode Black 订阅计划使用 Claude, GPT, Gemini 等模型。",
|
||||
"black.hero.title": "访问全球顶尖编程模型",
|
||||
@@ -461,7 +436,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "请更新您的付款方式并重试。",
|
||||
"workspace.reload.retrying": "正在重试...",
|
||||
"workspace.reload.retry": "重试",
|
||||
"workspace.reload.error.paymentFailed": "支付失败。",
|
||||
|
||||
"workspace.payments.title": "支付历史",
|
||||
"workspace.payments.subtitle": "近期支付交易。",
|
||||
@@ -578,10 +552,6 @@ export const dict = {
|
||||
"enterprise.form.send": "发送",
|
||||
"enterprise.form.sending": "正在发送...",
|
||||
"enterprise.form.success": "消息已发送,我们会尽快与您联系。",
|
||||
"enterprise.form.success.submitted": "表单提交成功。",
|
||||
"enterprise.form.error.allFieldsRequired": "所有字段均为必填项。",
|
||||
"enterprise.form.error.invalidEmailFormat": "邮箱格式无效。",
|
||||
"enterprise.form.error.internalServer": "内部服务器错误。",
|
||||
"enterprise.faq.title": "常见问题",
|
||||
"enterprise.faq.q1": "什么是 OpenCode 企业版?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -614,7 +584,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "代理",
|
||||
"bench.list.table.model": "模型",
|
||||
"bench.list.table.score": "分数",
|
||||
"bench.submission.error.allFieldsRequired": "所有字段均为必填项。",
|
||||
|
||||
"bench.detail.title": "基准测试 - {{task}}",
|
||||
"bench.detail.notFound": "未找到任务",
|
||||
|
||||
@@ -15,7 +15,6 @@ export const dict = {
|
||||
"nav.home": "首頁",
|
||||
"nav.openMenu": "開啟選單",
|
||||
"nav.getStartedFree": "免費開始使用",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "複製標誌(SVG)",
|
||||
"nav.context.copyWordmark": "複製字標(SVG)",
|
||||
@@ -43,13 +42,9 @@ export const dict = {
|
||||
"notFound.docs": "文件",
|
||||
"notFound.github": "GitHub",
|
||||
"notFound.discord": "Discord",
|
||||
"notFound.logoLightAlt": "opencode 淺色標誌",
|
||||
"notFound.logoDarkAlt": "opencode 深色標誌",
|
||||
|
||||
"user.logout": "登出",
|
||||
|
||||
"auth.callback.error.codeMissing": "找不到授權碼。",
|
||||
|
||||
"workspace.select": "選取工作區",
|
||||
"workspace.createNew": "+ 建立新工作區",
|
||||
"workspace.modal.title": "建立新工作區",
|
||||
@@ -81,8 +76,6 @@ export const dict = {
|
||||
"error.reloadAmountMin": "儲值金額必須至少為 ${{amount}}",
|
||||
"error.reloadTriggerMin": "餘額觸發門檻必須至少為 ${{amount}}",
|
||||
|
||||
"app.meta.description": "OpenCode - 開源編碼代理。",
|
||||
|
||||
"home.title": "OpenCode | 開源 AI 編碼代理",
|
||||
|
||||
"temp.title": "OpenCode | 專為終端打造的 AI 編碼代理",
|
||||
@@ -98,8 +91,6 @@ export const dict = {
|
||||
"temp.feature.models.afterLink": "支援 75+ 家 LLM 供應商,包括本地模型",
|
||||
"temp.screenshot.caption": "使用 tokyonight 主題的 OpenCode TUI",
|
||||
"temp.screenshot.alt": "使用 tokyonight 主題的 OpenCode TUI",
|
||||
"temp.logoLightAlt": "opencode 淺色標誌",
|
||||
"temp.logoDarkAlt": "opencode 深色標誌",
|
||||
|
||||
"home.banner.badge": "新",
|
||||
"home.banner.text": "桌面應用已推出 Beta",
|
||||
@@ -238,22 +229,6 @@ export const dict = {
|
||||
"zen.privacy.beforeExceptions": "所有 Zen 模型均在美國託管。供應商遵循零留存政策,不會將你的資料用於模型訓練,並且有",
|
||||
"zen.privacy.exceptionsLink": "以下例外情況",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "超出頻率限制。請稍後再試。",
|
||||
"zen.api.error.modelNotSupported": "不支援模型 {{model}}",
|
||||
"zen.api.error.modelFormatNotSupported": "模型 {{model}} 不支援格式 {{format}}",
|
||||
"zen.api.error.noProviderAvailable": "無可用的供應商",
|
||||
"zen.api.error.providerNotSupported": "不支援供應商 {{provider}}",
|
||||
"zen.api.error.missingApiKey": "缺少 API 金鑰。",
|
||||
"zen.api.error.invalidApiKey": "無效的 API 金鑰。",
|
||||
"zen.api.error.subscriptionQuotaExceeded": "超出訂閱配額。請在 {{retryIn}} 後重試。",
|
||||
"zen.api.error.subscriptionQuotaExceededUseFreeModels": "超出訂閱配額。你可以繼續使用免費模型。",
|
||||
"zen.api.error.noPaymentMethod": "無付款方式。請在此處新增付款方式:{{billingUrl}}",
|
||||
"zen.api.error.insufficientBalance": "餘額不足。請在此處管理你的帳務:{{billingUrl}}",
|
||||
"zen.api.error.workspaceMonthlyLimitReached":
|
||||
"你的工作區已達到每月支出限額 ${{amount}}。請在此處管理你的限額:{{billingUrl}}",
|
||||
"zen.api.error.userMonthlyLimitReached": "你已達到每月支出限額 ${{amount}}。請在此處管理你的限額:{{membersUrl}}",
|
||||
"zen.api.error.modelDisabled": "模型已停用",
|
||||
|
||||
"black.meta.title": "OpenCode Black | 存取全球最佳編碼模型",
|
||||
"black.meta.description": "透過 OpenCode Black 訂閱方案存取 Claude、GPT、Gemini 等模型。",
|
||||
"black.hero.title": "存取全球最佳編碼模型",
|
||||
@@ -461,7 +436,6 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "請更新你的付款方式並重試。",
|
||||
"workspace.reload.retrying": "重試中...",
|
||||
"workspace.reload.retry": "重試",
|
||||
"workspace.reload.error.paymentFailed": "付款失敗。",
|
||||
|
||||
"workspace.payments.title": "付款紀錄",
|
||||
"workspace.payments.subtitle": "最近的付款交易。",
|
||||
@@ -577,10 +551,6 @@ export const dict = {
|
||||
"enterprise.form.send": "傳送",
|
||||
"enterprise.form.sending": "傳送中...",
|
||||
"enterprise.form.success": "訊息已傳送,我們會盡快與你聯絡。",
|
||||
"enterprise.form.success.submitted": "表單已成功送出。",
|
||||
"enterprise.form.error.allFieldsRequired": "所有欄位均為必填。",
|
||||
"enterprise.form.error.invalidEmailFormat": "無效的電子郵件格式。",
|
||||
"enterprise.form.error.internalServer": "內部伺服器錯誤。",
|
||||
"enterprise.faq.title": "常見問題",
|
||||
"enterprise.faq.q1": "什麼是 OpenCode Enterprise?",
|
||||
"enterprise.faq.a1":
|
||||
@@ -613,7 +583,6 @@ export const dict = {
|
||||
"bench.list.table.agent": "代理",
|
||||
"bench.list.table.model": "模型",
|
||||
"bench.list.table.score": "分數",
|
||||
"bench.submission.error.allFieldsRequired": "所有欄位均為必填。",
|
||||
|
||||
"bench.detail.title": "評測 - {{task}}",
|
||||
"bench.detail.notFound": "找不到任務",
|
||||
|
||||
@@ -48,9 +48,6 @@ const map = {
|
||||
"Provider is required": "error.providerRequired",
|
||||
"API key is required": "error.apiKeyRequired",
|
||||
"Model is required": "error.modelRequired",
|
||||
"workspace.reload.error.paymentFailed": "workspace.reload.error.paymentFailed",
|
||||
"Payment failed": "workspace.reload.error.paymentFailed",
|
||||
"Payment failed.": "workspace.reload.error.paymentFailed",
|
||||
} as const satisfies Record<string, Key>
|
||||
|
||||
export function formErrorReloadAmountMin(amount: number) {
|
||||
|
||||
@@ -16,8 +16,8 @@ export default function NotFound() {
|
||||
<div data-component="content">
|
||||
<section data-component="top">
|
||||
<a href={language.route("/")} data-slot="logo-link">
|
||||
<img data-slot="logo light" src={logoLight} alt={i18n.t("notFound.logoLightAlt")} />
|
||||
<img data-slot="logo dark" src={logoDark} alt={i18n.t("notFound.logoDarkAlt")} />
|
||||
<img data-slot="logo light" src={logoLight} alt="opencode logo light" />
|
||||
<img data-slot="logo dark" src={logoDark} alt="opencode logo dark" />
|
||||
</a>
|
||||
<h1 data-slot="title">{i18n.t("notFound.heading")}</h1>
|
||||
</section>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { APIEvent } from "@solidjs/start/server"
|
||||
import { AWS } from "@opencode-ai/console-core/aws.js"
|
||||
import { i18n } from "~/i18n"
|
||||
import { localeFromRequest } from "~/lib/language"
|
||||
|
||||
interface EnterpriseFormData {
|
||||
name: string
|
||||
@@ -11,19 +9,18 @@ interface EnterpriseFormData {
|
||||
}
|
||||
|
||||
export async function POST(event: APIEvent) {
|
||||
const dict = i18n(localeFromRequest(event.request))
|
||||
try {
|
||||
const body = (await event.request.json()) as EnterpriseFormData
|
||||
|
||||
// Validate required fields
|
||||
if (!body.name || !body.role || !body.email || !body.message) {
|
||||
return Response.json({ error: dict["enterprise.form.error.allFieldsRequired"] }, { status: 400 })
|
||||
return Response.json({ error: "All fields are required" }, { status: 400 })
|
||||
}
|
||||
|
||||
// Validate email format
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
if (!emailRegex.test(body.email)) {
|
||||
return Response.json({ error: dict["enterprise.form.error.invalidEmailFormat"] }, { status: 400 })
|
||||
return Response.json({ error: "Invalid email format" }, { status: 400 })
|
||||
}
|
||||
|
||||
// Create email content
|
||||
@@ -42,9 +39,9 @@ ${body.email}`.trim()
|
||||
replyTo: body.email,
|
||||
})
|
||||
|
||||
return Response.json({ success: true, message: dict["enterprise.form.success.submitted"] }, { status: 200 })
|
||||
return Response.json({ success: true, message: "Form submitted successfully" }, { status: 200 })
|
||||
} catch (error) {
|
||||
console.error("Error processing enterprise form:", error)
|
||||
return Response.json({ error: dict["enterprise.form.error.internalServer"] }, { status: 500 })
|
||||
return Response.json({ error: "Internal server error" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,15 @@ import { redirect } from "@solidjs/router"
|
||||
import type { APIEvent } from "@solidjs/start/server"
|
||||
import { AuthClient } from "~/context/auth"
|
||||
import { useAuthSession } from "~/context/auth"
|
||||
import { i18n } from "~/i18n"
|
||||
import { localeFromRequest, route } from "~/lib/language"
|
||||
|
||||
export async function GET(input: APIEvent) {
|
||||
const url = new URL(input.request.url)
|
||||
const locale = localeFromRequest(input.request)
|
||||
const dict = i18n(locale)
|
||||
|
||||
try {
|
||||
const code = url.searchParams.get("code")
|
||||
if (!code) throw new Error(dict["auth.callback.error.codeMissing"])
|
||||
if (!code) throw new Error("No code found")
|
||||
const result = await AuthClient.exchange(code, `${url.origin}${url.pathname}`)
|
||||
if (result.err) throw new Error(result.err.message)
|
||||
const decoded = AuthClient.decode(result.tokens.access, {} as any)
|
||||
|
||||
@@ -2,8 +2,6 @@ import type { APIEvent } from "@solidjs/start/server"
|
||||
import { Database } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { BenchmarkTable } from "@opencode-ai/console-core/schema/benchmark.sql.js"
|
||||
import { Identifier } from "@opencode-ai/console-core/identifier.js"
|
||||
import { i18n } from "~/i18n"
|
||||
import { localeFromRequest } from "~/lib/language"
|
||||
|
||||
interface SubmissionBody {
|
||||
model: string
|
||||
@@ -12,11 +10,10 @@ interface SubmissionBody {
|
||||
}
|
||||
|
||||
export async function POST(event: APIEvent) {
|
||||
const dict = i18n(localeFromRequest(event.request))
|
||||
const body = (await event.request.json()) as SubmissionBody
|
||||
|
||||
if (!body.model || !body.agent || !body.result) {
|
||||
return Response.json({ error: dict["bench.submission.error.allFieldsRequired"] }, { status: 400 })
|
||||
return Response.json({ error: "All fields are required" }, { status: 400 })
|
||||
}
|
||||
|
||||
await Database.use((tx) =>
|
||||
|
||||
@@ -33,7 +33,6 @@ const brandAssets = "/opencode-brand-assets.zip"
|
||||
|
||||
export default function Brand() {
|
||||
const i18n = useI18n()
|
||||
const alt = i18n.t("brand.meta.description")
|
||||
const downloadFile = async (url: string, filename: string) => {
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
@@ -89,7 +88,7 @@ export default function Brand() {
|
||||
|
||||
<div data-component="brand-grid">
|
||||
<div>
|
||||
<img src={previewLogoLight} alt={alt} />
|
||||
<img src={previewLogoLight} alt="OpenCode brand guidelines" />
|
||||
<div data-component="actions">
|
||||
<button onClick={() => downloadFile(logoLightPng, "opencode-logo-light.png")}>
|
||||
PNG
|
||||
@@ -116,7 +115,7 @@ export default function Brand() {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<img src={previewLogoDark} alt={alt} />
|
||||
<img src={previewLogoDark} alt="OpenCode brand guidelines" />
|
||||
<div data-component="actions">
|
||||
<button onClick={() => downloadFile(logoDarkPng, "opencode-logo-dark.png")}>
|
||||
PNG
|
||||
@@ -143,7 +142,7 @@ export default function Brand() {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<img src={previewLogoLightSquare} alt={alt} />
|
||||
<img src={previewLogoLightSquare} alt="OpenCode brand guidelines" />
|
||||
<div data-component="actions">
|
||||
<button onClick={() => downloadFile(logoLightSquarePng, "opencode-logo-light-square.png")}>
|
||||
PNG
|
||||
@@ -170,7 +169,7 @@ export default function Brand() {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<img src={previewLogoDarkSquare} alt={alt} />
|
||||
<img src={previewLogoDarkSquare} alt="OpenCode brand guidelines" />
|
||||
<div data-component="actions">
|
||||
<button onClick={() => downloadFile(logoDarkSquarePng, "opencode-logo-dark-square.png")}>
|
||||
PNG
|
||||
@@ -197,7 +196,7 @@ export default function Brand() {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<img src={previewWordmarkLight} alt={alt} />
|
||||
<img src={previewWordmarkLight} alt="OpenCode brand guidelines" />
|
||||
<div data-component="actions">
|
||||
<button onClick={() => downloadFile(wordmarkLightPng, "opencode-wordmark-light.png")}>
|
||||
PNG
|
||||
@@ -224,7 +223,7 @@ export default function Brand() {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<img src={previewWordmarkDark} alt={alt} />
|
||||
<img src={previewWordmarkDark} alt="OpenCode brand guidelines" />
|
||||
<div data-component="actions">
|
||||
<button onClick={() => downloadFile(wordmarkDarkPng, "opencode-wordmark-dark.png")}>
|
||||
PNG
|
||||
@@ -251,7 +250,7 @@ export default function Brand() {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<img src={previewWordmarkSimpleLight} alt={alt} />
|
||||
<img src={previewWordmarkSimpleLight} alt="OpenCode brand guidelines" />
|
||||
<div data-component="actions">
|
||||
<button onClick={() => downloadFile(wordmarkSimpleLightPng, "opencode-wordmark-simple-light.png")}>
|
||||
PNG
|
||||
@@ -278,7 +277,7 @@ export default function Brand() {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<img src={previewWordmarkSimpleDark} alt={alt} />
|
||||
<img src={previewWordmarkSimpleDark} alt="OpenCode brand guidelines" />
|
||||
<div data-component="actions">
|
||||
<button onClick={() => downloadFile(wordmarkSimpleDarkPng, "opencode-wordmark-simple-dark.png")}>
|
||||
PNG
|
||||
|
||||
@@ -19,7 +19,7 @@ const downloadNames: Record<string, string> = {
|
||||
|
||||
export async function GET({ params: { platform, channel } }: APIEvent) {
|
||||
const assetName = assetNames[platform]
|
||||
if (!assetName) return new Response(null, { status: 404 })
|
||||
if (!assetName) return new Response("Not Found", { status: 404 })
|
||||
|
||||
const resp = await fetch(
|
||||
`https://github.com/anomalyco/${channel === "stable" ? "opencode" : "opencode-beta"}/releases/latest/download/${assetName}`,
|
||||
|
||||
@@ -306,7 +306,7 @@ export async function POST(input: APIEvent) {
|
||||
.update(BillingTable)
|
||||
.set({
|
||||
reload: false,
|
||||
reloadError: errorMessage ?? "workspace.reload.error.paymentFailed",
|
||||
reloadError: errorMessage ?? "Payment failed.",
|
||||
timeReloadError: sql`now()`,
|
||||
})
|
||||
.where(eq(BillingTable.workspaceID, Actor.workspace())),
|
||||
|
||||
@@ -47,8 +47,8 @@ export default function Home() {
|
||||
|
||||
<div data-component="content">
|
||||
<section data-component="top">
|
||||
<img data-slot="logo light" src={logoLight} alt={i18n.t("temp.logoLightAlt")} />
|
||||
<img data-slot="logo dark" src={logoDark} alt={i18n.t("temp.logoDarkAlt")} />
|
||||
<img data-slot="logo light" src={logoLight} alt="opencode logo light" />
|
||||
<img data-slot="logo dark" src={logoDark} alt="opencode logo dark" />
|
||||
<h1 data-slot="title">{i18n.t("temp.hero.title")}</h1>
|
||||
<div data-slot="login">
|
||||
<a href="/auth">{i18n.t("temp.zen")}</a>
|
||||
|
||||
@@ -12,7 +12,6 @@ import { queryBillingInfo } from "../../common"
|
||||
import styles from "./lite-section.module.css"
|
||||
import { useI18n } from "~/context/i18n"
|
||||
import { useLanguage } from "~/context/language"
|
||||
import { formError } from "~/lib/form-error"
|
||||
|
||||
const queryLiteSubscription = query(async (workspaceID: string) => {
|
||||
"use server"
|
||||
@@ -115,7 +114,7 @@ const createSessionUrl = action(async (workspaceID: string, returnUrl: string) =
|
||||
const setLiteUseBalance = action(async (form: FormData) => {
|
||||
"use server"
|
||||
const workspaceID = form.get("workspaceID")?.toString()
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
if (!workspaceID) return { error: "Workspace ID is required" }
|
||||
const useBalance = form.get("useBalance")?.toString() === "true"
|
||||
|
||||
return json(
|
||||
|
||||
@@ -202,8 +202,7 @@ export function ReloadSection() {
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
})}
|
||||
. {i18n.t("workspace.reload.reason")}{" "}
|
||||
{localizeError(i18n.t, billingInfo()?.reloadError ?? undefined).replace(/\.$/, "")}.{" "}
|
||||
. {i18n.t("workspace.reload.reason")} {billingInfo()?.reloadError?.replace(/\.$/, "")}.{" "}
|
||||
{i18n.t("workspace.reload.updatePaymentMethod")}
|
||||
</p>
|
||||
<form action={reload} method="post" data-slot="create-form">
|
||||
|
||||
@@ -35,8 +35,6 @@ import { createTrialLimiter } from "./trialLimiter"
|
||||
import { createStickyTracker } from "./stickyProviderTracker"
|
||||
import { LiteData } from "@opencode-ai/console-core/lite.js"
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import { i18n, type Key } from "~/i18n"
|
||||
import { localeFromRequest } from "~/lib/language"
|
||||
|
||||
type ZenData = Awaited<ReturnType<typeof ZenData.list>>
|
||||
type RetryOptions = {
|
||||
@@ -45,15 +43,6 @@ type RetryOptions = {
|
||||
}
|
||||
type BillingSource = "anonymous" | "free" | "byok" | "subscription" | "lite" | "balance"
|
||||
|
||||
function resolve(text: string, params?: Record<string, string | number>) {
|
||||
if (!params) return text
|
||||
return text.replace(/\{\{(\w+)\}\}/g, (raw, key) => {
|
||||
const value = params[key]
|
||||
if (value === undefined || value === null) return raw
|
||||
return String(value)
|
||||
})
|
||||
}
|
||||
|
||||
export async function handler(
|
||||
input: APIEvent,
|
||||
opts: {
|
||||
@@ -71,8 +60,6 @@ export async function handler(
|
||||
|
||||
const MAX_FAILOVER_RETRIES = 3
|
||||
const MAX_429_RETRIES = 3
|
||||
const dict = i18n(localeFromRequest(input.request))
|
||||
const t = (key: Key, params?: Record<string, string | number>) => resolve(dict[key], params)
|
||||
const ADMIN_WORKSPACES = [
|
||||
"wrk_01K46JDFR0E75SG2Q8K172KF3Y", // frank
|
||||
"wrk_01K6W1A3VE0KMNVSCQT43BG2SX", // opencode bench
|
||||
@@ -99,7 +86,7 @@ export async function handler(
|
||||
const dataDumper = createDataDumper(sessionId, requestId, projectId)
|
||||
const trialLimiter = createTrialLimiter(modelInfo.trial, ip, ocClient)
|
||||
const isTrial = await trialLimiter?.isTrial()
|
||||
const rateLimiter = createRateLimiter(modelInfo.rateLimit, ip, input.request)
|
||||
const rateLimiter = createRateLimiter(modelInfo.rateLimit, ip, input.request.headers)
|
||||
await rateLimiter?.check()
|
||||
const stickyTracker = createStickyTracker(modelInfo.stickyProvider, sessionId)
|
||||
const stickyProvider = await stickyTracker?.get()
|
||||
@@ -372,20 +359,14 @@ export async function handler(
|
||||
}
|
||||
|
||||
function validateModel(zenData: ZenData, reqModel: string) {
|
||||
if (!(reqModel in zenData.models)) throw new ModelError(t("zen.api.error.modelNotSupported", { model: reqModel }))
|
||||
if (!(reqModel in zenData.models)) throw new ModelError(`Model ${reqModel} not supported`)
|
||||
|
||||
const modelId = reqModel as keyof typeof zenData.models
|
||||
const modelData = Array.isArray(zenData.models[modelId])
|
||||
? zenData.models[modelId].find((model) => opts.format === model.formatFilter)
|
||||
: zenData.models[modelId]
|
||||
|
||||
if (!modelData)
|
||||
throw new ModelError(
|
||||
t("zen.api.error.modelFormatNotSupported", {
|
||||
model: reqModel,
|
||||
format: opts.format,
|
||||
}),
|
||||
)
|
||||
if (!modelData) throw new ModelError(`Model ${reqModel} not supported for format ${opts.format}`)
|
||||
|
||||
logger.metric({ model: modelId })
|
||||
|
||||
@@ -437,9 +418,8 @@ export async function handler(
|
||||
return modelInfo.providers.find((provider) => provider.id === modelInfo.fallbackProvider)
|
||||
})()
|
||||
|
||||
if (!modelProvider) throw new ModelError(t("zen.api.error.noProviderAvailable"))
|
||||
if (!(modelProvider.id in zenData.providers))
|
||||
throw new ModelError(t("zen.api.error.providerNotSupported", { provider: modelProvider.id }))
|
||||
if (!modelProvider) throw new ModelError("No provider available")
|
||||
if (!(modelProvider.id in zenData.providers)) throw new ModelError(`Provider ${modelProvider.id} not supported`)
|
||||
|
||||
return {
|
||||
...modelProvider,
|
||||
@@ -459,7 +439,7 @@ export async function handler(
|
||||
const apiKey = opts.parseApiKey(input.request.headers)
|
||||
if (!apiKey || apiKey === "public") {
|
||||
if (modelInfo.allowAnonymous) return
|
||||
throw new AuthError(t("zen.api.error.missingApiKey"))
|
||||
throw new AuthError("Missing API key.")
|
||||
}
|
||||
|
||||
const data = await Database.use((tx) =>
|
||||
@@ -540,13 +520,13 @@ export async function handler(
|
||||
.then((rows) => rows[0]),
|
||||
)
|
||||
|
||||
if (!data) throw new AuthError(t("zen.api.error.invalidApiKey"))
|
||||
if (!data) throw new AuthError("Invalid API key.")
|
||||
if (
|
||||
modelInfo.id.startsWith("alpha-") &&
|
||||
Resource.App.stage === "production" &&
|
||||
!ADMIN_WORKSPACES.includes(data.workspaceID)
|
||||
)
|
||||
throw new AuthError(t("zen.api.error.modelNotSupported", { model: modelInfo.id }))
|
||||
throw new AuthError(`Model ${modelInfo.id} not supported`)
|
||||
|
||||
logger.metric({
|
||||
api_key: data.apiKey,
|
||||
@@ -610,9 +590,7 @@ export async function handler(
|
||||
})
|
||||
if (result.status === "rate-limited")
|
||||
throw new SubscriptionUsageLimitError(
|
||||
t("zen.api.error.subscriptionQuotaExceeded", {
|
||||
retryIn: formatRetryTime(result.resetInSec),
|
||||
}),
|
||||
`Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`,
|
||||
result.resetInSec,
|
||||
)
|
||||
}
|
||||
@@ -628,9 +606,7 @@ export async function handler(
|
||||
})
|
||||
if (result.status === "rate-limited")
|
||||
throw new SubscriptionUsageLimitError(
|
||||
t("zen.api.error.subscriptionQuotaExceeded", {
|
||||
retryIn: formatRetryTime(result.resetInSec),
|
||||
}),
|
||||
`Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`,
|
||||
result.resetInSec,
|
||||
)
|
||||
}
|
||||
@@ -656,7 +632,7 @@ export async function handler(
|
||||
})
|
||||
if (result.status === "rate-limited")
|
||||
throw new SubscriptionUsageLimitError(
|
||||
t("zen.api.error.subscriptionQuotaExceededUseFreeModels"),
|
||||
`Subscription quota exceeded. You can continue using free models.`,
|
||||
result.resetInSec,
|
||||
)
|
||||
}
|
||||
@@ -671,7 +647,7 @@ export async function handler(
|
||||
})
|
||||
if (result.status === "rate-limited")
|
||||
throw new SubscriptionUsageLimitError(
|
||||
t("zen.api.error.subscriptionQuotaExceededUseFreeModels"),
|
||||
`Subscription quota exceeded. You can continue using free models.`,
|
||||
result.resetInSec,
|
||||
)
|
||||
}
|
||||
@@ -686,7 +662,7 @@ export async function handler(
|
||||
})
|
||||
if (result.status === "rate-limited")
|
||||
throw new SubscriptionUsageLimitError(
|
||||
t("zen.api.error.subscriptionQuotaExceededUseFreeModels"),
|
||||
`Subscription quota exceeded. You can continue using free models.`,
|
||||
result.resetInSec,
|
||||
)
|
||||
}
|
||||
@@ -699,10 +675,14 @@ export async function handler(
|
||||
|
||||
// Validate pay as you go billing
|
||||
const billing = authInfo.billing
|
||||
const billingUrl = `https://opencode.ai/workspace/${authInfo.workspaceID}/billing`
|
||||
const membersUrl = `https://opencode.ai/workspace/${authInfo.workspaceID}/members`
|
||||
if (!billing.paymentMethodID) throw new CreditsError(t("zen.api.error.noPaymentMethod", { billingUrl }))
|
||||
if (billing.balance <= 0) throw new CreditsError(t("zen.api.error.insufficientBalance", { billingUrl }))
|
||||
if (!billing.paymentMethodID)
|
||||
throw new CreditsError(
|
||||
`No payment method. Add a payment method here: https://opencode.ai/workspace/${authInfo.workspaceID}/billing`,
|
||||
)
|
||||
if (billing.balance <= 0)
|
||||
throw new CreditsError(
|
||||
`Insufficient balance. Manage your billing here: https://opencode.ai/workspace/${authInfo.workspaceID}/billing`,
|
||||
)
|
||||
|
||||
const now = new Date()
|
||||
const currentYear = now.getUTCFullYear()
|
||||
@@ -716,10 +696,7 @@ export async function handler(
|
||||
currentMonth === billing.timeMonthlyUsageUpdated.getUTCMonth()
|
||||
)
|
||||
throw new MonthlyLimitError(
|
||||
t("zen.api.error.workspaceMonthlyLimitReached", {
|
||||
amount: billing.monthlyLimit,
|
||||
billingUrl,
|
||||
}),
|
||||
`Your workspace has reached its monthly spending limit of $${billing.monthlyLimit}. Manage your limits here: https://opencode.ai/workspace/${authInfo.workspaceID}/billing`,
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -731,10 +708,7 @@ export async function handler(
|
||||
currentMonth === authInfo.user.timeMonthlyUsageUpdated.getUTCMonth()
|
||||
)
|
||||
throw new UserLimitError(
|
||||
t("zen.api.error.userMonthlyLimitReached", {
|
||||
amount: authInfo.user.monthlyLimit,
|
||||
membersUrl,
|
||||
}),
|
||||
`You have reached your monthly spending limit of $${authInfo.user.monthlyLimit}. Manage your limits here: https://opencode.ai/workspace/${authInfo.workspaceID}/members`,
|
||||
)
|
||||
|
||||
return "balance"
|
||||
@@ -742,7 +716,7 @@ export async function handler(
|
||||
|
||||
function validateModelSettings(authInfo: AuthInfo) {
|
||||
if (!authInfo) return
|
||||
if (authInfo.isDisabled) throw new ModelError(t("zen.api.error.modelDisabled"))
|
||||
if (authInfo.isDisabled) throw new ModelError("Model is disabled")
|
||||
}
|
||||
|
||||
function updateProviderKey(authInfo: AuthInfo, providerInfo: ProviderInfo) {
|
||||
|
||||
@@ -3,14 +3,11 @@ import { IpRateLimitTable } from "@opencode-ai/console-core/schema/ip.sql.js"
|
||||
import { FreeUsageLimitError } from "./error"
|
||||
import { logger } from "./logger"
|
||||
import { ZenData } from "@opencode-ai/console-core/model.js"
|
||||
import { i18n } from "~/i18n"
|
||||
import { localeFromRequest } from "~/lib/language"
|
||||
|
||||
export function createRateLimiter(limit: ZenData.RateLimit | undefined, rawIp: string, request: Request) {
|
||||
export function createRateLimiter(limit: ZenData.RateLimit | undefined, rawIp: string, headers: Headers) {
|
||||
if (!limit) return
|
||||
const dict = i18n(localeFromRequest(request))
|
||||
|
||||
const limitValue = limit.checkHeader && !request.headers.get(limit.checkHeader) ? limit.fallbackValue! : limit.value
|
||||
const limitValue = limit.checkHeader && !headers.get(limit.checkHeader) ? limit.fallbackValue! : limit.value
|
||||
|
||||
const ip = !rawIp.length ? "unknown" : rawIp
|
||||
const now = Date.now()
|
||||
@@ -39,7 +36,7 @@ export function createRateLimiter(limit: ZenData.RateLimit | undefined, rawIp: s
|
||||
logger.debug(`rate limit total: ${total}`)
|
||||
if (total >= limitValue)
|
||||
throw new FreeUsageLimitError(
|
||||
dict["zen.api.error.rateLimitExceeded"],
|
||||
`Rate limit exceeded. Please try again later.`,
|
||||
limit.period === "day" ? getRetryAfterDay(now) : getRetryAfterHour(rows, intervals, limitValue, now),
|
||||
)
|
||||
},
|
||||
|
||||
@@ -23,6 +23,7 @@ import { MessageNav } from "@opencode-ai/ui/message-nav"
|
||||
import { preloadMultiFileDiff, PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
|
||||
import { FileSSR } from "@opencode-ai/ui/file-ssr"
|
||||
import { clientOnly } from "@solidjs/start"
|
||||
import { type IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { Meta, Title } from "@solidjs/meta"
|
||||
import { Base64 } from "js-base64"
|
||||
|
||||
@@ -267,9 +268,10 @@ export default function () {
|
||||
</div>
|
||||
<div class="flex gap-4 items-center">
|
||||
<div class="flex gap-2 items-center">
|
||||
<Show when={provider()}>
|
||||
<ProviderIcon id={provider()!} class="size-3.5 shrink-0 text-icon-strong-base" />
|
||||
</Show>
|
||||
<ProviderIcon
|
||||
id={provider() as IconName}
|
||||
class="size-3.5 shrink-0 text-icon-strong-base"
|
||||
/>
|
||||
<div class="text-12-regular text-text-base">{model()?.name ?? modelID()}</div>
|
||||
</div>
|
||||
<div class="text-12-regular text-text-weaker">
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
CREATE TABLE `workspace` (
|
||||
`id` text PRIMARY KEY,
|
||||
`branch` text,
|
||||
`project_id` text NOT NULL,
|
||||
`config` text NOT NULL,
|
||||
CONSTRAINT `fk_workspace_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE
|
||||
);
|
||||
@@ -1,959 +0,0 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"id": "1f1dbf2d-bf66-4b25-8af4-4ba7633b7e40",
|
||||
"prevIds": ["d2736e43-700f-4e9e-8151-9f2f0d967bc8"],
|
||||
"ddl": [
|
||||
{
|
||||
"name": "workspace",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "control_account",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "project",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "message",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "part",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "permission",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "session",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "todo",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "session_share",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "branch",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "project_id",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "config",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "email",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "url",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "access_token",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "refresh_token",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "token_expiry",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "active",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "worktree",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "vcs",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "name",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "icon_url",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "icon_color",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_initialized",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "sandboxes",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "commands",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "message_id",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "project_id",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "project_id",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "parent_id",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "slug",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "directory",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "title",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "version",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "share_url",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_additions",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_deletions",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_files",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_diffs",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "revert",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "permission",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_compacting",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_archived",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "content",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "status",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "priority",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "position",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "secret",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "url",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"tableTo": "project",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_workspace_project_id_project_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_message_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"columns": ["message_id"],
|
||||
"tableTo": "message",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_part_message_id_message_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"tableTo": "project",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_permission_project_id_project_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"tableTo": "project",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_session_project_id_project_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_todo_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_session_share_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"columns": ["email", "url"],
|
||||
"nameExplicit": false,
|
||||
"name": "control_account_pk",
|
||||
"entityType": "pks",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id", "position"],
|
||||
"nameExplicit": false,
|
||||
"name": "todo_pk",
|
||||
"entityType": "pks",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"nameExplicit": false,
|
||||
"name": "workspace_pk",
|
||||
"table": "workspace",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"nameExplicit": false,
|
||||
"name": "project_pk",
|
||||
"table": "project",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"nameExplicit": false,
|
||||
"name": "message_pk",
|
||||
"table": "message",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"nameExplicit": false,
|
||||
"name": "part_pk",
|
||||
"table": "part",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"nameExplicit": false,
|
||||
"name": "permission_pk",
|
||||
"table": "permission",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"nameExplicit": false,
|
||||
"name": "session_pk",
|
||||
"table": "session",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"nameExplicit": false,
|
||||
"name": "session_share_pk",
|
||||
"table": "session_share",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "message_session_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "message_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "part_message_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "part_session_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "project_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_project_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "parent_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_parent_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "todo_session_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "todo"
|
||||
}
|
||||
],
|
||||
"renames": []
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
ALTER TABLE `session` ADD `workspace_id` text;--> statement-breakpoint
|
||||
CREATE INDEX `session_workspace_idx` ON `session` (`workspace_id`);
|
||||
@@ -1,983 +0,0 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"id": "572fb732-56f4-4b1e-b981-77152c9980dd",
|
||||
"prevIds": ["1f1dbf2d-bf66-4b25-8af4-4ba7633b7e40"],
|
||||
"ddl": [
|
||||
{
|
||||
"name": "workspace",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "control_account",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "project",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "message",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "part",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "permission",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "session",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "todo",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "session_share",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "branch",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "project_id",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "config",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "email",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "url",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "access_token",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "refresh_token",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "token_expiry",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "active",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "worktree",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "vcs",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "name",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "icon_url",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "icon_color",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_initialized",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "sandboxes",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "commands",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "message_id",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "project_id",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "project_id",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "workspace_id",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "parent_id",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "slug",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "directory",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "title",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "version",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "share_url",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_additions",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_deletions",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_files",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_diffs",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "revert",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "permission",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_compacting",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_archived",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "content",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "status",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "priority",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "position",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "secret",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "url",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"tableTo": "project",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_workspace_project_id_project_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_message_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"columns": ["message_id"],
|
||||
"tableTo": "message",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_part_message_id_message_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"tableTo": "project",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_permission_project_id_project_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"tableTo": "project",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_session_project_id_project_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_todo_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_session_share_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"columns": ["email", "url"],
|
||||
"nameExplicit": false,
|
||||
"name": "control_account_pk",
|
||||
"entityType": "pks",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id", "position"],
|
||||
"nameExplicit": false,
|
||||
"name": "todo_pk",
|
||||
"entityType": "pks",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"nameExplicit": false,
|
||||
"name": "workspace_pk",
|
||||
"table": "workspace",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"nameExplicit": false,
|
||||
"name": "project_pk",
|
||||
"table": "project",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"nameExplicit": false,
|
||||
"name": "message_pk",
|
||||
"table": "message",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"nameExplicit": false,
|
||||
"name": "part_pk",
|
||||
"table": "part",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"nameExplicit": false,
|
||||
"name": "permission_pk",
|
||||
"table": "permission",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"nameExplicit": false,
|
||||
"name": "session_pk",
|
||||
"table": "session",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"nameExplicit": false,
|
||||
"name": "session_share_pk",
|
||||
"table": "session_share",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "message_session_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "message_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "part_message_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "part_session_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "project_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_project_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "workspace_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_workspace_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "parent_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_parent_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "todo_session_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "todo"
|
||||
}
|
||||
],
|
||||
"renames": []
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
CREATE TABLE `account` (
|
||||
`id` text PRIMARY KEY,
|
||||
`email` text NOT NULL,
|
||||
`url` text NOT NULL,
|
||||
`access_token` text NOT NULL,
|
||||
`refresh_token` text NOT NULL,
|
||||
`token_expiry` integer,
|
||||
`workspace_id` text,
|
||||
`time_created` integer NOT NULL,
|
||||
`time_updated` integer NOT NULL
|
||||
);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,247 +0,0 @@
|
||||
import { eq, sql, isNotNull } from "drizzle-orm"
|
||||
import { Database } from "@/storage/db"
|
||||
import { AccountTable } from "./account.sql"
|
||||
import z from "zod"
|
||||
|
||||
export namespace Account {
|
||||
export const Account = z.object({
|
||||
id: z.string(),
|
||||
email: z.string(),
|
||||
url: z.string(),
|
||||
workspace_id: z.string().nullable(),
|
||||
})
|
||||
export type Account = z.infer<typeof Account>
|
||||
|
||||
function fromRow(row: (typeof AccountTable)["$inferSelect"]): Account {
|
||||
return {
|
||||
id: row.id,
|
||||
email: row.email,
|
||||
url: row.url,
|
||||
workspace_id: row.workspace_id,
|
||||
}
|
||||
}
|
||||
|
||||
export function active(): Account | undefined {
|
||||
const row = Database.use((db) => db.select().from(AccountTable).where(isNotNull(AccountTable.workspace_id)).get())
|
||||
return row ? fromRow(row) : undefined
|
||||
}
|
||||
|
||||
export function list(): Account[] {
|
||||
return Database.use((db) => db.select().from(AccountTable).all().map(fromRow))
|
||||
}
|
||||
|
||||
export function remove(accountID: string) {
|
||||
Database.use((db) => db.delete(AccountTable).where(eq(AccountTable.id, accountID)).run())
|
||||
}
|
||||
|
||||
export function use(accountID: string, workspaceID: string | null) {
|
||||
Database.use((db) =>
|
||||
db.update(AccountTable).set({ workspace_id: workspaceID }).where(eq(AccountTable.id, accountID)).run(),
|
||||
)
|
||||
}
|
||||
|
||||
export async function workspaces(accountID: string): Promise<{ id: string; name: string }[]> {
|
||||
const row = Database.use((db) => db.select().from(AccountTable).where(eq(AccountTable.id, accountID)).get())
|
||||
if (!row) return []
|
||||
|
||||
const access = await token(accountID)
|
||||
if (!access) return []
|
||||
|
||||
const res = await fetch(`${row.url}/api/orgs`, {
|
||||
headers: { authorization: `Bearer ${access}` },
|
||||
})
|
||||
|
||||
if (!res.ok) return []
|
||||
|
||||
const json = (await res.json()) as Array<{ id?: string; name?: string }>
|
||||
return json.map((x) => ({ id: x.id ?? "", name: x.name ?? "" }))
|
||||
}
|
||||
|
||||
export async function config(accountID: string, workspaceID: string): Promise<Record<string, unknown> | undefined> {
|
||||
const row = Database.use((db) => db.select().from(AccountTable).where(eq(AccountTable.id, accountID)).get())
|
||||
if (!row) return undefined
|
||||
|
||||
const access = await token(accountID)
|
||||
if (!access) return undefined
|
||||
|
||||
const res = await fetch(`${row.url}/api/config`, {
|
||||
headers: { authorization: `Bearer ${access}`, "x-org-id": workspaceID },
|
||||
})
|
||||
|
||||
if (!res.ok) return undefined
|
||||
const result = (await res.json()) as Record<string, any>
|
||||
return result.config
|
||||
}
|
||||
|
||||
export async function token(accountID: string): Promise<string | undefined> {
|
||||
const row = Database.use((db) => db.select().from(AccountTable).where(eq(AccountTable.id, accountID)).get())
|
||||
if (!row) return undefined
|
||||
if (row.token_expiry && row.token_expiry > Date.now()) return row.access_token
|
||||
|
||||
const res = await fetch(`${row.url}/oauth/token`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: row.refresh_token,
|
||||
}).toString(),
|
||||
})
|
||||
|
||||
if (!res.ok) return
|
||||
|
||||
const json = (await res.json()) as {
|
||||
access_token: string
|
||||
refresh_token?: string
|
||||
expires_in?: number
|
||||
}
|
||||
|
||||
Database.use((db) =>
|
||||
db
|
||||
.update(AccountTable)
|
||||
.set({
|
||||
access_token: json.access_token,
|
||||
refresh_token: json.refresh_token ?? row.refresh_token,
|
||||
token_expiry: json.expires_in ? Date.now() + json.expires_in * 1000 : undefined,
|
||||
})
|
||||
.where(eq(AccountTable.id, row.id))
|
||||
.run(),
|
||||
)
|
||||
|
||||
return json.access_token
|
||||
}
|
||||
|
||||
export type Login = {
|
||||
code: string
|
||||
user: string
|
||||
url: string
|
||||
server: string
|
||||
expiry: number
|
||||
interval: number
|
||||
}
|
||||
|
||||
export async function login(url?: string): Promise<Login> {
|
||||
const server = url ?? "https://web-14275-d60e67f5-pyqs0590.onporter.run"
|
||||
const res = await fetch(`${server}/auth/device/code`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ client_id: "opencode-cli" }),
|
||||
})
|
||||
|
||||
if (!res.ok) throw new Error(`Failed to initiate device flow: ${await res.text()}`)
|
||||
|
||||
const json = (await res.json()) as {
|
||||
device_code: string
|
||||
user_code: string
|
||||
verification_uri_complete: string
|
||||
expires_in: number
|
||||
interval: number
|
||||
}
|
||||
|
||||
const full = `${server}${json.verification_uri_complete}`
|
||||
|
||||
return {
|
||||
code: json.device_code,
|
||||
user: json.user_code,
|
||||
url: full,
|
||||
server,
|
||||
expiry: json.expires_in,
|
||||
interval: json.interval,
|
||||
}
|
||||
}
|
||||
|
||||
export async function poll(
|
||||
input: Login,
|
||||
): Promise<
|
||||
| { type: "success"; email: string }
|
||||
| { type: "pending" }
|
||||
| { type: "slow" }
|
||||
| { type: "expired" }
|
||||
| { type: "denied" }
|
||||
| { type: "error"; msg: string }
|
||||
> {
|
||||
const res = await fetch(`${input.server}/auth/device/token`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
||||
device_code: input.code,
|
||||
client_id: "opencode-cli",
|
||||
}),
|
||||
})
|
||||
|
||||
const json = (await res.json()) as {
|
||||
access_token?: string
|
||||
refresh_token?: string
|
||||
expires_in?: number
|
||||
error?: string
|
||||
error_description?: string
|
||||
}
|
||||
|
||||
if (json.access_token) {
|
||||
const me = await fetch(`${input.server}/api/user`, {
|
||||
headers: { authorization: `Bearer ${json.access_token}` },
|
||||
})
|
||||
const user = (await me.json()) as { id?: string; email?: string }
|
||||
if (!user.id || !user.email) {
|
||||
return { type: "error", msg: "No id or email in response" }
|
||||
}
|
||||
const id = user.id
|
||||
const email = user.email
|
||||
|
||||
const access = json.access_token
|
||||
const expiry = Date.now() + json.expires_in! * 1000
|
||||
const refresh = json.refresh_token ?? ""
|
||||
|
||||
// Fetch workspaces and get first one
|
||||
const orgsRes = await fetch(`${input.server}/api/orgs`, {
|
||||
headers: { authorization: `Bearer ${access}` },
|
||||
})
|
||||
const orgs = (await orgsRes.json()) as Array<{ id?: string; name?: string }>
|
||||
const firstWorkspaceId = orgs.length > 0 ? orgs[0].id : null
|
||||
|
||||
Database.use((db) => {
|
||||
db.update(AccountTable).set({ workspace_id: null }).run()
|
||||
db.insert(AccountTable)
|
||||
.values({
|
||||
id,
|
||||
email,
|
||||
url: input.server,
|
||||
access_token: access,
|
||||
refresh_token: refresh,
|
||||
token_expiry: expiry,
|
||||
workspace_id: firstWorkspaceId,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: AccountTable.id,
|
||||
set: {
|
||||
access_token: access,
|
||||
refresh_token: refresh,
|
||||
token_expiry: expiry,
|
||||
workspace_id: firstWorkspaceId,
|
||||
},
|
||||
})
|
||||
.run()
|
||||
})
|
||||
|
||||
return { type: "success", email }
|
||||
}
|
||||
|
||||
if (json.error === "authorization_pending") {
|
||||
return { type: "pending" }
|
||||
}
|
||||
|
||||
if (json.error === "slow_down") {
|
||||
return { type: "slow" }
|
||||
}
|
||||
|
||||
if (json.error === "expired_token") {
|
||||
return { type: "expired" }
|
||||
}
|
||||
|
||||
if (json.error === "access_denied") {
|
||||
return { type: "denied" }
|
||||
}
|
||||
|
||||
return { type: "error", msg: json.error || JSON.stringify(json) }
|
||||
}
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
import { cmd } from "./cmd"
|
||||
import * as prompts from "@clack/prompts"
|
||||
import { UI } from "../ui"
|
||||
import { Account } from "@/account"
|
||||
|
||||
export const LoginCommand = cmd({
|
||||
command: "login [url]",
|
||||
describe: "log in to an opencode account",
|
||||
builder: (yargs) =>
|
||||
yargs.positional("url", {
|
||||
describe: "server URL",
|
||||
type: "string",
|
||||
}),
|
||||
async handler(args) {
|
||||
UI.empty()
|
||||
prompts.intro("Log in")
|
||||
|
||||
const url = args.url as string | undefined
|
||||
const login = await Account.login(url)
|
||||
|
||||
prompts.log.info("Go to: " + login.url)
|
||||
prompts.log.info("Enter code: " + login.user)
|
||||
|
||||
try {
|
||||
const open =
|
||||
process.platform === "darwin"
|
||||
? ["open", login.url]
|
||||
: process.platform === "win32"
|
||||
? ["cmd", "/c", "start", login.url]
|
||||
: ["xdg-open", login.url]
|
||||
Bun.spawn(open, { stdout: "ignore", stderr: "ignore" })
|
||||
} catch {}
|
||||
|
||||
const spinner = prompts.spinner()
|
||||
spinner.start("Waiting for authorization...")
|
||||
|
||||
let wait = login.interval * 1000
|
||||
while (true) {
|
||||
await Bun.sleep(wait)
|
||||
|
||||
const result = await Account.poll(login)
|
||||
|
||||
if (result.type === "success") {
|
||||
spinner.stop("Logged in as " + result.email)
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
|
||||
if (result.type === "pending") continue
|
||||
|
||||
if (result.type === "slow") {
|
||||
wait += 5000
|
||||
continue
|
||||
}
|
||||
|
||||
if (result.type === "expired") {
|
||||
spinner.stop("Device code expired", 1)
|
||||
return
|
||||
}
|
||||
|
||||
if (result.type === "denied") {
|
||||
spinner.stop("Authorization denied", 1)
|
||||
return
|
||||
}
|
||||
|
||||
spinner.stop("Error: " + result.msg, 1)
|
||||
return
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const LogoutCommand = cmd({
|
||||
command: "logout [email]",
|
||||
describe: "log out from an account",
|
||||
builder: (yargs) =>
|
||||
yargs.positional("email", {
|
||||
describe: "account email to log out from",
|
||||
type: "string",
|
||||
}),
|
||||
async handler(args) {
|
||||
const email = args.email as string | undefined
|
||||
|
||||
if (email) {
|
||||
const accounts = Account.list()
|
||||
const match = accounts.find((a) => a.email === email)
|
||||
if (!match) {
|
||||
UI.println("Account not found: " + email)
|
||||
return
|
||||
}
|
||||
Account.remove(match.id)
|
||||
UI.println("Logged out from " + email)
|
||||
return
|
||||
}
|
||||
|
||||
const active = Account.active()
|
||||
if (!active) {
|
||||
UI.println("Not logged in")
|
||||
return
|
||||
}
|
||||
Account.remove(active.id)
|
||||
UI.println("Logged out from " + active.email)
|
||||
},
|
||||
})
|
||||
|
||||
export const SwitchCommand = cmd({
|
||||
command: "switch",
|
||||
describe: "switch active workspace",
|
||||
async handler() {
|
||||
UI.empty()
|
||||
|
||||
const active = Account.active()
|
||||
if (!active) {
|
||||
UI.println("Not logged in")
|
||||
return
|
||||
}
|
||||
|
||||
const workspaces = await Account.workspaces(active.id)
|
||||
if (workspaces.length === 0) {
|
||||
UI.println("No workspaces found")
|
||||
return
|
||||
}
|
||||
|
||||
prompts.intro("Switch workspace")
|
||||
|
||||
const opts = workspaces.map((w) => ({
|
||||
value: w.id,
|
||||
label: w.id === active.workspace_id ? w.name + UI.Style.TEXT_DIM + " (active)" : w.name,
|
||||
}))
|
||||
|
||||
const selected = await prompts.select({
|
||||
message: "Select workspace",
|
||||
options: opts,
|
||||
})
|
||||
|
||||
if (prompts.isCancel(selected)) return
|
||||
|
||||
Account.use(active.id, selected as string)
|
||||
prompts.outro("Switched to " + workspaces.find((w) => w.id === selected)?.name)
|
||||
},
|
||||
})
|
||||
|
||||
export const WorkspacesCommand = cmd({
|
||||
command: "workspaces",
|
||||
aliases: ["workspace"],
|
||||
describe: "list all workspaces",
|
||||
async handler() {
|
||||
const accounts = Account.list()
|
||||
|
||||
if (accounts.length === 0) {
|
||||
UI.println("No accounts found")
|
||||
return
|
||||
}
|
||||
|
||||
for (const account of accounts) {
|
||||
const workspaces = await Account.workspaces(account.id)
|
||||
for (const space of workspaces) {
|
||||
UI.println([space.name, account.email, space.id].join("\t"))
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1,438 +0,0 @@
|
||||
import { Auth } from "../../auth"
|
||||
import { cmd } from "./cmd"
|
||||
import * as prompts from "@clack/prompts"
|
||||
import { UI } from "../ui"
|
||||
import { ModelsDev } from "../../provider/models"
|
||||
import { map, pipe, sortBy, values } from "remeda"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import { Config } from "../../config/config"
|
||||
import { Global } from "../../global"
|
||||
import { Plugin } from "../../plugin"
|
||||
import { Instance } from "../../project/instance"
|
||||
import type { Hooks } from "@opencode-ai/plugin"
|
||||
import { Process } from "../../util/process"
|
||||
import { text } from "node:stream/consumers"
|
||||
|
||||
type PluginAuth = NonNullable<Hooks["auth"]>
|
||||
|
||||
async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string): Promise<boolean> {
|
||||
let index = 0
|
||||
if (plugin.auth.methods.length > 1) {
|
||||
const method = await prompts.select({
|
||||
message: "Login method",
|
||||
options: [
|
||||
...plugin.auth.methods.map((x, index) => ({
|
||||
label: x.label,
|
||||
value: index.toString(),
|
||||
})),
|
||||
],
|
||||
})
|
||||
if (prompts.isCancel(method)) throw new UI.CancelledError()
|
||||
index = parseInt(method)
|
||||
}
|
||||
const method = plugin.auth.methods[index]
|
||||
|
||||
await Bun.sleep(10)
|
||||
const inputs: Record<string, string> = {}
|
||||
if (method.prompts) {
|
||||
for (const prompt of method.prompts) {
|
||||
if (prompt.condition && !prompt.condition(inputs)) {
|
||||
continue
|
||||
}
|
||||
if (prompt.type === "select") {
|
||||
const value = await prompts.select({
|
||||
message: prompt.message,
|
||||
options: prompt.options,
|
||||
})
|
||||
if (prompts.isCancel(value)) throw new UI.CancelledError()
|
||||
inputs[prompt.key] = value
|
||||
} else {
|
||||
const value = await prompts.text({
|
||||
message: prompt.message,
|
||||
placeholder: prompt.placeholder,
|
||||
validate: prompt.validate ? (v) => prompt.validate!(v ?? "") : undefined,
|
||||
})
|
||||
if (prompts.isCancel(value)) throw new UI.CancelledError()
|
||||
inputs[prompt.key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (method.type === "oauth") {
|
||||
const authorize = await method.authorize(inputs)
|
||||
|
||||
if (authorize.url) {
|
||||
prompts.log.info("Go to: " + authorize.url)
|
||||
}
|
||||
|
||||
if (authorize.method === "auto") {
|
||||
if (authorize.instructions) {
|
||||
prompts.log.info(authorize.instructions)
|
||||
}
|
||||
const spinner = prompts.spinner()
|
||||
spinner.start("Waiting for authorization...")
|
||||
const result = await authorize.callback()
|
||||
if (result.type === "failed") {
|
||||
spinner.stop("Failed to authorize", 1)
|
||||
}
|
||||
if (result.type === "success") {
|
||||
const saveProvider = result.provider ?? provider
|
||||
if ("refresh" in result) {
|
||||
const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
|
||||
await Auth.set(saveProvider, {
|
||||
type: "oauth",
|
||||
refresh,
|
||||
access,
|
||||
expires,
|
||||
...extraFields,
|
||||
})
|
||||
}
|
||||
if ("key" in result) {
|
||||
await Auth.set(saveProvider, {
|
||||
type: "api",
|
||||
key: result.key,
|
||||
})
|
||||
}
|
||||
spinner.stop("Login successful")
|
||||
}
|
||||
}
|
||||
|
||||
if (authorize.method === "code") {
|
||||
const code = await prompts.text({
|
||||
message: "Paste the authorization code here: ",
|
||||
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
||||
})
|
||||
if (prompts.isCancel(code)) throw new UI.CancelledError()
|
||||
const result = await authorize.callback(code)
|
||||
if (result.type === "failed") {
|
||||
prompts.log.error("Failed to authorize")
|
||||
}
|
||||
if (result.type === "success") {
|
||||
const saveProvider = result.provider ?? provider
|
||||
if ("refresh" in result) {
|
||||
const { type: _, provider: __, refresh, access, expires, ...extraFields } = result
|
||||
await Auth.set(saveProvider, {
|
||||
type: "oauth",
|
||||
refresh,
|
||||
access,
|
||||
expires,
|
||||
...extraFields,
|
||||
})
|
||||
}
|
||||
if ("key" in result) {
|
||||
await Auth.set(saveProvider, {
|
||||
type: "api",
|
||||
key: result.key,
|
||||
})
|
||||
}
|
||||
prompts.log.success("Login successful")
|
||||
}
|
||||
}
|
||||
|
||||
prompts.outro("Done")
|
||||
return true
|
||||
}
|
||||
|
||||
if (method.type === "api") {
|
||||
if (method.authorize) {
|
||||
const result = await method.authorize(inputs)
|
||||
if (result.type === "failed") {
|
||||
prompts.log.error("Failed to authorize")
|
||||
}
|
||||
if (result.type === "success") {
|
||||
const saveProvider = result.provider ?? provider
|
||||
await Auth.set(saveProvider, {
|
||||
type: "api",
|
||||
key: result.key,
|
||||
})
|
||||
prompts.log.success("Login successful")
|
||||
}
|
||||
prompts.outro("Done")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export function resolvePluginProviders(input: {
|
||||
hooks: Hooks[]
|
||||
existingProviders: Record<string, unknown>
|
||||
disabled: Set<string>
|
||||
enabled?: Set<string>
|
||||
providerNames: Record<string, string | undefined>
|
||||
}): Array<{ id: string; name: string }> {
|
||||
const seen = new Set<string>()
|
||||
const result: Array<{ id: string; name: string }> = []
|
||||
|
||||
for (const hook of input.hooks) {
|
||||
if (!hook.auth) continue
|
||||
const id = hook.auth.provider
|
||||
if (seen.has(id)) continue
|
||||
seen.add(id)
|
||||
if (Object.hasOwn(input.existingProviders, id)) continue
|
||||
if (input.disabled.has(id)) continue
|
||||
if (input.enabled && !input.enabled.has(id)) continue
|
||||
result.push({
|
||||
id,
|
||||
name: input.providerNames[id] ?? id,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export const ProvidersCommand = cmd({
|
||||
command: "providers",
|
||||
aliases: ["auth"],
|
||||
describe: "manage AI providers and credentials",
|
||||
builder: (yargs) =>
|
||||
yargs.command(ProvidersListCommand).command(ProvidersLoginCommand).command(ProvidersLogoutCommand).demandCommand(),
|
||||
async handler() {},
|
||||
})
|
||||
|
||||
export const ProvidersListCommand = cmd({
|
||||
command: "list",
|
||||
aliases: ["ls"],
|
||||
describe: "list providers and credentials",
|
||||
async handler(_args) {
|
||||
UI.empty()
|
||||
const authPath = path.join(Global.Path.data, "auth.json")
|
||||
const homedir = os.homedir()
|
||||
const displayPath = authPath.startsWith(homedir) ? authPath.replace(homedir, "~") : authPath
|
||||
prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`)
|
||||
const results = Object.entries(await Auth.all())
|
||||
const database = await ModelsDev.get()
|
||||
|
||||
for (const [providerID, result] of results) {
|
||||
const name = database[providerID]?.name || providerID
|
||||
prompts.log.info(`${name} ${UI.Style.TEXT_DIM}${result.type}`)
|
||||
}
|
||||
|
||||
prompts.outro(`${results.length} credentials`)
|
||||
|
||||
const activeEnvVars: Array<{ provider: string; envVar: string }> = []
|
||||
|
||||
for (const [providerID, provider] of Object.entries(database)) {
|
||||
for (const envVar of provider.env) {
|
||||
if (process.env[envVar]) {
|
||||
activeEnvVars.push({
|
||||
provider: provider.name || providerID,
|
||||
envVar,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (activeEnvVars.length > 0) {
|
||||
UI.empty()
|
||||
prompts.intro("Environment")
|
||||
|
||||
for (const { provider, envVar } of activeEnvVars) {
|
||||
prompts.log.info(`${provider} ${UI.Style.TEXT_DIM}${envVar}`)
|
||||
}
|
||||
|
||||
prompts.outro(`${activeEnvVars.length} environment variable` + (activeEnvVars.length === 1 ? "" : "s"))
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const ProvidersLoginCommand = cmd({
|
||||
command: "login [url]",
|
||||
describe: "log in to a provider",
|
||||
builder: (yargs) =>
|
||||
yargs.positional("url", {
|
||||
describe: "opencode auth provider",
|
||||
type: "string",
|
||||
}),
|
||||
async handler(args) {
|
||||
await Instance.provide({
|
||||
directory: process.cwd(),
|
||||
async fn() {
|
||||
UI.empty()
|
||||
prompts.intro("Add credential")
|
||||
if (args.url) {
|
||||
const wellknown = await fetch(`${args.url}/.well-known/opencode`).then((x) => x.json() as any)
|
||||
prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
|
||||
const proc = Process.spawn(wellknown.auth.command, {
|
||||
stdout: "pipe",
|
||||
})
|
||||
if (!proc.stdout) {
|
||||
prompts.log.error("Failed")
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
const [exit, token] = await Promise.all([proc.exited, text(proc.stdout)])
|
||||
if (exit !== 0) {
|
||||
prompts.log.error("Failed")
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
await Auth.set(args.url, {
|
||||
type: "wellknown",
|
||||
key: wellknown.auth.env,
|
||||
token: token.trim(),
|
||||
})
|
||||
prompts.log.success("Logged into " + args.url)
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
await ModelsDev.refresh().catch(() => {})
|
||||
|
||||
const config = await Config.get()
|
||||
|
||||
const disabled = new Set(config.disabled_providers ?? [])
|
||||
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
|
||||
|
||||
const providers = await ModelsDev.get().then((x) => {
|
||||
const filtered: Record<string, (typeof x)[string]> = {}
|
||||
for (const [key, value] of Object.entries(x)) {
|
||||
if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) {
|
||||
filtered[key] = value
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
})
|
||||
|
||||
const priority: Record<string, number> = {
|
||||
opencode: 0,
|
||||
anthropic: 1,
|
||||
"github-copilot": 2,
|
||||
openai: 3,
|
||||
google: 4,
|
||||
openrouter: 5,
|
||||
vercel: 6,
|
||||
}
|
||||
const pluginProviders = resolvePluginProviders({
|
||||
hooks: await Plugin.list(),
|
||||
existingProviders: providers,
|
||||
disabled,
|
||||
enabled,
|
||||
providerNames: Object.fromEntries(Object.entries(config.provider ?? {}).map(([id, p]) => [id, p.name])),
|
||||
})
|
||||
let provider = await prompts.autocomplete({
|
||||
message: "Select provider",
|
||||
maxItems: 8,
|
||||
options: [
|
||||
...pipe(
|
||||
providers,
|
||||
values(),
|
||||
sortBy(
|
||||
(x) => priority[x.id] ?? 99,
|
||||
(x) => x.name ?? x.id,
|
||||
),
|
||||
map((x) => ({
|
||||
label: x.name,
|
||||
value: x.id,
|
||||
hint: {
|
||||
opencode: "recommended",
|
||||
anthropic: "Claude Max or API key",
|
||||
openai: "ChatGPT Plus/Pro or API key",
|
||||
}[x.id],
|
||||
})),
|
||||
),
|
||||
...pluginProviders.map((x) => ({
|
||||
label: x.name,
|
||||
value: x.id,
|
||||
hint: "plugin",
|
||||
})),
|
||||
{
|
||||
value: "other",
|
||||
label: "Other",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
||||
|
||||
const plugin = await Plugin.list().then((x) => x.findLast((x) => x.auth?.provider === provider))
|
||||
if (plugin && plugin.auth) {
|
||||
const handled = await handlePluginAuth({ auth: plugin.auth }, provider)
|
||||
if (handled) return
|
||||
}
|
||||
|
||||
if (provider === "other") {
|
||||
provider = await prompts.text({
|
||||
message: "Enter provider id",
|
||||
validate: (x) => (x && x.match(/^[0-9a-z-]+$/) ? undefined : "a-z, 0-9 and hyphens only"),
|
||||
})
|
||||
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
||||
provider = provider.replace(/^@ai-sdk\//, "")
|
||||
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
||||
|
||||
const customPlugin = await Plugin.list().then((x) => x.findLast((x) => x.auth?.provider === provider))
|
||||
if (customPlugin && customPlugin.auth) {
|
||||
const handled = await handlePluginAuth({ auth: customPlugin.auth }, provider)
|
||||
if (handled) return
|
||||
}
|
||||
|
||||
prompts.log.warn(
|
||||
`This only stores a credential for ${provider} - you will need configure it in opencode.json, check the docs for examples.`,
|
||||
)
|
||||
}
|
||||
|
||||
if (provider === "amazon-bedrock") {
|
||||
prompts.log.info(
|
||||
"Amazon Bedrock authentication priority:\n" +
|
||||
" 1. Bearer token (AWS_BEARER_TOKEN_BEDROCK or /connect)\n" +
|
||||
" 2. AWS credential chain (profile, access keys, IAM roles, EKS IRSA)\n\n" +
|
||||
"Configure via opencode.json options (profile, region, endpoint) or\n" +
|
||||
"AWS environment variables (AWS_PROFILE, AWS_REGION, AWS_ACCESS_KEY_ID, AWS_WEB_IDENTITY_TOKEN_FILE).",
|
||||
)
|
||||
}
|
||||
|
||||
if (provider === "opencode") {
|
||||
prompts.log.info("Create an api key at https://opencode.ai/auth")
|
||||
}
|
||||
|
||||
if (provider === "vercel") {
|
||||
prompts.log.info("You can create an api key at https://vercel.link/ai-gateway-token")
|
||||
}
|
||||
|
||||
if (["cloudflare", "cloudflare-ai-gateway"].includes(provider)) {
|
||||
prompts.log.info(
|
||||
"Cloudflare AI Gateway can be configured with CLOUDFLARE_GATEWAY_ID, CLOUDFLARE_ACCOUNT_ID, and CLOUDFLARE_API_TOKEN environment variables. Read more: https://opencode.ai/docs/providers/#cloudflare-ai-gateway",
|
||||
)
|
||||
}
|
||||
|
||||
const key = await prompts.password({
|
||||
message: "Enter your API key",
|
||||
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
||||
})
|
||||
if (prompts.isCancel(key)) throw new UI.CancelledError()
|
||||
await Auth.set(provider, {
|
||||
type: "api",
|
||||
key,
|
||||
})
|
||||
|
||||
prompts.outro("Done")
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const ProvidersLogoutCommand = cmd({
|
||||
command: "logout",
|
||||
describe: "log out from a configured provider",
|
||||
async handler(_args) {
|
||||
UI.empty()
|
||||
const credentials = await Auth.all().then((x) => Object.entries(x))
|
||||
prompts.intro("Remove credential")
|
||||
if (credentials.length === 0) {
|
||||
prompts.log.error("No credentials found")
|
||||
return
|
||||
}
|
||||
const database = await ModelsDev.get()
|
||||
const providerID = await prompts.select({
|
||||
message: "Select provider",
|
||||
options: credentials.map(([key, value]) => ({
|
||||
label: (database[key]?.name || key) + UI.Style.TEXT_DIM + " (" + value.type + ")",
|
||||
value: key,
|
||||
})),
|
||||
})
|
||||
if (prompts.isCancel(providerID)) throw new UI.CancelledError()
|
||||
await Auth.remove(providerID)
|
||||
prompts.outro("Logout successful")
|
||||
},
|
||||
})
|
||||
@@ -2,9 +2,6 @@ import { Server } from "../../server/server"
|
||||
import { cmd } from "./cmd"
|
||||
import { withNetworkOptions, resolveNetworkOptions } from "../network"
|
||||
import { Flag } from "../../flag/flag"
|
||||
import { Workspace } from "../../control-plane/workspace"
|
||||
import { Project } from "../../project/project"
|
||||
import { Installation } from "../../installation"
|
||||
|
||||
export const ServeCommand = cmd({
|
||||
command: "serve",
|
||||
@@ -17,15 +14,7 @@ export const ServeCommand = cmd({
|
||||
const opts = await resolveNetworkOptions(args)
|
||||
const server = Server.listen(opts)
|
||||
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
|
||||
|
||||
let workspaceSync: Array<ReturnType<typeof Workspace.startSyncing>> = []
|
||||
// Only available in development right now
|
||||
if (Installation.isLocal()) {
|
||||
workspaceSync = Project.list().map((project) => Workspace.startSyncing(project))
|
||||
}
|
||||
|
||||
await new Promise(() => {})
|
||||
await server.stop()
|
||||
await Promise.all(workspaceSync.map((item) => item.stop()))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
For,
|
||||
Match,
|
||||
on,
|
||||
onMount,
|
||||
Show,
|
||||
Switch,
|
||||
useContext,
|
||||
@@ -1324,8 +1323,6 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
|
||||
return props.message.time.completed - user.time.created
|
||||
})
|
||||
|
||||
const keybind = useKeybind()
|
||||
|
||||
return (
|
||||
<>
|
||||
<For each={props.parts}>
|
||||
@@ -1343,14 +1340,6 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
<Show when={props.parts.some((x) => x.type === "tool" && x.tool === "task")}>
|
||||
<box paddingTop={1} paddingLeft={3}>
|
||||
<text fg={theme.text}>
|
||||
{keybind.print("session_child_first")}
|
||||
<span style={{ fg: theme.textMuted }}> view subagents</span>
|
||||
</text>
|
||||
</box>
|
||||
</Show>
|
||||
<Show when={props.message.error && props.message.error.name !== "MessageAbortedError"}>
|
||||
<box
|
||||
border={["left"]}
|
||||
@@ -1620,7 +1609,6 @@ function InlineTool(props: {
|
||||
iconColor?: RGBA
|
||||
complete: any
|
||||
pending: string
|
||||
spinner?: boolean
|
||||
children: JSX.Element
|
||||
part: ToolPart
|
||||
}) {
|
||||
@@ -1677,18 +1665,11 @@ function InlineTool(props: {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={props.spinner}>
|
||||
<Spinner color={fg()} children={props.children} />
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<text paddingLeft={3} fg={fg()} attributes={denied() ? TextAttributes.STRIKETHROUGH : undefined}>
|
||||
<Show fallback={<>~ {props.pending}</>} when={props.complete}>
|
||||
<span style={{ fg: props.iconColor }}>{props.icon}</span> {props.children}
|
||||
</Show>
|
||||
</text>
|
||||
</Match>
|
||||
</Switch>
|
||||
<text paddingLeft={3} fg={fg()} attributes={denied() ? TextAttributes.STRIKETHROUGH : undefined}>
|
||||
<Show fallback={<>~ {props.pending}</>} when={props.complete}>
|
||||
<span style={{ fg: props.iconColor }}>{props.icon}</span> {props.children}
|
||||
</Show>
|
||||
</text>
|
||||
<Show when={error() && !denied()}>
|
||||
<text fg={theme.error}>{error()}</text>
|
||||
</Show>
|
||||
@@ -1855,7 +1836,6 @@ function Glob(props: ToolProps<typeof GlobTool>) {
|
||||
|
||||
function Read(props: ToolProps<typeof ReadTool>) {
|
||||
const { theme } = useTheme()
|
||||
const isRunning = createMemo(() => props.part.state.status === "running")
|
||||
const loaded = createMemo(() => {
|
||||
if (props.part.state.status !== "completed") return []
|
||||
if (props.part.state.time.compacted) return []
|
||||
@@ -1865,13 +1845,7 @@ function Read(props: ToolProps<typeof ReadTool>) {
|
||||
})
|
||||
return (
|
||||
<>
|
||||
<InlineTool
|
||||
icon="→"
|
||||
pending="Reading file..."
|
||||
complete={props.input.filePath}
|
||||
spinner={isRunning()}
|
||||
part={props.part}
|
||||
>
|
||||
<InlineTool icon="→" pending="Reading file..." complete={props.input.filePath} part={props.part}>
|
||||
Read {normalizePath(props.input.filePath!)} {input(props.input, ["filePath"])}
|
||||
</InlineTool>
|
||||
<For each={loaded()}>
|
||||
@@ -1947,60 +1921,62 @@ function Task(props: ToolProps<typeof TaskTool>) {
|
||||
const local = useLocal()
|
||||
const sync = useSync()
|
||||
|
||||
onMount(() => {
|
||||
if (props.metadata.sessionId && !sync.data.message[props.metadata.sessionId]?.length)
|
||||
sync.session.sync(props.metadata.sessionId)
|
||||
})
|
||||
|
||||
const messages = createMemo(() => sync.data.message[props.metadata.sessionId ?? ""] ?? [])
|
||||
|
||||
const tools = createMemo(() => {
|
||||
return messages().flatMap((msg) =>
|
||||
const sessionID = props.metadata.sessionId
|
||||
const msgs = sync.data.message[sessionID ?? ""] ?? []
|
||||
return msgs.flatMap((msg) =>
|
||||
(sync.data.part[msg.id] ?? [])
|
||||
.filter((part): part is ToolPart => part.type === "tool")
|
||||
.map((part) => ({ tool: part.tool, state: part.state })),
|
||||
)
|
||||
})
|
||||
|
||||
const current = createMemo(() => tools().findLast((x) => (x.state as any).title))
|
||||
const current = createMemo(() => tools().findLast((x) => x.state.status !== "pending"))
|
||||
|
||||
const isRunning = createMemo(() => props.part.state.status === "running")
|
||||
|
||||
const duration = createMemo(() => {
|
||||
const first = messages().find((x) => x.role === "user")?.time.created
|
||||
const assistant = messages().findLast((x) => x.role === "assistant")?.time.completed
|
||||
if (!first || !assistant) return 0
|
||||
return assistant - first
|
||||
})
|
||||
|
||||
return (
|
||||
<InlineTool
|
||||
icon="≡"
|
||||
spinner={isRunning()}
|
||||
complete={props.input.description}
|
||||
pending="Delegating..."
|
||||
part={props.part}
|
||||
>
|
||||
{props.input.description}
|
||||
<Show when={isRunning() && tools().length > 0}>
|
||||
{" "}
|
||||
· {tools().length} toolcalls
|
||||
<Show fallback={"\n└ Running..."} when={current()}>
|
||||
{(item) => {
|
||||
const title = createMemo(() => (item().state as any).title)
|
||||
return (
|
||||
<>
|
||||
{"\n"}└ {Locale.titlecase(item().tool)} {title()}
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</Show>
|
||||
</Show>
|
||||
<Show when={duration() && props.part.state.status === "completed"}>
|
||||
{"\n "}
|
||||
{tools().length} toolcalls · {Locale.duration(duration())}
|
||||
</Show>
|
||||
</InlineTool>
|
||||
<Switch>
|
||||
<Match when={props.input.description || props.input.subagent_type}>
|
||||
<BlockTool
|
||||
title={"# " + Locale.titlecase(props.input.subagent_type ?? "unknown") + " Task"}
|
||||
onClick={
|
||||
props.metadata.sessionId
|
||||
? () => navigate({ type: "session", sessionID: props.metadata.sessionId! })
|
||||
: undefined
|
||||
}
|
||||
part={props.part}
|
||||
spinner={isRunning()}
|
||||
>
|
||||
<box>
|
||||
<text style={{ fg: theme.textMuted }}>
|
||||
{props.input.description} ({tools().length} toolcalls)
|
||||
</text>
|
||||
<Show when={current()}>
|
||||
{(item) => {
|
||||
const title = item().state.status === "completed" ? (item().state as any).title : ""
|
||||
return (
|
||||
<text style={{ fg: item().state.status === "error" ? theme.error : theme.textMuted }}>
|
||||
└ {Locale.titlecase(item().tool)} {title}
|
||||
</text>
|
||||
)
|
||||
}}
|
||||
</Show>
|
||||
</box>
|
||||
<Show when={props.metadata.sessionId}>
|
||||
<text fg={theme.text}>
|
||||
{keybind.print("session_child_first")}
|
||||
<span style={{ fg: theme.textMuted }}> view subagents</span>
|
||||
</text>
|
||||
</Show>
|
||||
</BlockTool>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<InlineTool icon="#" pending="Delegating..." complete={props.input.subagent_type} part={props.part}>
|
||||
{props.input.subagent_type} Task {props.input.description}
|
||||
</InlineTool>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,59 @@
|
||||
import { cmd } from "./cmd"
|
||||
import { withNetworkOptions, resolveNetworkOptions } from "../network"
|
||||
import { WorkspaceServer } from "../../control-plane/workspace-server/server"
|
||||
import { Installation } from "../../installation"
|
||||
|
||||
export const WorkspaceServeCommand = cmd({
|
||||
command: "workspace-serve",
|
||||
builder: (yargs) => withNetworkOptions(yargs),
|
||||
describe: "starts a remote workspace event server",
|
||||
describe: "starts a remote workspace websocket server",
|
||||
handler: async (args) => {
|
||||
const opts = await resolveNetworkOptions(args)
|
||||
const server = WorkspaceServer.Listen(opts)
|
||||
console.log(`workspace event server listening on http://${server.hostname}:${server.port}/event`)
|
||||
const server = Bun.serve<{ id: string }>({
|
||||
hostname: opts.hostname,
|
||||
port: opts.port,
|
||||
fetch(req, server) {
|
||||
const url = new URL(req.url)
|
||||
if (url.pathname === "/ws") {
|
||||
const id = Bun.randomUUIDv7()
|
||||
if (server.upgrade(req, { data: { id } })) return
|
||||
return new Response("Upgrade failed", { status: 400 })
|
||||
}
|
||||
|
||||
if (url.pathname === "/health") {
|
||||
return new Response("ok", {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "text/plain; charset=utf-8",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
service: "workspace-server",
|
||||
ws: `ws://${server.hostname}:${server.port}/ws`,
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
},
|
||||
},
|
||||
)
|
||||
},
|
||||
websocket: {
|
||||
open(ws) {
|
||||
ws.send(JSON.stringify({ type: "ready", id: ws.data.id }))
|
||||
},
|
||||
message(ws, msg) {
|
||||
const text = typeof msg === "string" ? msg : msg.toString()
|
||||
ws.send(JSON.stringify({ type: "message", id: ws.data.id, text }))
|
||||
},
|
||||
close() {},
|
||||
},
|
||||
})
|
||||
|
||||
console.log(`workspace websocket server listening on ws://${server.hostname}:${server.port}/ws`)
|
||||
await new Promise(() => {})
|
||||
await server.stop()
|
||||
},
|
||||
})
|
||||
|
||||
@@ -32,7 +32,7 @@ import { Glob } from "../util/glob"
|
||||
import { PackageRegistry } from "@/bun/registry"
|
||||
import { proxied } from "@/util/proxied"
|
||||
import { iife } from "@/util/iife"
|
||||
import { Account } from "@/account"
|
||||
import { Control } from "@/control"
|
||||
import { ConfigPaths } from "./paths"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
|
||||
@@ -107,6 +107,10 @@ export namespace Config {
|
||||
}
|
||||
}
|
||||
|
||||
const token = await Control.token()
|
||||
if (token) {
|
||||
}
|
||||
|
||||
// Global user config overrides remote config.
|
||||
result = mergeConfigConcatArrays(result, await global())
|
||||
|
||||
@@ -173,15 +177,6 @@ export namespace Config {
|
||||
log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT")
|
||||
}
|
||||
|
||||
const active = Account.active()
|
||||
if (active?.workspace_id) {
|
||||
const config = await Account.config(active.id, active.workspace_id)
|
||||
result = mergeConfigConcatArrays(result, config ?? {})
|
||||
const token = await Account.token(active.id)
|
||||
// TODO: this is bad
|
||||
process.env["OPENCODE_CONTROL_TOKEN"] = token
|
||||
}
|
||||
|
||||
// Load managed config files last (highest priority) - enterprise admin-controlled
|
||||
// Kept separate from directories array to avoid write operations when installing plugins
|
||||
// which would fail on system directories requiring elevated permissions
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { WorktreeAdaptor } from "./worktree"
|
||||
import type { Config } from "../config"
|
||||
import type { Adaptor } from "./types"
|
||||
|
||||
export function getAdaptor(config: Config): Adaptor {
|
||||
switch (config.type) {
|
||||
case "worktree":
|
||||
return WorktreeAdaptor
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { Config } from "../config"
|
||||
|
||||
export type Adaptor<T extends Config = Config> = {
|
||||
create(from: T, branch?: string | null): Promise<{ config: T; init: () => Promise<void> }>
|
||||
remove(from: T): Promise<void>
|
||||
request(from: T, method: string, url: string, data?: BodyInit, signal?: AbortSignal): Promise<Response | undefined>
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Worktree } from "@/worktree"
|
||||
import type { Config } from "../config"
|
||||
import type { Adaptor } from "./types"
|
||||
|
||||
type WorktreeConfig = Extract<Config, { type: "worktree" }>
|
||||
|
||||
export const WorktreeAdaptor: Adaptor<WorktreeConfig> = {
|
||||
async create(_from: WorktreeConfig, _branch: string) {
|
||||
const next = await Worktree.create(undefined)
|
||||
return {
|
||||
config: {
|
||||
type: "worktree",
|
||||
directory: next.directory,
|
||||
},
|
||||
// Hack for now: `Worktree.create` puts all its async code in a
|
||||
// `setTimeout` so it doesn't use this, but we should change that
|
||||
init: async () => {},
|
||||
}
|
||||
},
|
||||
async remove(config: WorktreeConfig) {
|
||||
await Worktree.remove({ directory: config.directory })
|
||||
},
|
||||
async request(_from: WorktreeConfig, _method: string, _url: string, _data?: BodyInit, _signal?: AbortSignal) {
|
||||
throw new Error("worktree does not support request")
|
||||
},
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import z from "zod"
|
||||
|
||||
export const Config = z.discriminatedUnion("type", [
|
||||
z.object({
|
||||
directory: z.string(),
|
||||
type: z.literal("worktree"),
|
||||
}),
|
||||
])
|
||||
|
||||
export type Config = z.infer<typeof Config>
|
||||
@@ -1,46 +0,0 @@
|
||||
import { Instance } from "@/project/instance"
|
||||
import type { MiddlewareHandler } from "hono"
|
||||
import { Installation } from "../installation"
|
||||
import { getAdaptor } from "./adaptors"
|
||||
import { Workspace } from "./workspace"
|
||||
|
||||
// This middleware forwards all non-GET requests if the workspace is a
|
||||
// remote. The remote workspace needs to handle session mutations
|
||||
async function proxySessionRequest(req: Request) {
|
||||
if (req.method === "GET") return
|
||||
if (!Instance.directory.startsWith("wrk_")) return
|
||||
|
||||
const workspace = await Workspace.get(Instance.directory)
|
||||
if (!workspace) {
|
||||
return new Response(`Workspace not found: ${Instance.directory}`, {
|
||||
status: 500,
|
||||
headers: {
|
||||
"content-type": "text/plain; charset=utf-8",
|
||||
},
|
||||
})
|
||||
}
|
||||
if (workspace.config.type === "worktree") return
|
||||
|
||||
const url = new URL(req.url)
|
||||
const body = req.method === "HEAD" ? undefined : await req.arrayBuffer()
|
||||
return getAdaptor(workspace.config).request(
|
||||
workspace.config,
|
||||
req.method,
|
||||
`${url.pathname}${url.search}`,
|
||||
body,
|
||||
req.signal,
|
||||
)
|
||||
}
|
||||
|
||||
export const SessionProxyMiddleware: MiddlewareHandler = async (c, next) => {
|
||||
// Only available in development for now
|
||||
if (!Installation.isLocal()) {
|
||||
return next()
|
||||
}
|
||||
|
||||
const response = await proxySessionRequest(c.req.raw)
|
||||
if (response) {
|
||||
return response
|
||||
}
|
||||
return next()
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
export async function parseSSE(
|
||||
body: ReadableStream<Uint8Array>,
|
||||
signal: AbortSignal,
|
||||
onEvent: (event: unknown) => void,
|
||||
) {
|
||||
const reader = body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buf = ""
|
||||
let last = ""
|
||||
let retry = 1000
|
||||
|
||||
const abort = () => {
|
||||
void reader.cancel().catch(() => undefined)
|
||||
}
|
||||
|
||||
signal.addEventListener("abort", abort)
|
||||
|
||||
try {
|
||||
while (!signal.aborted) {
|
||||
const chunk = await reader.read().catch(() => ({ done: true, value: undefined as Uint8Array | undefined }))
|
||||
if (chunk.done) break
|
||||
|
||||
buf += decoder.decode(chunk.value, { stream: true })
|
||||
buf = buf.replace(/\r\n/g, "\n").replace(/\r/g, "\n")
|
||||
|
||||
const chunks = buf.split("\n\n")
|
||||
buf = chunks.pop() ?? ""
|
||||
|
||||
chunks.forEach((chunk) => {
|
||||
const data: string[] = []
|
||||
chunk.split("\n").forEach((line) => {
|
||||
if (line.startsWith("data:")) {
|
||||
data.push(line.replace(/^data:\s*/, ""))
|
||||
return
|
||||
}
|
||||
if (line.startsWith("id:")) {
|
||||
last = line.replace(/^id:\s*/, "")
|
||||
return
|
||||
}
|
||||
if (line.startsWith("retry:")) {
|
||||
const parsed = Number.parseInt(line.replace(/^retry:\s*/, ""), 10)
|
||||
if (!Number.isNaN(parsed)) retry = parsed
|
||||
}
|
||||
})
|
||||
|
||||
if (!data.length) return
|
||||
const raw = data.join("\n")
|
||||
try {
|
||||
onEvent(JSON.parse(raw))
|
||||
} catch {
|
||||
onEvent({
|
||||
type: "sse.message",
|
||||
properties: {
|
||||
data: raw,
|
||||
id: last || undefined,
|
||||
retry,
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
signal.removeEventListener("abort", abort)
|
||||
reader.releaseLock()
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Context } from "../util/context"
|
||||
|
||||
interface Context {
|
||||
workspaceID?: string
|
||||
}
|
||||
|
||||
const context = Context.create<Context>("workspace")
|
||||
|
||||
export const WorkspaceContext = {
|
||||
async provide<R>(input: { workspaceID?: string; fn: () => R }): Promise<R> {
|
||||
return context.provide({ workspaceID: input.workspaceID }, async () => {
|
||||
return input.fn()
|
||||
})
|
||||
},
|
||||
|
||||
get workspaceID() {
|
||||
try {
|
||||
return context.use().workspaceID
|
||||
} catch (e) {
|
||||
return undefined
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { GlobalBus } from "../../bus/global"
|
||||
import { Hono } from "hono"
|
||||
import { streamSSE } from "hono/streaming"
|
||||
|
||||
export function WorkspaceServerRoutes() {
|
||||
return new Hono().get("/event", async (c) => {
|
||||
c.header("X-Accel-Buffering", "no")
|
||||
c.header("X-Content-Type-Options", "nosniff")
|
||||
return streamSSE(c, async (stream) => {
|
||||
const send = async (event: unknown) => {
|
||||
await stream.writeSSE({
|
||||
data: JSON.stringify(event),
|
||||
})
|
||||
}
|
||||
const handler = async (event: { directory?: string; payload: unknown }) => {
|
||||
await send(event.payload)
|
||||
}
|
||||
GlobalBus.on("event", handler)
|
||||
await send({ type: "server.connected", properties: {} })
|
||||
const heartbeat = setInterval(() => {
|
||||
void send({ type: "server.heartbeat", properties: {} })
|
||||
}, 10_000)
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
stream.onAbort(() => {
|
||||
clearInterval(heartbeat)
|
||||
GlobalBus.off("event", handler)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Hono } from "hono"
|
||||
import { SessionRoutes } from "../../server/routes/session"
|
||||
import { WorkspaceServerRoutes } from "./routes"
|
||||
|
||||
export namespace WorkspaceServer {
|
||||
export function App() {
|
||||
const session = new Hono()
|
||||
.use("*", async (c, next) => {
|
||||
if (c.req.method === "GET") return c.notFound()
|
||||
await next()
|
||||
})
|
||||
.route("/", SessionRoutes())
|
||||
|
||||
return new Hono().route("/session", session).route("/", WorkspaceServerRoutes())
|
||||
}
|
||||
|
||||
export function Listen(opts: { hostname: string; port: number }) {
|
||||
return Bun.serve({
|
||||
hostname: opts.hostname,
|
||||
port: opts.port,
|
||||
fetch: App().fetch,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { sqliteTable, text } from "drizzle-orm/sqlite-core"
|
||||
import { ProjectTable } from "@/project/project.sql"
|
||||
import type { Config } from "./config"
|
||||
|
||||
export const WorkspaceTable = sqliteTable("workspace", {
|
||||
id: text().primaryKey(),
|
||||
branch: text(),
|
||||
project_id: text()
|
||||
.notNull()
|
||||
.references(() => ProjectTable.id, { onDelete: "cascade" }),
|
||||
config: text({ mode: "json" }).notNull().$type<Config>(),
|
||||
})
|
||||
@@ -1,160 +0,0 @@
|
||||
import z from "zod"
|
||||
import { Identifier } from "@/id/id"
|
||||
import { fn } from "@/util/fn"
|
||||
import { Database, eq } from "@/storage/db"
|
||||
import { Project } from "@/project/project"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
import { Log } from "@/util/log"
|
||||
import { WorkspaceTable } from "./workspace.sql"
|
||||
import { Config } from "./config"
|
||||
import { getAdaptor } from "./adaptors"
|
||||
import { parseSSE } from "./sse"
|
||||
|
||||
export namespace Workspace {
|
||||
export const Event = {
|
||||
Ready: BusEvent.define(
|
||||
"workspace.ready",
|
||||
z.object({
|
||||
name: z.string(),
|
||||
}),
|
||||
),
|
||||
Failed: BusEvent.define(
|
||||
"workspace.failed",
|
||||
z.object({
|
||||
message: z.string(),
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
export const Info = z
|
||||
.object({
|
||||
id: Identifier.schema("workspace"),
|
||||
branch: z.string().nullable(),
|
||||
projectID: z.string(),
|
||||
config: Config,
|
||||
})
|
||||
.meta({
|
||||
ref: "Workspace",
|
||||
})
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
function fromRow(row: typeof WorkspaceTable.$inferSelect): Info {
|
||||
return {
|
||||
id: row.id,
|
||||
branch: row.branch,
|
||||
projectID: row.project_id,
|
||||
config: row.config,
|
||||
}
|
||||
}
|
||||
|
||||
export const create = fn(
|
||||
z.object({
|
||||
id: Identifier.schema("workspace").optional(),
|
||||
projectID: Info.shape.projectID,
|
||||
branch: Info.shape.branch,
|
||||
config: Info.shape.config,
|
||||
}),
|
||||
async (input) => {
|
||||
const id = Identifier.ascending("workspace", input.id)
|
||||
|
||||
const { config, init } = await getAdaptor(input.config).create(input.config, input.branch)
|
||||
|
||||
const info: Info = {
|
||||
id,
|
||||
projectID: input.projectID,
|
||||
branch: input.branch,
|
||||
config,
|
||||
}
|
||||
|
||||
setTimeout(async () => {
|
||||
await init()
|
||||
|
||||
Database.use((db) => {
|
||||
db.insert(WorkspaceTable)
|
||||
.values({
|
||||
id: info.id,
|
||||
branch: info.branch,
|
||||
project_id: info.projectID,
|
||||
config: info.config,
|
||||
})
|
||||
.run()
|
||||
})
|
||||
|
||||
GlobalBus.emit("event", {
|
||||
directory: id,
|
||||
payload: {
|
||||
type: Event.Ready.type,
|
||||
properties: {},
|
||||
},
|
||||
})
|
||||
}, 0)
|
||||
|
||||
return info
|
||||
},
|
||||
)
|
||||
|
||||
export function list(project: Project.Info) {
|
||||
const rows = Database.use((db) =>
|
||||
db.select().from(WorkspaceTable).where(eq(WorkspaceTable.project_id, project.id)).all(),
|
||||
)
|
||||
return rows.map(fromRow).sort((a, b) => a.id.localeCompare(b.id))
|
||||
}
|
||||
|
||||
export const get = fn(Identifier.schema("workspace"), async (id) => {
|
||||
const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get())
|
||||
if (!row) return
|
||||
return fromRow(row)
|
||||
})
|
||||
|
||||
export const remove = fn(Identifier.schema("workspace"), async (id) => {
|
||||
const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get())
|
||||
if (row) {
|
||||
const info = fromRow(row)
|
||||
await getAdaptor(info.config).remove(info.config)
|
||||
Database.use((db) => db.delete(WorkspaceTable).where(eq(WorkspaceTable.id, id)).run())
|
||||
return info
|
||||
}
|
||||
})
|
||||
const log = Log.create({ service: "workspace-sync" })
|
||||
|
||||
async function workspaceEventLoop(space: Info, stop: AbortSignal) {
|
||||
while (!stop.aborted) {
|
||||
const res = await getAdaptor(space.config)
|
||||
.request(space.config, "GET", "/event", undefined, stop)
|
||||
.catch(() => undefined)
|
||||
if (!res || !res.ok || !res.body) {
|
||||
await Bun.sleep(1000)
|
||||
continue
|
||||
}
|
||||
await parseSSE(res.body, stop, (event) => {
|
||||
GlobalBus.emit("event", {
|
||||
directory: space.id,
|
||||
payload: event,
|
||||
})
|
||||
})
|
||||
// Wait 250ms and retry if SSE connection fails
|
||||
await Bun.sleep(250)
|
||||
}
|
||||
}
|
||||
|
||||
export function startSyncing(project: Project.Info) {
|
||||
const stop = new AbortController()
|
||||
const spaces = list(project).filter((space) => space.config.type !== "worktree")
|
||||
|
||||
spaces.forEach((space) => {
|
||||
void workspaceEventLoop(space, stop.signal).catch((error) => {
|
||||
log.warn("workspace sync listener failed", {
|
||||
workspaceID: space.id,
|
||||
error,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
async stop() {
|
||||
stop.abort()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,7 @@
|
||||
import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core"
|
||||
import { sqliteTable, text, integer, primaryKey, uniqueIndex } from "drizzle-orm/sqlite-core"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { Timestamps } from "@/storage/schema.sql"
|
||||
|
||||
export const AccountTable = sqliteTable("account", {
|
||||
id: text().primaryKey(),
|
||||
email: text().notNull(),
|
||||
url: text().notNull(),
|
||||
access_token: text().notNull(),
|
||||
refresh_token: text().notNull(),
|
||||
token_expiry: integer(),
|
||||
workspace_id: text(),
|
||||
...Timestamps,
|
||||
})
|
||||
|
||||
// LEGACY
|
||||
export const ControlAccountTable = sqliteTable(
|
||||
"control_account",
|
||||
{
|
||||
67
packages/opencode/src/control/index.ts
Normal file
67
packages/opencode/src/control/index.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { eq, and } from "drizzle-orm"
|
||||
import { Database } from "@/storage/db"
|
||||
import { ControlAccountTable } from "./control.sql"
|
||||
import z from "zod"
|
||||
|
||||
export * from "./control.sql"
|
||||
|
||||
export namespace Control {
|
||||
export const Account = z.object({
|
||||
email: z.string(),
|
||||
url: z.string(),
|
||||
})
|
||||
export type Account = z.infer<typeof Account>
|
||||
|
||||
function fromRow(row: (typeof ControlAccountTable)["$inferSelect"]): Account {
|
||||
return {
|
||||
email: row.email,
|
||||
url: row.url,
|
||||
}
|
||||
}
|
||||
|
||||
export function account(): Account | undefined {
|
||||
const row = Database.use((db) =>
|
||||
db.select().from(ControlAccountTable).where(eq(ControlAccountTable.active, true)).get(),
|
||||
)
|
||||
return row ? fromRow(row) : undefined
|
||||
}
|
||||
|
||||
export async function token(): Promise<string | undefined> {
|
||||
const row = Database.use((db) =>
|
||||
db.select().from(ControlAccountTable).where(eq(ControlAccountTable.active, true)).get(),
|
||||
)
|
||||
if (!row) return undefined
|
||||
if (row.token_expiry && row.token_expiry > Date.now()) return row.access_token
|
||||
|
||||
const res = await fetch(`${row.url}/oauth/token`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: row.refresh_token,
|
||||
}).toString(),
|
||||
})
|
||||
|
||||
if (!res.ok) return
|
||||
|
||||
const json = (await res.json()) as {
|
||||
access_token: string
|
||||
refresh_token?: string
|
||||
expires_in?: number
|
||||
}
|
||||
|
||||
Database.use((db) =>
|
||||
db
|
||||
.update(ControlAccountTable)
|
||||
.set({
|
||||
access_token: json.access_token,
|
||||
refresh_token: json.refresh_token ?? row.refresh_token,
|
||||
token_expiry: json.expires_in ? Date.now() + json.expires_in * 1000 : undefined,
|
||||
})
|
||||
.where(and(eq(ControlAccountTable.email, row.email), eq(ControlAccountTable.url, row.url)))
|
||||
.run(),
|
||||
)
|
||||
|
||||
return json.access_token
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ export namespace Identifier {
|
||||
part: "prt",
|
||||
pty: "pty",
|
||||
tool: "tool",
|
||||
workspace: "wrk",
|
||||
} as const
|
||||
|
||||
export function schema(prefix: keyof typeof prefixes) {
|
||||
|
||||
@@ -3,8 +3,7 @@ import { hideBin } from "yargs/helpers"
|
||||
import { RunCommand } from "./cli/cmd/run"
|
||||
import { GenerateCommand } from "./cli/cmd/generate"
|
||||
import { Log } from "./util/log"
|
||||
import { LoginCommand, LogoutCommand, SwitchCommand, WorkspacesCommand } from "./cli/cmd/account"
|
||||
import { ProvidersCommand } from "./cli/cmd/providers"
|
||||
import { AuthCommand } from "./cli/cmd/auth"
|
||||
import { AgentCommand } from "./cli/cmd/agent"
|
||||
import { UpgradeCommand } from "./cli/cmd/upgrade"
|
||||
import { UninstallCommand } from "./cli/cmd/uninstall"
|
||||
@@ -77,7 +76,6 @@ let cli = yargs(hideBin(process.argv))
|
||||
|
||||
process.env.AGENT = "1"
|
||||
process.env.OPENCODE = "1"
|
||||
process.env.OPENCODE_PID = String(process.pid)
|
||||
|
||||
Log.Default.info("opencode", {
|
||||
version: Installation.VERSION,
|
||||
@@ -130,11 +128,7 @@ let cli = yargs(hideBin(process.argv))
|
||||
.command(RunCommand)
|
||||
.command(GenerateCommand)
|
||||
.command(DebugCommand)
|
||||
.command(LoginCommand)
|
||||
.command(LogoutCommand)
|
||||
.command(SwitchCommand)
|
||||
.command(WorkspacesCommand)
|
||||
.command(ProvidersCommand)
|
||||
.command(AuthCommand)
|
||||
.command(AgentCommand)
|
||||
.command(UpgradeCommand)
|
||||
.command(UninstallCommand)
|
||||
|
||||
@@ -160,28 +160,6 @@ export namespace MCP {
|
||||
return typeof entry === "object" && entry !== null && "type" in entry
|
||||
}
|
||||
|
||||
async function descendants(pid: number): Promise<number[]> {
|
||||
if (process.platform === "win32") return []
|
||||
const pids: number[] = []
|
||||
const queue = [pid]
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift()!
|
||||
const proc = Bun.spawn(["pgrep", "-P", String(current)], { stdout: "pipe", stderr: "pipe" })
|
||||
const [code, out] = await Promise.all([proc.exited, new Response(proc.stdout).text()]).catch(
|
||||
() => [-1, ""] as const,
|
||||
)
|
||||
if (code !== 0) continue
|
||||
for (const tok of out.trim().split(/\s+/)) {
|
||||
const cpid = parseInt(tok, 10)
|
||||
if (!isNaN(cpid) && pids.indexOf(cpid) === -1) {
|
||||
pids.push(cpid)
|
||||
queue.push(cpid)
|
||||
}
|
||||
}
|
||||
}
|
||||
return pids
|
||||
}
|
||||
|
||||
const state = Instance.state(
|
||||
async () => {
|
||||
const cfg = await Config.get()
|
||||
@@ -218,21 +196,6 @@ export namespace MCP {
|
||||
}
|
||||
},
|
||||
async (state) => {
|
||||
// The MCP SDK only signals the direct child process on close.
|
||||
// Servers like chrome-devtools-mcp spawn grandchild processes
|
||||
// (e.g. Chrome) that the SDK never reaches, leaving them orphaned.
|
||||
// Kill the full descendant tree first so the server exits promptly
|
||||
// and no processes are left behind.
|
||||
for (const client of Object.values(state.clients)) {
|
||||
const pid = (client.transport as any)?.pid
|
||||
if (typeof pid !== "number") continue
|
||||
for (const dpid of await descendants(pid)) {
|
||||
try {
|
||||
process.kill(dpid, "SIGTERM")
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
Object.values(state.clients).map((client) =>
|
||||
client.close().catch((error) => {
|
||||
|
||||
@@ -76,18 +76,6 @@ export namespace ProviderError {
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// If responseBody is HTML (e.g. from a gateway or proxy error page),
|
||||
// provide a human-readable message instead of dumping raw markup
|
||||
if (/^\s*<!doctype|^\s*<html/i.test(e.responseBody)) {
|
||||
if (e.statusCode === 401) {
|
||||
return "Unauthorized: request was blocked by a gateway or proxy. Your authentication token may be missing or expired — try running `opencode auth login <your provider URL>` to re-authenticate."
|
||||
}
|
||||
if (e.statusCode === 403) {
|
||||
return "Forbidden: request was blocked by a gateway or proxy. You may not have permission to access this resource — check your account and provider settings."
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
return `${msg}: ${e.responseBody}`
|
||||
}).trim()
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import { Session } from "../../session"
|
||||
import { zodToJsonSchema } from "zod-to-json-schema"
|
||||
import { errors } from "../error"
|
||||
import { lazy } from "../../util/lazy"
|
||||
import { WorkspaceRoutes } from "./workspace"
|
||||
|
||||
export const ExperimentalRoutes = lazy(() =>
|
||||
new Hono()
|
||||
@@ -113,7 +112,6 @@ export const ExperimentalRoutes = lazy(() =>
|
||||
return c.json(worktree)
|
||||
},
|
||||
)
|
||||
.route("/workspace", WorkspaceRoutes())
|
||||
.get(
|
||||
"/worktree",
|
||||
describeRoute({
|
||||
|
||||
@@ -16,13 +16,11 @@ import { Log } from "../../util/log"
|
||||
import { PermissionNext } from "@/permission/next"
|
||||
import { errors } from "../error"
|
||||
import { lazy } from "../../util/lazy"
|
||||
import { SessionProxyMiddleware } from "../../control-plane/session-proxy-middleware"
|
||||
|
||||
const log = Log.create({ service: "server" })
|
||||
|
||||
export const SessionRoutes = lazy(() =>
|
||||
new Hono()
|
||||
.use(SessionProxyMiddleware)
|
||||
.get(
|
||||
"/",
|
||||
describeRoute({
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import { Hono } from "hono"
|
||||
import { describeRoute, resolver, validator } from "hono-openapi"
|
||||
import z from "zod"
|
||||
import { Workspace } from "../../control-plane/workspace"
|
||||
import { Instance } from "../../project/instance"
|
||||
import { errors } from "../error"
|
||||
import { lazy } from "../../util/lazy"
|
||||
|
||||
export const WorkspaceRoutes = lazy(() =>
|
||||
new Hono()
|
||||
.post(
|
||||
"/:id",
|
||||
describeRoute({
|
||||
summary: "Create workspace",
|
||||
description: "Create a workspace for the current project.",
|
||||
operationId: "experimental.workspace.create",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Workspace created",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Workspace.Info),
|
||||
},
|
||||
},
|
||||
},
|
||||
...errors(400),
|
||||
},
|
||||
}),
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
id: Workspace.Info.shape.id,
|
||||
}),
|
||||
),
|
||||
validator(
|
||||
"json",
|
||||
z.object({
|
||||
branch: Workspace.Info.shape.branch,
|
||||
config: Workspace.Info.shape.config,
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const { id } = c.req.valid("param")
|
||||
const body = c.req.valid("json")
|
||||
const workspace = await Workspace.create({
|
||||
id,
|
||||
projectID: Instance.project.id,
|
||||
branch: body.branch,
|
||||
config: body.config,
|
||||
})
|
||||
return c.json(workspace)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/",
|
||||
describeRoute({
|
||||
summary: "List workspaces",
|
||||
description: "List all workspaces.",
|
||||
operationId: "experimental.workspace.list",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Workspaces",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.array(Workspace.Info)),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
return c.json(Workspace.list(Instance.project))
|
||||
},
|
||||
)
|
||||
.delete(
|
||||
"/:id",
|
||||
describeRoute({
|
||||
summary: "Remove workspace",
|
||||
description: "Remove an existing workspace.",
|
||||
operationId: "experimental.workspace.remove",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Workspace removed",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Workspace.Info.optional()),
|
||||
},
|
||||
},
|
||||
},
|
||||
...errors(400),
|
||||
},
|
||||
}),
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
id: Workspace.Info.shape.id,
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const { id } = c.req.valid("param")
|
||||
return c.json(await Workspace.remove(id))
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -21,7 +21,6 @@ import { Auth } from "../auth"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { Command } from "../command"
|
||||
import { Global } from "../global"
|
||||
import { WorkspaceContext } from "../control-plane/workspace-context"
|
||||
import { ProjectRoutes } from "./routes/project"
|
||||
import { SessionRoutes } from "./routes/session"
|
||||
import { PtyRoutes } from "./routes/pty"
|
||||
@@ -195,7 +194,6 @@ export namespace Server {
|
||||
)
|
||||
.use(async (c, next) => {
|
||||
if (c.req.path === "/log") return next()
|
||||
const workspaceID = c.req.query("workspace") || c.req.header("x-opencode-workspace")
|
||||
const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
|
||||
const directory = (() => {
|
||||
try {
|
||||
@@ -204,17 +202,11 @@ export namespace Server {
|
||||
return raw
|
||||
}
|
||||
})()
|
||||
|
||||
return WorkspaceContext.provide({
|
||||
workspaceID,
|
||||
return Instance.provide({
|
||||
directory,
|
||||
init: InstanceBootstrap,
|
||||
async fn() {
|
||||
return Instance.provide({
|
||||
directory,
|
||||
init: InstanceBootstrap,
|
||||
async fn() {
|
||||
return next()
|
||||
},
|
||||
})
|
||||
return next()
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -231,15 +223,7 @@ export namespace Server {
|
||||
},
|
||||
}),
|
||||
)
|
||||
.use(
|
||||
validator(
|
||||
"query",
|
||||
z.object({
|
||||
directory: z.string().optional(),
|
||||
workspace: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
)
|
||||
.use(validator("query", z.object({ directory: z.string().optional() })))
|
||||
.route("/project", ProjectRoutes())
|
||||
.route("/pty", PtyRoutes())
|
||||
.route("/config", ConfigRoutes())
|
||||
|
||||
@@ -22,7 +22,6 @@ import { SessionPrompt } from "./prompt"
|
||||
import { fn } from "@/util/fn"
|
||||
import { Command } from "../command"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
import { WorkspaceContext } from "../control-plane/workspace-context"
|
||||
|
||||
import type { Provider } from "@/provider/provider"
|
||||
import { PermissionNext } from "@/permission/next"
|
||||
@@ -64,7 +63,6 @@ export namespace Session {
|
||||
id: row.id,
|
||||
slug: row.slug,
|
||||
projectID: row.project_id,
|
||||
workspaceID: row.workspace_id ?? undefined,
|
||||
directory: row.directory,
|
||||
parentID: row.parent_id ?? undefined,
|
||||
title: row.title,
|
||||
@@ -86,7 +84,6 @@ export namespace Session {
|
||||
return {
|
||||
id: info.id,
|
||||
project_id: info.projectID,
|
||||
workspace_id: info.workspaceID,
|
||||
parent_id: info.parentID,
|
||||
slug: info.slug,
|
||||
directory: info.directory,
|
||||
@@ -121,7 +118,6 @@ export namespace Session {
|
||||
id: Identifier.schema("session"),
|
||||
slug: z.string(),
|
||||
projectID: z.string(),
|
||||
workspaceID: z.string().optional(),
|
||||
directory: z.string(),
|
||||
parentID: Identifier.schema("session").optional(),
|
||||
summary: z
|
||||
@@ -301,7 +297,6 @@ export namespace Session {
|
||||
version: Installation.VERSION,
|
||||
projectID: Instance.project.id,
|
||||
directory: input.directory,
|
||||
workspaceID: WorkspaceContext.workspaceID,
|
||||
parentID: input.parentID,
|
||||
title: input.title ?? createDefaultTitle(!!input.parentID),
|
||||
permission: input.permission,
|
||||
@@ -532,7 +527,6 @@ export namespace Session {
|
||||
|
||||
export function* list(input?: {
|
||||
directory?: string
|
||||
workspaceID?: string
|
||||
roots?: boolean
|
||||
start?: number
|
||||
search?: string
|
||||
@@ -541,9 +535,6 @@ export namespace Session {
|
||||
const project = Instance.project
|
||||
const conditions = [eq(SessionTable.project_id, project.id)]
|
||||
|
||||
if (WorkspaceContext.workspaceID) {
|
||||
conditions.push(eq(SessionTable.workspace_id, WorkspaceContext.workspaceID))
|
||||
}
|
||||
if (input?.directory) {
|
||||
conditions.push(eq(SessionTable.directory, input.directory))
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ export const SessionTable = sqliteTable(
|
||||
project_id: text()
|
||||
.notNull()
|
||||
.references(() => ProjectTable.id, { onDelete: "cascade" }),
|
||||
workspace_id: text(),
|
||||
parent_id: text(),
|
||||
slug: text().notNull(),
|
||||
directory: text().notNull(),
|
||||
@@ -32,11 +31,7 @@ export const SessionTable = sqliteTable(
|
||||
time_compacting: integer(),
|
||||
time_archived: integer(),
|
||||
},
|
||||
(table) => [
|
||||
index("session_project_idx").on(table.project_id),
|
||||
index("session_workspace_idx").on(table.workspace_id),
|
||||
index("session_parent_idx").on(table.parent_id),
|
||||
],
|
||||
(table) => [index("session_project_idx").on(table.project_id), index("session_parent_idx").on(table.parent_id)],
|
||||
)
|
||||
|
||||
export const MessageTable = sqliteTable(
|
||||
|
||||
@@ -11,6 +11,7 @@ import { NamedError } from "@opencode-ai/util/error"
|
||||
import z from "zod"
|
||||
import path from "path"
|
||||
import { readFileSync, readdirSync, existsSync } from "fs"
|
||||
import * as schema from "./schema"
|
||||
|
||||
declare const OPENCODE_MIGRATIONS: { sql: string; timestamp: number }[] | undefined
|
||||
|
||||
@@ -25,8 +26,10 @@ const log = Log.create({ service: "db" })
|
||||
|
||||
export namespace Database {
|
||||
export const Path = path.join(Global.Path.data, "opencode.db")
|
||||
type Schema = typeof schema
|
||||
export type Transaction = SQLiteTransaction<"sync", void, Schema>
|
||||
|
||||
type Client = SQLiteBunDatabase
|
||||
type Client = SQLiteBunDatabase<Schema>
|
||||
|
||||
type Journal = { sql: string; timestamp: number }[]
|
||||
|
||||
@@ -79,7 +82,7 @@ export namespace Database {
|
||||
sqlite.run("PRAGMA foreign_keys = ON")
|
||||
sqlite.run("PRAGMA wal_checkpoint(PASSIVE)")
|
||||
|
||||
const db = drizzle({ client: sqlite })
|
||||
const db = drizzle({ client: sqlite, schema })
|
||||
|
||||
// Apply schema migrations
|
||||
const entries =
|
||||
@@ -105,7 +108,7 @@ export namespace Database {
|
||||
Client.reset()
|
||||
}
|
||||
|
||||
export type TxOrDb = SQLiteTransaction<"sync", void, any, any> | Client
|
||||
export type TxOrDb = Transaction | Client
|
||||
|
||||
const ctx = Context.create<{
|
||||
tx: TxOrDb
|
||||
|
||||
4
packages/opencode/src/storage/schema.ts
Normal file
4
packages/opencode/src/storage/schema.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { ControlAccountTable } from "../control/control.sql"
|
||||
export { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../session/session.sql"
|
||||
export { SessionShareTable } from "../share/share.sql"
|
||||
export { ProjectTable } from "../project/project.sql"
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test, expect, describe } from "bun:test"
|
||||
import { resolvePluginProviders } from "../../src/cli/cmd/providers"
|
||||
import { resolvePluginProviders } from "../../src/cli/cmd/auth"
|
||||
import type { Hooks } from "@opencode-ai/plugin"
|
||||
|
||||
function hookWithAuth(provider: string): Hooks {
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
import { afterEach, describe, expect, mock, test } from "bun:test"
|
||||
import { Identifier } from "../../src/id/id"
|
||||
import { Hono } from "hono"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { Project } from "../../src/project/project"
|
||||
import { WorkspaceTable } from "../../src/control-plane/workspace.sql"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Database } from "../../src/storage/db"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
|
||||
afterEach(async () => {
|
||||
mock.restore()
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
type State = {
|
||||
workspace?: "first" | "second"
|
||||
calls: Array<{ method: string; url: string; body?: string }>
|
||||
}
|
||||
|
||||
const remote = { type: "testing", name: "remote-a" } as unknown as typeof WorkspaceTable.$inferInsert.config
|
||||
|
||||
async function setup(state: State) {
|
||||
mock.module("../../src/control-plane/adaptors", () => ({
|
||||
getAdaptor: () => ({
|
||||
request: async (_config: unknown, method: string, url: string, data?: BodyInit) => {
|
||||
const body = data ? await new Response(data).text() : undefined
|
||||
state.calls.push({ method, url, body })
|
||||
return new Response("proxied", { status: 202 })
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
const { project } = await Project.fromDirectory(tmp.path)
|
||||
|
||||
const id1 = Identifier.descending("workspace")
|
||||
const id2 = Identifier.descending("workspace")
|
||||
|
||||
Database.use((db) =>
|
||||
db
|
||||
.insert(WorkspaceTable)
|
||||
.values([
|
||||
{
|
||||
id: id1,
|
||||
branch: "main",
|
||||
project_id: project.id,
|
||||
config: remote,
|
||||
},
|
||||
{
|
||||
id: id2,
|
||||
branch: "main",
|
||||
project_id: project.id,
|
||||
config: { type: "worktree", directory: tmp.path },
|
||||
},
|
||||
])
|
||||
.run(),
|
||||
)
|
||||
|
||||
const { SessionProxyMiddleware } = await import("../../src/control-plane/session-proxy-middleware")
|
||||
const app = new Hono().use(SessionProxyMiddleware)
|
||||
|
||||
return {
|
||||
id1,
|
||||
id2,
|
||||
app,
|
||||
async request(input: RequestInfo | URL, init?: RequestInit) {
|
||||
return Instance.provide({
|
||||
directory: state.workspace === "first" ? id1 : id2,
|
||||
fn: async () => app.request(input, init),
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe("control-plane/session-proxy-middleware", () => {
|
||||
test("forwards non-GET session requests for remote workspaces", async () => {
|
||||
const state: State = {
|
||||
workspace: "first",
|
||||
calls: [],
|
||||
}
|
||||
|
||||
const ctx = await setup(state)
|
||||
|
||||
ctx.app.post("/session/foo", (c) => c.text("local", 200))
|
||||
const response = await ctx.request("http://workspace.test/session/foo?x=1", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ hello: "world" }),
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
})
|
||||
|
||||
expect(response.status).toBe(202)
|
||||
expect(await response.text()).toBe("proxied")
|
||||
expect(state.calls).toEqual([
|
||||
{
|
||||
method: "POST",
|
||||
url: "/session/foo?x=1",
|
||||
body: '{"hello":"world"}',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("does not forward GET requests", async () => {
|
||||
const state: State = {
|
||||
workspace: "first",
|
||||
calls: [],
|
||||
}
|
||||
|
||||
const ctx = await setup(state)
|
||||
|
||||
ctx.app.get("/session/foo", (c) => c.text("local", 200))
|
||||
const response = await ctx.request("http://workspace.test/session/foo?x=1")
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(await response.text()).toBe("local")
|
||||
expect(state.calls).toEqual([])
|
||||
})
|
||||
|
||||
test("does not forward GET or POST requests for worktree workspaces", async () => {
|
||||
const state: State = {
|
||||
workspace: "second",
|
||||
calls: [],
|
||||
}
|
||||
|
||||
const ctx = await setup(state)
|
||||
|
||||
ctx.app.get("/session/foo", (c) => c.text("local-get", 200))
|
||||
ctx.app.post("/session/foo", (c) => c.text("local-post", 200))
|
||||
|
||||
const getResponse = await ctx.request("http://workspace.test/session/foo?x=1")
|
||||
const postResponse = await ctx.request("http://workspace.test/session/foo?x=1", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ hello: "world" }),
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
})
|
||||
|
||||
expect(getResponse.status).toBe(200)
|
||||
expect(await getResponse.text()).toBe("local-get")
|
||||
expect(postResponse.status).toBe(200)
|
||||
expect(await postResponse.text()).toBe("local-post")
|
||||
expect(state.calls).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -1,56 +0,0 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import { parseSSE } from "../../src/control-plane/sse"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
|
||||
afterEach(async () => {
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
function stream(chunks: string[]) {
|
||||
return new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
const encoder = new TextEncoder()
|
||||
chunks.forEach((chunk) => controller.enqueue(encoder.encode(chunk)))
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe("control-plane/sse", () => {
|
||||
test("parses JSON events with CRLF and multiline data blocks", async () => {
|
||||
const events: unknown[] = []
|
||||
const stop = new AbortController()
|
||||
|
||||
await parseSSE(
|
||||
stream([
|
||||
'data: {"type":"one","properties":{"ok":true}}\r\n\r\n',
|
||||
'data: {"type":"two",\r\ndata: "properties":{"n":2}}\r\n\r\n',
|
||||
]),
|
||||
stop.signal,
|
||||
(event) => events.push(event),
|
||||
)
|
||||
|
||||
expect(events).toEqual([
|
||||
{ type: "one", properties: { ok: true } },
|
||||
{ type: "two", properties: { n: 2 } },
|
||||
])
|
||||
})
|
||||
|
||||
test("falls back to sse.message for non-json payload", async () => {
|
||||
const events: unknown[] = []
|
||||
const stop = new AbortController()
|
||||
|
||||
await parseSSE(stream(["id: abc\nretry: 1500\ndata: hello world\n\n"]), stop.signal, (event) => events.push(event))
|
||||
|
||||
expect(events).toEqual([
|
||||
{
|
||||
type: "sse.message",
|
||||
properties: {
|
||||
data: "hello world",
|
||||
id: "abc",
|
||||
retry: 1500,
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -1,65 +0,0 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import { Log } from "../../src/util/log"
|
||||
import { WorkspaceServer } from "../../src/control-plane/workspace-server/server"
|
||||
import { parseSSE } from "../../src/control-plane/sse"
|
||||
import { GlobalBus } from "../../src/bus/global"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
|
||||
afterEach(async () => {
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
Log.init({ print: false })
|
||||
|
||||
describe("control-plane/workspace-server SSE", () => {
|
||||
test("streams GlobalBus events and parseSSE reads them", async () => {
|
||||
const app = WorkspaceServer.App()
|
||||
const stop = new AbortController()
|
||||
const seen: unknown[] = []
|
||||
|
||||
try {
|
||||
const response = await app.request("/event", {
|
||||
signal: stop.signal,
|
||||
})
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body).toBeDefined()
|
||||
|
||||
const done = new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error("timed out waiting for workspace.test event"))
|
||||
}, 3000)
|
||||
|
||||
void parseSSE(response.body!, stop.signal, (event) => {
|
||||
seen.push(event)
|
||||
const next = event as { type?: string }
|
||||
if (next.type === "server.connected") {
|
||||
GlobalBus.emit("event", {
|
||||
payload: {
|
||||
type: "workspace.test",
|
||||
properties: { ok: true },
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
if (next.type !== "workspace.test") return
|
||||
clearTimeout(timeout)
|
||||
resolve()
|
||||
}).catch((error) => {
|
||||
clearTimeout(timeout)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
|
||||
await done
|
||||
|
||||
expect(seen.some((event) => (event as { type?: string }).type === "server.connected")).toBe(true)
|
||||
expect(seen).toContainEqual({
|
||||
type: "workspace.test",
|
||||
properties: { ok: true },
|
||||
})
|
||||
} finally {
|
||||
stop.abort()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,97 +0,0 @@
|
||||
import { afterEach, describe, expect, mock, test } from "bun:test"
|
||||
import { Identifier } from "../../src/id/id"
|
||||
import { Log } from "../../src/util/log"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { Project } from "../../src/project/project"
|
||||
import { Database } from "../../src/storage/db"
|
||||
import { WorkspaceTable } from "../../src/control-plane/workspace.sql"
|
||||
import { GlobalBus } from "../../src/bus/global"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
|
||||
afterEach(async () => {
|
||||
mock.restore()
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
Log.init({ print: false })
|
||||
|
||||
const seen: string[] = []
|
||||
const remote = { type: "testing", name: "remote-a" } as unknown as typeof WorkspaceTable.$inferInsert.config
|
||||
|
||||
mock.module("../../src/control-plane/adaptors", () => ({
|
||||
getAdaptor: (config: { type: string }) => {
|
||||
seen.push(config.type)
|
||||
return {
|
||||
async create() {
|
||||
throw new Error("not used")
|
||||
},
|
||||
async remove() {},
|
||||
async request() {
|
||||
const body = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
const encoder = new TextEncoder()
|
||||
controller.enqueue(encoder.encode('data: {"type":"remote.ready","properties":{}}\n\n'))
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
return new Response(body, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "text/event-stream",
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
describe("control-plane/workspace.startSyncing", () => {
|
||||
test("syncs only remote workspaces and emits remote SSE events", async () => {
|
||||
const { Workspace } = await import("../../src/control-plane/workspace")
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
const { project } = await Project.fromDirectory(tmp.path)
|
||||
|
||||
const id1 = Identifier.descending("workspace")
|
||||
const id2 = Identifier.descending("workspace")
|
||||
|
||||
Database.use((db) =>
|
||||
db
|
||||
.insert(WorkspaceTable)
|
||||
.values([
|
||||
{
|
||||
id: id1,
|
||||
branch: "main",
|
||||
project_id: project.id,
|
||||
config: remote,
|
||||
},
|
||||
{
|
||||
id: id2,
|
||||
branch: "main",
|
||||
project_id: project.id,
|
||||
config: { type: "worktree", directory: tmp.path },
|
||||
},
|
||||
])
|
||||
.run(),
|
||||
)
|
||||
|
||||
const done = new Promise<void>((resolve) => {
|
||||
const listener = (event: { directory?: string; payload: { type: string } }) => {
|
||||
if (event.directory !== id1) return
|
||||
if (event.payload.type !== "remote.ready") return
|
||||
GlobalBus.off("event", listener)
|
||||
resolve()
|
||||
}
|
||||
GlobalBus.on("event", listener)
|
||||
})
|
||||
|
||||
const sync = Workspace.startSyncing(project)
|
||||
await Promise.race([
|
||||
done,
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error("timed out waiting for sync event")), 2000)),
|
||||
])
|
||||
|
||||
await sync.stop()
|
||||
expect(seen).toContain("testing")
|
||||
expect(seen).not.toContain("worktree")
|
||||
})
|
||||
})
|
||||
@@ -1,11 +0,0 @@
|
||||
import { rm } from "fs/promises"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Database } from "../../src/storage/db"
|
||||
|
||||
export async function resetDatabase() {
|
||||
await Instance.disposeAll().catch(() => undefined)
|
||||
Database.close()
|
||||
await rm(Database.Path, { force: true }).catch(() => undefined)
|
||||
await rm(`${Database.Path}-wal`, { force: true }).catch(() => undefined)
|
||||
await rm(`${Database.Path}-shm`, { force: true }).catch(() => undefined)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user