mirror of
https://github.com/anomalyco/opencode.git
synced 2026-03-02 20:53:56 +00:00
Compare commits
38 Commits
sidebar-fa
...
cli-auth-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a44f78c34a | ||
|
|
a5d727e7f9 | ||
|
|
7b5b665b4a | ||
|
|
b5515dd2f7 | ||
|
|
d16e5b98dc | ||
|
|
9dbf3a2042 | ||
|
|
d1938a472d | ||
|
|
c0483affa6 | ||
|
|
ae0f69e1fa | ||
|
|
90270c615d | ||
|
|
6b7e6bde4d | ||
|
|
b15fb21191 | ||
|
|
c8866e60ba | ||
|
|
f5eade1d2b | ||
|
|
438610aa64 | ||
|
|
c4c0b23bff | ||
|
|
38704acacd | ||
|
|
4d968ebd64 | ||
|
|
b88e8e0e0b | ||
|
|
3ee1653f40 | ||
|
|
fcd733e3d6 | ||
|
|
cec16dfe95 | ||
|
|
114eb42444 | ||
|
|
e1e18c7abd | ||
|
|
971bd30516 | ||
|
|
2a2082233d | ||
|
|
267d2c82de | ||
|
|
0b8c1f1f7d | ||
|
|
2eb1d4cb9a | ||
|
|
d2a8f44c22 | ||
|
|
1f1f36aac1 | ||
|
|
7f851da15e | ||
|
|
a3bdb974b3 | ||
|
|
46d678fce9 | ||
|
|
1f2348c1ef | ||
|
|
f347194e31 | ||
|
|
7ff2710ce3 | ||
|
|
c12ce2ffff |
1
.sisyphus/notepads/tr-polish/decisions.md
Normal file
1
.sisyphus/notepads/tr-polish/decisions.md
Normal file
@@ -0,0 +1 @@
|
||||
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
.sisyphus/notepads/tr-polish/learnings.md
Normal file
1
.sisyphus/notepads/tr-polish/learnings.md
Normal file
@@ -0,0 +1 @@
|
||||
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,3 +111,7 @@ 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,6 +27,7 @@
|
||||
<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.13",
|
||||
"@pierre/diffs": "1.1.0-beta.18",
|
||||
"@playwright/test": "1.51.0",
|
||||
"@solid-primitives/storage": "4.3.3",
|
||||
"@solidjs/meta": "0.29.4",
|
||||
@@ -1469,7 +1469,9 @@
|
||||
|
||||
"@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.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=="],
|
||||
"@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=="],
|
||||
|
||||
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-dZoLhWe4smBsOF7WczMySLXSAB1YRO1vfhiOCL1rBf0=",
|
||||
"aarch64-linux": "sha256-J7nIz1xuVZEHun5WRZkYRySz29B0A8g5g0RRxnIWTYU=",
|
||||
"aarch64-darwin": "sha256-R2PuhX+EjUBuLE8MF0G0fcUwNaU+5n6V6uVeK89ulzw=",
|
||||
"x86_64-darwin": "sha256-Bvzfz9TsTpYriZNLSLgpNcNb+BgtkgpjoWqdOtF2IBg="
|
||||
"x86_64-linux": "sha256-2XLuizbG90QDUQL+1M90XxfVZxjkIQ1cFYS46nnVO7g=",
|
||||
"aarch64-linux": "sha256-hlckiGAtbpAlwgcE7KgzKKRq9T2FEOSq3Q1MhuHfZ2c=",
|
||||
"aarch64-darwin": "sha256-V/8Kay+5bDb/BSVgBQhSMwzmRmkNGl3U0HFMVbVcMak=",
|
||||
"x86_64-darwin": "sha256-duLDF88Q/hXK5jwBy4dVxMSiTTS0R4obp9MlTuOF/Pw="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.13",
|
||||
"@pierre/diffs": "1.1.0-beta.18",
|
||||
"@solid-primitives/storage": "4.3.3",
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"diff": "8.0.2",
|
||||
|
||||
@@ -145,6 +145,7 @@ 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,7 +4,6 @@ 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"
|
||||
@@ -447,7 +446,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 as IconName} class="size-5 shrink-0 icon-strong-base" />
|
||||
<ProviderIcon id={props.provider} 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,7 +1,6 @@
|
||||
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"
|
||||
@@ -95,7 +94,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 as IconName} />
|
||||
<ProviderIcon data-slot="list-item-extra-icon" id={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>
|
||||
|
||||
@@ -5,18 +5,12 @@ 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()
|
||||
@@ -69,7 +63,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={icon(i.id)} />
|
||||
<ProviderIcon data-slot="list-item-extra-icon" id={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,7 +23,6 @@ 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"
|
||||
@@ -1398,7 +1397,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
>
|
||||
<Show when={local.model.current()?.provider?.id}>
|
||||
<ProviderIcon
|
||||
id={local.model.current()!.provider.id as IconName}
|
||||
id={local.model.current()!.provider.id}
|
||||
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
||||
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
||||
/>
|
||||
@@ -1428,7 +1427,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
>
|
||||
<Show when={local.model.current()?.provider?.id}>
|
||||
<ProviderIcon
|
||||
id={local.model.current()!.provider.id as IconName}
|
||||
id={local.model.current()!.provider.id}
|
||||
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
||||
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
||||
/>
|
||||
|
||||
@@ -4,7 +4,6 @@ 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"
|
||||
@@ -98,7 +97,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 as IconName} class="size-5 shrink-0 icon-strong-base" />
|
||||
<ProviderIcon id={group.category} 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,7 +3,6 @@ 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"
|
||||
@@ -33,11 +32,6 @@ 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()
|
||||
@@ -154,7 +148,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={icon(item.id)} class="size-5 shrink-0 icon-strong-base" />
|
||||
<ProviderIcon id={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>
|
||||
@@ -185,7 +179,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={icon(item.id)} class="size-5 shrink-0 icon-strong-base" />
|
||||
<ProviderIcon id={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">
|
||||
@@ -228,7 +222,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={icon("synthetic")} class="size-5 shrink-0 icon-strong-base" />
|
||||
<ProviderIcon id="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,12 +43,11 @@ 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)
|
||||
}
|
||||
@@ -105,7 +104,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
return globalSync.child(directory)
|
||||
}
|
||||
const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/")
|
||||
const messagePageSize = 400
|
||||
const messagePageSize = 200
|
||||
const inflight = new Map<string, Promise<void>>()
|
||||
const inflightDiff = new Map<string, Promise<void>>()
|
||||
const inflightTodo = new Map<string, Promise<void>>()
|
||||
@@ -122,20 +121,12 @@ 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)
|
||||
.filter((m) => !!m?.id)
|
||||
.sort((a, b) => cmp(a.id, b.id))
|
||||
const session = items.map((x) => x.info).sort((a, b) => cmp(a.id, b.id))
|
||||
const part = items.map((message) => ({ id: message.info.id, part: sortParts(message.parts) }))
|
||||
return {
|
||||
session,
|
||||
@@ -159,8 +150,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
.then((next) => {
|
||||
batch(() => {
|
||||
input.setStore("message", input.sessionID, reconcile(next.session, { key: "id" }))
|
||||
for (const message of next.part) {
|
||||
input.setStore("part", message.id, reconcile(message.part, { key: "id" }))
|
||||
for (const p of next.part) {
|
||||
input.setStore("part", p.id, p.part)
|
||||
}
|
||||
setMeta("limit", key, input.limit)
|
||||
setMeta("complete", key, next.complete)
|
||||
@@ -229,17 +220,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
const client = sdk.client
|
||||
const [store, setStore] = globalSync.child(directory)
|
||||
const key = keyFor(directory, sessionID)
|
||||
const hasSession = (() => {
|
||||
const match = Binary.search(store.session, sessionID, (s) => s.id)
|
||||
return match.found
|
||||
})()
|
||||
const hasSession = Binary.search(store.session, sessionID, (s) => s.id).found
|
||||
|
||||
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 limit = meta.limit[key] ?? messagePageSize
|
||||
|
||||
const sessionReq = hasSession
|
||||
? Promise.resolve()
|
||||
@@ -259,16 +242,13 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
)
|
||||
})
|
||||
|
||||
const messagesReq =
|
||||
hasMessages && hydrated
|
||||
? Promise.resolve()
|
||||
: loadMessages({
|
||||
directory,
|
||||
client,
|
||||
setStore,
|
||||
sessionID,
|
||||
limit,
|
||||
})
|
||||
const messagesReq = loadMessages({
|
||||
directory,
|
||||
client,
|
||||
setStore,
|
||||
sessionID,
|
||||
limit,
|
||||
})
|
||||
|
||||
return runInflight(inflight, key, () => Promise.all([sessionReq, messagesReq]).then(() => {}))
|
||||
},
|
||||
@@ -290,14 +270,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 (globalSync.data.session_todo[sessionID] === undefined) {
|
||||
if (cached === undefined) {
|
||||
globalSync.todo.set(sessionID, existing)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const cached = globalSync.data.session_todo[sessionID]
|
||||
if (cached !== undefined) {
|
||||
setStore("todo", sessionID, reconcile(cached, { key: "id" }))
|
||||
}
|
||||
@@ -324,11 +304,12 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
const key = keyFor(sdk.directory, sessionID)
|
||||
return meta.loading[key] ?? false
|
||||
},
|
||||
async loadMore(sessionID: string, count = messagePageSize) {
|
||||
async loadMore(sessionID: string, count?: number) {
|
||||
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
|
||||
|
||||
@@ -338,7 +319,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
client,
|
||||
setStore,
|
||||
sessionID,
|
||||
limit: currentLimit + count,
|
||||
limit: currentLimit + step,
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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 tarafından sunulan ücretsiz modeller",
|
||||
"dialog.model.unpaid.freeModels.title": "OpenCode'un sunduğu ü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": "Tekrar",
|
||||
"provider.custom.error.duplicate": "Çakışma",
|
||||
|
||||
"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": "İçin yeni bir bileşen oluştur...",
|
||||
"prompt.example.13": "... iç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": "İçin bir CLI komutu oluştur...",
|
||||
"prompt.example.24": "... iç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": "@bahsetmek için dosyayı bırakın",
|
||||
"prompt.dropzone.file.label": "Dosyayı referans göstermek için 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 alanı başlatma betiği",
|
||||
"dialog.project.edit.worktree.startup": "Çalışma ağacı 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.",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { onCleanup, Show, Match, Switch, createMemo, createEffect, on, onMount } from "solid-js"
|
||||
import { onCleanup, Show, Match, Switch, createMemo, createEffect, on, onMount, untrack } from "solid-js"
|
||||
import { createMediaQuery } from "@solid-primitives/media"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
import { useLocal } from "@/context/local"
|
||||
@@ -32,6 +32,215 @@ 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()
|
||||
@@ -178,7 +387,6 @@ 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,
|
||||
@@ -211,7 +419,6 @@ 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",
|
||||
@@ -220,20 +427,6 @@ 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
|
||||
@@ -302,13 +495,15 @@ export default function Page() {
|
||||
|
||||
const hasScrollGesture = () => Date.now() - ui.scrollGesture < scrollGestureWindowMs
|
||||
|
||||
createEffect(() => {
|
||||
sdk.directory
|
||||
const id = params.id
|
||||
if (!id) return
|
||||
void sync.session.sync(id)
|
||||
void sync.session.todo(id)
|
||||
})
|
||||
createEffect(
|
||||
on([() => sdk.directory, () => params.id] as const, ([, id]) => {
|
||||
if (!id) return
|
||||
untrack(() => {
|
||||
void sync.session.sync(id)
|
||||
void sync.session.todo(id)
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
@@ -894,88 +1089,16 @@ export default function Page() {
|
||||
},
|
||||
)
|
||||
|
||||
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 },
|
||||
),
|
||||
)
|
||||
const historyWindow = createSessionHistoryWindow({
|
||||
sessionID: () => params.id,
|
||||
messagesReady,
|
||||
visibleUserMessages,
|
||||
historyMore,
|
||||
historyLoading,
|
||||
loadMore: (sessionID) => sync.session.history.loadMore(sessionID),
|
||||
userScrolled: autoScroll.userScrolled,
|
||||
scroller: () => scroller,
|
||||
})
|
||||
|
||||
createResizeObserver(
|
||||
() => promptDock,
|
||||
@@ -1002,13 +1125,12 @@ export default function Page() {
|
||||
sessionID: () => params.id,
|
||||
messagesReady,
|
||||
visibleUserMessages,
|
||||
turnStart: () => store.turnStart,
|
||||
turnStart: historyWindow.turnStart,
|
||||
currentMessageId: () => store.messageId,
|
||||
pendingMessage: () => ui.pendingMessage,
|
||||
setPendingMessage: (value) => setUi("pendingMessage", value),
|
||||
setActiveMessage,
|
||||
setTurnStart: (value) => setStore("turnStart", value),
|
||||
scheduleTurnBackfill,
|
||||
setTurnStart: historyWindow.setTurnStart,
|
||||
autoScroll,
|
||||
scroller: () => scroller,
|
||||
anchor,
|
||||
@@ -1021,7 +1143,6 @@ export default function Page() {
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
cancelTurnBackfill()
|
||||
document.removeEventListener("keydown", handleKeyDown)
|
||||
scrollSpy.destroy()
|
||||
if (scrollStateFrame !== undefined) cancelAnimationFrame(scrollStateFrame)
|
||||
@@ -1076,6 +1197,7 @@ export default function Page() {
|
||||
hasScrollGesture={hasScrollGesture}
|
||||
isDesktop={isDesktop()}
|
||||
onScrollSpyScroll={scrollSpy.onScroll}
|
||||
onTurnBackfillScroll={historyWindow.onScrollerScroll}
|
||||
onAutoScrollInteraction={autoScroll.handleInteraction}
|
||||
centered={centered()}
|
||||
setContentRef={(el) => {
|
||||
@@ -1085,21 +1207,16 @@ export default function Page() {
|
||||
const root = scroller
|
||||
if (root) scheduleScrollState(root)
|
||||
}}
|
||||
turnStart={store.turnStart}
|
||||
onRenderEarlier={() => setStore("turnStart", 0)}
|
||||
turnStart={historyWindow.turnStart()}
|
||||
historyMore={historyMore()}
|
||||
historyLoading={historyLoading()}
|
||||
onLoadEarlier={() => {
|
||||
const id = params.id
|
||||
if (!id) return
|
||||
setStore("turnStart", 0)
|
||||
sync.session.history.loadMore(id)
|
||||
void historyWindow.loadAndReveal()
|
||||
}}
|
||||
renderedUserMessages={renderedUserMessages()}
|
||||
renderedUserMessages={historyWindow.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, type JSX } from "solid-js"
|
||||
import { For, createEffect, createMemo, on, onCleanup, Show, startTransition, 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,6 +81,103 @@ 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
|
||||
@@ -93,11 +190,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
|
||||
@@ -105,7 +202,6 @@ export function MessageTimeline(props: {
|
||||
anchor: (id: string) => string
|
||||
onRegisterMessage: (el: HTMLDivElement, id: string) => void
|
||||
onUnregisterMessage: (id: string) => void
|
||||
lastUserMessageID?: string
|
||||
}) {
|
||||
let touchGesture: number | undefined
|
||||
|
||||
@@ -127,6 +223,13 @@ 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: "",
|
||||
@@ -343,8 +446,10 @@ 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,
|
||||
"opacity-0 translate-y-2 scale-95 pointer-events-none": !props.scroll.overflow || props.scroll.bottom,
|
||||
"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(),
|
||||
}}
|
||||
>
|
||||
<button
|
||||
@@ -393,6 +498,7 @@ export function MessageTimeline(props: {
|
||||
}}
|
||||
onScroll={(e) => {
|
||||
props.onScheduleScrollState(e.currentTarget)
|
||||
props.onTurnBackfillScroll()
|
||||
if (!props.hasScrollGesture()) return
|
||||
props.onAutoScrollHandleScroll()
|
||||
props.onMarkScrollGesture(e.currentTarget)
|
||||
@@ -530,14 +636,7 @@ export function MessageTimeline(props: {
|
||||
"mt-0": !props.centered,
|
||||
}}
|
||||
>
|
||||
<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}>
|
||||
<Show when={props.turnStart > 0 || props.historyMore}>
|
||||
<div class="w-full flex justify-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -552,9 +651,10 @@ export function MessageTimeline(props: {
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
<For each={props.renderedUserMessages}>
|
||||
<For each={staging.messages()}>
|
||||
{(message) => {
|
||||
const comments = createMemo(() => messageComments(sync.data.part[message.id] ?? []))
|
||||
const commentCount = createMemo(() => comments().length)
|
||||
return (
|
||||
<div
|
||||
id={props.anchor(message.id)}
|
||||
@@ -567,8 +667,9 @@ 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={comments().length > 0}>
|
||||
<Show when={commentCount() > 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">
|
||||
@@ -601,7 +702,6 @@ 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,7 +19,6 @@ 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
|
||||
@@ -58,7 +57,6 @@ 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,9 +7,21 @@ import { Font } from "@opencode-ai/ui/font"
|
||||
import "@ibm/plex/css/ibm-plex.css"
|
||||
import "./app.css"
|
||||
import { LanguageProvider } from "~/context/language"
|
||||
import { I18nProvider } from "~/context/i18n"
|
||||
import { I18nProvider, useI18n } 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
|
||||
@@ -19,10 +31,7 @@ export default function App() {
|
||||
<LanguageProvider>
|
||||
<I18nProvider>
|
||||
<MetaProvider>
|
||||
<Title>opencode</Title>
|
||||
<Meta name="description" content="OpenCode - The open source coding agent." />
|
||||
<Favicon />
|
||||
<Font />
|
||||
<AppMeta />
|
||||
<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="OpenCode" width="189" height="34" />
|
||||
<img data-slot="logo dark" src={logoDark} alt="OpenCode" width="189" height="34" />
|
||||
<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" />
|
||||
</A>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ export const dict = {
|
||||
"nav.home": "الرئيسية",
|
||||
"nav.openMenu": "فتح القائمة",
|
||||
"nav.getStartedFree": "ابدأ مجانا",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "نسخ الشعار كـ SVG",
|
||||
"nav.context.copyWordmark": "نسخ اسم العلامة كـ SVG",
|
||||
@@ -42,9 +43,13 @@ 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": "إنشاء مساحة عمل جديدة",
|
||||
@@ -76,6 +81,8 @@ export const dict = {
|
||||
"error.reloadAmountMin": "يجب أن يكون مبلغ الشحن ${{amount}} على الأقل",
|
||||
"error.reloadTriggerMin": "يجب أن يكون حد الرصيد ${{amount}} على الأقل",
|
||||
|
||||
"app.meta.description": "OpenCode - وكيل البرمجة مفتوح المصدر.",
|
||||
|
||||
"home.title": "OpenCode | وكيل برمجة بالذكاء الاصطناعي مفتوح المصدر",
|
||||
|
||||
"temp.title": "opencode | وكيل برمجة بالذكاء الاصطناعي مبني للطرفية",
|
||||
@@ -91,6 +98,8 @@ 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": "تطبيق سطح المكتب متاح بنسخة تجريبية",
|
||||
@@ -238,6 +247,24 @@ 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": "الوصول إلى أفضل نماذج البرمجة في العالم",
|
||||
@@ -446,6 +473,7 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "يرجى تحديث طريقة الدفع والمحاولة مرة أخرى.",
|
||||
"workspace.reload.retrying": "جارٍ إعادة المحاولة...",
|
||||
"workspace.reload.retry": "أعد المحاولة",
|
||||
"workspace.reload.error.paymentFailed": "فشلت عملية الدفع.",
|
||||
|
||||
"workspace.payments.title": "سجل المدفوعات",
|
||||
"workspace.payments.subtitle": "معاملات الدفع الأخيرة.",
|
||||
@@ -563,6 +591,10 @@ 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":
|
||||
@@ -595,6 +627,7 @@ 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,6 +15,7 @@ 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",
|
||||
@@ -42,9 +43,13 @@ 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",
|
||||
@@ -76,6 +81,8 @@ 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",
|
||||
@@ -91,6 +98,8 @@ 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",
|
||||
@@ -242,6 +251,24 @@ 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",
|
||||
@@ -451,6 +478,7 @@ 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.",
|
||||
@@ -571,6 +599,10 @@ 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":
|
||||
@@ -603,6 +635,7 @@ 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,6 +15,7 @@ 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",
|
||||
@@ -42,9 +43,13 @@ 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",
|
||||
@@ -76,6 +81,8 @@ 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",
|
||||
@@ -91,6 +98,8 @@ 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",
|
||||
@@ -240,6 +249,24 @@ 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",
|
||||
@@ -449,6 +476,7 @@ 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.",
|
||||
@@ -567,6 +595,10 @@ 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":
|
||||
@@ -599,6 +631,7 @@ 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,6 +15,7 @@ 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",
|
||||
@@ -42,9 +43,13 @@ 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",
|
||||
@@ -76,6 +81,8 @@ 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",
|
||||
@@ -91,6 +98,8 @@ 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",
|
||||
@@ -242,6 +251,24 @@ 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",
|
||||
@@ -451,6 +478,7 @@ 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.",
|
||||
@@ -571,6 +599,10 @@ 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":
|
||||
@@ -603,6 +635,7 @@ 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,6 +11,7 @@ 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",
|
||||
@@ -38,9 +39,13 @@ 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",
|
||||
@@ -72,6 +77,8 @@ 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",
|
||||
@@ -87,6 +94,8 @@ 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",
|
||||
@@ -234,6 +243,24 @@ 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",
|
||||
@@ -443,6 +470,7 @@ 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.",
|
||||
@@ -561,6 +589,10 @@ 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":
|
||||
@@ -593,6 +625,7 @@ 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,6 +15,7 @@ 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",
|
||||
@@ -42,9 +43,13 @@ 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",
|
||||
@@ -76,6 +81,8 @@ 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",
|
||||
@@ -91,6 +98,8 @@ 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",
|
||||
@@ -243,6 +252,24 @@ 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",
|
||||
@@ -452,6 +479,7 @@ 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.",
|
||||
@@ -571,6 +599,10 @@ 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":
|
||||
@@ -603,6 +635,7 @@ 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,6 +3,7 @@ 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",
|
||||
@@ -15,6 +16,7 @@ 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",
|
||||
@@ -42,6 +44,8 @@ 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",
|
||||
|
||||
@@ -75,6 +79,7 @@ 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",
|
||||
|
||||
@@ -91,6 +96,8 @@ 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",
|
||||
@@ -246,6 +253,24 @@ 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",
|
||||
@@ -457,6 +482,7 @@ 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.",
|
||||
@@ -581,6 +607,10 @@ 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":
|
||||
@@ -640,4 +670,5 @@ 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,6 +15,7 @@ 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",
|
||||
@@ -42,9 +43,13 @@ 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",
|
||||
@@ -76,6 +81,8 @@ 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",
|
||||
@@ -91,6 +98,8 @@ 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",
|
||||
@@ -240,6 +249,24 @@ 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.",
|
||||
@@ -451,6 +478,7 @@ 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.",
|
||||
@@ -569,6 +597,10 @@ 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":
|
||||
@@ -601,6 +633,7 @@ 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,6 +15,7 @@ export const dict = {
|
||||
"nav.home": "ホーム",
|
||||
"nav.openMenu": "メニューを開く",
|
||||
"nav.getStartedFree": "無料ではじめる",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "ロゴをSVGでコピー",
|
||||
"nav.context.copyWordmark": "ワードマークをSVGでコピー",
|
||||
@@ -42,9 +43,13 @@ 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": "新しいワークスペースを作成",
|
||||
@@ -76,6 +81,8 @@ export const dict = {
|
||||
"error.reloadAmountMin": "リロード額は少なくとも ${{amount}} である必要があります",
|
||||
"error.reloadTriggerMin": "残高トリガーは少なくとも ${{amount}} である必要があります",
|
||||
|
||||
"app.meta.description": "OpenCode - オープンソースのコーディングエージェント。",
|
||||
|
||||
"home.title": "OpenCode | オープンソースのAIコーディングエージェント",
|
||||
|
||||
"temp.title": "OpenCode | ターミナル向けに構築されたAIコーディングエージェント",
|
||||
@@ -91,6 +98,8 @@ 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": "デスクトップアプリのベータ版が利用可能",
|
||||
@@ -239,6 +248,25 @@ 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": "世界最高峰のコーディングモデルすべてにアクセス",
|
||||
@@ -448,6 +476,7 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "支払い方法を更新して、もう一度お試しください。",
|
||||
"workspace.reload.retrying": "再試行中...",
|
||||
"workspace.reload.retry": "再試行",
|
||||
"workspace.reload.error.paymentFailed": "支払いに失敗しました。",
|
||||
|
||||
"workspace.payments.title": "支払い履歴",
|
||||
"workspace.payments.subtitle": "最近の支払い取引。",
|
||||
@@ -568,6 +597,10 @@ 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":
|
||||
@@ -600,6 +633,7 @@ 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,6 +15,7 @@ export const dict = {
|
||||
"nav.home": "홈",
|
||||
"nav.openMenu": "메뉴 열기",
|
||||
"nav.getStartedFree": "무료로 시작하기",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "로고를 SVG로 복사",
|
||||
"nav.context.copyWordmark": "워드마크를 SVG로 복사",
|
||||
@@ -42,9 +43,13 @@ 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": "새 워크스페이스 만들기",
|
||||
@@ -76,6 +81,8 @@ export const dict = {
|
||||
"error.reloadAmountMin": "충전 금액은 최소 ${{amount}}이어야 합니다",
|
||||
"error.reloadTriggerMin": "잔액 트리거는 최소 ${{amount}}이어야 합니다",
|
||||
|
||||
"app.meta.description": "OpenCode - 오픈 소스 코딩 에이전트.",
|
||||
|
||||
"home.title": "OpenCode | 오픈 소스 AI 코딩 에이전트",
|
||||
|
||||
"temp.title": "OpenCode | 터미널을 위해 만들어진 AI 코딩 에이전트",
|
||||
@@ -91,6 +98,8 @@ 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": "데스크톱 앱 베타 버전 출시",
|
||||
@@ -236,6 +245,24 @@ 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": "세계 최고의 코딩 모델에 액세스하세요",
|
||||
@@ -445,6 +472,7 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "결제 수단을 업데이트하고 다시 시도해 주세요.",
|
||||
"workspace.reload.retrying": "재시도 중...",
|
||||
"workspace.reload.retry": "재시도",
|
||||
"workspace.reload.error.paymentFailed": "결제에 실패했습니다.",
|
||||
|
||||
"workspace.payments.title": "결제 내역",
|
||||
"workspace.payments.subtitle": "최근 결제 거래 내역입니다.",
|
||||
@@ -562,6 +590,10 @@ 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":
|
||||
@@ -594,6 +626,7 @@ 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,6 +15,7 @@ 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",
|
||||
@@ -42,9 +43,13 @@ 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",
|
||||
@@ -76,6 +81,8 @@ 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",
|
||||
@@ -91,6 +98,8 @@ 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",
|
||||
@@ -240,6 +249,24 @@ 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",
|
||||
@@ -449,6 +476,7 @@ 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.",
|
||||
@@ -567,6 +595,10 @@ 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":
|
||||
@@ -599,6 +631,7 @@ 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,6 +14,7 @@ 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",
|
||||
@@ -41,9 +42,13 @@ 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",
|
||||
@@ -75,6 +80,8 @@ 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",
|
||||
@@ -90,6 +97,8 @@ 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",
|
||||
@@ -241,6 +250,24 @@ 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",
|
||||
@@ -450,6 +477,7 @@ 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.",
|
||||
@@ -570,6 +598,10 @@ 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":
|
||||
@@ -602,6 +634,7 @@ 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,6 +15,7 @@ export const dict = {
|
||||
"nav.home": "Главная",
|
||||
"nav.openMenu": "Открыть меню",
|
||||
"nav.getStartedFree": "Начать бесплатно",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "Скопировать логотип как SVG",
|
||||
"nav.context.copyWordmark": "Скопировать название как SVG",
|
||||
@@ -42,9 +43,13 @@ 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": "Создать рабочее пространство",
|
||||
@@ -76,6 +81,8 @@ export const dict = {
|
||||
"error.reloadAmountMin": "Сумма пополнения должна быть не менее ${{amount}}",
|
||||
"error.reloadTriggerMin": "Порог баланса должен быть не менее ${{amount}}",
|
||||
|
||||
"app.meta.description": "OpenCode - AI-агент с открытым кодом для программирования.",
|
||||
|
||||
"home.title": "OpenCode | AI-агент с открытым кодом для программирования",
|
||||
|
||||
"temp.title": "opencode | AI-агент для программирования в терминале",
|
||||
@@ -91,6 +98,8 @@ 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": "Доступно десктопное приложение (бета)",
|
||||
@@ -244,6 +253,24 @@ 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": "Доступ к лучшим моделям для кодинга в мире",
|
||||
@@ -455,6 +482,7 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "Пожалуйста, обновите способ оплаты и попробуйте снова.",
|
||||
"workspace.reload.retrying": "Повторная попытка...",
|
||||
"workspace.reload.retry": "Повторить",
|
||||
"workspace.reload.error.paymentFailed": "Ошибка оплаты.",
|
||||
|
||||
"workspace.payments.title": "История платежей",
|
||||
"workspace.payments.subtitle": "Недавние транзакции.",
|
||||
@@ -574,6 +602,10 @@ 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":
|
||||
@@ -606,6 +638,7 @@ 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,6 +15,7 @@ export const dict = {
|
||||
"nav.home": "หน้าหลัก",
|
||||
"nav.openMenu": "เปิดเมนู",
|
||||
"nav.getStartedFree": "เริ่มต้นฟรี",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "คัดลอกโลโก้เป็น SVG",
|
||||
"nav.context.copyWordmark": "คัดลอกตัวอักษรแบรนด์เป็น SVG",
|
||||
@@ -42,9 +43,13 @@ 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 ใหม่",
|
||||
@@ -76,6 +81,8 @@ export const dict = {
|
||||
"error.reloadAmountMin": "จำนวนเงินที่โหลดซ้ำต้องมีอย่างน้อย ${{amount}}",
|
||||
"error.reloadTriggerMin": "ยอดคงเหลือที่กระตุ้นต้องมีอย่างน้อย ${{amount}}",
|
||||
|
||||
"app.meta.description": "OpenCode - เอเจนต์เขียนโค้ดแบบโอเพนซอร์ส",
|
||||
|
||||
"home.title": "OpenCode | เอเจนต์เขียนโค้ดด้วย AI แบบโอเพนซอร์ส",
|
||||
|
||||
"temp.title": "OpenCode | เอเจนต์เขียนโค้ด AI ที่สร้างมาเพื่อเทอร์มินัล",
|
||||
@@ -91,6 +98,8 @@ 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": "แอปเดสก์ท็อปพร้อมใช้งานในเวอร์ชันเบต้า",
|
||||
@@ -239,6 +248,24 @@ 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": "เข้าถึงโมเดลเขียนโค้ดที่ดีที่สุดในโลก",
|
||||
@@ -448,6 +475,7 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "โปรดอัปเดตวิธีการชำระเงินของคุณแล้วลองอีกครั้ง",
|
||||
"workspace.reload.retrying": "กำลังลองอีกครั้ง...",
|
||||
"workspace.reload.retry": "ลองอีกครั้ง",
|
||||
"workspace.reload.error.paymentFailed": "การชำระเงินล้มเหลว",
|
||||
|
||||
"workspace.payments.title": "ประวัติการชำระเงิน",
|
||||
"workspace.payments.subtitle": "รายการธุรกรรมการชำระเงินล่าสุด",
|
||||
@@ -566,6 +594,10 @@ 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":
|
||||
@@ -598,6 +630,7 @@ 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,6 +15,7 @@ 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",
|
||||
@@ -42,9 +43,13 @@ 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",
|
||||
@@ -76,6 +81,8 @@ 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ı",
|
||||
@@ -91,6 +98,8 @@ 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",
|
||||
@@ -242,6 +251,24 @@ 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",
|
||||
@@ -451,6 +478,7 @@ 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.",
|
||||
@@ -571,6 +599,10 @@ 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":
|
||||
@@ -603,6 +635,7 @@ 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,6 +15,7 @@ export const dict = {
|
||||
"nav.home": "首页",
|
||||
"nav.openMenu": "打开菜单",
|
||||
"nav.getStartedFree": "免费开始",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "复制 Logo (SVG)",
|
||||
"nav.context.copyWordmark": "复制商标 (SVG)",
|
||||
@@ -42,9 +43,13 @@ 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": "新建工作区",
|
||||
@@ -76,6 +81,8 @@ export const dict = {
|
||||
"error.reloadAmountMin": "充值金额必须至少为 ${{amount}}",
|
||||
"error.reloadTriggerMin": "余额触发阈值必须至少为 ${{amount}}",
|
||||
|
||||
"app.meta.description": "OpenCode - 开源编程代理。",
|
||||
|
||||
"home.title": "OpenCode | 开源 AI 编程代理",
|
||||
|
||||
"temp.title": "OpenCode | 专为终端打造的 AI 编程代理",
|
||||
@@ -91,6 +98,8 @@ 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 版现已推出",
|
||||
@@ -229,6 +238,22 @@ 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": "访问全球顶尖编程模型",
|
||||
@@ -436,6 +461,7 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "请更新您的付款方式并重试。",
|
||||
"workspace.reload.retrying": "正在重试...",
|
||||
"workspace.reload.retry": "重试",
|
||||
"workspace.reload.error.paymentFailed": "支付失败。",
|
||||
|
||||
"workspace.payments.title": "支付历史",
|
||||
"workspace.payments.subtitle": "近期支付交易。",
|
||||
@@ -552,6 +578,10 @@ 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":
|
||||
@@ -584,6 +614,7 @@ 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,6 +15,7 @@ export const dict = {
|
||||
"nav.home": "首頁",
|
||||
"nav.openMenu": "開啟選單",
|
||||
"nav.getStartedFree": "免費開始使用",
|
||||
"nav.logoAlt": "OpenCode",
|
||||
|
||||
"nav.context.copyLogo": "複製標誌(SVG)",
|
||||
"nav.context.copyWordmark": "複製字標(SVG)",
|
||||
@@ -42,9 +43,13 @@ 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": "建立新工作區",
|
||||
@@ -76,6 +81,8 @@ export const dict = {
|
||||
"error.reloadAmountMin": "儲值金額必須至少為 ${{amount}}",
|
||||
"error.reloadTriggerMin": "餘額觸發門檻必須至少為 ${{amount}}",
|
||||
|
||||
"app.meta.description": "OpenCode - 開源編碼代理。",
|
||||
|
||||
"home.title": "OpenCode | 開源 AI 編碼代理",
|
||||
|
||||
"temp.title": "OpenCode | 專為終端打造的 AI 編碼代理",
|
||||
@@ -91,6 +98,8 @@ 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",
|
||||
@@ -229,6 +238,22 @@ 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": "存取全球最佳編碼模型",
|
||||
@@ -436,6 +461,7 @@ export const dict = {
|
||||
"workspace.reload.updatePaymentMethod": "請更新你的付款方式並重試。",
|
||||
"workspace.reload.retrying": "重試中...",
|
||||
"workspace.reload.retry": "重試",
|
||||
"workspace.reload.error.paymentFailed": "付款失敗。",
|
||||
|
||||
"workspace.payments.title": "付款紀錄",
|
||||
"workspace.payments.subtitle": "最近的付款交易。",
|
||||
@@ -551,6 +577,10 @@ 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":
|
||||
@@ -583,6 +613,7 @@ 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,6 +48,9 @@ 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="opencode logo light" />
|
||||
<img data-slot="logo dark" src={logoDark} alt="opencode logo dark" />
|
||||
<img data-slot="logo light" src={logoLight} alt={i18n.t("notFound.logoLightAlt")} />
|
||||
<img data-slot="logo dark" src={logoDark} alt={i18n.t("notFound.logoDarkAlt")} />
|
||||
</a>
|
||||
<h1 data-slot="title">{i18n.t("notFound.heading")}</h1>
|
||||
</section>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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
|
||||
@@ -9,18 +11,19 @@ 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: "All fields are required" }, { status: 400 })
|
||||
return Response.json({ error: dict["enterprise.form.error.allFieldsRequired"] }, { status: 400 })
|
||||
}
|
||||
|
||||
// Validate email format
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
if (!emailRegex.test(body.email)) {
|
||||
return Response.json({ error: "Invalid email format" }, { status: 400 })
|
||||
return Response.json({ error: dict["enterprise.form.error.invalidEmailFormat"] }, { status: 400 })
|
||||
}
|
||||
|
||||
// Create email content
|
||||
@@ -39,9 +42,9 @@ ${body.email}`.trim()
|
||||
replyTo: body.email,
|
||||
})
|
||||
|
||||
return Response.json({ success: true, message: "Form submitted successfully" }, { status: 200 })
|
||||
return Response.json({ success: true, message: dict["enterprise.form.success.submitted"] }, { status: 200 })
|
||||
} catch (error) {
|
||||
console.error("Error processing enterprise form:", error)
|
||||
return Response.json({ error: "Internal server error" }, { status: 500 })
|
||||
return Response.json({ error: dict["enterprise.form.error.internalServer"] }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,17 @@ 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("No code found")
|
||||
if (!code) throw new Error(dict["auth.callback.error.codeMissing"])
|
||||
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,6 +2,8 @@ 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
|
||||
@@ -10,10 +12,11 @@ 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: "All fields are required" }, { status: 400 })
|
||||
return Response.json({ error: dict["bench.submission.error.allFieldsRequired"] }, { status: 400 })
|
||||
}
|
||||
|
||||
await Database.use((tx) =>
|
||||
|
||||
@@ -33,6 +33,7 @@ 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)
|
||||
@@ -88,7 +89,7 @@ export default function Brand() {
|
||||
|
||||
<div data-component="brand-grid">
|
||||
<div>
|
||||
<img src={previewLogoLight} alt="OpenCode brand guidelines" />
|
||||
<img src={previewLogoLight} alt={alt} />
|
||||
<div data-component="actions">
|
||||
<button onClick={() => downloadFile(logoLightPng, "opencode-logo-light.png")}>
|
||||
PNG
|
||||
@@ -115,7 +116,7 @@ export default function Brand() {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<img src={previewLogoDark} alt="OpenCode brand guidelines" />
|
||||
<img src={previewLogoDark} alt={alt} />
|
||||
<div data-component="actions">
|
||||
<button onClick={() => downloadFile(logoDarkPng, "opencode-logo-dark.png")}>
|
||||
PNG
|
||||
@@ -142,7 +143,7 @@ export default function Brand() {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<img src={previewLogoLightSquare} alt="OpenCode brand guidelines" />
|
||||
<img src={previewLogoLightSquare} alt={alt} />
|
||||
<div data-component="actions">
|
||||
<button onClick={() => downloadFile(logoLightSquarePng, "opencode-logo-light-square.png")}>
|
||||
PNG
|
||||
@@ -169,7 +170,7 @@ export default function Brand() {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<img src={previewLogoDarkSquare} alt="OpenCode brand guidelines" />
|
||||
<img src={previewLogoDarkSquare} alt={alt} />
|
||||
<div data-component="actions">
|
||||
<button onClick={() => downloadFile(logoDarkSquarePng, "opencode-logo-dark-square.png")}>
|
||||
PNG
|
||||
@@ -196,7 +197,7 @@ export default function Brand() {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<img src={previewWordmarkLight} alt="OpenCode brand guidelines" />
|
||||
<img src={previewWordmarkLight} alt={alt} />
|
||||
<div data-component="actions">
|
||||
<button onClick={() => downloadFile(wordmarkLightPng, "opencode-wordmark-light.png")}>
|
||||
PNG
|
||||
@@ -223,7 +224,7 @@ export default function Brand() {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<img src={previewWordmarkDark} alt="OpenCode brand guidelines" />
|
||||
<img src={previewWordmarkDark} alt={alt} />
|
||||
<div data-component="actions">
|
||||
<button onClick={() => downloadFile(wordmarkDarkPng, "opencode-wordmark-dark.png")}>
|
||||
PNG
|
||||
@@ -250,7 +251,7 @@ export default function Brand() {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<img src={previewWordmarkSimpleLight} alt="OpenCode brand guidelines" />
|
||||
<img src={previewWordmarkSimpleLight} alt={alt} />
|
||||
<div data-component="actions">
|
||||
<button onClick={() => downloadFile(wordmarkSimpleLightPng, "opencode-wordmark-simple-light.png")}>
|
||||
PNG
|
||||
@@ -277,7 +278,7 @@ export default function Brand() {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<img src={previewWordmarkSimpleDark} alt="OpenCode brand guidelines" />
|
||||
<img src={previewWordmarkSimpleDark} alt={alt} />
|
||||
<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("Not Found", { status: 404 })
|
||||
if (!assetName) return new Response(null, { 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 ?? "Payment failed.",
|
||||
reloadError: errorMessage ?? "workspace.reload.error.paymentFailed",
|
||||
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="opencode logo light" />
|
||||
<img data-slot="logo dark" src={logoDark} alt="opencode logo dark" />
|
||||
<img data-slot="logo light" src={logoLight} alt={i18n.t("temp.logoLightAlt")} />
|
||||
<img data-slot="logo dark" src={logoDark} alt={i18n.t("temp.logoDarkAlt")} />
|
||||
<h1 data-slot="title">{i18n.t("temp.hero.title")}</h1>
|
||||
<div data-slot="login">
|
||||
<a href="/auth">{i18n.t("temp.zen")}</a>
|
||||
|
||||
@@ -12,6 +12,7 @@ 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"
|
||||
@@ -114,7 +115,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: "Workspace ID is required" }
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
const useBalance = form.get("useBalance")?.toString() === "true"
|
||||
|
||||
return json(
|
||||
|
||||
@@ -202,7 +202,8 @@ export function ReloadSection() {
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
})}
|
||||
. {i18n.t("workspace.reload.reason")} {billingInfo()?.reloadError?.replace(/\.$/, "")}.{" "}
|
||||
. {i18n.t("workspace.reload.reason")}{" "}
|
||||
{localizeError(i18n.t, billingInfo()?.reloadError ?? undefined).replace(/\.$/, "")}.{" "}
|
||||
{i18n.t("workspace.reload.updatePaymentMethod")}
|
||||
</p>
|
||||
<form action={reload} method="post" data-slot="create-form">
|
||||
|
||||
@@ -35,6 +35,8 @@ 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 = {
|
||||
@@ -43,6 +45,15 @@ 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: {
|
||||
@@ -60,6 +71,8 @@ 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
|
||||
@@ -86,7 +99,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.headers)
|
||||
const rateLimiter = createRateLimiter(modelInfo.rateLimit, ip, input.request)
|
||||
await rateLimiter?.check()
|
||||
const stickyTracker = createStickyTracker(modelInfo.stickyProvider, sessionId)
|
||||
const stickyProvider = await stickyTracker?.get()
|
||||
@@ -359,14 +372,20 @@ export async function handler(
|
||||
}
|
||||
|
||||
function validateModel(zenData: ZenData, reqModel: string) {
|
||||
if (!(reqModel in zenData.models)) throw new ModelError(`Model ${reqModel} not supported`)
|
||||
if (!(reqModel in zenData.models)) throw new ModelError(t("zen.api.error.modelNotSupported", { model: reqModel }))
|
||||
|
||||
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(`Model ${reqModel} not supported for format ${opts.format}`)
|
||||
if (!modelData)
|
||||
throw new ModelError(
|
||||
t("zen.api.error.modelFormatNotSupported", {
|
||||
model: reqModel,
|
||||
format: opts.format,
|
||||
}),
|
||||
)
|
||||
|
||||
logger.metric({ model: modelId })
|
||||
|
||||
@@ -418,8 +437,9 @@ export async function handler(
|
||||
return modelInfo.providers.find((provider) => provider.id === modelInfo.fallbackProvider)
|
||||
})()
|
||||
|
||||
if (!modelProvider) throw new ModelError("No provider available")
|
||||
if (!(modelProvider.id in zenData.providers)) throw new ModelError(`Provider ${modelProvider.id} not supported`)
|
||||
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 }))
|
||||
|
||||
return {
|
||||
...modelProvider,
|
||||
@@ -439,7 +459,7 @@ export async function handler(
|
||||
const apiKey = opts.parseApiKey(input.request.headers)
|
||||
if (!apiKey || apiKey === "public") {
|
||||
if (modelInfo.allowAnonymous) return
|
||||
throw new AuthError("Missing API key.")
|
||||
throw new AuthError(t("zen.api.error.missingApiKey"))
|
||||
}
|
||||
|
||||
const data = await Database.use((tx) =>
|
||||
@@ -520,13 +540,13 @@ export async function handler(
|
||||
.then((rows) => rows[0]),
|
||||
)
|
||||
|
||||
if (!data) throw new AuthError("Invalid API key.")
|
||||
if (!data) throw new AuthError(t("zen.api.error.invalidApiKey"))
|
||||
if (
|
||||
modelInfo.id.startsWith("alpha-") &&
|
||||
Resource.App.stage === "production" &&
|
||||
!ADMIN_WORKSPACES.includes(data.workspaceID)
|
||||
)
|
||||
throw new AuthError(`Model ${modelInfo.id} not supported`)
|
||||
throw new AuthError(t("zen.api.error.modelNotSupported", { model: modelInfo.id }))
|
||||
|
||||
logger.metric({
|
||||
api_key: data.apiKey,
|
||||
@@ -590,7 +610,9 @@ export async function handler(
|
||||
})
|
||||
if (result.status === "rate-limited")
|
||||
throw new SubscriptionUsageLimitError(
|
||||
`Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`,
|
||||
t("zen.api.error.subscriptionQuotaExceeded", {
|
||||
retryIn: formatRetryTime(result.resetInSec),
|
||||
}),
|
||||
result.resetInSec,
|
||||
)
|
||||
}
|
||||
@@ -606,7 +628,9 @@ export async function handler(
|
||||
})
|
||||
if (result.status === "rate-limited")
|
||||
throw new SubscriptionUsageLimitError(
|
||||
`Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`,
|
||||
t("zen.api.error.subscriptionQuotaExceeded", {
|
||||
retryIn: formatRetryTime(result.resetInSec),
|
||||
}),
|
||||
result.resetInSec,
|
||||
)
|
||||
}
|
||||
@@ -632,7 +656,7 @@ export async function handler(
|
||||
})
|
||||
if (result.status === "rate-limited")
|
||||
throw new SubscriptionUsageLimitError(
|
||||
`Subscription quota exceeded. You can continue using free models.`,
|
||||
t("zen.api.error.subscriptionQuotaExceededUseFreeModels"),
|
||||
result.resetInSec,
|
||||
)
|
||||
}
|
||||
@@ -647,7 +671,7 @@ export async function handler(
|
||||
})
|
||||
if (result.status === "rate-limited")
|
||||
throw new SubscriptionUsageLimitError(
|
||||
`Subscription quota exceeded. You can continue using free models.`,
|
||||
t("zen.api.error.subscriptionQuotaExceededUseFreeModels"),
|
||||
result.resetInSec,
|
||||
)
|
||||
}
|
||||
@@ -662,7 +686,7 @@ export async function handler(
|
||||
})
|
||||
if (result.status === "rate-limited")
|
||||
throw new SubscriptionUsageLimitError(
|
||||
`Subscription quota exceeded. You can continue using free models.`,
|
||||
t("zen.api.error.subscriptionQuotaExceededUseFreeModels"),
|
||||
result.resetInSec,
|
||||
)
|
||||
}
|
||||
@@ -675,14 +699,10 @@ export async function handler(
|
||||
|
||||
// Validate pay as you go billing
|
||||
const billing = authInfo.billing
|
||||
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 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 }))
|
||||
|
||||
const now = new Date()
|
||||
const currentYear = now.getUTCFullYear()
|
||||
@@ -696,7 +716,10 @@ export async function handler(
|
||||
currentMonth === billing.timeMonthlyUsageUpdated.getUTCMonth()
|
||||
)
|
||||
throw new MonthlyLimitError(
|
||||
`Your workspace has reached its monthly spending limit of $${billing.monthlyLimit}. Manage your limits here: https://opencode.ai/workspace/${authInfo.workspaceID}/billing`,
|
||||
t("zen.api.error.workspaceMonthlyLimitReached", {
|
||||
amount: billing.monthlyLimit,
|
||||
billingUrl,
|
||||
}),
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -708,7 +731,10 @@ export async function handler(
|
||||
currentMonth === authInfo.user.timeMonthlyUsageUpdated.getUTCMonth()
|
||||
)
|
||||
throw new UserLimitError(
|
||||
`You have reached your monthly spending limit of $${authInfo.user.monthlyLimit}. Manage your limits here: https://opencode.ai/workspace/${authInfo.workspaceID}/members`,
|
||||
t("zen.api.error.userMonthlyLimitReached", {
|
||||
amount: authInfo.user.monthlyLimit,
|
||||
membersUrl,
|
||||
}),
|
||||
)
|
||||
|
||||
return "balance"
|
||||
@@ -716,7 +742,7 @@ export async function handler(
|
||||
|
||||
function validateModelSettings(authInfo: AuthInfo) {
|
||||
if (!authInfo) return
|
||||
if (authInfo.isDisabled) throw new ModelError("Model is disabled")
|
||||
if (authInfo.isDisabled) throw new ModelError(t("zen.api.error.modelDisabled"))
|
||||
}
|
||||
|
||||
function updateProviderKey(authInfo: AuthInfo, providerInfo: ProviderInfo) {
|
||||
|
||||
@@ -3,11 +3,14 @@ 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, headers: Headers) {
|
||||
export function createRateLimiter(limit: ZenData.RateLimit | undefined, rawIp: string, request: Request) {
|
||||
if (!limit) return
|
||||
const dict = i18n(localeFromRequest(request))
|
||||
|
||||
const limitValue = limit.checkHeader && !headers.get(limit.checkHeader) ? limit.fallbackValue! : limit.value
|
||||
const limitValue = limit.checkHeader && !request.headers.get(limit.checkHeader) ? limit.fallbackValue! : limit.value
|
||||
|
||||
const ip = !rawIp.length ? "unknown" : rawIp
|
||||
const now = Date.now()
|
||||
@@ -36,7 +39,7 @@ export function createRateLimiter(limit: ZenData.RateLimit | undefined, rawIp: s
|
||||
logger.debug(`rate limit total: ${total}`)
|
||||
if (total >= limitValue)
|
||||
throw new FreeUsageLimitError(
|
||||
`Rate limit exceeded. Please try again later.`,
|
||||
dict["zen.api.error.rateLimitExceeded"],
|
||||
limit.period === "day" ? getRetryAfterDay(now) : getRetryAfterHour(rows, intervals, limitValue, now),
|
||||
)
|
||||
},
|
||||
|
||||
@@ -23,7 +23,6 @@ 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"
|
||||
|
||||
@@ -268,10 +267,9 @@ export default function () {
|
||||
</div>
|
||||
<div class="flex gap-4 items-center">
|
||||
<div class="flex gap-2 items-center">
|
||||
<ProviderIcon
|
||||
id={provider() as IconName}
|
||||
class="size-3.5 shrink-0 text-icon-strong-base"
|
||||
/>
|
||||
<Show when={provider()}>
|
||||
<ProviderIcon id={provider()!} class="size-3.5 shrink-0 text-icon-strong-base" />
|
||||
</Show>
|
||||
<div class="text-12-regular text-text-base">{model()?.name ?? modelID()}</div>
|
||||
</div>
|
||||
<div class="text-12-regular text-text-weaker">
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
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
|
||||
);
|
||||
@@ -0,0 +1,959 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `session` ADD `workspace_id` text;--> statement-breakpoint
|
||||
CREATE INDEX `session_workspace_idx` ON `session` (`workspace_id`);
|
||||
@@ -0,0 +1,983 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
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,7 +1,18 @@
|
||||
import { sqliteTable, text, integer, primaryKey, uniqueIndex } from "drizzle-orm/sqlite-core"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core"
|
||||
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",
|
||||
{
|
||||
247
packages/opencode/src/account/index.ts
Normal file
247
packages/opencode/src/account/index.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
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) }
|
||||
}
|
||||
}
|
||||
161
packages/opencode/src/cli/cmd/account.ts
Normal file
161
packages/opencode/src/cli/cmd/account.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
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"))
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
438
packages/opencode/src/cli/cmd/providers.ts
Normal file
438
packages/opencode/src/cli/cmd/providers.ts
Normal file
@@ -0,0 +1,438 @@
|
||||
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,6 +2,9 @@ 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",
|
||||
@@ -14,7 +17,15 @@ 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,6 +7,7 @@ import {
|
||||
For,
|
||||
Match,
|
||||
on,
|
||||
onMount,
|
||||
Show,
|
||||
Switch,
|
||||
useContext,
|
||||
@@ -1323,6 +1324,8 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
|
||||
return props.message.time.completed - user.time.created
|
||||
})
|
||||
|
||||
const keybind = useKeybind()
|
||||
|
||||
return (
|
||||
<>
|
||||
<For each={props.parts}>
|
||||
@@ -1340,6 +1343,14 @@ 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"]}
|
||||
@@ -1609,6 +1620,7 @@ function InlineTool(props: {
|
||||
iconColor?: RGBA
|
||||
complete: any
|
||||
pending: string
|
||||
spinner?: boolean
|
||||
children: JSX.Element
|
||||
part: ToolPart
|
||||
}) {
|
||||
@@ -1665,11 +1677,18 @@ function InlineTool(props: {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
<Show when={error() && !denied()}>
|
||||
<text fg={theme.error}>{error()}</text>
|
||||
</Show>
|
||||
@@ -1836,6 +1855,7 @@ 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 []
|
||||
@@ -1845,7 +1865,13 @@ function Read(props: ToolProps<typeof ReadTool>) {
|
||||
})
|
||||
return (
|
||||
<>
|
||||
<InlineTool icon="→" pending="Reading file..." complete={props.input.filePath} part={props.part}>
|
||||
<InlineTool
|
||||
icon="→"
|
||||
pending="Reading file..."
|
||||
complete={props.input.filePath}
|
||||
spinner={isRunning()}
|
||||
part={props.part}
|
||||
>
|
||||
Read {normalizePath(props.input.filePath!)} {input(props.input, ["filePath"])}
|
||||
</InlineTool>
|
||||
<For each={loaded()}>
|
||||
@@ -1921,62 +1947,60 @@ 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(() => {
|
||||
const sessionID = props.metadata.sessionId
|
||||
const msgs = sync.data.message[sessionID ?? ""] ?? []
|
||||
return msgs.flatMap((msg) =>
|
||||
return messages().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.status !== "pending"))
|
||||
const current = createMemo(() => tools().findLast((x) => (x.state as any).title))
|
||||
|
||||
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 (
|
||||
<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>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,59 +1,16 @@
|
||||
import { cmd } from "./cmd"
|
||||
import { withNetworkOptions, resolveNetworkOptions } from "../network"
|
||||
import { Installation } from "../../installation"
|
||||
import { WorkspaceServer } from "../../control-plane/workspace-server/server"
|
||||
|
||||
export const WorkspaceServeCommand = cmd({
|
||||
command: "workspace-serve",
|
||||
builder: (yargs) => withNetworkOptions(yargs),
|
||||
describe: "starts a remote workspace websocket server",
|
||||
describe: "starts a remote workspace event server",
|
||||
handler: async (args) => {
|
||||
const opts = await resolveNetworkOptions(args)
|
||||
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`)
|
||||
const server = WorkspaceServer.Listen(opts)
|
||||
console.log(`workspace event server listening on http://${server.hostname}:${server.port}/event`)
|
||||
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 { Control } from "@/control"
|
||||
import { Account } from "@/account"
|
||||
import { ConfigPaths } from "./paths"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
|
||||
@@ -107,10 +107,6 @@ export namespace Config {
|
||||
}
|
||||
}
|
||||
|
||||
const token = await Control.token()
|
||||
if (token) {
|
||||
}
|
||||
|
||||
// Global user config overrides remote config.
|
||||
result = mergeConfigConcatArrays(result, await global())
|
||||
|
||||
@@ -177,6 +173,15 @@ 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
|
||||
|
||||
10
packages/opencode/src/control-plane/adaptors/index.ts
Normal file
10
packages/opencode/src/control-plane/adaptors/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
7
packages/opencode/src/control-plane/adaptors/types.ts
Normal file
7
packages/opencode/src/control-plane/adaptors/types.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
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>
|
||||
}
|
||||
26
packages/opencode/src/control-plane/adaptors/worktree.ts
Normal file
26
packages/opencode/src/control-plane/adaptors/worktree.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
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")
|
||||
},
|
||||
}
|
||||
10
packages/opencode/src/control-plane/config.ts
Normal file
10
packages/opencode/src/control-plane/config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
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>
|
||||
@@ -0,0 +1,46 @@
|
||||
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()
|
||||
}
|
||||
66
packages/opencode/src/control-plane/sse.ts
Normal file
66
packages/opencode/src/control-plane/sse.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
23
packages/opencode/src/control-plane/workspace-context.ts
Normal file
23
packages/opencode/src/control-plane/workspace-context.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
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
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
12
packages/opencode/src/control-plane/workspace.sql.ts
Normal file
12
packages/opencode/src/control-plane/workspace.sql.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
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>(),
|
||||
})
|
||||
160
packages/opencode/src/control-plane/workspace.ts
Normal file
160
packages/opencode/src/control-plane/workspace.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
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,67 +0,0 @@
|
||||
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,6 +11,7 @@ export namespace Identifier {
|
||||
part: "prt",
|
||||
pty: "pty",
|
||||
tool: "tool",
|
||||
workspace: "wrk",
|
||||
} as const
|
||||
|
||||
export function schema(prefix: keyof typeof prefixes) {
|
||||
|
||||
@@ -3,7 +3,8 @@ import { hideBin } from "yargs/helpers"
|
||||
import { RunCommand } from "./cli/cmd/run"
|
||||
import { GenerateCommand } from "./cli/cmd/generate"
|
||||
import { Log } from "./util/log"
|
||||
import { AuthCommand } from "./cli/cmd/auth"
|
||||
import { LoginCommand, LogoutCommand, SwitchCommand, WorkspacesCommand } from "./cli/cmd/account"
|
||||
import { ProvidersCommand } from "./cli/cmd/providers"
|
||||
import { AgentCommand } from "./cli/cmd/agent"
|
||||
import { UpgradeCommand } from "./cli/cmd/upgrade"
|
||||
import { UninstallCommand } from "./cli/cmd/uninstall"
|
||||
@@ -76,6 +77,7 @@ 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,
|
||||
@@ -128,7 +130,11 @@ let cli = yargs(hideBin(process.argv))
|
||||
.command(RunCommand)
|
||||
.command(GenerateCommand)
|
||||
.command(DebugCommand)
|
||||
.command(AuthCommand)
|
||||
.command(LoginCommand)
|
||||
.command(LogoutCommand)
|
||||
.command(SwitchCommand)
|
||||
.command(WorkspacesCommand)
|
||||
.command(ProvidersCommand)
|
||||
.command(AgentCommand)
|
||||
.command(UpgradeCommand)
|
||||
.command(UninstallCommand)
|
||||
|
||||
@@ -160,6 +160,28 @@ 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()
|
||||
@@ -196,6 +218,21 @@ 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,6 +76,18 @@ 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,6 +10,7 @@ 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()
|
||||
@@ -112,6 +113,7 @@ export const ExperimentalRoutes = lazy(() =>
|
||||
return c.json(worktree)
|
||||
},
|
||||
)
|
||||
.route("/workspace", WorkspaceRoutes())
|
||||
.get(
|
||||
"/worktree",
|
||||
describeRoute({
|
||||
|
||||
@@ -16,11 +16,13 @@ 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({
|
||||
|
||||
104
packages/opencode/src/server/routes/workspace.ts
Normal file
104
packages/opencode/src/server/routes/workspace.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
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,6 +21,7 @@ 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"
|
||||
@@ -194,6 +195,7 @@ 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 {
|
||||
@@ -202,11 +204,17 @@ export namespace Server {
|
||||
return raw
|
||||
}
|
||||
})()
|
||||
return Instance.provide({
|
||||
directory,
|
||||
init: InstanceBootstrap,
|
||||
|
||||
return WorkspaceContext.provide({
|
||||
workspaceID,
|
||||
async fn() {
|
||||
return next()
|
||||
return Instance.provide({
|
||||
directory,
|
||||
init: InstanceBootstrap,
|
||||
async fn() {
|
||||
return next()
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -223,7 +231,15 @@ export namespace Server {
|
||||
},
|
||||
}),
|
||||
)
|
||||
.use(validator("query", z.object({ directory: z.string().optional() })))
|
||||
.use(
|
||||
validator(
|
||||
"query",
|
||||
z.object({
|
||||
directory: z.string().optional(),
|
||||
workspace: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
)
|
||||
.route("/project", ProjectRoutes())
|
||||
.route("/pty", PtyRoutes())
|
||||
.route("/config", ConfigRoutes())
|
||||
|
||||
@@ -22,6 +22,7 @@ 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"
|
||||
@@ -63,6 +64,7 @@ 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,
|
||||
@@ -84,6 +86,7 @@ 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,
|
||||
@@ -118,6 +121,7 @@ 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
|
||||
@@ -297,6 +301,7 @@ 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,
|
||||
@@ -527,6 +532,7 @@ export namespace Session {
|
||||
|
||||
export function* list(input?: {
|
||||
directory?: string
|
||||
workspaceID?: string
|
||||
roots?: boolean
|
||||
start?: number
|
||||
search?: string
|
||||
@@ -535,6 +541,9 @@ 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,6 +15,7 @@ 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(),
|
||||
@@ -31,7 +32,11 @@ export const SessionTable = sqliteTable(
|
||||
time_compacting: integer(),
|
||||
time_archived: integer(),
|
||||
},
|
||||
(table) => [index("session_project_idx").on(table.project_id), index("session_parent_idx").on(table.parent_id)],
|
||||
(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),
|
||||
],
|
||||
)
|
||||
|
||||
export const MessageTable = sqliteTable(
|
||||
|
||||
@@ -11,7 +11,6 @@ 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
|
||||
|
||||
@@ -26,10 +25,8 @@ 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<Schema>
|
||||
type Client = SQLiteBunDatabase
|
||||
|
||||
type Journal = { sql: string; timestamp: number }[]
|
||||
|
||||
@@ -82,7 +79,7 @@ export namespace Database {
|
||||
sqlite.run("PRAGMA foreign_keys = ON")
|
||||
sqlite.run("PRAGMA wal_checkpoint(PASSIVE)")
|
||||
|
||||
const db = drizzle({ client: sqlite, schema })
|
||||
const db = drizzle({ client: sqlite })
|
||||
|
||||
// Apply schema migrations
|
||||
const entries =
|
||||
@@ -108,7 +105,7 @@ export namespace Database {
|
||||
Client.reset()
|
||||
}
|
||||
|
||||
export type TxOrDb = Transaction | Client
|
||||
export type TxOrDb = SQLiteTransaction<"sync", void, any, any> | Client
|
||||
|
||||
const ctx = Context.create<{
|
||||
tx: TxOrDb
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
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/auth"
|
||||
import { resolvePluginProviders } from "../../src/cli/cmd/providers"
|
||||
import type { Hooks } from "@opencode-ai/plugin"
|
||||
|
||||
function hookWithAuth(provider: string): Hooks {
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
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([])
|
||||
})
|
||||
})
|
||||
56
packages/opencode/test/control-plane/sse.test.ts
Normal file
56
packages/opencode/test/control-plane/sse.test.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
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,
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,65 @@
|
||||
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()
|
||||
}
|
||||
})
|
||||
})
|
||||
97
packages/opencode/test/control-plane/workspace-sync.test.ts
Normal file
97
packages/opencode/test/control-plane/workspace-sync.test.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
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")
|
||||
})
|
||||
})
|
||||
11
packages/opencode/test/fixture/db.ts
Normal file
11
packages/opencode/test/fixture/db.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
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
@@ -808,6 +808,7 @@ export type Session = {
|
||||
id: string
|
||||
slug: string
|
||||
projectID: string
|
||||
workspaceID?: string
|
||||
directory: string
|
||||
parentID?: string
|
||||
summary?: {
|
||||
@@ -887,6 +888,35 @@ export type EventVcsBranchUpdated = {
|
||||
}
|
||||
}
|
||||
|
||||
export type EventWorktreeReady = {
|
||||
type: "worktree.ready"
|
||||
properties: {
|
||||
name: string
|
||||
branch: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventWorktreeFailed = {
|
||||
type: "worktree.failed"
|
||||
properties: {
|
||||
message: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventWorkspaceReady = {
|
||||
type: "workspace.ready"
|
||||
properties: {
|
||||
name: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventWorkspaceFailed = {
|
||||
type: "workspace.failed"
|
||||
properties: {
|
||||
message: string
|
||||
}
|
||||
}
|
||||
|
||||
export type Pty = {
|
||||
id: string
|
||||
title: string
|
||||
@@ -926,21 +956,6 @@ export type EventPtyDeleted = {
|
||||
}
|
||||
}
|
||||
|
||||
export type EventWorktreeReady = {
|
||||
type: "worktree.ready"
|
||||
properties: {
|
||||
name: string
|
||||
branch: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventWorktreeFailed = {
|
||||
type: "worktree.failed"
|
||||
properties: {
|
||||
message: string
|
||||
}
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| EventInstallationUpdated
|
||||
| EventInstallationUpdateAvailable
|
||||
@@ -979,12 +994,14 @@ export type Event =
|
||||
| EventSessionDiff
|
||||
| EventSessionError
|
||||
| EventVcsBranchUpdated
|
||||
| EventWorktreeReady
|
||||
| EventWorktreeFailed
|
||||
| EventWorkspaceReady
|
||||
| EventWorkspaceFailed
|
||||
| EventPtyCreated
|
||||
| EventPtyUpdated
|
||||
| EventPtyExited
|
||||
| EventPtyDeleted
|
||||
| EventWorktreeReady
|
||||
| EventWorktreeFailed
|
||||
|
||||
export type GlobalEvent = {
|
||||
directory: string
|
||||
@@ -1627,6 +1644,16 @@ export type WorktreeCreateInput = {
|
||||
startCommand?: string
|
||||
}
|
||||
|
||||
export type Workspace = {
|
||||
id: string
|
||||
branch: string | null
|
||||
projectID: string
|
||||
config: {
|
||||
directory: string
|
||||
type: "worktree"
|
||||
}
|
||||
}
|
||||
|
||||
export type WorktreeRemoveInput = {
|
||||
directory: string
|
||||
}
|
||||
@@ -1645,6 +1672,7 @@ export type GlobalSession = {
|
||||
id: string
|
||||
slug: string
|
||||
projectID: string
|
||||
workspaceID?: string
|
||||
directory: string
|
||||
parentID?: string
|
||||
summary?: {
|
||||
@@ -2025,6 +2053,7 @@ export type ProjectListData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/project"
|
||||
}
|
||||
@@ -2043,6 +2072,7 @@ export type ProjectCurrentData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/project/current"
|
||||
}
|
||||
@@ -2076,6 +2106,7 @@ export type ProjectUpdateData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/project/{projectID}"
|
||||
}
|
||||
@@ -2107,6 +2138,7 @@ export type PtyListData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/pty"
|
||||
}
|
||||
@@ -2133,6 +2165,7 @@ export type PtyCreateData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/pty"
|
||||
}
|
||||
@@ -2162,6 +2195,7 @@ export type PtyRemoveData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/pty/{ptyID}"
|
||||
}
|
||||
@@ -2191,6 +2225,7 @@ export type PtyGetData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/pty/{ptyID}"
|
||||
}
|
||||
@@ -2226,6 +2261,7 @@ export type PtyUpdateData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/pty/{ptyID}"
|
||||
}
|
||||
@@ -2255,6 +2291,7 @@ export type PtyConnectData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/pty/{ptyID}/connect"
|
||||
}
|
||||
@@ -2282,6 +2319,7 @@ export type ConfigGetData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/config"
|
||||
}
|
||||
@@ -2300,6 +2338,7 @@ export type ConfigUpdateData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/config"
|
||||
}
|
||||
@@ -2327,6 +2366,7 @@ export type ConfigProvidersData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/config/providers"
|
||||
}
|
||||
@@ -2350,6 +2390,7 @@ export type ToolIdsData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/experimental/tool/ids"
|
||||
}
|
||||
@@ -2377,6 +2418,7 @@ export type ToolListData = {
|
||||
path?: never
|
||||
query: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
provider: string
|
||||
model: string
|
||||
}
|
||||
@@ -2406,6 +2448,7 @@ export type WorktreeRemoveData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/experimental/worktree"
|
||||
}
|
||||
@@ -2433,6 +2476,7 @@ export type WorktreeListData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/experimental/worktree"
|
||||
}
|
||||
@@ -2451,6 +2495,7 @@ export type WorktreeCreateData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/experimental/worktree"
|
||||
}
|
||||
@@ -2473,11 +2518,102 @@ export type WorktreeCreateResponses = {
|
||||
|
||||
export type WorktreeCreateResponse = WorktreeCreateResponses[keyof WorktreeCreateResponses]
|
||||
|
||||
export type ExperimentalWorkspaceRemoveData = {
|
||||
body?: never
|
||||
path: {
|
||||
id: string
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/experimental/workspace/{id}"
|
||||
}
|
||||
|
||||
export type ExperimentalWorkspaceRemoveErrors = {
|
||||
/**
|
||||
* Bad request
|
||||
*/
|
||||
400: BadRequestError
|
||||
}
|
||||
|
||||
export type ExperimentalWorkspaceRemoveError =
|
||||
ExperimentalWorkspaceRemoveErrors[keyof ExperimentalWorkspaceRemoveErrors]
|
||||
|
||||
export type ExperimentalWorkspaceRemoveResponses = {
|
||||
/**
|
||||
* Workspace removed
|
||||
*/
|
||||
200: Workspace
|
||||
}
|
||||
|
||||
export type ExperimentalWorkspaceRemoveResponse =
|
||||
ExperimentalWorkspaceRemoveResponses[keyof ExperimentalWorkspaceRemoveResponses]
|
||||
|
||||
export type ExperimentalWorkspaceCreateData = {
|
||||
body?: {
|
||||
branch: string | null
|
||||
config: {
|
||||
directory: string
|
||||
type: "worktree"
|
||||
}
|
||||
}
|
||||
path: {
|
||||
id: string
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/experimental/workspace/{id}"
|
||||
}
|
||||
|
||||
export type ExperimentalWorkspaceCreateErrors = {
|
||||
/**
|
||||
* Bad request
|
||||
*/
|
||||
400: BadRequestError
|
||||
}
|
||||
|
||||
export type ExperimentalWorkspaceCreateError =
|
||||
ExperimentalWorkspaceCreateErrors[keyof ExperimentalWorkspaceCreateErrors]
|
||||
|
||||
export type ExperimentalWorkspaceCreateResponses = {
|
||||
/**
|
||||
* Workspace created
|
||||
*/
|
||||
200: Workspace
|
||||
}
|
||||
|
||||
export type ExperimentalWorkspaceCreateResponse =
|
||||
ExperimentalWorkspaceCreateResponses[keyof ExperimentalWorkspaceCreateResponses]
|
||||
|
||||
export type ExperimentalWorkspaceListData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/experimental/workspace"
|
||||
}
|
||||
|
||||
export type ExperimentalWorkspaceListResponses = {
|
||||
/**
|
||||
* Workspaces
|
||||
*/
|
||||
200: Array<Workspace>
|
||||
}
|
||||
|
||||
export type ExperimentalWorkspaceListResponse =
|
||||
ExperimentalWorkspaceListResponses[keyof ExperimentalWorkspaceListResponses]
|
||||
|
||||
export type WorktreeResetData = {
|
||||
body?: WorktreeResetInput
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/experimental/worktree/reset"
|
||||
}
|
||||
@@ -2508,6 +2644,7 @@ export type ExperimentalSessionListData = {
|
||||
* Filter sessions by project directory
|
||||
*/
|
||||
directory?: string
|
||||
workspace?: string
|
||||
/**
|
||||
* Only return root sessions (no parentID)
|
||||
*/
|
||||
@@ -2550,6 +2687,7 @@ export type ExperimentalResourceListData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/experimental/resource"
|
||||
}
|
||||
@@ -2574,6 +2712,7 @@ export type SessionListData = {
|
||||
* Filter sessions by project directory
|
||||
*/
|
||||
directory?: string
|
||||
workspace?: string
|
||||
/**
|
||||
* Only return root sessions (no parentID)
|
||||
*/
|
||||
@@ -2612,6 +2751,7 @@ export type SessionCreateData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session"
|
||||
}
|
||||
@@ -2639,6 +2779,7 @@ export type SessionStatusData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/status"
|
||||
}
|
||||
@@ -2670,6 +2811,7 @@ export type SessionDeleteData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}"
|
||||
}
|
||||
@@ -2703,6 +2845,7 @@ export type SessionGetData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}"
|
||||
}
|
||||
@@ -2741,6 +2884,7 @@ export type SessionUpdateData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}"
|
||||
}
|
||||
@@ -2774,6 +2918,7 @@ export type SessionChildrenData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/children"
|
||||
}
|
||||
@@ -2810,6 +2955,7 @@ export type SessionTodoData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/todo"
|
||||
}
|
||||
@@ -2850,6 +2996,7 @@ export type SessionInitData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/init"
|
||||
}
|
||||
@@ -2885,6 +3032,7 @@ export type SessionForkData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/fork"
|
||||
}
|
||||
@@ -2905,6 +3053,7 @@ export type SessionAbortData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/abort"
|
||||
}
|
||||
@@ -2938,6 +3087,7 @@ export type SessionUnshareData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/share"
|
||||
}
|
||||
@@ -2971,6 +3121,7 @@ export type SessionShareData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/share"
|
||||
}
|
||||
@@ -3004,6 +3155,7 @@ export type SessionDiffData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
messageID?: string
|
||||
}
|
||||
url: "/session/{sessionID}/diff"
|
||||
@@ -3032,6 +3184,7 @@ export type SessionSummarizeData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/summarize"
|
||||
}
|
||||
@@ -3068,6 +3221,7 @@ export type SessionMessagesData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
limit?: number
|
||||
}
|
||||
url: "/session/{sessionID}/message"
|
||||
@@ -3126,6 +3280,7 @@ export type SessionPromptData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/message"
|
||||
}
|
||||
@@ -3169,6 +3324,7 @@ export type SessionDeleteMessageData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/message/{messageID}"
|
||||
}
|
||||
@@ -3209,6 +3365,7 @@ export type SessionMessageData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/message/{messageID}"
|
||||
}
|
||||
@@ -3256,6 +3413,7 @@ export type PartDeleteData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/message/{messageID}/part/{partID}"
|
||||
}
|
||||
@@ -3300,6 +3458,7 @@ export type PartUpdateData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/message/{messageID}/part/{partID}"
|
||||
}
|
||||
@@ -3354,6 +3513,7 @@ export type SessionPromptAsyncData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/prompt_async"
|
||||
}
|
||||
@@ -3405,6 +3565,7 @@ export type SessionCommandData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/command"
|
||||
}
|
||||
@@ -3451,6 +3612,7 @@ export type SessionShellData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/shell"
|
||||
}
|
||||
@@ -3487,6 +3649,7 @@ export type SessionRevertData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/revert"
|
||||
}
|
||||
@@ -3520,6 +3683,7 @@ export type SessionUnrevertData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/unrevert"
|
||||
}
|
||||
@@ -3556,6 +3720,7 @@ export type PermissionRespondData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/session/{sessionID}/permissions/{permissionID}"
|
||||
}
|
||||
@@ -3592,6 +3757,7 @@ export type PermissionReplyData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/permission/{requestID}/reply"
|
||||
}
|
||||
@@ -3623,6 +3789,7 @@ export type PermissionListData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/permission"
|
||||
}
|
||||
@@ -3641,6 +3808,7 @@ export type QuestionListData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/question"
|
||||
}
|
||||
@@ -3666,6 +3834,7 @@ export type QuestionReplyData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/question/{requestID}/reply"
|
||||
}
|
||||
@@ -3699,6 +3868,7 @@ export type QuestionRejectData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/question/{requestID}/reject"
|
||||
}
|
||||
@@ -3730,6 +3900,7 @@ export type ProviderListData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/provider"
|
||||
}
|
||||
@@ -3815,6 +3986,7 @@ export type ProviderAuthData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/provider/auth"
|
||||
}
|
||||
@@ -3845,6 +4017,7 @@ export type ProviderOauthAuthorizeData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/provider/{providerID}/oauth/authorize"
|
||||
}
|
||||
@@ -3886,6 +4059,7 @@ export type ProviderOauthCallbackData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/provider/{providerID}/oauth/callback"
|
||||
}
|
||||
@@ -3913,6 +4087,7 @@ export type FindTextData = {
|
||||
path?: never
|
||||
query: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
pattern: string
|
||||
}
|
||||
url: "/find"
|
||||
@@ -3948,6 +4123,7 @@ export type FindFilesData = {
|
||||
path?: never
|
||||
query: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
query: string
|
||||
dirs?: "true" | "false"
|
||||
type?: "file" | "directory"
|
||||
@@ -3970,6 +4146,7 @@ export type FindSymbolsData = {
|
||||
path?: never
|
||||
query: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
query: string
|
||||
}
|
||||
url: "/find/symbol"
|
||||
@@ -3989,6 +4166,7 @@ export type FileListData = {
|
||||
path?: never
|
||||
query: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
path: string
|
||||
}
|
||||
url: "/file"
|
||||
@@ -4008,6 +4186,7 @@ export type FileReadData = {
|
||||
path?: never
|
||||
query: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
path: string
|
||||
}
|
||||
url: "/file/content"
|
||||
@@ -4027,6 +4206,7 @@ export type FileStatusData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/file/status"
|
||||
}
|
||||
@@ -4045,6 +4225,7 @@ export type McpStatusData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/mcp"
|
||||
}
|
||||
@@ -4068,6 +4249,7 @@ export type McpAddData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/mcp"
|
||||
}
|
||||
@@ -4099,6 +4281,7 @@ export type McpAuthRemoveData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/mcp/{name}/auth"
|
||||
}
|
||||
@@ -4130,6 +4313,7 @@ export type McpAuthStartData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/mcp/{name}/auth"
|
||||
}
|
||||
@@ -4173,6 +4357,7 @@ export type McpAuthCallbackData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/mcp/{name}/auth/callback"
|
||||
}
|
||||
@@ -4206,6 +4391,7 @@ export type McpAuthAuthenticateData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/mcp/{name}/auth/authenticate"
|
||||
}
|
||||
@@ -4239,6 +4425,7 @@ export type McpConnectData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/mcp/{name}/connect"
|
||||
}
|
||||
@@ -4259,6 +4446,7 @@ export type McpDisconnectData = {
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/mcp/{name}/disconnect"
|
||||
}
|
||||
@@ -4279,6 +4467,7 @@ export type TuiAppendPromptData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/tui/append-prompt"
|
||||
}
|
||||
@@ -4306,6 +4495,7 @@ export type TuiOpenHelpData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/tui/open-help"
|
||||
}
|
||||
@@ -4324,6 +4514,7 @@ export type TuiOpenSessionsData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/tui/open-sessions"
|
||||
}
|
||||
@@ -4342,6 +4533,7 @@ export type TuiOpenThemesData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/tui/open-themes"
|
||||
}
|
||||
@@ -4360,6 +4552,7 @@ export type TuiOpenModelsData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/tui/open-models"
|
||||
}
|
||||
@@ -4378,6 +4571,7 @@ export type TuiSubmitPromptData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/tui/submit-prompt"
|
||||
}
|
||||
@@ -4396,6 +4590,7 @@ export type TuiClearPromptData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/tui/clear-prompt"
|
||||
}
|
||||
@@ -4416,6 +4611,7 @@ export type TuiExecuteCommandData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/tui/execute-command"
|
||||
}
|
||||
@@ -4451,6 +4647,7 @@ export type TuiShowToastData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/tui/show-toast"
|
||||
}
|
||||
@@ -4469,6 +4666,7 @@ export type TuiPublishData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/tui/publish"
|
||||
}
|
||||
@@ -4501,6 +4699,7 @@ export type TuiSelectSessionData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/tui/select-session"
|
||||
}
|
||||
@@ -4532,6 +4731,7 @@ export type TuiControlNextData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/tui/control/next"
|
||||
}
|
||||
@@ -4553,6 +4753,7 @@ export type TuiControlResponseData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/tui/control/response"
|
||||
}
|
||||
@@ -4571,6 +4772,7 @@ export type InstanceDisposeData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/instance/dispose"
|
||||
}
|
||||
@@ -4589,6 +4791,7 @@ export type PathGetData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/path"
|
||||
}
|
||||
@@ -4607,6 +4810,7 @@ export type VcsGetData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/vcs"
|
||||
}
|
||||
@@ -4625,6 +4829,7 @@ export type CommandListData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/command"
|
||||
}
|
||||
@@ -4662,6 +4867,7 @@ export type AppLogData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/log"
|
||||
}
|
||||
@@ -4689,6 +4895,7 @@ export type AppAgentsData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/agent"
|
||||
}
|
||||
@@ -4707,6 +4914,7 @@ export type AppSkillsData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/skill"
|
||||
}
|
||||
@@ -4730,6 +4938,7 @@ export type LspStatusData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/lsp"
|
||||
}
|
||||
@@ -4748,6 +4957,7 @@ export type FormatterStatusData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/formatter"
|
||||
}
|
||||
@@ -4766,6 +4976,7 @@ export type EventSubscribeData = {
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
}
|
||||
url: "/event"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -225,6 +225,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="compaction-part"] {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
[data-slot="compaction-part-divider"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[data-slot="compaction-part-line"] {
|
||||
flex: 1 1 auto;
|
||||
height: 1px;
|
||||
background: var(--border-weak-base);
|
||||
}
|
||||
|
||||
[data-slot="compaction-part-label"] {
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="reasoning-part"] {
|
||||
width: 100%;
|
||||
color: var(--text-base);
|
||||
@@ -649,6 +676,7 @@
|
||||
[data-component="reasoning-part"],
|
||||
[data-component="tool-error"],
|
||||
[data-component="tool-output"],
|
||||
[data-component="bash-output"],
|
||||
[data-component="edit-content"],
|
||||
[data-component="write-content"],
|
||||
[data-component="todos"],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user