mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-10 02:44:21 +00:00
Compare commits
124 Commits
snapshot-v
...
sqlite2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1de66812bf | ||
|
|
832902c8e3 | ||
|
|
3d6fb29f0c | ||
|
|
9824370f82 | ||
|
|
371e106faa | ||
|
|
19809e7680 | ||
|
|
389afef336 | ||
|
|
274bb948e7 | ||
|
|
d9b4535d64 | ||
|
|
3dc720ff9c | ||
|
|
56b340b5d5 | ||
|
|
ba740eaefd | ||
|
|
39c5da4405 | ||
|
|
83708c295c | ||
|
|
a84bdd7cd7 | ||
|
|
110f6804fb | ||
|
|
e5ec2f9991 | ||
|
|
7bca3fbf18 | ||
|
|
d578f80f00 | ||
|
|
dc53086c1e | ||
|
|
f74c0339cc | ||
|
|
fb94b4f8e8 | ||
|
|
8ad4768ecd | ||
|
|
24fd8c166d | ||
|
|
a7c5d5ac4c | ||
|
|
5be1202eea | ||
|
|
373b2270e7 | ||
|
|
94d0c9940a | ||
|
|
05355a6b5c | ||
|
|
7ff51183ce | ||
|
|
bda0cbdec7 | ||
|
|
acc53d9f61 | ||
|
|
30f0d3b394 | ||
|
|
03f3029dc6 | ||
|
|
aed7bb8c09 | ||
|
|
dd2d232a9d | ||
|
|
993ac55e39 | ||
|
|
93a11ddedf | ||
|
|
94feb811ca | ||
|
|
b0ceec9b19 | ||
|
|
40b111d92c | ||
|
|
520110e864 | ||
|
|
d4a68b0f4e | ||
|
|
019cfd4a52 | ||
|
|
687210a55d | ||
|
|
b12eab782f | ||
|
|
99ea1351ce | ||
|
|
d40dffb854 | ||
|
|
0cd52f830c | ||
|
|
62f38087b8 | ||
|
|
79879b43ce | ||
|
|
a598ecac1f | ||
|
|
9ac54adbb2 | ||
|
|
6490fb0148 | ||
|
|
43811b62d2 | ||
|
|
de0f4ef80b | ||
|
|
9a7f54f21a | ||
|
|
27c8a08144 | ||
|
|
80c1c59ed3 | ||
|
|
7c6b8d7a8a | ||
|
|
5952891b1e | ||
|
|
d7c8a3f50d | ||
|
|
ce353819e8 | ||
|
|
2dae94e5a3 | ||
|
|
c6adc19e41 | ||
|
|
ce56166510 | ||
|
|
5911e4c06a | ||
|
|
42fb840f22 | ||
|
|
4dcfdf6572 | ||
|
|
25f3d6d5a9 | ||
|
|
e19a9e9614 | ||
|
|
fcc903489b | ||
|
|
949e69a9bf | ||
|
|
8c30f551e2 | ||
|
|
cb721497c1 | ||
|
|
4ec6293054 | ||
|
|
b7a323355c | ||
|
|
d4f053042c | ||
|
|
5f552534c7 | ||
|
|
ad5b790bb3 | ||
|
|
ed87341c4f | ||
|
|
794ecab028 | ||
|
|
eeb235724b | ||
|
|
61084e7f6f | ||
|
|
200aef2eb3 | ||
|
|
f6e375a555 | ||
|
|
db908deee5 | ||
|
|
7b72cc3a48 | ||
|
|
b8cbfd48ec | ||
|
|
498cbb2c26 | ||
|
|
d6fbd255b6 | ||
|
|
2de1c82bf7 | ||
|
|
34ebb3d051 | ||
|
|
9c3e3c1ab5 | ||
|
|
3ea499f04e | ||
|
|
ab13c1d1c4 | ||
|
|
53b610c331 | ||
|
|
e3519356f2 | ||
|
|
2619acc0ff | ||
|
|
1bc45dc266 | ||
|
|
2e8feb1c78 | ||
|
|
00e60899cc | ||
|
|
30a918e9d4 | ||
|
|
ac16068140 | ||
|
|
19a41ab297 | ||
|
|
cd174d8cba | ||
|
|
246e901e42 | ||
|
|
0ccef1b31f | ||
|
|
7706f5b6a8 | ||
|
|
63e38555c9 | ||
|
|
f40685ab13 | ||
|
|
a48a5a3462 | ||
|
|
5e1639de2b | ||
|
|
2b05833c32 | ||
|
|
acdcf7fa88 | ||
|
|
bf0754caeb | ||
|
|
4d50a32979 | ||
|
|
57edb0ddc5 | ||
|
|
a614b78c6d | ||
|
|
b9f5a34247 | ||
|
|
81b47a44e2 | ||
|
|
0c1c07467e | ||
|
|
105688bf90 | ||
|
|
1e7b4768b1 |
82
.github/workflows/docs-locale-sync.yml
vendored
Normal file
82
.github/workflows/docs-locale-sync.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
name: docs-locale-sync
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- packages/web/src/content/docs/*.mdx
|
||||
|
||||
jobs:
|
||||
sync-locales:
|
||||
if: github.actor != 'opencode-agent[bot]'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Setup git committer
|
||||
id: committer
|
||||
uses: ./.github/actions/setup-git-committer
|
||||
with:
|
||||
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
|
||||
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
|
||||
|
||||
- name: Compute changed English docs
|
||||
id: changes
|
||||
run: |
|
||||
FILES=$(git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" -- 'packages/web/src/content/docs/*.mdx' || true)
|
||||
if [ -z "$FILES" ]; then
|
||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No English docs changed in push range"
|
||||
exit 0
|
||||
fi
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
{
|
||||
echo "files<<EOF"
|
||||
echo "$FILES"
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Sync locale docs with OpenCode
|
||||
if: steps.changes.outputs.has_changes == 'true'
|
||||
uses: sst/opencode/github@latest
|
||||
env:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
with:
|
||||
model: opencode/gpt-5.2
|
||||
agent: docs
|
||||
prompt: |
|
||||
Update localized docs to match the latest English docs changes.
|
||||
|
||||
Changed English doc files:
|
||||
<changed_english_docs>
|
||||
${{ steps.changes.outputs.files }}
|
||||
</changed_english_docs>
|
||||
|
||||
Requirements:
|
||||
1. Update all relevant locale docs under packages/web/src/content/docs/<locale>/ so they reflect these English page changes.
|
||||
2. Preserve frontmatter keys, internal links, code blocks, and existing locale-specific metadata unless the English change requires an update.
|
||||
3. Keep locale docs structure aligned with their corresponding English pages.
|
||||
4. Do not modify English source docs in packages/web/src/content/docs/*.mdx.
|
||||
5. If no locale updates are needed, make no changes.
|
||||
|
||||
- name: Commit and push locale docs updates
|
||||
if: steps.changes.outputs.has_changes == 'true'
|
||||
run: |
|
||||
if [ -z "$(git status --porcelain)" ]; then
|
||||
echo "No locale docs changes to commit"
|
||||
exit 0
|
||||
fi
|
||||
git add -A
|
||||
git commit -m "docs(i18n): sync locale docs from english changes"
|
||||
git pull --rebase --autostash origin "$GITHUB_REF_NAME"
|
||||
git push origin HEAD:"$GITHUB_REF_NAME"
|
||||
3
.github/workflows/nix-hashes.yml
vendored
3
.github/workflows/nix-hashes.yml
vendored
@@ -12,6 +12,9 @@ on:
|
||||
- "package.json"
|
||||
- "packages/*/package.json"
|
||||
- "flake.lock"
|
||||
- "nix/node_modules.nix"
|
||||
- "nix/scripts/**"
|
||||
- "patches/**"
|
||||
- ".github/workflows/nix-hashes.yml"
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -16,15 +16,12 @@ wip:
|
||||
|
||||
For anything in the packages/web use the docs: prefix.
|
||||
|
||||
For anything in the packages/app use the ignore: prefix.
|
||||
|
||||
prefer to explain WHY something was done from an end user perspective instead of
|
||||
WHAT was done.
|
||||
|
||||
do not do generic messages like "improved agent experience" be very specific
|
||||
about what user facing changes were made
|
||||
|
||||
if there are changes do a git pull --rebase
|
||||
if there are conflicts DO NOT FIX THEM. notify me and I will fix them
|
||||
|
||||
## GIT DIFF
|
||||
|
||||
@@ -110,3 +110,4 @@ 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`.
|
||||
|
||||
@@ -135,6 +135,16 @@ const ZEN_MODELS = [
|
||||
new sst.Secret("ZEN_MODELS8"),
|
||||
new sst.Secret("ZEN_MODELS9"),
|
||||
new sst.Secret("ZEN_MODELS10"),
|
||||
new sst.Secret("ZEN_MODELS11"),
|
||||
new sst.Secret("ZEN_MODELS12"),
|
||||
new sst.Secret("ZEN_MODELS13"),
|
||||
new sst.Secret("ZEN_MODELS14"),
|
||||
new sst.Secret("ZEN_MODELS15"),
|
||||
new sst.Secret("ZEN_MODELS16"),
|
||||
new sst.Secret("ZEN_MODELS17"),
|
||||
new sst.Secret("ZEN_MODELS18"),
|
||||
new sst.Secret("ZEN_MODELS19"),
|
||||
new sst.Secret("ZEN_MODELS20"),
|
||||
]
|
||||
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
||||
const STRIPE_PUBLISHABLE_KEY = new sst.Secret("STRIPE_PUBLISHABLE_KEY")
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-1IpZnnN6+acCcV0AgO4OVdvgf4TFBFId5dms5W5ecA0=",
|
||||
"aarch64-linux": "sha256-TKmPhXokOav46ucP9AFwHGgKmB9CdGCcUtwqUtLlzG4=",
|
||||
"aarch64-darwin": "sha256-xJQuw3+QHYnlClDrafQKPQyR+aqyAEofvYYjCowHDps=",
|
||||
"x86_64-darwin": "sha256-ywU3Oka2QNGKu/HI+//3bdYJ9qo1N7K5Wr2vpTgSM/g="
|
||||
"x86_64-linux": "sha256-cvRBvHRuunNjF07c4GVHl5rRgoTn1qfI/HdJWtOV63M=",
|
||||
"aarch64-linux": "sha256-DJUI4pMZ7wQTnyOiuDHALmZz7FZtrTbzRzCuNOShmWE=",
|
||||
"aarch64-darwin": "sha256-JnkqDwuC7lNsjafV+jOGfvs8K1xC8rk5CTOW+spjiCA=",
|
||||
"x86_64-darwin": "sha256-GBeTqq2vDn/mXplYNglrAT2xajjFVzB4ATHnMS0j7z4="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ stdenvNoCC.mkDerivation {
|
||||
../bun.lock
|
||||
../package.json
|
||||
../patches
|
||||
../install
|
||||
../install # required by desktop build (cli.rs include_str!)
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,27 +1,32 @@
|
||||
import { lstat, mkdir, readdir, rm, symlink } from "fs/promises"
|
||||
import { join, relative } from "path"
|
||||
|
||||
type SemverLike = {
|
||||
valid: (value: string) => string | null
|
||||
rcompare: (left: string, right: string) => number
|
||||
}
|
||||
|
||||
type Entry = {
|
||||
dir: string
|
||||
version: string
|
||||
label: string
|
||||
}
|
||||
|
||||
async function isDirectory(path: string) {
|
||||
try {
|
||||
const info = await lstat(path)
|
||||
return info.isDirectory()
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const isValidSemver = (v: string) => Bun.semver.satisfies(v, "x.x.x")
|
||||
|
||||
const root = process.cwd()
|
||||
const bunRoot = join(root, "node_modules/.bun")
|
||||
const linkRoot = join(bunRoot, "node_modules")
|
||||
const directories = (await readdir(bunRoot)).sort()
|
||||
|
||||
const versions = new Map<string, Entry[]>()
|
||||
|
||||
for (const entry of directories) {
|
||||
const full = join(bunRoot, entry)
|
||||
const info = await lstat(full)
|
||||
if (!info.isDirectory()) {
|
||||
if (!(await isDirectory(full))) {
|
||||
continue
|
||||
}
|
||||
const parsed = parseEntry(entry)
|
||||
@@ -29,37 +34,23 @@ for (const entry of directories) {
|
||||
continue
|
||||
}
|
||||
const list = versions.get(parsed.name) ?? []
|
||||
list.push({ dir: full, version: parsed.version, label: entry })
|
||||
list.push({ dir: full, version: parsed.version })
|
||||
versions.set(parsed.name, list)
|
||||
}
|
||||
|
||||
const semverModule = (await import(join(bunRoot, "node_modules/semver"))) as
|
||||
| SemverLike
|
||||
| {
|
||||
default: SemverLike
|
||||
}
|
||||
const semver = "default" in semverModule ? semverModule.default : semverModule
|
||||
const selections = new Map<string, Entry>()
|
||||
|
||||
for (const [slug, list] of versions) {
|
||||
list.sort((a, b) => {
|
||||
const left = semver.valid(a.version)
|
||||
const right = semver.valid(b.version)
|
||||
if (left && right) {
|
||||
const delta = semver.rcompare(left, right)
|
||||
if (delta !== 0) {
|
||||
return delta
|
||||
}
|
||||
}
|
||||
if (left && !right) {
|
||||
return -1
|
||||
}
|
||||
if (!left && right) {
|
||||
return 1
|
||||
}
|
||||
const aValid = isValidSemver(a.version)
|
||||
const bValid = isValidSemver(b.version)
|
||||
if (aValid && bValid) return -Bun.semver.order(a.version, b.version)
|
||||
if (aValid) return -1
|
||||
if (bValid) return 1
|
||||
return b.version.localeCompare(a.version)
|
||||
})
|
||||
selections.set(slug, list[0])
|
||||
const first = list[0]
|
||||
if (first) selections.set(slug, first)
|
||||
}
|
||||
|
||||
await rm(linkRoot, { recursive: true, force: true })
|
||||
@@ -77,10 +68,7 @@ for (const [slug, entry] of Array.from(selections.entries()).sort((a, b) => a[0]
|
||||
await mkdir(parent, { recursive: true })
|
||||
const linkPath = join(parent, leaf)
|
||||
const desired = join(entry.dir, "node_modules", slug)
|
||||
const exists = await lstat(desired)
|
||||
.then((info) => info.isDirectory())
|
||||
.catch(() => false)
|
||||
if (!exists) {
|
||||
if (!(await isDirectory(desired))) {
|
||||
continue
|
||||
}
|
||||
const relativeTarget = relative(parent, desired)
|
||||
|
||||
@@ -8,7 +8,7 @@ type PackageManifest = {
|
||||
|
||||
const root = process.cwd()
|
||||
const bunRoot = join(root, "node_modules/.bun")
|
||||
const bunEntries = (await safeReadDir(bunRoot)).sort()
|
||||
const bunEntries = (await readdir(bunRoot)).sort()
|
||||
let rewritten = 0
|
||||
|
||||
for (const entry of bunEntries) {
|
||||
@@ -45,11 +45,11 @@ for (const entry of bunEntries) {
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[normalize-bun-binaries] rewrote ${rewritten} links`)
|
||||
console.log(`[normalize-bun-binaries] rebuilt ${rewritten} links`)
|
||||
|
||||
async function collectPackages(modulesRoot: string) {
|
||||
const found: string[] = []
|
||||
const topLevel = (await safeReadDir(modulesRoot)).sort()
|
||||
const topLevel = (await readdir(modulesRoot)).sort()
|
||||
for (const name of topLevel) {
|
||||
if (name === ".bin" || name === ".bun") {
|
||||
continue
|
||||
@@ -59,7 +59,7 @@ async function collectPackages(modulesRoot: string) {
|
||||
continue
|
||||
}
|
||||
if (name.startsWith("@")) {
|
||||
const scoped = (await safeReadDir(full)).sort()
|
||||
const scoped = (await readdir(full)).sort()
|
||||
for (const child of scoped) {
|
||||
const scopedDir = join(full, child)
|
||||
if (await isDirectory(scopedDir)) {
|
||||
@@ -121,14 +121,6 @@ async function isDirectory(path: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function safeReadDir(path: string) {
|
||||
try {
|
||||
return await readdir(path)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeBinName(name: string) {
|
||||
const slash = name.lastIndexOf("/")
|
||||
if (slash >= 0) {
|
||||
|
||||
@@ -40,6 +40,8 @@
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"diff": "8.0.2",
|
||||
"dompurify": "3.3.1",
|
||||
"drizzle-kit": "1.0.0-beta.12-a5629fb",
|
||||
"drizzle-orm": "1.0.0-beta.12-a5629fb",
|
||||
"ai": "5.0.124",
|
||||
"hono": "4.10.7",
|
||||
"hono-openapi": "1.1.2",
|
||||
|
||||
@@ -84,7 +84,7 @@ function ServerKey(props: ParentProps) {
|
||||
)
|
||||
}
|
||||
|
||||
export function AppInterface(props: { defaultUrl?: string; children?: JSX.Element }) {
|
||||
export function AppInterface(props: { defaultUrl?: string; children?: JSX.Element; isSidecar?: boolean }) {
|
||||
const platform = usePlatform()
|
||||
|
||||
const stored = (() => {
|
||||
@@ -106,7 +106,7 @@ export function AppInterface(props: { defaultUrl?: string; children?: JSX.Elemen
|
||||
}
|
||||
|
||||
return (
|
||||
<ServerProvider defaultUrl={defaultServerUrl()}>
|
||||
<ServerProvider defaultUrl={defaultServerUrl()} isSidecar={props.isSidecar}>
|
||||
<ServerKey>
|
||||
<GlobalSDKProvider>
|
||||
<GlobalSyncProvider>
|
||||
|
||||
@@ -31,7 +31,7 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
||||
const dataUrl = reader.result as string
|
||||
const attachment: ImageAttachmentPart = {
|
||||
type: "image",
|
||||
id: crypto.randomUUID(),
|
||||
id: crypto.randomUUID?.() ?? Math.random().toString(16).slice(2),
|
||||
filename: file.name,
|
||||
mime: file.type,
|
||||
dataUrl,
|
||||
|
||||
@@ -137,7 +137,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
|
||||
const projectDirectory = sdk.directory
|
||||
const isNewSession = !params.id
|
||||
const worktreeSelection = input.newSessionWorktree ?? "main"
|
||||
const worktreeSelection = input.newSessionWorktree || "main"
|
||||
|
||||
let sessionDirectory = projectDirectory
|
||||
let client = sdk.client
|
||||
|
||||
@@ -79,15 +79,16 @@ describe("getSessionContextMetrics", () => {
|
||||
expect(metrics.context?.usage).toBeNull()
|
||||
})
|
||||
|
||||
test("memoizes by message and provider array identity", () => {
|
||||
test("recomputes when message array is mutated in place", () => {
|
||||
const messages = [assistant("a1", { input: 10, output: 10, reasoning: 10, read: 10, write: 10 }, 0.25)]
|
||||
const providers = [{ id: "openai", models: {} }]
|
||||
|
||||
const one = getSessionContextMetrics(messages, providers)
|
||||
messages.push(assistant("a2", { input: 100, output: 20, reasoning: 0, read: 0, write: 0 }, 0.75))
|
||||
const two = getSessionContextMetrics(messages, providers)
|
||||
const three = getSessionContextMetrics([...messages], providers)
|
||||
|
||||
expect(two).toBe(one)
|
||||
expect(three).not.toBe(one)
|
||||
expect(one.context?.message.id).toBe("a1")
|
||||
expect(two.context?.message.id).toBe("a2")
|
||||
expect(two.totalCost).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -34,8 +34,6 @@ type Metrics = {
|
||||
context: Context | undefined
|
||||
}
|
||||
|
||||
const cache = new WeakMap<Message[], WeakMap<Provider[], Metrics>>()
|
||||
|
||||
const tokenTotal = (msg: AssistantMessage) => {
|
||||
return msg.tokens.input + msg.tokens.output + msg.tokens.reasoning + msg.tokens.cache.read + msg.tokens.cache.write
|
||||
}
|
||||
@@ -80,15 +78,5 @@ const build = (messages: Message[], providers: Provider[]): Metrics => {
|
||||
}
|
||||
|
||||
export function getSessionContextMetrics(messages: Message[], providers: Provider[]) {
|
||||
const byProvider = cache.get(messages)
|
||||
if (byProvider) {
|
||||
const hit = byProvider.get(providers)
|
||||
if (hit) return hit
|
||||
}
|
||||
|
||||
const value = build(messages, providers)
|
||||
const next = byProvider ?? new WeakMap<Provider[], Metrics>()
|
||||
next.set(providers, value)
|
||||
if (!byProvider) cache.set(messages, next)
|
||||
return value
|
||||
return build(messages, providers)
|
||||
}
|
||||
|
||||
@@ -112,21 +112,35 @@ export function SessionHeader() {
|
||||
|
||||
const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({ finder: true })
|
||||
|
||||
const apps = createMemo(() => {
|
||||
if (os() === "macos") return MAC_APPS
|
||||
if (os() === "windows") return WINDOWS_APPS
|
||||
return LINUX_APPS
|
||||
})
|
||||
|
||||
const fileManager = createMemo(() => {
|
||||
if (os() === "macos") return { label: "Finder", icon: "finder" as const }
|
||||
if (os() === "windows") return { label: "File Explorer", icon: "file-explorer" as const }
|
||||
return { label: "File Manager", icon: "finder" as const }
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (platform.platform !== "desktop") return
|
||||
if (!platform.checkAppExists) return
|
||||
|
||||
const list = os()
|
||||
const apps = list === "macos" ? MAC_APPS : list === "windows" ? WINDOWS_APPS : list === "linux" ? LINUX_APPS : []
|
||||
if (apps.length === 0) return
|
||||
const list = apps()
|
||||
|
||||
setExists(Object.fromEntries(list.map((app) => [app.id, undefined])) as Partial<Record<OpenApp, boolean>>)
|
||||
|
||||
void Promise.all(
|
||||
apps.map((app) =>
|
||||
Promise.resolve(platform.checkAppExists?.(app.openWith)).then((value) => {
|
||||
const ok = Boolean(value)
|
||||
console.debug(`[session-header] App "${app.label}" (${app.openWith}): ${ok ? "exists" : "does not exist"}`)
|
||||
return [app.id, ok] as const
|
||||
}),
|
||||
list.map((app) =>
|
||||
Promise.resolve(platform.checkAppExists?.(app.openWith))
|
||||
.then((value) => Boolean(value))
|
||||
.catch(() => false)
|
||||
.then((ok) => {
|
||||
console.debug(`[session-header] App "${app.label}" (${app.openWith}): ${ok ? "exists" : "does not exist"}`)
|
||||
return [app.id, ok] as const
|
||||
}),
|
||||
),
|
||||
).then((entries) => {
|
||||
setExists(Object.fromEntries(entries) as Partial<Record<OpenApp, boolean>>)
|
||||
@@ -134,23 +148,23 @@ export function SessionHeader() {
|
||||
})
|
||||
|
||||
const options = createMemo(() => {
|
||||
if (os() === "macos") {
|
||||
return [{ id: "finder", label: "Finder", icon: "finder" }, ...MAC_APPS.filter((app) => exists[app.id])] as const
|
||||
}
|
||||
|
||||
if (os() === "windows") {
|
||||
return [
|
||||
{ id: "finder", label: "File Explorer", icon: "file-explorer" },
|
||||
...WINDOWS_APPS.filter((app) => exists[app.id]),
|
||||
] as const
|
||||
}
|
||||
|
||||
return [
|
||||
{ id: "finder", label: "File Manager", icon: "finder" },
|
||||
...LINUX_APPS.filter((app) => exists[app.id]),
|
||||
{ id: "finder", label: fileManager().label, icon: fileManager().icon },
|
||||
...apps().filter((app) => exists[app.id]),
|
||||
] as const
|
||||
})
|
||||
|
||||
type OpenIcon = OpenApp | "file-explorer"
|
||||
const base = new Set<OpenIcon>(["finder", "vscode", "cursor", "zed"])
|
||||
const size = (id: OpenIcon) => (base.has(id) ? "size-4" : "size-[19px]")
|
||||
|
||||
const checksReady = createMemo(() => {
|
||||
if (platform.platform !== "desktop") return true
|
||||
if (!platform.checkAppExists) return true
|
||||
const list = apps()
|
||||
return list.every((app) => exists[app.id] !== undefined)
|
||||
})
|
||||
|
||||
const [prefs, setPrefs] = persisted(Persist.global("open.app"), createStore({ app: "finder" as OpenApp }))
|
||||
|
||||
const canOpen = createMemo(() => platform.platform === "desktop" && !!platform.openPath && server.isLocal())
|
||||
@@ -158,6 +172,7 @@ export function SessionHeader() {
|
||||
|
||||
createEffect(() => {
|
||||
if (platform.platform !== "desktop") return
|
||||
if (!checksReady()) return
|
||||
const value = prefs.app
|
||||
if (options().some((o) => o.id === value)) return
|
||||
setPrefs("app", options()[0]?.id ?? "finder")
|
||||
@@ -283,7 +298,7 @@ export function SessionHeader() {
|
||||
<Portal mount={mount()}>
|
||||
<button
|
||||
type="button"
|
||||
class="hidden md:flex w-[320px] max-w-full min-w-0 p-1 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus-visible:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
|
||||
class="hidden md:flex w-[320px] max-w-full min-w-0 h-[24px] px-2 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-base bg-surface-panel transition-colors cursor-default hover:bg-surface-raised-base-hover focus-visible:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
|
||||
onClick={() => command.trigger("file.open")}
|
||||
aria-label={language.t("session.header.searchFiles")}
|
||||
>
|
||||
@@ -294,7 +309,11 @@ export function SessionHeader() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Show when={hotkey()}>{(keybind) => <Keybind class="shrink-0">{keybind()}</Keybind>}</Show>
|
||||
<Show when={hotkey()}>
|
||||
{(keybind) => (
|
||||
<Keybind class="shrink-0 !border-0 !bg-transparent !shadow-none px-0">{keybind()}</Keybind>
|
||||
)}
|
||||
</Show>
|
||||
</button>
|
||||
</Portal>
|
||||
)}
|
||||
@@ -303,6 +322,7 @@ export function SessionHeader() {
|
||||
{(mount) => (
|
||||
<Portal mount={mount()}>
|
||||
<div class="flex items-center gap-3">
|
||||
<StatusPopover />
|
||||
<Show when={projectDirectory()}>
|
||||
<div class="hidden xl:flex items-center">
|
||||
<Show
|
||||
@@ -322,62 +342,68 @@ export function SessionHeader() {
|
||||
}
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="rounded-sm h-[24px] py-1.5 pr-3 pl-2 gap-2 border-none shadow-none rounded-r-none"
|
||||
onClick={() => openDir(current().id)}
|
||||
aria-label={language.t("session.header.open.ariaLabel", { app: current().label })}
|
||||
>
|
||||
<AppIcon id={current().icon} class="size-5" />
|
||||
<span class="text-12-regular text-text-strong">
|
||||
{language.t("session.header.open.action", { app: current().label })}
|
||||
</span>
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
icon="chevron-down"
|
||||
<div class="flex h-[24px] box-border items-center rounded-md border border-border-base bg-surface-panel overflow-hidden">
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="rounded-sm h-[24px] w-auto px-1.5 border-none shadow-none rounded-l-none data-[expanded]:bg-surface-raised-base-active"
|
||||
aria-label={language.t("session.header.open.menu")}
|
||||
/>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content placement="bottom-end" gutter={6}>
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.GroupLabel>{language.t("session.header.openIn")}</DropdownMenu.GroupLabel>
|
||||
<DropdownMenu.RadioGroup
|
||||
value={prefs.app}
|
||||
onChange={(value) => {
|
||||
if (!OPEN_APPS.includes(value as OpenApp)) return
|
||||
setPrefs("app", value as OpenApp)
|
||||
}}
|
||||
>
|
||||
{options().map((o) => (
|
||||
<DropdownMenu.RadioItem value={o.id} onSelect={() => openDir(o.id)}>
|
||||
<AppIcon id={o.icon} class="size-5" />
|
||||
<DropdownMenu.ItemLabel>{o.label}</DropdownMenu.ItemLabel>
|
||||
<DropdownMenu.ItemIndicator>
|
||||
<Icon name="check-small" size="small" class="text-icon-weak" />
|
||||
</DropdownMenu.ItemIndicator>
|
||||
</DropdownMenu.RadioItem>
|
||||
))}
|
||||
</DropdownMenu.RadioGroup>
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onSelect={copyPath}>
|
||||
<Icon name="copy" size="small" class="text-icon-weak" />
|
||||
<DropdownMenu.ItemLabel>
|
||||
{language.t("session.header.open.copyPath")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
class="rounded-none h-full py-0 pr-3 pl-2 gap-1.5 border-none shadow-none"
|
||||
onClick={() => openDir(current().id)}
|
||||
aria-label={language.t("session.header.open.ariaLabel", { app: current().label })}
|
||||
>
|
||||
<div class="flex size-5 shrink-0 items-center justify-center">
|
||||
<AppIcon id={current().icon} class="size-4" />
|
||||
</div>
|
||||
<span class="text-12-regular text-text-strong">Open</span>
|
||||
</Button>
|
||||
<div class="self-stretch w-px bg-border-base/70" />
|
||||
<DropdownMenu gutter={6} placement="bottom-end">
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
icon="chevron-down"
|
||||
variant="ghost"
|
||||
class="rounded-none h-full w-[24px] p-0 border-none shadow-none data-[expanded]:bg-surface-raised-base-active"
|
||||
aria-label={language.t("session.header.open.menu")}
|
||||
/>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.GroupLabel>{language.t("session.header.openIn")}</DropdownMenu.GroupLabel>
|
||||
<DropdownMenu.RadioGroup
|
||||
value={prefs.app}
|
||||
onChange={(value) => {
|
||||
if (!OPEN_APPS.includes(value as OpenApp)) return
|
||||
setPrefs("app", value as OpenApp)
|
||||
}}
|
||||
>
|
||||
{options().map((o) => (
|
||||
<DropdownMenu.RadioItem value={o.id} onSelect={() => openDir(o.id)}>
|
||||
<div class="flex size-5 shrink-0 items-center justify-center">
|
||||
<AppIcon id={o.icon} class={size(o.icon)} />
|
||||
</div>
|
||||
<DropdownMenu.ItemLabel>{o.label}</DropdownMenu.ItemLabel>
|
||||
<DropdownMenu.ItemIndicator>
|
||||
<Icon name="check-small" size="small" class="text-icon-weak" />
|
||||
</DropdownMenu.ItemIndicator>
|
||||
</DropdownMenu.RadioItem>
|
||||
))}
|
||||
</DropdownMenu.RadioGroup>
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onSelect={copyPath}>
|
||||
<div class="flex size-5 shrink-0 items-center justify-center">
|
||||
<Icon name="copy" size="small" class="text-icon-weak" />
|
||||
</div>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{language.t("session.header.open.copyPath")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
<StatusPopover />
|
||||
<Show when={showShare()}>
|
||||
<div class="flex items-center">
|
||||
<Popover
|
||||
@@ -393,8 +419,9 @@ export function SessionHeader() {
|
||||
class="rounded-xl [&_[data-slot=popover-close-button]]:hidden"
|
||||
triggerAs={Button}
|
||||
triggerProps={{
|
||||
variant: "secondary",
|
||||
class: "rounded-sm h-[24px] px-3",
|
||||
variant: "ghost",
|
||||
class:
|
||||
"rounded-md h-[24px] px-3 border border-border-base bg-surface-panel shadow-none data-[expanded]:bg-surface-raised-base-active",
|
||||
classList: { "rounded-r-none": shareUrl() !== undefined },
|
||||
style: { scale: 1 },
|
||||
}}
|
||||
@@ -420,7 +447,14 @@ export function SessionHeader() {
|
||||
}
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<TextField value={shareUrl() ?? ""} readOnly copyable tabIndex={-1} class="w-full" />
|
||||
<TextField
|
||||
value={shareUrl() ?? ""}
|
||||
readOnly
|
||||
copyable
|
||||
copyKind="link"
|
||||
tabIndex={-1}
|
||||
class="w-full"
|
||||
/>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
size="large"
|
||||
@@ -459,8 +493,8 @@ export function SessionHeader() {
|
||||
>
|
||||
<IconButton
|
||||
icon={state.copied ? "check" : "link"}
|
||||
variant="secondary"
|
||||
class="rounded-l-none"
|
||||
variant="ghost"
|
||||
class="rounded-l-none h-[24px] border border-border-base bg-surface-panel shadow-none"
|
||||
onClick={copyLink}
|
||||
disabled={state.unshare}
|
||||
aria-label={
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Show, createMemo } from "solid-js"
|
||||
import { DateTime } from "luxon"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
@@ -15,6 +16,7 @@ interface NewSessionViewProps {
|
||||
|
||||
export function NewSessionView(props: NewSessionViewProps) {
|
||||
const sync = useSync()
|
||||
const sdk = useSDK()
|
||||
const language = useLanguage()
|
||||
|
||||
const sandboxes = createMemo(() => sync.project?.sandboxes ?? [])
|
||||
@@ -24,11 +26,11 @@ export function NewSessionView(props: NewSessionViewProps) {
|
||||
if (options().includes(selection)) return selection
|
||||
return MAIN_WORKTREE
|
||||
})
|
||||
const projectRoot = createMemo(() => sync.project?.worktree ?? sync.data.path.directory)
|
||||
const projectRoot = createMemo(() => sync.project?.worktree ?? sdk.directory)
|
||||
const isWorktree = createMemo(() => {
|
||||
const project = sync.project
|
||||
if (!project) return false
|
||||
return sync.data.path.directory !== project.worktree
|
||||
return sdk.directory !== project.worktree
|
||||
})
|
||||
|
||||
const label = (value: string) => {
|
||||
@@ -45,7 +47,7 @@ export function NewSessionView(props: NewSessionViewProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="size-full flex flex-col justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto px-6 pb-[calc(var(--prompt-height,11.25rem)+64px)]">
|
||||
<div class="size-full flex flex-col justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto 2xl:max-w-[1000px] px-6 pb-[calc(var(--prompt-height,11.25rem)+64px)]">
|
||||
<div class="text-20-medium text-text-weaker">{language.t("command.session.new")}</div>
|
||||
<div class="flex justify-center items-center gap-3">
|
||||
<Icon name="folder" size="small" />
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Component, createMemo, type JSX } from "solid-js"
|
||||
import { Component, Show, createEffect, createMemo, createResource, type JSX } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { Select } from "@opencode-ai/ui/select"
|
||||
import { Switch } from "@opencode-ai/ui/switch"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { useLanguage } from "@/context/language"
|
||||
@@ -40,6 +42,8 @@ export const SettingsGeneral: Component = () => {
|
||||
checking: false,
|
||||
})
|
||||
|
||||
const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux")
|
||||
|
||||
const check = () => {
|
||||
if (!platform.checkUpdate) return
|
||||
setStore("checking", true)
|
||||
@@ -410,13 +414,49 @@ export const SettingsGeneral: Component = () => {
|
||||
</SettingsRow>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={linux()}>
|
||||
{(_) => {
|
||||
const [valueResource, actions] = createResource(() => platform.getDisplayBackend?.())
|
||||
const value = () => (valueResource.state === "pending" ? undefined : valueResource.latest)
|
||||
|
||||
const onChange = (checked: boolean) =>
|
||||
platform.setDisplayBackend?.(checked ? "wayland" : "auto").finally(() => actions.refetch())
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-1">
|
||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.display")}</h3>
|
||||
|
||||
<div class="bg-surface-raised-base px-4 rounded-lg">
|
||||
<SettingsRow
|
||||
title={
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{language.t("settings.general.row.wayland.title")}</span>
|
||||
<Tooltip value={language.t("settings.general.row.wayland.tooltip")} placement="top">
|
||||
<span class="text-text-weak">
|
||||
<Icon name="help" size="small" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
}
|
||||
description={language.t("settings.general.row.wayland.description")}
|
||||
>
|
||||
<div data-action="settings-wayland">
|
||||
<Switch checked={value() === "wayland"} onChange={onChange} />
|
||||
</div>
|
||||
</SettingsRow>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface SettingsRowProps {
|
||||
title: string
|
||||
title: string | JSX.Element
|
||||
description: string | JSX.Element
|
||||
children: JSX.Element
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ export function StatusPopover() {
|
||||
triggerProps={{
|
||||
variant: "ghost",
|
||||
class:
|
||||
"rounded-sm w-[75px] h-[24px] py-1.5 pr-3 pl-2 gap-2 border-none shadow-none data-[expanded]:bg-surface-raised-base-active",
|
||||
"rounded-md h-[24px] px-3 gap-2 border border-border-base bg-surface-panel shadow-none data-[expanded]:bg-surface-raised-base-active",
|
||||
style: { scale: 1 },
|
||||
}}
|
||||
trigger={
|
||||
|
||||
@@ -53,7 +53,7 @@ function createCommentSessionState(store: Store<CommentStore>, setStore: SetStor
|
||||
|
||||
const add = (input: Omit<LineComment, "id" | "time">) => {
|
||||
const next: LineComment = {
|
||||
id: crypto.randomUUID(),
|
||||
id: crypto.randomUUID?.() ?? Math.random().toString(16).slice(2),
|
||||
time: Date.now(),
|
||||
...input,
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getFilename } from "@opencode-ai/util/path"
|
||||
import { useSDK } from "./sdk"
|
||||
import { useSync } from "./sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { createPathHelpers } from "./file/path"
|
||||
import {
|
||||
approxBytes,
|
||||
@@ -50,9 +51,11 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||
useSync()
|
||||
const params = useParams()
|
||||
const language = useLanguage()
|
||||
const layout = useLayout()
|
||||
|
||||
const scope = createMemo(() => sdk.directory)
|
||||
const path = createPathHelpers(scope)
|
||||
const tabs = layout.tabs(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
|
||||
const inflight = new Map<string, Promise<void>>()
|
||||
const [store, setStore] = createStore<{
|
||||
@@ -183,6 +186,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||
invalidateFromWatcher(e.details, {
|
||||
normalize: path.normalize,
|
||||
hasFile: (file) => Boolean(store.file[file]),
|
||||
isOpen: (file) => tabs.all().some((tab) => path.pathFromTab(tab) === file),
|
||||
loadFile: (file) => {
|
||||
void load(file, { force: true })
|
||||
},
|
||||
|
||||
@@ -27,6 +27,37 @@ describe("file watcher invalidation", () => {
|
||||
expect(refresh).toEqual(["src"])
|
||||
})
|
||||
|
||||
test("reloads files that are open in tabs", () => {
|
||||
const loads: string[] = []
|
||||
|
||||
invalidateFromWatcher(
|
||||
{
|
||||
type: "file.watcher.updated",
|
||||
properties: {
|
||||
file: "src/open.ts",
|
||||
event: "change",
|
||||
},
|
||||
},
|
||||
{
|
||||
normalize: (input) => input,
|
||||
hasFile: () => false,
|
||||
isOpen: (path) => path === "src/open.ts",
|
||||
loadFile: (path) => loads.push(path),
|
||||
node: () => ({
|
||||
path: "src/open.ts",
|
||||
type: "file",
|
||||
name: "open.ts",
|
||||
absolute: "/repo/src/open.ts",
|
||||
ignored: false,
|
||||
}),
|
||||
isDirLoaded: () => false,
|
||||
refreshDir: () => {},
|
||||
},
|
||||
)
|
||||
|
||||
expect(loads).toEqual(["src/open.ts"])
|
||||
})
|
||||
|
||||
test("refreshes only changed loaded directory nodes", () => {
|
||||
const refresh: string[] = []
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ type WatcherEvent = {
|
||||
type WatcherOps = {
|
||||
normalize: (input: string) => string
|
||||
hasFile: (path: string) => boolean
|
||||
isOpen?: (path: string) => boolean
|
||||
loadFile: (path: string) => void
|
||||
node: (path: string) => FileNode | undefined
|
||||
isDirLoaded: (path: string) => boolean
|
||||
@@ -27,7 +28,7 @@ export function invalidateFromWatcher(event: WatcherEvent, ops: WatcherOps) {
|
||||
if (!path) return
|
||||
if (path.startsWith(".git/")) return
|
||||
|
||||
if (ops.hasFile(path)) {
|
||||
if (ops.hasFile(path) || ops.isOpen?.(path)) {
|
||||
ops.loadFile(path)
|
||||
}
|
||||
|
||||
|
||||
@@ -231,6 +231,24 @@ export function applyDirectoryEvent(input: {
|
||||
}
|
||||
break
|
||||
}
|
||||
case "message.part.delta": {
|
||||
const props = event.properties as { messageID: string; partID: string; field: string; delta: string }
|
||||
const parts = input.store.part[props.messageID]
|
||||
if (!parts) break
|
||||
const result = Binary.search(parts, props.partID, (p) => p.id)
|
||||
if (!result.found) break
|
||||
input.setStore(
|
||||
"part",
|
||||
props.messageID,
|
||||
produce((draft) => {
|
||||
const part = draft[result.index]
|
||||
const field = props.field as keyof typeof part
|
||||
const existing = part[field] as string | undefined
|
||||
;(part[field] as string) = (existing ?? "") + props.delta
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
case "vcs.branch.updated": {
|
||||
const props = event.properties as { branch: string }
|
||||
const next = { branch: props.branch }
|
||||
|
||||
@@ -57,6 +57,12 @@ export type Platform = {
|
||||
/** Set the default server URL to use on app startup (platform-specific) */
|
||||
setDefaultServerUrl?(url: string | null): Promise<void> | void
|
||||
|
||||
/** Get the preferred display backend (desktop only) */
|
||||
getDisplayBackend?(): Promise<DisplayBackend | null> | DisplayBackend | null
|
||||
|
||||
/** Set the preferred display backend (desktop only) */
|
||||
setDisplayBackend?(backend: DisplayBackend): Promise<void>
|
||||
|
||||
/** Parse markdown to HTML using native parser (desktop only, returns unprocessed code blocks) */
|
||||
parseMarkdown?(markdown: string): Promise<string>
|
||||
|
||||
@@ -70,6 +76,8 @@ export type Platform = {
|
||||
readClipboardImage?(): Promise<File | null>
|
||||
}
|
||||
|
||||
export type DisplayBackend = "auto" | "wayland"
|
||||
|
||||
export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({
|
||||
name: "Platform",
|
||||
init: (props: { value: Platform }) => {
|
||||
|
||||
@@ -28,13 +28,14 @@ function projectsKey(url: string) {
|
||||
|
||||
export const { use: useServer, provider: ServerProvider } = createSimpleContext({
|
||||
name: "Server",
|
||||
init: (props: { defaultUrl: string }) => {
|
||||
init: (props: { defaultUrl: string; isSidecar?: boolean }) => {
|
||||
const platform = usePlatform()
|
||||
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
Persist.global("server", ["server.v3"]),
|
||||
createStore({
|
||||
list: [] as string[],
|
||||
currentSidecarUrl: "",
|
||||
projects: {} as Record<string, StoredProject[]>,
|
||||
lastProject: {} as Record<string, string>,
|
||||
}),
|
||||
@@ -59,7 +60,13 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
|
||||
const fallback = normalizeServerUrl(props.defaultUrl)
|
||||
if (fallback && url === fallback) {
|
||||
setState("active", url)
|
||||
batch(() => {
|
||||
if (!store.list.includes(url)) {
|
||||
// Add the fallback url to the list if it's not already in the list
|
||||
setStore("list", store.list.length, url)
|
||||
}
|
||||
setState("active", url)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -89,7 +96,20 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
|
||||
if (state.active) return
|
||||
const url = normalizeServerUrl(props.defaultUrl)
|
||||
if (!url) return
|
||||
setState("active", url)
|
||||
batch(() => {
|
||||
// Remove the previous startup sidecar url
|
||||
if (store.currentSidecarUrl) {
|
||||
remove(store.currentSidecarUrl)
|
||||
}
|
||||
|
||||
// Add the new sidecar url
|
||||
if (props.isSidecar && props.defaultUrl) {
|
||||
add(props.defaultUrl)
|
||||
setStore("currentSidecarUrl", props.defaultUrl)
|
||||
}
|
||||
|
||||
setState("active", url)
|
||||
})
|
||||
})
|
||||
|
||||
const isReady = createMemo(() => ready() && !!state.active)
|
||||
|
||||
@@ -44,6 +44,7 @@ export const dict = {
|
||||
|
||||
"command.session.new": "جلسة جديدة",
|
||||
"command.file.open": "فتح ملف",
|
||||
"command.tab.close": "إغلاق علامة التبويب",
|
||||
"command.context.addSelection": "إضافة التحديد إلى السياق",
|
||||
"command.context.addSelection.description": "إضافة الأسطر المحددة من الملف الحالي",
|
||||
"command.input.focus": "التركيز على حقل الإدخال",
|
||||
|
||||
@@ -44,6 +44,7 @@ export const dict = {
|
||||
|
||||
"command.session.new": "Nova sessão",
|
||||
"command.file.open": "Abrir arquivo",
|
||||
"command.tab.close": "Fechar aba",
|
||||
"command.context.addSelection": "Adicionar seleção ao contexto",
|
||||
"command.context.addSelection.description": "Adicionar as linhas selecionadas do arquivo atual",
|
||||
"command.input.focus": "Focar entrada",
|
||||
|
||||
@@ -44,6 +44,7 @@ export const dict = {
|
||||
|
||||
"command.session.new": "Ny session",
|
||||
"command.file.open": "Åbn fil",
|
||||
"command.tab.close": "Luk fane",
|
||||
"command.context.addSelection": "Tilføj markering til kontekst",
|
||||
"command.context.addSelection.description": "Tilføj markerede linjer fra den aktuelle fil",
|
||||
"command.input.focus": "Fokuser inputfelt",
|
||||
|
||||
@@ -48,6 +48,7 @@ export const dict = {
|
||||
|
||||
"command.session.new": "Neue Sitzung",
|
||||
"command.file.open": "Datei öffnen",
|
||||
"command.tab.close": "Tab schließen",
|
||||
"command.context.addSelection": "Auswahl zum Kontext hinzufügen",
|
||||
"command.context.addSelection.description": "Ausgewählte Zeilen aus der aktuellen Datei hinzufügen",
|
||||
"command.input.focus": "Eingabefeld fokussieren",
|
||||
|
||||
@@ -588,6 +588,7 @@ export const dict = {
|
||||
"settings.general.section.notifications": "System notifications",
|
||||
"settings.general.section.updates": "Updates",
|
||||
"settings.general.section.sounds": "Sound effects",
|
||||
"settings.general.section.display": "Display",
|
||||
|
||||
"settings.general.row.language.title": "Language",
|
||||
"settings.general.row.language.description": "Change the display language for OpenCode",
|
||||
@@ -598,6 +599,11 @@ export const dict = {
|
||||
"settings.general.row.font.title": "Font",
|
||||
"settings.general.row.font.description": "Customise the mono font used in code blocks",
|
||||
|
||||
"settings.general.row.wayland.title": "Use native Wayland",
|
||||
"settings.general.row.wayland.description": "Disable X11 fallback on Wayland. Requires restart.",
|
||||
"settings.general.row.wayland.tooltip":
|
||||
"On Linux with mixed refresh-rate monitors, native Wayland can be more stable.",
|
||||
|
||||
"settings.general.row.releaseNotes.title": "Release notes",
|
||||
"settings.general.row.releaseNotes.description": "Show What's New popups after updates",
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ export const dict = {
|
||||
|
||||
"command.session.new": "Nueva sesión",
|
||||
"command.file.open": "Abrir archivo",
|
||||
"command.tab.close": "Cerrar pestaña",
|
||||
"command.context.addSelection": "Añadir selección al contexto",
|
||||
"command.context.addSelection.description": "Añadir las líneas seleccionadas del archivo actual",
|
||||
"command.input.focus": "Enfocar entrada",
|
||||
|
||||
@@ -44,6 +44,7 @@ export const dict = {
|
||||
|
||||
"command.session.new": "Nouvelle session",
|
||||
"command.file.open": "Ouvrir un fichier",
|
||||
"command.tab.close": "Fermer l'onglet",
|
||||
"command.context.addSelection": "Ajouter la sélection au contexte",
|
||||
"command.context.addSelection.description": "Ajouter les lignes sélectionnées du fichier actuel",
|
||||
"command.input.focus": "Focus input",
|
||||
|
||||
@@ -44,6 +44,7 @@ export const dict = {
|
||||
|
||||
"command.session.new": "新しいセッション",
|
||||
"command.file.open": "ファイルを開く",
|
||||
"command.tab.close": "タブを閉じる",
|
||||
"command.context.addSelection": "選択範囲をコンテキストに追加",
|
||||
"command.context.addSelection.description": "現在のファイルから選択した行を追加",
|
||||
"command.input.focus": "入力欄にフォーカス",
|
||||
|
||||
@@ -48,6 +48,7 @@ export const dict = {
|
||||
|
||||
"command.session.new": "새 세션",
|
||||
"command.file.open": "파일 열기",
|
||||
"command.tab.close": "탭 닫기",
|
||||
"command.context.addSelection": "선택 영역을 컨텍스트에 추가",
|
||||
"command.context.addSelection.description": "현재 파일에서 선택한 줄을 추가",
|
||||
"command.input.focus": "입력창 포커스",
|
||||
|
||||
@@ -47,6 +47,7 @@ export const dict = {
|
||||
|
||||
"command.session.new": "Ny sesjon",
|
||||
"command.file.open": "Åpne fil",
|
||||
"command.tab.close": "Lukk fane",
|
||||
"command.context.addSelection": "Legg til markering i kontekst",
|
||||
"command.context.addSelection.description": "Legg til valgte linjer fra gjeldende fil",
|
||||
"command.input.focus": "Fokuser inndata",
|
||||
|
||||
@@ -44,6 +44,7 @@ export const dict = {
|
||||
|
||||
"command.session.new": "Nowa sesja",
|
||||
"command.file.open": "Otwórz plik",
|
||||
"command.tab.close": "Zamknij kartę",
|
||||
"command.context.addSelection": "Dodaj zaznaczenie do kontekstu",
|
||||
"command.context.addSelection.description": "Dodaj zaznaczone linie z bieżącego pliku",
|
||||
"command.input.focus": "Fokus na pole wejściowe",
|
||||
|
||||
@@ -44,6 +44,7 @@ export const dict = {
|
||||
|
||||
"command.session.new": "Новая сессия",
|
||||
"command.file.open": "Открыть файл",
|
||||
"command.tab.close": "Закрыть вкладку",
|
||||
"command.context.addSelection": "Добавить выделение в контекст",
|
||||
"command.context.addSelection.description": "Добавить выбранные строки из текущего файла",
|
||||
"command.input.focus": "Фокус на поле ввода",
|
||||
|
||||
@@ -44,6 +44,7 @@ export const dict = {
|
||||
|
||||
"command.session.new": "เซสชันใหม่",
|
||||
"command.file.open": "เปิดไฟล์",
|
||||
"command.tab.close": "ปิดแท็บ",
|
||||
"command.context.addSelection": "เพิ่มส่วนที่เลือกไปยังบริบท",
|
||||
"command.context.addSelection.description": "เพิ่มบรรทัดที่เลือกจากไฟล์ปัจจุบัน",
|
||||
"command.input.focus": "โฟกัสช่องป้อนข้อมูล",
|
||||
|
||||
@@ -48,6 +48,7 @@ export const dict = {
|
||||
|
||||
"command.session.new": "新建会话",
|
||||
"command.file.open": "打开文件",
|
||||
"command.tab.close": "关闭标签页",
|
||||
"command.context.addSelection": "将所选内容添加到上下文",
|
||||
"command.context.addSelection.description": "添加当前文件中选中的行",
|
||||
"command.input.focus": "聚焦输入框",
|
||||
|
||||
@@ -48,6 +48,7 @@ export const dict = {
|
||||
|
||||
"command.session.new": "新增工作階段",
|
||||
"command.file.open": "開啟檔案",
|
||||
"command.tab.close": "關閉分頁",
|
||||
"command.context.addSelection": "將選取內容加入上下文",
|
||||
"command.context.addSelection.description": "加入目前檔案中選取的行",
|
||||
"command.input.focus": "聚焦輸入框",
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export { PlatformProvider, type Platform } from "./context/platform"
|
||||
export { PlatformProvider, type Platform, type DisplayBackend } from "./context/platform"
|
||||
export { AppBaseProviders, AppInterface } from "./app"
|
||||
export { useCommand } from "./context/command"
|
||||
|
||||
@@ -21,8 +21,11 @@ const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
|
||||
|
||||
export const ProjectIcon = (props: { project: LocalProject; class?: string; notify?: boolean }): JSX.Element => {
|
||||
const notification = useNotification()
|
||||
const unseenCount = createMemo(() => notification.project.unseenCount(props.project.worktree))
|
||||
const hasError = createMemo(() => notification.project.unseenHasError(props.project.worktree))
|
||||
const dirs = createMemo(() => [props.project.worktree, ...(props.project.sandboxes ?? [])])
|
||||
const unseenCount = createMemo(() =>
|
||||
dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0),
|
||||
)
|
||||
const hasError = createMemo(() => dirs().some((directory) => notification.project.unseenHasError(directory)))
|
||||
const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
|
||||
return (
|
||||
<div class={`relative size-8 shrink-0 rounded ${props.class ?? ""}`}>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useNavigate, useParams } from "@solidjs/router"
|
||||
import { createEffect, createMemo, For, Show, type Accessor, type JSX } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createSortable } from "@thisbeyond/solid-dnd"
|
||||
import { createMediaQuery } from "@solid-primitives/media"
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
@@ -114,7 +115,8 @@ export const SortableWorkspace = (props: {
|
||||
const busy = createMemo(() => props.ctx.isBusy(props.directory))
|
||||
const wasBusy = createMemo((prev) => prev || busy(), false)
|
||||
const loading = createMemo(() => open() && !booted() && sessions().length === 0 && !wasBusy())
|
||||
const showNew = createMemo(() => !loading() && (sessions().length === 0 || (active() && !params.id)))
|
||||
const touch = createMediaQuery("(hover: none)")
|
||||
const showNew = createMemo(() => !loading() && (touch() || sessions().length === 0 || (active() && !params.id)))
|
||||
const loadMore = async () => {
|
||||
setWorkspaceStore("limit", (limit) => limit + 5)
|
||||
await globalSync.project.loadSessions(props.directory)
|
||||
@@ -167,11 +169,9 @@ export const SortableWorkspace = (props: {
|
||||
openOnDblClick={false}
|
||||
/>
|
||||
</Show>
|
||||
<Icon
|
||||
name={open() ? "chevron-down" : "chevron-right"}
|
||||
size="small"
|
||||
class="shrink-0 text-icon-base opacity-0 transition-opacity group-hover/workspace:opacity-100 group-focus-within/workspace:opacity-100"
|
||||
/>
|
||||
<div class="flex items-center justify-center shrink-0 overflow-hidden w-0 opacity-0 transition-all duration-200 group-hover/workspace:w-3.5 group-hover/workspace:opacity-100 group-focus-within/workspace:w-3.5 group-focus-within/workspace:opacity-100">
|
||||
<Icon name={open() ? "chevron-down" : "chevron-right"} size="small" class="text-icon-base" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -196,7 +196,9 @@ export const SortableWorkspace = (props: {
|
||||
when={workspaceEditActive()}
|
||||
fallback={
|
||||
<Collapsible.Trigger
|
||||
class="flex items-center justify-between w-full pl-2 pr-16 py-1.5 rounded-md hover:bg-surface-raised-base-hover"
|
||||
class={`flex items-center justify-between w-full pl-2 py-1.5 rounded-md hover:bg-surface-raised-base-hover transition-[padding] duration-200 ${
|
||||
menu.open ? "pr-16" : "pr-2"
|
||||
} group-hover/workspace:pr-16 group-focus-within/workspace:pr-16`}
|
||||
data-action="workspace-toggle"
|
||||
data-workspace={base64Encode(props.directory)}
|
||||
>
|
||||
@@ -204,7 +206,13 @@ export const SortableWorkspace = (props: {
|
||||
</Collapsible.Trigger>
|
||||
}
|
||||
>
|
||||
<div class="flex items-center justify-between w-full pl-2 pr-16 py-1.5 rounded-md">{header()}</div>
|
||||
<div
|
||||
class={`flex items-center justify-between w-full pl-2 py-1.5 rounded-md transition-[padding] duration-200 ${
|
||||
menu.open ? "pr-16" : "pr-2"
|
||||
} group-hover/workspace:pr-16 group-focus-within/workspace:pr-16`}
|
||||
>
|
||||
{header()}
|
||||
</div>
|
||||
</Show>
|
||||
<div
|
||||
class="absolute right-1 top-1/2 -translate-y-1/2 flex items-center gap-0.5 transition-opacity"
|
||||
@@ -264,23 +272,25 @@ export const SortableWorkspace = (props: {
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
<Tooltip value={language.t("command.session.new")} placement="top">
|
||||
<IconButton
|
||||
icon="plus-small"
|
||||
variant="ghost"
|
||||
class="size-6 rounded-md opacity-0 pointer-events-none group-hover/workspace:opacity-100 group-hover/workspace:pointer-events-auto group-focus-within/workspace:opacity-100 group-focus-within/workspace:pointer-events-auto"
|
||||
data-action="workspace-new-session"
|
||||
data-workspace={base64Encode(props.directory)}
|
||||
aria-label={language.t("command.session.new")}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
props.ctx.setHoverSession(undefined)
|
||||
props.ctx.clearHoverProjectSoon()
|
||||
navigate(`/${slug()}/session`)
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Show when={!touch()}>
|
||||
<Tooltip value={language.t("command.session.new")} placement="top">
|
||||
<IconButton
|
||||
icon="plus-small"
|
||||
variant="ghost"
|
||||
class="size-6 rounded-md opacity-0 pointer-events-none group-hover/workspace:opacity-100 group-hover/workspace:pointer-events-auto group-focus-within/workspace:opacity-100 group-focus-within/workspace:pointer-events-auto"
|
||||
data-action="workspace-new-session"
|
||||
data-workspace={base64Encode(props.directory)}
|
||||
aria-label={language.t("command.session.new")}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
props.ctx.setHoverSession(undefined)
|
||||
props.ctx.clearHoverProjectSoon()
|
||||
navigate(`/${slug()}/session`)
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -591,7 +591,7 @@ export default function Page() {
|
||||
const newSessionWorktree = createMemo(() => {
|
||||
if (store.newSessionWorktree === "create") return "create"
|
||||
const project = sync.project
|
||||
if (project && sync.data.path.directory !== project.worktree) return sync.data.path.directory
|
||||
if (project && sdk.directory !== project.worktree) return sdk.directory
|
||||
return "main"
|
||||
})
|
||||
|
||||
@@ -1647,7 +1647,7 @@ export default function Page() {
|
||||
|
||||
const target = value === "main" ? sync.project?.worktree : value
|
||||
if (!target) return
|
||||
if (target === sync.data.path.directory) return
|
||||
if (target === sdk.directory) return
|
||||
layout.projects.open(target)
|
||||
navigate(`/${base64Encode(target)}/session`)
|
||||
}}
|
||||
|
||||
@@ -179,7 +179,7 @@ export function MessageTimeline(props: {
|
||||
"sticky top-0 z-30 bg-background-stronger": true,
|
||||
"w-full": true,
|
||||
"px-4 md:px-6": true,
|
||||
"md:max-w-200 md:mx-auto 3xl:max-w-[1200px]": props.centered,
|
||||
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
||||
}}
|
||||
>
|
||||
<div class="h-10 w-full flex items-center justify-between gap-2">
|
||||
@@ -278,7 +278,7 @@ export function MessageTimeline(props: {
|
||||
class="flex flex-col gap-12 items-start justify-start pb-[calc(var(--prompt-height,8rem)+64px)] md:pb-[calc(var(--prompt-height,10rem)+64px)] transition-[margin]"
|
||||
classList={{
|
||||
"w-full": true,
|
||||
"md:max-w-200 md:mx-auto 3xl:max-w-[1200px]": props.centered,
|
||||
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
||||
"mt-0.5": props.centered,
|
||||
"mt-0": !props.centered,
|
||||
}}
|
||||
@@ -321,7 +321,7 @@ export function MessageTimeline(props: {
|
||||
}}
|
||||
classList={{
|
||||
"min-w-0 w-full max-w-full": true,
|
||||
"md:max-w-200 3xl:max-w-[1200px]": props.centered,
|
||||
"md:max-w-200 2xl:max-w-[1000px]": props.centered,
|
||||
}}
|
||||
>
|
||||
<SessionTurn
|
||||
|
||||
@@ -31,7 +31,7 @@ export function SessionPromptDock(props: {
|
||||
<div
|
||||
classList={{
|
||||
"w-full px-4 pointer-events-auto": true,
|
||||
"md:max-w-200 md:mx-auto 3xl:max-w-[1200px]": props.centered,
|
||||
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
||||
}}
|
||||
>
|
||||
<Show when={props.questionRequest()} keyed>
|
||||
|
||||
@@ -351,8 +351,8 @@ export const dict = {
|
||||
"changelog.empty":
|
||||
"\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0639\u062b\u0648\u0631 \u0639\u0644\u0649 \u0623\u064a \u0625\u062f\u062e\u0627\u0644\u0627\u062a \u0641\u064a \u0633\u062c\u0644 \u0627\u0644\u062a\u063a\u064a\u064a\u0631\u0627\u062a.",
|
||||
"changelog.viewJson": "\u0639\u0631\u0636 JSON",
|
||||
"workspace.nav.zen": "زين",
|
||||
"workspace.nav.apiKeys": "API المفاتيح",
|
||||
"workspace.nav.zen": "Zen",
|
||||
"workspace.nav.apiKeys": "مفاتيح API",
|
||||
"workspace.nav.members": "أعضاء",
|
||||
"workspace.nav.billing": "الفواتير",
|
||||
"workspace.nav.settings": "إعدادات",
|
||||
@@ -365,14 +365,14 @@ export const dict = {
|
||||
"workspace.newUser.feature.quality.title": "أعلى جودة",
|
||||
"workspace.newUser.feature.quality.body":
|
||||
"الوصول إلى النماذج التي تم تكوينها لتحقيق الأداء الأمثل - لا يوجد تخفيضات أو توجيه إلى موفري الخدمة الأرخص.",
|
||||
"workspace.newUser.feature.lockin.title": "لا يوجد قفل",
|
||||
"workspace.newUser.feature.lockin.title": "بدون احتجاز بمزوّد واحد",
|
||||
"workspace.newUser.feature.lockin.body":
|
||||
"استخدم Zen مع أي وكيل ترميز، واستمر في استخدام موفري الخدمات الآخرين مع opencode وقتما تشاء.",
|
||||
"workspace.newUser.copyApiKey": "انسخ مفتاح API",
|
||||
"workspace.newUser.copyKey": "نسخ المفتاح",
|
||||
"workspace.newUser.copied": "منسوخ!",
|
||||
"workspace.newUser.step.enableBilling": "تمكين الفوترة",
|
||||
"workspace.newUser.step.login.before": "يجري",
|
||||
"workspace.newUser.step.login.before": "شغّل",
|
||||
"workspace.newUser.step.login.after": "وحدد opencode",
|
||||
"workspace.newUser.step.pasteKey": "الصق مفتاح API الخاص بك",
|
||||
"workspace.newUser.step.models.before": "ابدأ opencode ثم قم بالتشغيل",
|
||||
@@ -390,7 +390,7 @@ export const dict = {
|
||||
"workspace.providers.saving": "توفير...",
|
||||
"workspace.providers.save": "يحفظ",
|
||||
"workspace.providers.table.provider": "مزود",
|
||||
"workspace.providers.table.apiKey": "API المفتاح",
|
||||
"workspace.providers.table.apiKey": "مفتاح API",
|
||||
"workspace.usage.title": "تاريخ الاستخدام",
|
||||
"workspace.usage.subtitle": "استخدام وتكاليف API الأخيرة.",
|
||||
"workspace.usage.empty": "قم بإجراء أول مكالمة API للبدء.",
|
||||
@@ -398,25 +398,25 @@ export const dict = {
|
||||
"workspace.usage.table.model": "نموذج",
|
||||
"workspace.usage.table.input": "مدخل",
|
||||
"workspace.usage.table.output": "الإخراج",
|
||||
"workspace.usage.table.cost": "يكلف",
|
||||
"workspace.usage.table.cost": "التكلفة",
|
||||
"workspace.usage.breakdown.input": "مدخل",
|
||||
"workspace.usage.breakdown.cacheRead": "قراءة ذاكرة التخزين المؤقت",
|
||||
"workspace.usage.breakdown.cacheWrite": "كتابة ذاكرة التخزين المؤقت",
|
||||
"workspace.usage.breakdown.output": "الإخراج",
|
||||
"workspace.usage.breakdown.reasoning": "المنطق",
|
||||
"workspace.usage.subscription": "الاشتراك (${{amount}})",
|
||||
"workspace.cost.title": "يكلف",
|
||||
"workspace.cost.title": "التكلفة",
|
||||
"workspace.cost.subtitle": "تكاليف الاستخدام مقسمة حسب النموذج.",
|
||||
"workspace.cost.allModels": "جميع الموديلات",
|
||||
"workspace.cost.allModels": "جميع النماذج",
|
||||
"workspace.cost.allKeys": "جميع المفاتيح",
|
||||
"workspace.cost.deletedSuffix": "(محذوف)",
|
||||
"workspace.cost.empty": "لا توجد بيانات استخدام متاحة للفترة المحددة.",
|
||||
"workspace.cost.subscriptionShort": "الفرعية",
|
||||
"workspace.keys.title": "API المفاتيح",
|
||||
"workspace.cost.subscriptionShort": "اشتراك",
|
||||
"workspace.keys.title": "مفاتيح API",
|
||||
"workspace.keys.subtitle": "إدارة مفاتيح API الخاصة بك للوصول إلى خدمات opencode.",
|
||||
"workspace.keys.create": "قم بإنشاء مفتاح API",
|
||||
"workspace.keys.placeholder": "أدخل اسم المفتاح",
|
||||
"workspace.keys.empty": "قم بإنشاء مفتاح opencode للبوابة API",
|
||||
"workspace.keys.empty": "أنشئ مفتاح API لبوابة opencode",
|
||||
"workspace.keys.table.name": "اسم",
|
||||
"workspace.keys.table.key": "مفتاح",
|
||||
"workspace.keys.table.createdBy": "تم الإنشاء بواسطة",
|
||||
@@ -442,14 +442,14 @@ export const dict = {
|
||||
"workspace.members.table.email": "بريد إلكتروني",
|
||||
"workspace.members.table.role": "دور",
|
||||
"workspace.members.table.monthLimit": "حد الشهر",
|
||||
"workspace.members.role.admin": "مسؤل",
|
||||
"workspace.members.role.admin": "مسؤول",
|
||||
"workspace.members.role.adminDescription": "يمكن إدارة النماذج، والأعضاء، والفواتير",
|
||||
"workspace.members.role.member": "عضو",
|
||||
"workspace.members.role.memberDescription": "يمكنهم فقط إنشاء مفاتيح API لأنفسهم",
|
||||
"workspace.settings.title": "إعدادات",
|
||||
"workspace.settings.subtitle": "قم بتحديث اسم مساحة العمل الخاصة بك وتفضيلاتك.",
|
||||
"workspace.settings.workspaceName": "اسم مساحة العمل",
|
||||
"workspace.settings.defaultName": "تقصير",
|
||||
"workspace.settings.defaultName": "الافتراضي",
|
||||
"workspace.settings.updating": "جارٍ التحديث...",
|
||||
"workspace.settings.save": "يحفظ",
|
||||
"workspace.settings.edit": "يحرر",
|
||||
@@ -461,37 +461,37 @@ export const dict = {
|
||||
"workspace.billing.add": "أضف $",
|
||||
"workspace.billing.enterAmount": "أدخل المبلغ",
|
||||
"workspace.billing.loading": "تحميل...",
|
||||
"workspace.billing.addAction": "يضيف",
|
||||
"workspace.billing.addAction": "إضافة",
|
||||
"workspace.billing.addBalance": "إضافة الرصيد",
|
||||
"workspace.billing.linkedToStripe": "مرتبطة بالشريط",
|
||||
"workspace.billing.manage": "يدير",
|
||||
"workspace.billing.linkedToStripe": "مرتبط بـ Stripe",
|
||||
"workspace.billing.manage": "إدارة",
|
||||
"workspace.billing.enable": "تمكين الفوترة",
|
||||
"workspace.monthlyLimit.title": "الحد الشهري",
|
||||
"workspace.monthlyLimit.subtitle": "قم بتعيين حد الاستخدام الشهري لحسابك.",
|
||||
"workspace.monthlyLimit.placeholder": "50",
|
||||
"workspace.monthlyLimit.setting": "جلسة...",
|
||||
"workspace.monthlyLimit.setting": "جارٍ التعيين...",
|
||||
"workspace.monthlyLimit.set": "تعيين",
|
||||
"workspace.monthlyLimit.edit": "تحرير الحد",
|
||||
"workspace.monthlyLimit.noLimit": "لم يتم تعيين حد الاستخدام.",
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "الاستخدام الحالي ل",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "هو $",
|
||||
"workspace.reload.title": "إعادة التحميل التلقائي",
|
||||
"workspace.reload.disabled.before": "إعادة التحميل التلقائي هو",
|
||||
"workspace.reload.disabled.state": "عاجز",
|
||||
"workspace.reload.disabled.after": "تمكين إعادة التحميل تلقائيًا عندما يكون الرصيد منخفضًا.",
|
||||
"workspace.reload.enabled.before": "إعادة التحميل التلقائي هو",
|
||||
"workspace.reload.title": "إعادة الشحن التلقائي",
|
||||
"workspace.reload.disabled.before": "إعادة الشحن التلقائي",
|
||||
"workspace.reload.disabled.state": "معطّل",
|
||||
"workspace.reload.disabled.after": "فعّلها لإعادة شحن الرصيد تلقائيًا عندما يكون منخفضًا.",
|
||||
"workspace.reload.enabled.before": "إعادة الشحن التلقائي",
|
||||
"workspace.reload.enabled.state": "ممكّن",
|
||||
"workspace.reload.enabled.middle": "سنقوم بإعادة التحميل",
|
||||
"workspace.reload.enabled.middle": "سنعيد شحن رصيدك بمبلغ",
|
||||
"workspace.reload.processingFee": "رسوم المعالجة",
|
||||
"workspace.reload.enabled.after": "عندما يصل التوازن",
|
||||
"workspace.reload.enabled.after": "عندما يصل الرصيد إلى",
|
||||
"workspace.reload.edit": "يحرر",
|
||||
"workspace.reload.enable": "يُمكَِن",
|
||||
"workspace.reload.enableAutoReload": "تمكين إعادة التحميل التلقائي",
|
||||
"workspace.reload.reloadAmount": "إعادة تحميل $",
|
||||
"workspace.reload.enable": "تفعيل",
|
||||
"workspace.reload.enableAutoReload": "تفعيل إعادة الشحن التلقائي",
|
||||
"workspace.reload.reloadAmount": "مبلغ إعادة الشحن $",
|
||||
"workspace.reload.whenBalanceReaches": "عندما يصل الرصيد إلى $",
|
||||
"workspace.reload.saving": "توفير...",
|
||||
"workspace.reload.save": "يحفظ",
|
||||
"workspace.reload.failedAt": "فشلت عملية إعادة التحميل عند",
|
||||
"workspace.reload.failedAt": "فشلت إعادة الشحن في",
|
||||
"workspace.reload.reason": "سبب:",
|
||||
"workspace.reload.updatePaymentMethod": "يرجى تحديث طريقة الدفع الخاصة بك والمحاولة مرة أخرى.",
|
||||
"workspace.reload.retrying": "جارٍ إعادة المحاولة...",
|
||||
@@ -500,11 +500,11 @@ export const dict = {
|
||||
"workspace.payments.subtitle": "معاملات الدفع الأخيرة.",
|
||||
"workspace.payments.table.date": "تاريخ",
|
||||
"workspace.payments.table.paymentId": "معرف الدفع",
|
||||
"workspace.payments.table.amount": "كمية",
|
||||
"workspace.payments.table.amount": "المبلغ",
|
||||
"workspace.payments.table.receipt": "إيصال",
|
||||
"workspace.payments.type.credit": "ائتمان",
|
||||
"workspace.payments.type.subscription": "الاشتراك",
|
||||
"workspace.payments.view": "منظر",
|
||||
"workspace.payments.view": "عرض",
|
||||
"workspace.black.loading": "تحميل...",
|
||||
"workspace.black.time.day": "يوم",
|
||||
"workspace.black.time.days": "أيام",
|
||||
@@ -521,8 +521,8 @@ export const dict = {
|
||||
"workspace.black.subscription.resetsIn": "إعادة تعيين في",
|
||||
"workspace.black.subscription.useBalance": "استخدم رصيدك المتوفر بعد الوصول إلى حدود الاستخدام",
|
||||
"workspace.black.waitlist.title": "قائمة الانتظار",
|
||||
"workspace.black.waitlist.joined": "أنت على قائمة الانتظار للخطة السوداء {{plan}} دولار شهريًا OpenCode.",
|
||||
"workspace.black.waitlist.ready": "نحن على استعداد لتسجيلك في خطة Black {{plan}} الشهرية OpenCode.",
|
||||
"workspace.black.waitlist.joined": "أنت على قائمة الانتظار لخطة OpenCode Black بقيمة ${{plan}} شهريًا.",
|
||||
"workspace.black.waitlist.ready": "نحن مستعدون لتسجيلك في خطة OpenCode Black بقيمة ${{plan}} شهريًا.",
|
||||
"workspace.black.waitlist.leave": "ترك قائمة الانتظار",
|
||||
"workspace.black.waitlist.leaving": "مغادرة...",
|
||||
"workspace.black.waitlist.left": "غادر",
|
||||
|
||||
@@ -294,18 +294,18 @@ export const dict = {
|
||||
"workspace.home.billing.currentBalance": "Nuværende saldo",
|
||||
"workspace.newUser.feature.tested.title": "Testede og verificerede modeller",
|
||||
"workspace.newUser.feature.tested.body":
|
||||
"Vi har benchmarket og testet modeller specifikt til kodningsmidler for at sikre den bedste ydeevne.",
|
||||
"Vi har benchmarket og testet modeller specifikt til kodningsagenter for at sikre den bedste ydeevne.",
|
||||
"workspace.newUser.feature.quality.title": "Højeste kvalitet",
|
||||
"workspace.newUser.feature.quality.body":
|
||||
"Få adgang til modeller konfigureret til optimal ydeevne - ingen nedgraderinger eller routing til billigere udbydere.",
|
||||
"workspace.newUser.feature.lockin.title": "Ingen indlåsning",
|
||||
"workspace.newUser.feature.lockin.body":
|
||||
"Brug Zen med en hvilken som helst kodningsagent, og fortsæt med at bruge andre udbydere med opencode, når du vil.",
|
||||
"workspace.newUser.copyApiKey": "Kopiér nøglen API",
|
||||
"workspace.newUser.copyApiKey": "Kopiér API-nøgle",
|
||||
"workspace.newUser.copyKey": "Kopier nøgle",
|
||||
"workspace.newUser.copied": "Kopieret!",
|
||||
"workspace.newUser.step.enableBilling": "Aktiver fakturering",
|
||||
"workspace.newUser.step.login.before": "Løbe",
|
||||
"workspace.newUser.step.login.before": "Kør",
|
||||
"workspace.newUser.step.login.after": "og vælg opencode",
|
||||
"workspace.newUser.step.pasteKey": "Indsæt din API nøgle",
|
||||
"workspace.newUser.step.models.before": "Start opencode og kør",
|
||||
@@ -316,12 +316,12 @@ export const dict = {
|
||||
"workspace.models.table.enabled": "Aktiveret",
|
||||
"workspace.providers.title": "Medbring din egen nøgle",
|
||||
"workspace.providers.subtitle": "Konfigurer dine egne API nøgler fra AI-udbydere.",
|
||||
"workspace.providers.placeholder": "Indtast nøglen {{provider}} API ({{prefix}}...)",
|
||||
"workspace.providers.placeholder": "Indtast {{provider}} API-nøgle ({{prefix}}...)",
|
||||
"workspace.providers.configure": "Konfigurer",
|
||||
"workspace.providers.edit": "Redigere",
|
||||
"workspace.providers.edit": "Rediger",
|
||||
"workspace.providers.delete": "Slet",
|
||||
"workspace.providers.saving": "Gemmer...",
|
||||
"workspace.providers.save": "Spare",
|
||||
"workspace.providers.save": "Gem",
|
||||
"workspace.providers.table.provider": "Udbyder",
|
||||
"workspace.providers.table.apiKey": "API Nøgle",
|
||||
"workspace.usage.title": "Brugshistorik",
|
||||
@@ -330,15 +330,15 @@ export const dict = {
|
||||
"workspace.usage.table.date": "Dato",
|
||||
"workspace.usage.table.model": "Model",
|
||||
"workspace.usage.table.input": "Input",
|
||||
"workspace.usage.table.output": "Produktion",
|
||||
"workspace.usage.table.cost": "Koste",
|
||||
"workspace.usage.table.output": "Output",
|
||||
"workspace.usage.table.cost": "Omkostning",
|
||||
"workspace.usage.breakdown.input": "Input",
|
||||
"workspace.usage.breakdown.cacheRead": "Cache læst",
|
||||
"workspace.usage.breakdown.cacheWrite": "Cache skriv",
|
||||
"workspace.usage.breakdown.output": "Produktion",
|
||||
"workspace.usage.breakdown.output": "Output",
|
||||
"workspace.usage.breakdown.reasoning": "Ræsonnement",
|
||||
"workspace.usage.subscription": "abonnement (${{amount}})",
|
||||
"workspace.cost.title": "Koste",
|
||||
"workspace.cost.title": "Omkostninger",
|
||||
"workspace.cost.subtitle": "Brugsomkostninger opdelt efter model.",
|
||||
"workspace.cost.allModels": "Alle modeller",
|
||||
"workspace.cost.allKeys": "Alle nøgler",
|
||||
@@ -354,7 +354,7 @@ export const dict = {
|
||||
"workspace.keys.table.key": "Nøgle",
|
||||
"workspace.keys.table.createdBy": "Skabt af",
|
||||
"workspace.keys.table.lastUsed": "Sidst brugt",
|
||||
"workspace.keys.copyApiKey": "Kopiér nøglen API",
|
||||
"workspace.keys.copyApiKey": "Kopiér API-nøgle",
|
||||
"workspace.keys.delete": "Slet",
|
||||
"workspace.members.title": "Medlemmer",
|
||||
"workspace.members.subtitle": "Administrer arbejdsområdemedlemmer og deres tilladelser.",
|
||||
@@ -368,10 +368,10 @@ export const dict = {
|
||||
"workspace.members.noLimit": "Ingen grænse",
|
||||
"workspace.members.noLimitLowercase": "ingen grænse",
|
||||
"workspace.members.invited": "inviteret",
|
||||
"workspace.members.edit": "Redigere",
|
||||
"workspace.members.edit": "Rediger",
|
||||
"workspace.members.delete": "Slet",
|
||||
"workspace.members.saving": "Gemmer...",
|
||||
"workspace.members.save": "Spare",
|
||||
"workspace.members.save": "Gem",
|
||||
"workspace.members.table.email": "E-mail",
|
||||
"workspace.members.table.role": "Rolle",
|
||||
"workspace.members.table.monthLimit": "Månedsgrænse",
|
||||
@@ -382,10 +382,10 @@ export const dict = {
|
||||
"workspace.settings.title": "Indstillinger",
|
||||
"workspace.settings.subtitle": "Opdater dit arbejdsområdes navn og præferencer.",
|
||||
"workspace.settings.workspaceName": "Arbejdsområdets navn",
|
||||
"workspace.settings.defaultName": "Misligholdelse",
|
||||
"workspace.settings.defaultName": "Standard",
|
||||
"workspace.settings.updating": "Opdaterer...",
|
||||
"workspace.settings.save": "Spare",
|
||||
"workspace.settings.edit": "Redigere",
|
||||
"workspace.settings.save": "Gem",
|
||||
"workspace.settings.edit": "Rediger",
|
||||
"workspace.billing.title": "Fakturering",
|
||||
"workspace.billing.subtitle.beforeLink": "Administrer betalingsmetoder.",
|
||||
"workspace.billing.contactUs": "Kontakt os",
|
||||
@@ -394,10 +394,10 @@ export const dict = {
|
||||
"workspace.billing.add": "Tilføj $",
|
||||
"workspace.billing.enterAmount": "Indtast beløb",
|
||||
"workspace.billing.loading": "Indlæser...",
|
||||
"workspace.billing.addAction": "Tilføje",
|
||||
"workspace.billing.addAction": "Tilføj",
|
||||
"workspace.billing.addBalance": "Tilføj balance",
|
||||
"workspace.billing.linkedToStripe": "Forbundet til Stripe",
|
||||
"workspace.billing.manage": "Styre",
|
||||
"workspace.billing.manage": "Administrer",
|
||||
"workspace.billing.enable": "Aktiver fakturering",
|
||||
"workspace.monthlyLimit.title": "Månedlig grænse",
|
||||
"workspace.monthlyLimit.subtitle": "Indstil en månedlig forbrugsgrænse for din konto.",
|
||||
@@ -408,23 +408,23 @@ export const dict = {
|
||||
"workspace.monthlyLimit.noLimit": "Ingen forbrugsgrænse angivet.",
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Nuværende brug for",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "er $",
|
||||
"workspace.reload.title": "Automatisk genindlæsning",
|
||||
"workspace.reload.disabled.before": "Automatisk genindlæsning er",
|
||||
"workspace.reload.disabled.state": "handicappet",
|
||||
"workspace.reload.disabled.after": "Aktiver for automatisk at genindlæse, når balancen er lav.",
|
||||
"workspace.reload.enabled.before": "Automatisk genindlæsning er",
|
||||
"workspace.reload.title": "Automatisk genopfyldning",
|
||||
"workspace.reload.disabled.before": "Automatisk genopfyldning er",
|
||||
"workspace.reload.disabled.state": "deaktiveret",
|
||||
"workspace.reload.disabled.after": "Aktiver for automatisk at genopfylde, når saldoen er lav.",
|
||||
"workspace.reload.enabled.before": "Automatisk genopfyldning er",
|
||||
"workspace.reload.enabled.state": "aktiveret",
|
||||
"workspace.reload.enabled.middle": "Vi genindlæser",
|
||||
"workspace.reload.enabled.middle": "Vi genopfylder",
|
||||
"workspace.reload.processingFee": "ekspeditionsgebyr",
|
||||
"workspace.reload.enabled.after": "når balancen er nået",
|
||||
"workspace.reload.edit": "Redigere",
|
||||
"workspace.reload.edit": "Rediger",
|
||||
"workspace.reload.enable": "Aktiver",
|
||||
"workspace.reload.enableAutoReload": "Aktiver automatisk genindlæsning",
|
||||
"workspace.reload.reloadAmount": "Genindlæs $",
|
||||
"workspace.reload.enableAutoReload": "Aktiver automatisk genopfyldning",
|
||||
"workspace.reload.reloadAmount": "Genopfyld $",
|
||||
"workspace.reload.whenBalanceReaches": "Når saldoen når $",
|
||||
"workspace.reload.saving": "Gemmer...",
|
||||
"workspace.reload.save": "Spare",
|
||||
"workspace.reload.failedAt": "Genindlæsning mislykkedes kl",
|
||||
"workspace.reload.save": "Gem",
|
||||
"workspace.reload.failedAt": "Genopfyldning mislykkedes kl",
|
||||
"workspace.reload.reason": "Årsag:",
|
||||
"workspace.reload.updatePaymentMethod": "Opdater din betalingsmetode, og prøv igen.",
|
||||
"workspace.reload.retrying": "Prøver igen...",
|
||||
@@ -434,10 +434,10 @@ export const dict = {
|
||||
"workspace.payments.table.date": "Dato",
|
||||
"workspace.payments.table.paymentId": "Betalings-id",
|
||||
"workspace.payments.table.amount": "Beløb",
|
||||
"workspace.payments.table.receipt": "Modtagelse",
|
||||
"workspace.payments.table.receipt": "Kvittering",
|
||||
"workspace.payments.type.credit": "kredit",
|
||||
"workspace.payments.type.subscription": "abonnement",
|
||||
"workspace.payments.view": "Udsigt",
|
||||
"workspace.payments.view": "Vis",
|
||||
"workspace.black.loading": "Indlæser...",
|
||||
"workspace.black.time.day": "dag",
|
||||
"workspace.black.time.days": "dage",
|
||||
@@ -458,8 +458,8 @@ export const dict = {
|
||||
"workspace.black.waitlist.ready": "Vi er klar til at tilmelde dig ${{plan}} per måned OpenCode Black plan.",
|
||||
"workspace.black.waitlist.leave": "Forlad venteliste",
|
||||
"workspace.black.waitlist.leaving": "Forlader...",
|
||||
"workspace.black.waitlist.left": "Venstre",
|
||||
"workspace.black.waitlist.enroll": "Indskrive",
|
||||
"workspace.black.waitlist.left": "Forladt",
|
||||
"workspace.black.waitlist.enroll": "Tilmeld",
|
||||
"workspace.black.waitlist.enrolling": "Tilmelder...",
|
||||
"workspace.black.waitlist.enrolled": "Tilmeldt",
|
||||
"workspace.black.waitlist.enrollNote":
|
||||
|
||||
@@ -306,27 +306,27 @@ export const dict = {
|
||||
"workspace.newUser.feature.lockin.title": "Kein Lock-in",
|
||||
"workspace.newUser.feature.lockin.body":
|
||||
"Verwenden Sie Zen mit einem beliebigen Codierungsagenten und nutzen Sie weiterhin andere Anbieter mit opencode, wann immer Sie möchten.",
|
||||
"workspace.newUser.copyApiKey": "Kopieren Sie den Schlüssel API",
|
||||
"workspace.newUser.copyApiKey": "API-Schlüssel kopieren",
|
||||
"workspace.newUser.copyKey": "Schlüssel kopieren",
|
||||
"workspace.newUser.copied": "Kopiert!",
|
||||
"workspace.newUser.step.enableBilling": "Abrechnung aktivieren",
|
||||
"workspace.newUser.step.login.before": "Laufen",
|
||||
"workspace.newUser.step.login.before": "Führe",
|
||||
"workspace.newUser.step.login.after": "und wählen Sie opencode",
|
||||
"workspace.newUser.step.pasteKey": "Fügen Sie Ihren API-Schlüssel ein",
|
||||
"workspace.newUser.step.models.before": "Starten Sie opencode und führen Sie es aus",
|
||||
"workspace.newUser.step.models.before": "Starte opencode und führe",
|
||||
"workspace.newUser.step.models.after": "um ein Modell auszuwählen",
|
||||
"workspace.models.title": "Modelle",
|
||||
"workspace.models.subtitle.beforeLink":
|
||||
"Verwalten Sie, auf welche Modelle Arbeitsbereichsmitglieder zugreifen können.",
|
||||
"workspace.models.table.model": "Modell",
|
||||
"workspace.models.table.enabled": "Ermöglicht",
|
||||
"workspace.models.table.enabled": "Aktiviert",
|
||||
"workspace.providers.title": "Bringen Sie Ihren eigenen Schlüssel mit",
|
||||
"workspace.providers.subtitle": "Konfigurieren Sie Ihre eigenen API-Schlüssel von KI-Anbietern.",
|
||||
"workspace.providers.placeholder": "Geben Sie den Schlüssel {{provider}} API ein ({{prefix}}...)",
|
||||
"workspace.providers.configure": "Konfigurieren",
|
||||
"workspace.providers.edit": "Bearbeiten",
|
||||
"workspace.providers.delete": "Löschen",
|
||||
"workspace.providers.saving": "Sparen...",
|
||||
"workspace.providers.saving": "Wird gespeichert...",
|
||||
"workspace.providers.save": "Speichern",
|
||||
"workspace.providers.table.provider": "Anbieter",
|
||||
"workspace.providers.table.apiKey": "API-Schlüssel",
|
||||
@@ -335,14 +335,14 @@ export const dict = {
|
||||
"workspace.usage.empty": "Machen Sie Ihren ersten API-Aufruf, um loszulegen.",
|
||||
"workspace.usage.table.date": "Datum",
|
||||
"workspace.usage.table.model": "Modell",
|
||||
"workspace.usage.table.input": "Eingang",
|
||||
"workspace.usage.table.output": "Ausgabe",
|
||||
"workspace.usage.table.input": "Input",
|
||||
"workspace.usage.table.output": "Output",
|
||||
"workspace.usage.table.cost": "Kosten",
|
||||
"workspace.usage.breakdown.input": "Eingang",
|
||||
"workspace.usage.breakdown.input": "Input",
|
||||
"workspace.usage.breakdown.cacheRead": "Cache-Lesen",
|
||||
"workspace.usage.breakdown.cacheWrite": "Cache-Schreiben",
|
||||
"workspace.usage.breakdown.output": "Ausgabe",
|
||||
"workspace.usage.breakdown.reasoning": "Argumentation",
|
||||
"workspace.usage.breakdown.output": "Output",
|
||||
"workspace.usage.breakdown.reasoning": "Reasoning",
|
||||
"workspace.usage.subscription": "Abonnement (${{amount}})",
|
||||
"workspace.cost.title": "Kosten",
|
||||
"workspace.cost.subtitle": "Nutzungskosten aufgeschlüsselt nach Modell.",
|
||||
@@ -360,12 +360,12 @@ export const dict = {
|
||||
"workspace.keys.table.key": "Schlüssel",
|
||||
"workspace.keys.table.createdBy": "Erstellt von",
|
||||
"workspace.keys.table.lastUsed": "Zuletzt verwendet",
|
||||
"workspace.keys.copyApiKey": "Kopieren Sie den Schlüssel API",
|
||||
"workspace.keys.copyApiKey": "API-Schlüssel kopieren",
|
||||
"workspace.keys.delete": "Löschen",
|
||||
"workspace.members.title": "Mitglieder",
|
||||
"workspace.members.subtitle": "Verwalten Sie Arbeitsbereichsmitglieder und ihre Berechtigungen.",
|
||||
"workspace.members.invite": "Mitglied einladen",
|
||||
"workspace.members.inviting": "Einladend...",
|
||||
"workspace.members.inviting": "Wird eingeladen...",
|
||||
"workspace.members.beta.beforeLink": "Während der Betaversion sind Arbeitsbereiche für Teams kostenlos.",
|
||||
"workspace.members.form.invitee": "Eingeladen",
|
||||
"workspace.members.form.emailPlaceholder": "Geben Sie Ihre E-Mail-Adresse ein",
|
||||
@@ -376,7 +376,7 @@ export const dict = {
|
||||
"workspace.members.invited": "eingeladen",
|
||||
"workspace.members.edit": "Bearbeiten",
|
||||
"workspace.members.delete": "Löschen",
|
||||
"workspace.members.saving": "Sparen...",
|
||||
"workspace.members.saving": "Wird gespeichert...",
|
||||
"workspace.members.save": "Speichern",
|
||||
"workspace.members.table.email": "E-Mail",
|
||||
"workspace.members.table.role": "Rolle",
|
||||
@@ -408,30 +408,30 @@ export const dict = {
|
||||
"workspace.monthlyLimit.title": "Monatliches Limit",
|
||||
"workspace.monthlyLimit.subtitle": "Legen Sie ein monatliches Nutzungslimit für Ihr Konto fest.",
|
||||
"workspace.monthlyLimit.placeholder": "50",
|
||||
"workspace.monthlyLimit.setting": "Einstellung...",
|
||||
"workspace.monthlyLimit.set": "Satz",
|
||||
"workspace.monthlyLimit.setting": "Wird gesetzt...",
|
||||
"workspace.monthlyLimit.set": "Festlegen",
|
||||
"workspace.monthlyLimit.edit": "Limit bearbeiten",
|
||||
"workspace.monthlyLimit.noLimit": "Kein Nutzungslimit festgelegt.",
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Aktuelle Nutzung für",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "ist $",
|
||||
"workspace.reload.title": "Automatisches Neuladen",
|
||||
"workspace.reload.disabled.before": "Automatisches Nachladen ist",
|
||||
"workspace.reload.title": "Automatische Aufladung",
|
||||
"workspace.reload.disabled.before": "Automatische Aufladung ist",
|
||||
"workspace.reload.disabled.state": "deaktiviert",
|
||||
"workspace.reload.disabled.after":
|
||||
"Aktivieren Sie diese Option, um das Guthaben automatisch neu zu laden, wenn das Guthaben niedrig ist.",
|
||||
"workspace.reload.enabled.before": "Automatisches Nachladen ist",
|
||||
"workspace.reload.enabled.state": "ermöglicht",
|
||||
"workspace.reload.enabled.middle": "Wir laden nach",
|
||||
"Aktivieren Sie diese Option, damit bei niedrigem Kontostand automatisch aufgeladen wird.",
|
||||
"workspace.reload.enabled.before": "Automatische Aufladung ist",
|
||||
"workspace.reload.enabled.state": "aktiviert",
|
||||
"workspace.reload.enabled.middle": "Wir laden auf",
|
||||
"workspace.reload.processingFee": "Bearbeitungsgebühr",
|
||||
"workspace.reload.enabled.after": "wenn das Gleichgewicht erreicht ist",
|
||||
"workspace.reload.enabled.after": "sobald der Kontostand",
|
||||
"workspace.reload.edit": "Bearbeiten",
|
||||
"workspace.reload.enable": "Aktivieren",
|
||||
"workspace.reload.enableAutoReload": "Aktivieren Sie das automatische Neuladen",
|
||||
"workspace.reload.reloadAmount": "$ neu laden",
|
||||
"workspace.reload.whenBalanceReaches": "Wenn der Saldo $ erreicht",
|
||||
"workspace.reload.saving": "Sparen...",
|
||||
"workspace.reload.enableAutoReload": "Automatische Aufladung aktivieren",
|
||||
"workspace.reload.reloadAmount": "Aufladebetrag $",
|
||||
"workspace.reload.whenBalanceReaches": "Wenn der Kontostand $ erreicht",
|
||||
"workspace.reload.saving": "Wird gespeichert...",
|
||||
"workspace.reload.save": "Speichern",
|
||||
"workspace.reload.failedAt": "Neuladen fehlgeschlagen bei",
|
||||
"workspace.reload.failedAt": "Aufladung fehlgeschlagen am",
|
||||
"workspace.reload.reason": "Grund:",
|
||||
"workspace.reload.updatePaymentMethod": "Bitte aktualisieren Sie Ihre Zahlungsmethode und versuchen Sie es erneut.",
|
||||
"workspace.reload.retrying": "Erneuter Versuch...",
|
||||
@@ -440,11 +440,11 @@ export const dict = {
|
||||
"workspace.payments.subtitle": "Letzte Zahlungsvorgänge.",
|
||||
"workspace.payments.table.date": "Datum",
|
||||
"workspace.payments.table.paymentId": "Zahlungs-ID",
|
||||
"workspace.payments.table.amount": "Menge",
|
||||
"workspace.payments.table.amount": "Betrag",
|
||||
"workspace.payments.table.receipt": "Quittung",
|
||||
"workspace.payments.type.credit": "Kredit",
|
||||
"workspace.payments.type.subscription": "Abonnement",
|
||||
"workspace.payments.view": "Sicht",
|
||||
"workspace.payments.view": "Anzeigen",
|
||||
"workspace.black.loading": "Laden...",
|
||||
"workspace.black.time.day": "Tag",
|
||||
"workspace.black.time.days": "Tage",
|
||||
@@ -454,21 +454,21 @@ export const dict = {
|
||||
"workspace.black.time.minutes": "Minuten",
|
||||
"workspace.black.time.fewSeconds": "ein paar Sekunden",
|
||||
"workspace.black.subscription.title": "Abonnement",
|
||||
"workspace.black.subscription.message": "Sie haben OpenCode Black für {{plan}} pro Monat abonniert.",
|
||||
"workspace.black.subscription.message": "Sie haben OpenCode Black für ${{plan}} pro Monat abonniert.",
|
||||
"workspace.black.subscription.manage": "Abonnement verwalten",
|
||||
"workspace.black.subscription.rollingUsage": "5-stündige Nutzung",
|
||||
"workspace.black.subscription.weeklyUsage": "Wöchentliche Nutzung",
|
||||
"workspace.black.subscription.resetsIn": "Wird zurückgesetzt",
|
||||
"workspace.black.subscription.resetsIn": "Zurückgesetzt in",
|
||||
"workspace.black.subscription.useBalance":
|
||||
"Nutzen Sie Ihr verfügbares Guthaben, nachdem Sie die Nutzungslimits erreicht haben",
|
||||
"workspace.black.waitlist.title": "Warteliste",
|
||||
"workspace.black.waitlist.joined":
|
||||
"Sie stehen auf der Warteliste für den Black-Plan im Wert von ${{plan}} pro Monat OpenCode.",
|
||||
"Sie stehen auf der Warteliste für den OpenCode Black Tarif für ${{plan}} pro Monat.",
|
||||
"workspace.black.waitlist.ready":
|
||||
"Wir sind bereit, Sie für den Black-Plan im Wert von ${{plan}} pro Monat OpenCode anzumelden.",
|
||||
"Wir können Sie jetzt in den OpenCode Black Tarif für ${{plan}} pro Monat aufnehmen.",
|
||||
"workspace.black.waitlist.leave": "Warteliste verlassen",
|
||||
"workspace.black.waitlist.leaving": "Verlassen...",
|
||||
"workspace.black.waitlist.left": "Links",
|
||||
"workspace.black.waitlist.left": "Verlassen",
|
||||
"workspace.black.waitlist.enroll": "Einschreiben",
|
||||
"workspace.black.waitlist.enrolling": "Anmeldung...",
|
||||
"workspace.black.waitlist.enrolled": "Eingeschrieben",
|
||||
|
||||
@@ -284,8 +284,8 @@ export const dict = {
|
||||
"changelog.hero.subtitle": "Nuovi aggiornamenti e miglioramenti per OpenCode",
|
||||
"changelog.empty": "Nessuna voce di changelog trovata.",
|
||||
"changelog.viewJson": "Visualizza JSON",
|
||||
"workspace.nav.zen": "zen",
|
||||
"workspace.nav.apiKeys": "API Chiavi",
|
||||
"workspace.nav.zen": "Zen",
|
||||
"workspace.nav.apiKeys": "Chiavi API",
|
||||
"workspace.nav.members": "Membri",
|
||||
"workspace.nav.billing": "Fatturazione",
|
||||
"workspace.nav.settings": "Impostazioni",
|
||||
@@ -299,14 +299,14 @@ export const dict = {
|
||||
"workspace.newUser.feature.quality.title": "Massima qualità",
|
||||
"workspace.newUser.feature.quality.body":
|
||||
"Modelli di accesso configurati per prestazioni ottimali: senza downgrade o instradamento verso fornitori più economici.",
|
||||
"workspace.newUser.feature.lockin.title": "Nessun blocco",
|
||||
"workspace.newUser.feature.lockin.title": "Nessun lock-in",
|
||||
"workspace.newUser.feature.lockin.body":
|
||||
"Utilizza Zen con qualsiasi agente di codifica e continua a utilizzare altri provider con opencode ogni volta che vuoi.",
|
||||
"workspace.newUser.copyApiKey": "Copia la chiave API",
|
||||
"workspace.newUser.copyKey": "Copia chiave",
|
||||
"workspace.newUser.copied": "Copiato!",
|
||||
"workspace.newUser.step.enableBilling": "Abilita fatturazione",
|
||||
"workspace.newUser.step.login.before": "Correre",
|
||||
"workspace.newUser.step.login.before": "Esegui",
|
||||
"workspace.newUser.step.login.after": "e seleziona opencode",
|
||||
"workspace.newUser.step.pasteKey": "Incolla la tua chiave API",
|
||||
"workspace.newUser.step.models.before": "Avvia opencode ed esegui",
|
||||
@@ -315,16 +315,16 @@ export const dict = {
|
||||
"workspace.models.subtitle.beforeLink": "Gestire i modelli a cui possono accedere i membri dell'area di lavoro.",
|
||||
"workspace.models.table.model": "Modello",
|
||||
"workspace.models.table.enabled": "Abilitato",
|
||||
"workspace.providers.title": "Porta la tua chiave",
|
||||
"workspace.providers.title": "Bring Your Own Key (BYOK)",
|
||||
"workspace.providers.subtitle": "Configura le tue chiavi API dai fornitori di intelligenza artificiale.",
|
||||
"workspace.providers.placeholder": "Inserisci la chiave {{provider}} API ({{prefix}}...)",
|
||||
"workspace.providers.configure": "Configura",
|
||||
"workspace.providers.edit": "Modificare",
|
||||
"workspace.providers.delete": "Eliminare",
|
||||
"workspace.providers.saving": "Risparmio...",
|
||||
"workspace.providers.saving": "Salvataggio in corso...",
|
||||
"workspace.providers.save": "Salva",
|
||||
"workspace.providers.table.provider": "Fornitore",
|
||||
"workspace.providers.table.apiKey": "API Chiave",
|
||||
"workspace.providers.table.apiKey": "Chiave API",
|
||||
"workspace.usage.title": "Cronologia dell'utilizzo",
|
||||
"workspace.usage.subtitle": "Utilizzo e costi recenti di API.",
|
||||
"workspace.usage.empty": "Effettua la tua prima chiamata API per iniziare.",
|
||||
@@ -346,7 +346,7 @@ export const dict = {
|
||||
"workspace.cost.deletedSuffix": "(eliminato)",
|
||||
"workspace.cost.empty": "Nessun dato di utilizzo disponibile per il periodo selezionato.",
|
||||
"workspace.cost.subscriptionShort": "sub",
|
||||
"workspace.keys.title": "API Chiavi",
|
||||
"workspace.keys.title": "Chiavi API",
|
||||
"workspace.keys.subtitle": "Gestisci le tue chiavi API per accedere ai servizi opencode.",
|
||||
"workspace.keys.create": "Crea chiave API",
|
||||
"workspace.keys.placeholder": "Inserisci il nome della chiave",
|
||||
@@ -360,7 +360,7 @@ export const dict = {
|
||||
"workspace.members.title": "Membri",
|
||||
"workspace.members.subtitle": "Gestire i membri dell'area di lavoro e le relative autorizzazioni.",
|
||||
"workspace.members.invite": "Invita membro",
|
||||
"workspace.members.inviting": "Invitante...",
|
||||
"workspace.members.inviting": "Invito in corso...",
|
||||
"workspace.members.beta.beforeLink": "Gli spazi di lavoro sono gratuiti per i team durante la beta.",
|
||||
"workspace.members.form.invitee": "Invitato",
|
||||
"workspace.members.form.emailPlaceholder": "Inserisci l'e-mail",
|
||||
@@ -371,12 +371,12 @@ export const dict = {
|
||||
"workspace.members.invited": "invitato",
|
||||
"workspace.members.edit": "Modificare",
|
||||
"workspace.members.delete": "Eliminare",
|
||||
"workspace.members.saving": "Risparmio...",
|
||||
"workspace.members.saving": "Salvataggio in corso...",
|
||||
"workspace.members.save": "Salva",
|
||||
"workspace.members.table.email": "E-mail",
|
||||
"workspace.members.table.role": "Ruolo",
|
||||
"workspace.members.table.monthLimit": "Limite mensile",
|
||||
"workspace.members.role.admin": "Ammin",
|
||||
"workspace.members.role.admin": "Admin",
|
||||
"workspace.members.role.adminDescription": "Può gestire modelli, membri e fatturazione",
|
||||
"workspace.members.role.member": "Membro",
|
||||
"workspace.members.role.memberDescription": "Possono generare chiavi API solo per se stessi",
|
||||
@@ -388,42 +388,42 @@ export const dict = {
|
||||
"workspace.settings.save": "Salva",
|
||||
"workspace.settings.edit": "Modificare",
|
||||
"workspace.billing.title": "Fatturazione",
|
||||
"workspace.billing.subtitle.beforeLink": "Gestire i metodi di pagamento.",
|
||||
"workspace.billing.subtitle.beforeLink": "Gestisci i metodi di pagamento.",
|
||||
"workspace.billing.contactUs": "Contattaci",
|
||||
"workspace.billing.subtitle.afterLink": "se hai qualche domanda",
|
||||
"workspace.billing.currentBalance": "Saldo attuale",
|
||||
"workspace.billing.add": "Aggiungi $",
|
||||
"workspace.billing.enterAmount": "Inserisci l'importo",
|
||||
"workspace.billing.loading": "Caricamento...",
|
||||
"workspace.billing.addAction": "Aggiungere",
|
||||
"workspace.billing.addAction": "Aggiungi",
|
||||
"workspace.billing.addBalance": "Aggiungi saldo",
|
||||
"workspace.billing.linkedToStripe": "Collegato a Stripe",
|
||||
"workspace.billing.manage": "Maneggio",
|
||||
"workspace.billing.manage": "Gestisci",
|
||||
"workspace.billing.enable": "Abilita fatturazione",
|
||||
"workspace.monthlyLimit.title": "Limite mensile",
|
||||
"workspace.monthlyLimit.subtitle": "Imposta un limite di utilizzo mensile per il tuo account.",
|
||||
"workspace.monthlyLimit.placeholder": "50",
|
||||
"workspace.monthlyLimit.setting": "Collocamento...",
|
||||
"workspace.monthlyLimit.setting": "Impostazione in corso...",
|
||||
"workspace.monthlyLimit.set": "Impostato",
|
||||
"workspace.monthlyLimit.edit": "Modifica limite",
|
||||
"workspace.monthlyLimit.noLimit": "Nessun limite di utilizzo impostato.",
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "Utilizzo attuale per",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "è $",
|
||||
"workspace.reload.title": "Ricarica automatica",
|
||||
"workspace.reload.disabled.before": "La ricarica automatica lo è",
|
||||
"workspace.reload.disabled.before": "La ricarica automatica è",
|
||||
"workspace.reload.disabled.state": "disabilitato",
|
||||
"workspace.reload.disabled.after": "Abilita la ricarica automatica quando il saldo è basso.",
|
||||
"workspace.reload.enabled.before": "La ricarica automatica lo è",
|
||||
"workspace.reload.enabled.before": "La ricarica automatica è",
|
||||
"workspace.reload.enabled.state": "abilitato",
|
||||
"workspace.reload.enabled.middle": "Ricaricheremo",
|
||||
"workspace.reload.processingFee": "tassa di elaborazione",
|
||||
"workspace.reload.enabled.after": "quando l'equilibrio raggiunge",
|
||||
"workspace.reload.enabled.after": "quando il saldo raggiunge",
|
||||
"workspace.reload.edit": "Modificare",
|
||||
"workspace.reload.enable": "Abilitare",
|
||||
"workspace.reload.enableAutoReload": "Abilita ricarica automatica",
|
||||
"workspace.reload.reloadAmount": "Ricarica $",
|
||||
"workspace.reload.whenBalanceReaches": "Quando il saldo raggiunge $",
|
||||
"workspace.reload.saving": "Risparmio...",
|
||||
"workspace.reload.saving": "Salvataggio in corso...",
|
||||
"workspace.reload.save": "Salva",
|
||||
"workspace.reload.failedAt": "Ricarica non riuscita a",
|
||||
"workspace.reload.reason": "Motivo:",
|
||||
@@ -434,11 +434,11 @@ export const dict = {
|
||||
"workspace.payments.subtitle": "Transazioni di pagamento recenti.",
|
||||
"workspace.payments.table.date": "Data",
|
||||
"workspace.payments.table.paymentId": "ID pagamento",
|
||||
"workspace.payments.table.amount": "Quantità",
|
||||
"workspace.payments.table.amount": "Importo",
|
||||
"workspace.payments.table.receipt": "Ricevuta",
|
||||
"workspace.payments.type.credit": "credito",
|
||||
"workspace.payments.type.subscription": "sottoscrizione",
|
||||
"workspace.payments.view": "Visualizzazione",
|
||||
"workspace.payments.view": "Visualizza",
|
||||
"workspace.black.loading": "Caricamento...",
|
||||
"workspace.black.time.day": "giorno",
|
||||
"workspace.black.time.days": "giorni",
|
||||
@@ -452,14 +452,14 @@ export const dict = {
|
||||
"workspace.black.subscription.manage": "Gestisci abbonamento",
|
||||
"workspace.black.subscription.rollingUsage": "Utilizzo di 5 ore",
|
||||
"workspace.black.subscription.weeklyUsage": "Utilizzo settimanale",
|
||||
"workspace.black.subscription.resetsIn": "Si reimposta",
|
||||
"workspace.black.subscription.resetsIn": "Si reimposta tra",
|
||||
"workspace.black.subscription.useBalance": "Utilizza il saldo disponibile dopo aver raggiunto i limiti di utilizzo",
|
||||
"workspace.black.waitlist.title": "Lista d'attesa",
|
||||
"workspace.black.waitlist.joined": "Sei in lista d'attesa per il piano nero ${{plan}} al mese OpenCode.",
|
||||
"workspace.black.waitlist.joined": "Sei in lista d'attesa per il piano OpenCode Black da ${{plan}} al mese.",
|
||||
"workspace.black.waitlist.ready": "Siamo pronti per iscriverti al piano OpenCode Black da ${{plan}} al mese.",
|
||||
"workspace.black.waitlist.leave": "Lascia la lista d'attesa",
|
||||
"workspace.black.waitlist.leaving": "In partenza...",
|
||||
"workspace.black.waitlist.left": "Sinistra",
|
||||
"workspace.black.waitlist.leaving": "Uscita in corso...",
|
||||
"workspace.black.waitlist.left": "Uscito dalla lista d'attesa",
|
||||
"workspace.black.waitlist.enroll": "Iscriversi",
|
||||
"workspace.black.waitlist.enrolling": "Iscrizione...",
|
||||
"workspace.black.waitlist.enrolled": "Iscritto",
|
||||
|
||||
@@ -293,9 +293,9 @@ export const dict = {
|
||||
"changelog.hero.subtitle": "OpenCode \u7684\u65b0\u66f4\u65b0\u4e0e\u6539\u8fdb",
|
||||
"changelog.empty": "\u672a\u627e\u5230\u66f4\u65b0\u65e5\u5fd7\u6761\u76ee\u3002",
|
||||
"changelog.viewJson": "\u67e5\u770b JSON",
|
||||
"workspace.nav.zen": "禅",
|
||||
"workspace.nav.zen": "Zen",
|
||||
"workspace.nav.apiKeys": "API 键",
|
||||
"workspace.nav.members": "会员",
|
||||
"workspace.nav.members": "成员",
|
||||
"workspace.nav.billing": "计费",
|
||||
"workspace.nav.settings": "设置",
|
||||
"workspace.home.banner.beforeLink": "编码代理的可靠优化模型。",
|
||||
@@ -310,26 +310,26 @@ export const dict = {
|
||||
"workspace.newUser.feature.lockin.body":
|
||||
"将 Zen 与任何编码代理结合使用,并在需要时继续将其他提供程序与 opencode 结合使用。",
|
||||
"workspace.newUser.copyApiKey": "复制 API 密钥",
|
||||
"workspace.newUser.copyKey": "复制钥匙",
|
||||
"workspace.newUser.copied": "复制了!",
|
||||
"workspace.newUser.copyKey": "复制密钥",
|
||||
"workspace.newUser.copied": "已复制!",
|
||||
"workspace.newUser.step.enableBilling": "启用计费",
|
||||
"workspace.newUser.step.login.before": "跑步",
|
||||
"workspace.newUser.step.login.before": "运行",
|
||||
"workspace.newUser.step.login.after": "并选择 opencode",
|
||||
"workspace.newUser.step.pasteKey": "粘贴您的 API 密钥",
|
||||
"workspace.newUser.step.models.before": "启动 opencode 并运行",
|
||||
"workspace.newUser.step.models.after": "选择型号",
|
||||
"workspace.models.title": "型号",
|
||||
"workspace.newUser.step.models.after": "选择模型",
|
||||
"workspace.models.title": "模型",
|
||||
"workspace.models.subtitle.beforeLink": "管理工作区成员可以访问哪些模型。",
|
||||
"workspace.models.table.model": "模型",
|
||||
"workspace.models.table.enabled": "启用",
|
||||
"workspace.providers.title": "带上你自己的钥匙",
|
||||
"workspace.providers.title": "自带密钥",
|
||||
"workspace.providers.subtitle": "从 AI 提供商处配置您自己的 API 密钥。",
|
||||
"workspace.providers.placeholder": "输入 {{provider}} API 密钥({{prefix}}...)",
|
||||
"workspace.providers.configure": "配置",
|
||||
"workspace.providers.edit": "编辑",
|
||||
"workspace.providers.delete": "删除",
|
||||
"workspace.providers.saving": "保存...",
|
||||
"workspace.providers.save": "节省",
|
||||
"workspace.providers.save": "保存",
|
||||
"workspace.providers.table.provider": "提供者",
|
||||
"workspace.providers.table.apiKey": "API 密钥",
|
||||
"workspace.usage.title": "使用历史",
|
||||
@@ -348,25 +348,25 @@ export const dict = {
|
||||
"workspace.usage.subscription": "订阅 (${{amount}})",
|
||||
"workspace.cost.title": "成本",
|
||||
"workspace.cost.subtitle": "按型号细分的使用成本。",
|
||||
"workspace.cost.allModels": "所有型号",
|
||||
"workspace.cost.allKeys": "所有按键",
|
||||
"workspace.cost.allModels": "所有模型",
|
||||
"workspace.cost.allKeys": "所有密钥",
|
||||
"workspace.cost.deletedSuffix": "(已删除)",
|
||||
"workspace.cost.empty": "所选期间没有可用的使用数据。",
|
||||
"workspace.cost.subscriptionShort": "子",
|
||||
"workspace.cost.subscriptionShort": "订",
|
||||
"workspace.keys.title": "API 键",
|
||||
"workspace.keys.subtitle": "管理您的 API 密钥以访问 opencode 服务。",
|
||||
"workspace.keys.create": "创建 API 密钥",
|
||||
"workspace.keys.placeholder": "输入按键名称",
|
||||
"workspace.keys.placeholder": "输入密钥名称",
|
||||
"workspace.keys.empty": "创建 opencode 网关 API 密钥",
|
||||
"workspace.keys.table.name": "姓名",
|
||||
"workspace.keys.table.key": "钥匙",
|
||||
"workspace.keys.table.name": "名称",
|
||||
"workspace.keys.table.key": "密钥",
|
||||
"workspace.keys.table.createdBy": "创建者",
|
||||
"workspace.keys.table.lastUsed": "最后使用",
|
||||
"workspace.keys.copyApiKey": "复制 API 密钥",
|
||||
"workspace.keys.delete": "删除",
|
||||
"workspace.members.title": "会员",
|
||||
"workspace.members.title": "成员",
|
||||
"workspace.members.subtitle": "管理工作区成员及其权限。",
|
||||
"workspace.members.invite": "邀请会员",
|
||||
"workspace.members.invite": "邀请成员",
|
||||
"workspace.members.inviting": "邀请...",
|
||||
"workspace.members.beta.beforeLink": "测试期间,工作空间对团队免费。",
|
||||
"workspace.members.form.invitee": "受邀者",
|
||||
@@ -379,11 +379,11 @@ export const dict = {
|
||||
"workspace.members.edit": "编辑",
|
||||
"workspace.members.delete": "删除",
|
||||
"workspace.members.saving": "保存...",
|
||||
"workspace.members.save": "节省",
|
||||
"workspace.members.save": "保存",
|
||||
"workspace.members.table.email": "电子邮件",
|
||||
"workspace.members.table.role": "角色",
|
||||
"workspace.members.table.monthLimit": "月份限制",
|
||||
"workspace.members.role.admin": "行政",
|
||||
"workspace.members.table.monthLimit": "月限额",
|
||||
"workspace.members.role.admin": "管理员",
|
||||
"workspace.members.role.adminDescription": "可以管理模型、成员和计费",
|
||||
"workspace.members.role.member": "成员",
|
||||
"workspace.members.role.memberDescription": "只能为自己生成 API 密钥",
|
||||
@@ -392,7 +392,7 @@ export const dict = {
|
||||
"workspace.settings.workspaceName": "工作区名称",
|
||||
"workspace.settings.defaultName": "默认",
|
||||
"workspace.settings.updating": "更新中...",
|
||||
"workspace.settings.save": "节省",
|
||||
"workspace.settings.save": "保存",
|
||||
"workspace.settings.edit": "编辑",
|
||||
"workspace.billing.title": "计费",
|
||||
"workspace.billing.subtitle.beforeLink": "管理付款方式。",
|
||||
@@ -404,35 +404,35 @@ export const dict = {
|
||||
"workspace.billing.loading": "加载中...",
|
||||
"workspace.billing.addAction": "添加",
|
||||
"workspace.billing.addBalance": "添加余额",
|
||||
"workspace.billing.linkedToStripe": "链接到条纹",
|
||||
"workspace.billing.linkedToStripe": "已绑定 Stripe",
|
||||
"workspace.billing.manage": "管理",
|
||||
"workspace.billing.enable": "启用计费",
|
||||
"workspace.monthlyLimit.title": "每月限额",
|
||||
"workspace.monthlyLimit.subtitle": "为您的帐户设置每月使用限额。",
|
||||
"workspace.monthlyLimit.placeholder": "50",
|
||||
"workspace.monthlyLimit.setting": "环境...",
|
||||
"workspace.monthlyLimit.set": "放",
|
||||
"workspace.monthlyLimit.setting": "设置中...",
|
||||
"workspace.monthlyLimit.set": "设置",
|
||||
"workspace.monthlyLimit.edit": "编辑限制",
|
||||
"workspace.monthlyLimit.noLimit": "没有设置使用限制。",
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "当前使用情况为",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "是 $",
|
||||
"workspace.reload.title": "自动重新加载",
|
||||
"workspace.reload.disabled.before": "自动重新加载是",
|
||||
"workspace.reload.disabled.state": "残疾人",
|
||||
"workspace.reload.disabled.after": "启用余额不足时自动充值。",
|
||||
"workspace.reload.enabled.before": "自动重新加载是",
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "当前",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "的使用量为 $",
|
||||
"workspace.reload.title": "自动充值",
|
||||
"workspace.reload.disabled.before": "自动充值已",
|
||||
"workspace.reload.disabled.state": "停用",
|
||||
"workspace.reload.disabled.after": "启用后将在余额较低时自动充值。",
|
||||
"workspace.reload.enabled.before": "自动充值已",
|
||||
"workspace.reload.enabled.state": "已启用",
|
||||
"workspace.reload.enabled.middle": "我们将重新加载",
|
||||
"workspace.reload.processingFee": "加工费",
|
||||
"workspace.reload.enabled.middle": "我们将自动充值",
|
||||
"workspace.reload.processingFee": "手续费",
|
||||
"workspace.reload.enabled.after": "当余额达到",
|
||||
"workspace.reload.edit": "编辑",
|
||||
"workspace.reload.enable": "使能够",
|
||||
"workspace.reload.enableAutoReload": "启用自动重新加载",
|
||||
"workspace.reload.reloadAmount": "重新加载 $",
|
||||
"workspace.reload.enable": "启用",
|
||||
"workspace.reload.enableAutoReload": "启用自动充值",
|
||||
"workspace.reload.reloadAmount": "充值 $",
|
||||
"workspace.reload.whenBalanceReaches": "当余额达到 $",
|
||||
"workspace.reload.saving": "保存...",
|
||||
"workspace.reload.save": "节省",
|
||||
"workspace.reload.failedAt": "重新加载失败于",
|
||||
"workspace.reload.save": "保存",
|
||||
"workspace.reload.failedAt": "充值失败于",
|
||||
"workspace.reload.reason": "原因:",
|
||||
"workspace.reload.updatePaymentMethod": "请更新您的付款方式并重试。",
|
||||
"workspace.reload.retrying": "正在重试...",
|
||||
@@ -441,11 +441,11 @@ export const dict = {
|
||||
"workspace.payments.subtitle": "最近的付款交易。",
|
||||
"workspace.payments.table.date": "日期",
|
||||
"workspace.payments.table.paymentId": "付款ID",
|
||||
"workspace.payments.table.amount": "数量",
|
||||
"workspace.payments.table.amount": "金额",
|
||||
"workspace.payments.table.receipt": "收据",
|
||||
"workspace.payments.type.credit": "信用",
|
||||
"workspace.payments.type.subscription": "订阅",
|
||||
"workspace.payments.view": "看法",
|
||||
"workspace.payments.view": "查看",
|
||||
"workspace.black.loading": "加载中...",
|
||||
"workspace.black.time.day": "天",
|
||||
"workspace.black.time.days": "天",
|
||||
@@ -455,20 +455,20 @@ export const dict = {
|
||||
"workspace.black.time.minutes": "分钟",
|
||||
"workspace.black.time.fewSeconds": "几秒钟",
|
||||
"workspace.black.subscription.title": "订阅",
|
||||
"workspace.black.subscription.message": "您已订阅 OpenCode Black,每月费用为 {{plan}} 美元。",
|
||||
"workspace.black.subscription.message": "您已订阅 OpenCode Black,费用为每月 ${{plan}}。",
|
||||
"workspace.black.subscription.manage": "管理订阅",
|
||||
"workspace.black.subscription.rollingUsage": "5小时使用",
|
||||
"workspace.black.subscription.weeklyUsage": "每周使用量",
|
||||
"workspace.black.subscription.resetsIn": "重置于",
|
||||
"workspace.black.subscription.useBalance": "达到使用限额后使用您的可用余额",
|
||||
"workspace.black.waitlist.title": "候补名单",
|
||||
"workspace.black.waitlist.joined": "您正在等待每月 ${{plan}} OpenCode 黑色计划。",
|
||||
"workspace.black.waitlist.ready": "我们已准备好让您加入每月 {{plan}} 美元的 OpenCode 黑色计划。",
|
||||
"workspace.black.waitlist.joined": "您已加入每月 ${{plan}} 的 OpenCode Black 方案候补名单。",
|
||||
"workspace.black.waitlist.ready": "我们已准备好将您加入每月 ${{plan}} 的 OpenCode Black 方案。",
|
||||
"workspace.black.waitlist.leave": "离开候补名单",
|
||||
"workspace.black.waitlist.leaving": "离开...",
|
||||
"workspace.black.waitlist.left": "左边",
|
||||
"workspace.black.waitlist.enroll": "注册",
|
||||
"workspace.black.waitlist.enrolling": "正在报名...",
|
||||
"workspace.black.waitlist.enrolled": "已注册",
|
||||
"workspace.black.waitlist.left": "已退出",
|
||||
"workspace.black.waitlist.enroll": "加入",
|
||||
"workspace.black.waitlist.enrolling": "加入中...",
|
||||
"workspace.black.waitlist.enrolled": "已加入",
|
||||
"workspace.black.waitlist.enrollNote": "单击“注册”后,您的订阅将立即开始,并且将从您的卡中扣费。",
|
||||
} satisfies Dict
|
||||
|
||||
@@ -293,9 +293,9 @@ export const dict = {
|
||||
"changelog.hero.subtitle": "OpenCode \u7684\u65b0\u66f4\u65b0\u8207\u6539\u5584",
|
||||
"changelog.empty": "\u627e\u4e0d\u5230\u66f4\u65b0\u65e5\u8a8c\u9805\u76ee\u3002",
|
||||
"changelog.viewJson": "\u6aa2\u8996 JSON",
|
||||
"workspace.nav.zen": "禪",
|
||||
"workspace.nav.zen": "Zen",
|
||||
"workspace.nav.apiKeys": "API 鍵",
|
||||
"workspace.nav.members": "會員",
|
||||
"workspace.nav.members": "成員",
|
||||
"workspace.nav.billing": "計費",
|
||||
"workspace.nav.settings": "設定",
|
||||
"workspace.home.banner.beforeLink": "編碼代理的可靠優化模型。",
|
||||
@@ -310,26 +310,26 @@ export const dict = {
|
||||
"workspace.newUser.feature.lockin.body":
|
||||
"將 Zen 與任何編碼代理結合使用,並在需要時繼續將其他提供程序與 opencode 結合使用。",
|
||||
"workspace.newUser.copyApiKey": "複製 API 密鑰",
|
||||
"workspace.newUser.copyKey": "複製鑰匙",
|
||||
"workspace.newUser.copied": "複製了!",
|
||||
"workspace.newUser.copyKey": "複製密鑰",
|
||||
"workspace.newUser.copied": "已複製!",
|
||||
"workspace.newUser.step.enableBilling": "啟用計費",
|
||||
"workspace.newUser.step.login.before": "跑步",
|
||||
"workspace.newUser.step.login.before": "執行",
|
||||
"workspace.newUser.step.login.after": "並選擇 opencode",
|
||||
"workspace.newUser.step.pasteKey": "粘貼您的 API 密鑰",
|
||||
"workspace.newUser.step.models.before": "啟動 opencode 並運行",
|
||||
"workspace.newUser.step.models.after": "選擇型號",
|
||||
"workspace.models.title": "型號",
|
||||
"workspace.newUser.step.models.after": "選擇模型",
|
||||
"workspace.models.title": "模型",
|
||||
"workspace.models.subtitle.beforeLink": "管理工作區成員可以訪問哪些模型。",
|
||||
"workspace.models.table.model": "模型",
|
||||
"workspace.models.table.enabled": "啟用",
|
||||
"workspace.providers.title": "帶上你自己的鑰匙",
|
||||
"workspace.providers.title": "自帶密鑰",
|
||||
"workspace.providers.subtitle": "從 AI 提供商處配置您自己的 API 密鑰。",
|
||||
"workspace.providers.placeholder": "輸入 {{provider}} API 密鑰({{prefix}}...)",
|
||||
"workspace.providers.configure": "配置",
|
||||
"workspace.providers.edit": "編輯",
|
||||
"workspace.providers.delete": "刪除",
|
||||
"workspace.providers.saving": "保存...",
|
||||
"workspace.providers.save": "節省",
|
||||
"workspace.providers.save": "儲存",
|
||||
"workspace.providers.table.provider": "提供者",
|
||||
"workspace.providers.table.apiKey": "API 密鑰",
|
||||
"workspace.usage.title": "使用歷史",
|
||||
@@ -348,25 +348,25 @@ export const dict = {
|
||||
"workspace.usage.subscription": "訂閱 (${{amount}})",
|
||||
"workspace.cost.title": "成本",
|
||||
"workspace.cost.subtitle": "按型號細分的使用成本。",
|
||||
"workspace.cost.allModels": "所有型號",
|
||||
"workspace.cost.allKeys": "所有按鍵",
|
||||
"workspace.cost.allModels": "所有模型",
|
||||
"workspace.cost.allKeys": "所有密鑰",
|
||||
"workspace.cost.deletedSuffix": "(已刪除)",
|
||||
"workspace.cost.empty": "所選期間沒有可用的使用數據。",
|
||||
"workspace.cost.subscriptionShort": "子",
|
||||
"workspace.cost.subscriptionShort": "訂",
|
||||
"workspace.keys.title": "API 鍵",
|
||||
"workspace.keys.subtitle": "管理您的 API 密鑰以訪問 opencode 服務。",
|
||||
"workspace.keys.create": "創建 API 密鑰",
|
||||
"workspace.keys.placeholder": "輸入按鍵名稱",
|
||||
"workspace.keys.placeholder": "輸入密鑰名稱",
|
||||
"workspace.keys.empty": "創建 opencode 網關 API 密鑰",
|
||||
"workspace.keys.table.name": "姓名",
|
||||
"workspace.keys.table.key": "鑰匙",
|
||||
"workspace.keys.table.name": "名稱",
|
||||
"workspace.keys.table.key": "密鑰",
|
||||
"workspace.keys.table.createdBy": "創建者",
|
||||
"workspace.keys.table.lastUsed": "最後使用",
|
||||
"workspace.keys.copyApiKey": "複製 API 密鑰",
|
||||
"workspace.keys.delete": "刪除",
|
||||
"workspace.members.title": "會員",
|
||||
"workspace.members.title": "成員",
|
||||
"workspace.members.subtitle": "管理工作區成員及其權限。",
|
||||
"workspace.members.invite": "邀請會員",
|
||||
"workspace.members.invite": "邀請成員",
|
||||
"workspace.members.inviting": "邀請...",
|
||||
"workspace.members.beta.beforeLink": "測試期間,工作空間對團隊免費。",
|
||||
"workspace.members.form.invitee": "受邀者",
|
||||
@@ -379,11 +379,11 @@ export const dict = {
|
||||
"workspace.members.edit": "編輯",
|
||||
"workspace.members.delete": "刪除",
|
||||
"workspace.members.saving": "保存...",
|
||||
"workspace.members.save": "節省",
|
||||
"workspace.members.save": "儲存",
|
||||
"workspace.members.table.email": "電子郵件",
|
||||
"workspace.members.table.role": "角色",
|
||||
"workspace.members.table.monthLimit": "月份限制",
|
||||
"workspace.members.role.admin": "行政",
|
||||
"workspace.members.table.monthLimit": "月限額",
|
||||
"workspace.members.role.admin": "管理員",
|
||||
"workspace.members.role.adminDescription": "可以管理模型、成員和計費",
|
||||
"workspace.members.role.member": "成員",
|
||||
"workspace.members.role.memberDescription": "只能為自己生成 API 密鑰",
|
||||
@@ -392,7 +392,7 @@ export const dict = {
|
||||
"workspace.settings.workspaceName": "工作區名稱",
|
||||
"workspace.settings.defaultName": "預設",
|
||||
"workspace.settings.updating": "更新中...",
|
||||
"workspace.settings.save": "節省",
|
||||
"workspace.settings.save": "儲存",
|
||||
"workspace.settings.edit": "編輯",
|
||||
"workspace.billing.title": "計費",
|
||||
"workspace.billing.subtitle.beforeLink": "管理付款方式。",
|
||||
@@ -404,35 +404,35 @@ export const dict = {
|
||||
"workspace.billing.loading": "載入中...",
|
||||
"workspace.billing.addAction": "添加",
|
||||
"workspace.billing.addBalance": "添加餘額",
|
||||
"workspace.billing.linkedToStripe": "鏈接到條紋",
|
||||
"workspace.billing.linkedToStripe": "已連結 Stripe",
|
||||
"workspace.billing.manage": "管理",
|
||||
"workspace.billing.enable": "啟用計費",
|
||||
"workspace.monthlyLimit.title": "每月限額",
|
||||
"workspace.monthlyLimit.subtitle": "為您的帳戶設置每月使用限額。",
|
||||
"workspace.monthlyLimit.placeholder": "50",
|
||||
"workspace.monthlyLimit.setting": "環境...",
|
||||
"workspace.monthlyLimit.set": "放",
|
||||
"workspace.monthlyLimit.setting": "設定中...",
|
||||
"workspace.monthlyLimit.set": "設定",
|
||||
"workspace.monthlyLimit.edit": "編輯限制",
|
||||
"workspace.monthlyLimit.noLimit": "沒有設置使用限制。",
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "當前使用情況為",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "是 $",
|
||||
"workspace.reload.title": "自動重新加載",
|
||||
"workspace.reload.disabled.before": "自動重新加載是",
|
||||
"workspace.reload.disabled.state": "殘疾人",
|
||||
"workspace.reload.disabled.after": "啟用餘額不足時自動充值。",
|
||||
"workspace.reload.enabled.before": "自動重新加載是",
|
||||
"workspace.monthlyLimit.currentUsage.beforeMonth": "當前",
|
||||
"workspace.monthlyLimit.currentUsage.beforeAmount": "的使用量為 $",
|
||||
"workspace.reload.title": "自動儲值",
|
||||
"workspace.reload.disabled.before": "自動儲值已",
|
||||
"workspace.reload.disabled.state": "停用",
|
||||
"workspace.reload.disabled.after": "啟用後會在餘額偏低時自動儲值。",
|
||||
"workspace.reload.enabled.before": "自動儲值已",
|
||||
"workspace.reload.enabled.state": "已啟用",
|
||||
"workspace.reload.enabled.middle": "我們將重新加載",
|
||||
"workspace.reload.processingFee": "加工費",
|
||||
"workspace.reload.enabled.middle": "我們將自動儲值",
|
||||
"workspace.reload.processingFee": "手續費",
|
||||
"workspace.reload.enabled.after": "當餘額達到",
|
||||
"workspace.reload.edit": "編輯",
|
||||
"workspace.reload.enable": "使能夠",
|
||||
"workspace.reload.enableAutoReload": "啟用自動重新加載",
|
||||
"workspace.reload.reloadAmount": "重新加載 $",
|
||||
"workspace.reload.enable": "啟用",
|
||||
"workspace.reload.enableAutoReload": "啟用自動儲值",
|
||||
"workspace.reload.reloadAmount": "儲值 $",
|
||||
"workspace.reload.whenBalanceReaches": "當餘額達到 $",
|
||||
"workspace.reload.saving": "保存...",
|
||||
"workspace.reload.save": "節省",
|
||||
"workspace.reload.failedAt": "重新加載失敗於",
|
||||
"workspace.reload.save": "儲存",
|
||||
"workspace.reload.failedAt": "儲值失敗於",
|
||||
"workspace.reload.reason": "原因:",
|
||||
"workspace.reload.updatePaymentMethod": "請更新您的付款方式並重試。",
|
||||
"workspace.reload.retrying": "正在重試...",
|
||||
@@ -441,11 +441,11 @@ export const dict = {
|
||||
"workspace.payments.subtitle": "最近的付款交易。",
|
||||
"workspace.payments.table.date": "日期",
|
||||
"workspace.payments.table.paymentId": "付款ID",
|
||||
"workspace.payments.table.amount": "數量",
|
||||
"workspace.payments.table.amount": "金額",
|
||||
"workspace.payments.table.receipt": "收據",
|
||||
"workspace.payments.type.credit": "信用",
|
||||
"workspace.payments.type.subscription": "訂閱",
|
||||
"workspace.payments.view": "看法",
|
||||
"workspace.payments.view": "查看",
|
||||
"workspace.black.loading": "載入中...",
|
||||
"workspace.black.time.day": "天",
|
||||
"workspace.black.time.days": "天",
|
||||
@@ -455,20 +455,20 @@ export const dict = {
|
||||
"workspace.black.time.minutes": "分鐘",
|
||||
"workspace.black.time.fewSeconds": "幾秒鐘",
|
||||
"workspace.black.subscription.title": "訂閱",
|
||||
"workspace.black.subscription.message": "您已訂閱 OpenCode Black,每月費用為 {{plan}} 美元。",
|
||||
"workspace.black.subscription.message": "您已訂閱 OpenCode Black,費用為每月 ${{plan}}。",
|
||||
"workspace.black.subscription.manage": "管理訂閱",
|
||||
"workspace.black.subscription.rollingUsage": "5小時使用",
|
||||
"workspace.black.subscription.weeklyUsage": "每週使用量",
|
||||
"workspace.black.subscription.resetsIn": "重置於",
|
||||
"workspace.black.subscription.useBalance": "達到使用限額後使用您的可用餘額",
|
||||
"workspace.black.waitlist.title": "候補名單",
|
||||
"workspace.black.waitlist.joined": "您正在等待每月 ${{plan}} OpenCode 黑色計劃。",
|
||||
"workspace.black.waitlist.ready": "我們已準備好讓您加入每月 {{plan}} 美元的 OpenCode 黑色計劃。",
|
||||
"workspace.black.waitlist.joined": "您已加入每月 ${{plan}} 的 OpenCode Black 方案候補名單。",
|
||||
"workspace.black.waitlist.ready": "我們已準備好將您加入每月 ${{plan}} 的 OpenCode Black 方案。",
|
||||
"workspace.black.waitlist.leave": "離開候補名單",
|
||||
"workspace.black.waitlist.leaving": "離開...",
|
||||
"workspace.black.waitlist.left": "左邊",
|
||||
"workspace.black.waitlist.enroll": "註冊",
|
||||
"workspace.black.waitlist.enrolling": "正在報名...",
|
||||
"workspace.black.waitlist.enrolled": "已註冊",
|
||||
"workspace.black.waitlist.left": "已退出",
|
||||
"workspace.black.waitlist.enroll": "加入",
|
||||
"workspace.black.waitlist.enrolling": "加入中...",
|
||||
"workspace.black.waitlist.enrolled": "已加入",
|
||||
"workspace.black.waitlist.enrollNote": "單擊“註冊”後,您的訂閱將立即開始,並且將從您的卡中扣費。",
|
||||
} satisfies Dict
|
||||
|
||||
@@ -68,6 +68,82 @@ const TAG = {
|
||||
tr: "tr",
|
||||
} satisfies Record<Locale, string>
|
||||
|
||||
const DOCS = {
|
||||
en: "root",
|
||||
zh: "zh-cn",
|
||||
zht: "zh-tw",
|
||||
ko: "ko",
|
||||
de: "de",
|
||||
es: "es",
|
||||
fr: "fr",
|
||||
it: "it",
|
||||
da: "da",
|
||||
ja: "ja",
|
||||
pl: "pl",
|
||||
ru: "ru",
|
||||
ar: "ar",
|
||||
no: "nb",
|
||||
br: "pt-br",
|
||||
th: "th",
|
||||
tr: "tr",
|
||||
} satisfies Record<Locale, string>
|
||||
|
||||
const DOCS_SEGMENT = new Set([
|
||||
"ar",
|
||||
"bs",
|
||||
"da",
|
||||
"de",
|
||||
"es",
|
||||
"fr",
|
||||
"it",
|
||||
"ja",
|
||||
"ko",
|
||||
"nb",
|
||||
"pl",
|
||||
"pt-br",
|
||||
"ru",
|
||||
"th",
|
||||
"tr",
|
||||
"zh-cn",
|
||||
"zh-tw",
|
||||
])
|
||||
|
||||
function suffix(pathname: string) {
|
||||
const index = pathname.search(/[?#]/)
|
||||
if (index === -1) {
|
||||
return {
|
||||
path: fix(pathname),
|
||||
suffix: "",
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
path: fix(pathname.slice(0, index)),
|
||||
suffix: pathname.slice(index),
|
||||
}
|
||||
}
|
||||
|
||||
export function docs(locale: Locale, pathname: string) {
|
||||
const value = DOCS[locale]
|
||||
const next = suffix(pathname)
|
||||
if (next.path !== "/docs" && next.path !== "/docs/" && !next.path.startsWith("/docs/")) {
|
||||
return `${next.path}${next.suffix}`
|
||||
}
|
||||
|
||||
if (value === "root") return `${next.path}${next.suffix}`
|
||||
|
||||
if (next.path === "/docs") return `/docs/${value}${next.suffix}`
|
||||
if (next.path === "/docs/") return `/docs/${value}/${next.suffix}`
|
||||
|
||||
const head = next.path.slice("/docs/".length).split("/")[0] ?? ""
|
||||
if (!head) return `/docs/${value}/${next.suffix}`
|
||||
if (DOCS_SEGMENT.has(head)) return `${next.path}${next.suffix}`
|
||||
if (head.startsWith("_")) return `${next.path}${next.suffix}`
|
||||
if (head.includes(".")) return `${next.path}${next.suffix}`
|
||||
|
||||
return `/docs/${value}${next.path.slice("/docs".length)}${next.suffix}`
|
||||
}
|
||||
|
||||
export function parseLocale(value: unknown): Locale | null {
|
||||
if (typeof value !== "string") return null
|
||||
if ((LOCALES as readonly string[]).includes(value)) return value as Locale
|
||||
@@ -90,7 +166,7 @@ export function strip(pathname: string) {
|
||||
|
||||
export function route(locale: Locale, pathname: string) {
|
||||
const next = strip(pathname)
|
||||
if (next.startsWith("/docs")) return next
|
||||
if (next.startsWith("/docs")) return docs(locale, next)
|
||||
if (next.startsWith("/auth")) return next
|
||||
if (next.startsWith("/workspace")) return next
|
||||
if (locale === "en") return next
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import type { APIEvent } from "@solidjs/start/server"
|
||||
import { LOCALE_HEADER, localeFromCookieHeader, parseLocale, tag } from "~/lib/language"
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import { docs, localeFromRequest, tag } from "~/lib/language"
|
||||
|
||||
async function handler(evt: APIEvent) {
|
||||
const req = evt.request.clone()
|
||||
const url = new URL(req.url)
|
||||
const targetUrl = `https://docs.opencode.ai${url.pathname}${url.search}`
|
||||
const locale = localeFromRequest(req)
|
||||
const host = Resource.App.stage === "production" ? "docs.opencode.ai" : "docs.dev.opencode.ai"
|
||||
const targetUrl = `https://${host}${docs(locale, url.pathname)}${url.search}`
|
||||
|
||||
const headers = new Headers(req.headers)
|
||||
const locale = parseLocale(req.headers.get(LOCALE_HEADER)) ?? localeFromCookieHeader(req.headers.get("cookie"))
|
||||
if (locale) headers.set("accept-language", tag(locale))
|
||||
headers.set("accept-language", tag(locale))
|
||||
|
||||
const response = await fetch(targetUrl, {
|
||||
method: req.method,
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import type { APIEvent } from "@solidjs/start/server"
|
||||
import { LOCALE_HEADER, localeFromCookieHeader, parseLocale, tag } from "~/lib/language"
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import { docs, localeFromRequest, tag } from "~/lib/language"
|
||||
|
||||
async function handler(evt: APIEvent) {
|
||||
const req = evt.request.clone()
|
||||
const url = new URL(req.url)
|
||||
const targetUrl = `https://docs.opencode.ai${url.pathname}${url.search}`
|
||||
const locale = localeFromRequest(req)
|
||||
const host = Resource.App.stage === "production" ? "docs.opencode.ai" : "docs.dev.opencode.ai"
|
||||
const targetUrl = `https://${host}${docs(locale, url.pathname)}${url.search}`
|
||||
|
||||
const headers = new Headers(req.headers)
|
||||
const locale = parseLocale(req.headers.get(LOCALE_HEADER)) ?? localeFromCookieHeader(req.headers.get("cookie"))
|
||||
if (locale) headers.set("accept-language", tag(locale))
|
||||
headers.set("accept-language", tag(locale))
|
||||
|
||||
const response = await fetch(targetUrl, {
|
||||
method: req.method,
|
||||
|
||||
@@ -294,7 +294,7 @@ export default function Download() {
|
||||
</span>
|
||||
<span>VS Code</span>
|
||||
</div>
|
||||
<a href="https://opencode.ai/docs/ide/" data-component="action-button">
|
||||
<a href={language.route("/docs/ide/")} data-component="action-button">
|
||||
{i18n.t("download.action.install")}
|
||||
</a>
|
||||
</div>
|
||||
@@ -318,7 +318,7 @@ export default function Download() {
|
||||
</span>
|
||||
<span>Cursor</span>
|
||||
</div>
|
||||
<a href="https://opencode.ai/docs/ide/" data-component="action-button">
|
||||
<a href={language.route("/docs/ide/")} data-component="action-button">
|
||||
{i18n.t("download.action.install")}
|
||||
</a>
|
||||
</div>
|
||||
@@ -335,7 +335,7 @@ export default function Download() {
|
||||
</span>
|
||||
<span>Zed</span>
|
||||
</div>
|
||||
<a href="https://opencode.ai/docs/ide/" data-component="action-button">
|
||||
<a href={language.route("/docs/ide/")} data-component="action-button">
|
||||
{i18n.t("download.action.install")}
|
||||
</a>
|
||||
</div>
|
||||
@@ -352,7 +352,7 @@ export default function Download() {
|
||||
</span>
|
||||
<span>Windsurf</span>
|
||||
</div>
|
||||
<a href="https://opencode.ai/docs/ide/" data-component="action-button">
|
||||
<a href={language.route("/docs/ide/")} data-component="action-button">
|
||||
{i18n.t("download.action.install")}
|
||||
</a>
|
||||
</div>
|
||||
@@ -369,7 +369,7 @@ export default function Download() {
|
||||
</span>
|
||||
<span>VSCodium</span>
|
||||
</div>
|
||||
<a href="https://opencode.ai/docs/ide/" data-component="action-button">
|
||||
<a href={language.route("/docs/ide/")} data-component="action-button">
|
||||
{i18n.t("download.action.install")}
|
||||
</a>
|
||||
</div>
|
||||
@@ -393,7 +393,7 @@ export default function Download() {
|
||||
</span>
|
||||
<span>GitHub</span>
|
||||
</div>
|
||||
<a href="https://opencode.ai/docs/github/" data-component="action-button">
|
||||
<a href={language.route("/docs/github/")} data-component="action-button">
|
||||
{i18n.t("download.action.install")}
|
||||
</a>
|
||||
</div>
|
||||
@@ -410,7 +410,7 @@ export default function Download() {
|
||||
</span>
|
||||
<span>GitLab</span>
|
||||
</div>
|
||||
<a href="https://opencode.ai/docs/gitlab/" data-component="action-button">
|
||||
<a href={language.route("/docs/gitlab/")} data-component="action-button">
|
||||
{i18n.t("download.action.install")}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import type { APIEvent } from "@solidjs/start/server"
|
||||
import { LOCALE_HEADER, localeFromCookieHeader, parseLocale, tag } from "~/lib/language"
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import { docs, localeFromRequest, tag } from "~/lib/language"
|
||||
|
||||
async function handler(evt: APIEvent) {
|
||||
const req = evt.request.clone()
|
||||
const url = new URL(req.url)
|
||||
const targetUrl = `https://docs.opencode.ai/docs${url.pathname}${url.search}`
|
||||
const locale = localeFromRequest(req)
|
||||
const host = Resource.App.stage === "production" ? "docs.opencode.ai" : "docs.dev.opencode.ai"
|
||||
const targetUrl = `https://${host}${docs(locale, `/docs${url.pathname}`)}${url.search}`
|
||||
|
||||
const headers = new Headers(req.headers)
|
||||
const locale = parseLocale(req.headers.get(LOCALE_HEADER)) ?? localeFromCookieHeader(req.headers.get("cookie"))
|
||||
if (locale) headers.set("accept-language", tag(locale))
|
||||
headers.set("accept-language", tag(locale))
|
||||
|
||||
const response = await fetch(targetUrl, {
|
||||
method: req.method,
|
||||
|
||||
@@ -9,10 +9,12 @@ import { GraphSection } from "./graph-section"
|
||||
import { IconLogo } from "~/component/icon"
|
||||
import { querySessionInfo, queryBillingInfo, createCheckoutUrl, formatBalance } from "../common"
|
||||
import { useI18n } from "~/context/i18n"
|
||||
import { useLanguage } from "~/context/language"
|
||||
|
||||
export default function () {
|
||||
const params = useParams()
|
||||
const i18n = useI18n()
|
||||
const language = useLanguage()
|
||||
const userInfo = createAsync(() => querySessionInfo(params.id!))
|
||||
const billingInfo = createAsync(() => queryBillingInfo(params.id!))
|
||||
const checkoutAction = useAction(createCheckoutUrl)
|
||||
@@ -38,7 +40,7 @@ export default function () {
|
||||
<p>
|
||||
<span>
|
||||
{i18n.t("workspace.home.banner.beforeLink")}{" "}
|
||||
<a target="_blank" href="/docs/zen">
|
||||
<a target="_blank" href={language.route("/docs/zen")}>
|
||||
{i18n.t("common.learnMore")}
|
||||
</a>
|
||||
.
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"@opencode-ai/console-resource": "workspace:*",
|
||||
"@planetscale/database": "1.19.0",
|
||||
"aws4fetch": "1.0.20",
|
||||
"drizzle-orm": "0.41.0",
|
||||
"drizzle-orm": "catalog:",
|
||||
"postgres": "3.4.7",
|
||||
"stripe": "18.0.0",
|
||||
"ulid": "catalog:",
|
||||
@@ -43,7 +43,7 @@
|
||||
"@tsconfig/node22": "22.0.2",
|
||||
"@types/bun": "1.3.0",
|
||||
"@types/node": "catalog:",
|
||||
"drizzle-kit": "0.30.5",
|
||||
"drizzle-kit": "catalog:",
|
||||
"mysql2": "3.14.4",
|
||||
"typescript": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
|
||||
@@ -9,7 +9,7 @@ const stage = process.argv[2]
|
||||
if (!stage) throw new Error("Stage is required")
|
||||
|
||||
const root = path.resolve(process.cwd(), "..", "..", "..")
|
||||
const PARTS = 10
|
||||
const PARTS = 20
|
||||
|
||||
// read the secret
|
||||
const ret = await $`bun sst secret list`.cwd(root).text()
|
||||
|
||||
@@ -9,7 +9,7 @@ const stage = process.argv[2]
|
||||
if (!stage) throw new Error("Stage is required")
|
||||
|
||||
const root = path.resolve(process.cwd(), "..", "..", "..")
|
||||
const PARTS = 10
|
||||
const PARTS = 20
|
||||
|
||||
// read the secret
|
||||
const ret = await $`bun sst secret list --stage ${stage}`.cwd(root).text()
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ZenData } from "../src/model"
|
||||
|
||||
const root = path.resolve(process.cwd(), "..", "..", "..")
|
||||
const models = await $`bun sst secret list`.cwd(root).text()
|
||||
const PARTS = 10
|
||||
const PARTS = 20
|
||||
|
||||
// read the line starting with "ZEN_MODELS"
|
||||
const lines = models.split("\n")
|
||||
|
||||
@@ -4,7 +4,6 @@ export * from "drizzle-orm"
|
||||
import { Client } from "@planetscale/database"
|
||||
|
||||
import { MySqlTransaction, type MySqlTransactionConfig } from "drizzle-orm/mysql-core"
|
||||
import type { ExtractTablesWithRelations } from "drizzle-orm"
|
||||
import type { PlanetScalePreparedQueryHKT, PlanetscaleQueryResultHKT } from "drizzle-orm/planetscale-serverless"
|
||||
import { Context } from "../context"
|
||||
import { memo } from "../util/memo"
|
||||
@@ -14,7 +13,7 @@ export namespace Database {
|
||||
PlanetscaleQueryResultHKT,
|
||||
PlanetScalePreparedQueryHKT,
|
||||
Record<string, never>,
|
||||
ExtractTablesWithRelations<Record<string, never>>
|
||||
any
|
||||
>
|
||||
|
||||
const client = memo(() => {
|
||||
@@ -23,7 +22,7 @@ export namespace Database {
|
||||
username: Resource.Database.username,
|
||||
password: Resource.Database.password,
|
||||
})
|
||||
const db = drizzle(result, {})
|
||||
const db = drizzle({ client: result })
|
||||
return db
|
||||
})
|
||||
|
||||
|
||||
@@ -86,7 +86,17 @@ export namespace ZenData {
|
||||
Resource.ZEN_MODELS7.value +
|
||||
Resource.ZEN_MODELS8.value +
|
||||
Resource.ZEN_MODELS9.value +
|
||||
Resource.ZEN_MODELS10.value,
|
||||
Resource.ZEN_MODELS10.value +
|
||||
Resource.ZEN_MODELS11.value +
|
||||
Resource.ZEN_MODELS12.value +
|
||||
Resource.ZEN_MODELS13.value +
|
||||
Resource.ZEN_MODELS14.value +
|
||||
Resource.ZEN_MODELS15.value +
|
||||
Resource.ZEN_MODELS16.value +
|
||||
Resource.ZEN_MODELS17.value +
|
||||
Resource.ZEN_MODELS18.value +
|
||||
Resource.ZEN_MODELS19.value +
|
||||
Resource.ZEN_MODELS20.value,
|
||||
)
|
||||
return ModelsSchema.parse(json)
|
||||
})
|
||||
|
||||
40
packages/console/core/sst-env.d.ts
vendored
40
packages/console/core/sst-env.d.ts
vendored
@@ -137,10 +137,50 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS11": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS12": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS13": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS14": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS15": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS16": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS17": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS18": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS19": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS2": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS20": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS3": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
|
||||
40
packages/console/function/sst-env.d.ts
vendored
40
packages/console/function/sst-env.d.ts
vendored
@@ -137,10 +137,50 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS11": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS12": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS13": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS14": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS15": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS16": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS17": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS18": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS19": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS2": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS20": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS3": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
|
||||
40
packages/console/resource/sst-env.d.ts
vendored
40
packages/console/resource/sst-env.d.ts
vendored
@@ -137,10 +137,50 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS11": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS12": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS13": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS14": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS15": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS16": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS17": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS18": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS19": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS2": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS20": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS3": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
|
||||
@@ -18,7 +18,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2.9.5", features = ["macos-private-api", "devtools"] }
|
||||
tauri = { version = "2.9.5", features = ["macos-private-api"] }
|
||||
tauri-plugin-opener = "2"
|
||||
tauri-plugin-deep-link = "2.4.6"
|
||||
tauri-plugin-shell = "2"
|
||||
@@ -56,6 +56,7 @@ webkit2gtk = "=2.0.2"
|
||||
objc2 = "0.6"
|
||||
objc2-web-kit = "0.3"
|
||||
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.61", features = [
|
||||
"Win32_Foundation",
|
||||
|
||||
@@ -2,6 +2,8 @@ mod cli;
|
||||
mod constants;
|
||||
#[cfg(windows)]
|
||||
mod job_object;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod linux_display;
|
||||
mod markdown;
|
||||
mod server;
|
||||
mod window_customizer;
|
||||
@@ -194,6 +196,43 @@ fn check_macos_app(app_name: &str) -> bool {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum LinuxDisplayBackend {
|
||||
Wayland,
|
||||
Auto,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
fn get_display_backend() -> Option<LinuxDisplayBackend> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let prefer = linux_display::read_wayland().unwrap_or(false);
|
||||
return Some(if prefer {
|
||||
LinuxDisplayBackend::Wayland
|
||||
} else {
|
||||
LinuxDisplayBackend::Auto
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
None
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
fn set_display_backend(_app: AppHandle, _backend: LinuxDisplayBackend) -> Result<(), String> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let prefer = matches!(_backend, LinuxDisplayBackend::Wayland);
|
||||
return linux_display::write_wayland(&_app, prefer);
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn check_linux_app(app_name: &str) -> bool {
|
||||
return true;
|
||||
@@ -209,6 +248,8 @@ pub fn run() {
|
||||
await_initialization,
|
||||
server::get_default_server_url,
|
||||
server::set_default_server_url,
|
||||
get_display_backend,
|
||||
set_display_backend,
|
||||
markdown::parse_markdown_command,
|
||||
check_app_exists
|
||||
])
|
||||
|
||||
47
packages/desktop/src-tauri/src/linux_display.rs
Normal file
47
packages/desktop/src-tauri/src/linux_display.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_store::StoreExt;
|
||||
|
||||
use crate::constants::SETTINGS_STORE;
|
||||
|
||||
pub const LINUX_DISPLAY_CONFIG_KEY: &str = "linuxDisplayConfig";
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
struct DisplayConfig {
|
||||
wayland: Option<bool>,
|
||||
}
|
||||
|
||||
fn dir() -> Option<PathBuf> {
|
||||
Some(dirs::data_dir()?.join("ai.opencode.desktop"))
|
||||
}
|
||||
|
||||
fn path() -> Option<PathBuf> {
|
||||
dir().map(|dir| dir.join(SETTINGS_STORE))
|
||||
}
|
||||
|
||||
pub fn read_wayland() -> Option<bool> {
|
||||
let path = path()?;
|
||||
let raw = std::fs::read_to_string(path).ok()?;
|
||||
let config = serde_json::from_str::<DisplayConfig>(&raw).ok()?;
|
||||
config.wayland
|
||||
}
|
||||
|
||||
pub fn write_wayland(app: &AppHandle, value: bool) -> Result<(), String> {
|
||||
let store = app
|
||||
.store(SETTINGS_STORE)
|
||||
.map_err(|e| format!("Failed to open settings store: {}", e))?;
|
||||
|
||||
store.set(
|
||||
LINUX_DISPLAY_CONFIG_KEY,
|
||||
json!(DisplayConfig {
|
||||
wayland: Some(value),
|
||||
}),
|
||||
);
|
||||
store
|
||||
.save()
|
||||
.map_err(|e| format!("Failed to save settings store: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -23,12 +23,16 @@ fn configure_display_backend() -> Option<String> {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Allow users to explicitly keep Wayland if they know their setup is stable.
|
||||
let allow_wayland = matches!(
|
||||
env::var("OC_ALLOW_WAYLAND"),
|
||||
Ok(v) if matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes")
|
||||
);
|
||||
let prefer_wayland = opencode_lib::linux_display::read_wayland().unwrap_or(false);
|
||||
let allow_wayland = prefer_wayland
|
||||
|| matches!(
|
||||
env::var("OC_ALLOW_WAYLAND"),
|
||||
Ok(v) if matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes")
|
||||
);
|
||||
if allow_wayland {
|
||||
if prefer_wayland {
|
||||
return Some("Wayland session detected; using native Wayland from settings".into());
|
||||
}
|
||||
return Some("Wayland session detected; respecting OC_ALLOW_WAYLAND=1".into());
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ export const commands = {
|
||||
awaitInitialization: (events: Channel) => __TAURI_INVOKE<ServerReadyData>("await_initialization", { events }),
|
||||
getDefaultServerUrl: () => __TAURI_INVOKE<string | null>("get_default_server_url"),
|
||||
setDefaultServerUrl: (url: string | null) => __TAURI_INVOKE<null>("set_default_server_url", { url }),
|
||||
getDisplayBackend: () => __TAURI_INVOKE<"wayland" | "auto" | null>("get_display_backend"),
|
||||
setDisplayBackend: (backend: LinuxDisplayBackend) => __TAURI_INVOKE<null>("set_display_backend", { backend }),
|
||||
parseMarkdownCommand: (markdown: string) => __TAURI_INVOKE<string>("parse_markdown_command", { markdown }),
|
||||
checkAppExists: (appName: string) => __TAURI_INVOKE<boolean>("check_app_exists", { appName }),
|
||||
};
|
||||
@@ -22,6 +24,8 @@ export const events = {
|
||||
/* Types */
|
||||
export type InitStep = { phase: "server_waiting" } | { phase: "sqlite_waiting" } | { phase: "done" };
|
||||
|
||||
export type LinuxDisplayBackend = "wayland" | "auto";
|
||||
|
||||
export type LoadingWindowComplete = null;
|
||||
|
||||
export type ServerReadyData = {
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
// @refresh reload
|
||||
import { webviewZoom } from "./webview-zoom"
|
||||
import { render } from "solid-js/web"
|
||||
import { AppBaseProviders, AppInterface, PlatformProvider, Platform, useCommand } from "@opencode-ai/app"
|
||||
import {
|
||||
AppBaseProviders,
|
||||
AppInterface,
|
||||
PlatformProvider,
|
||||
Platform,
|
||||
DisplayBackend,
|
||||
useCommand,
|
||||
} from "@opencode-ai/app"
|
||||
import { open, save } from "@tauri-apps/plugin-dialog"
|
||||
import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link"
|
||||
import { openPath as openerOpenPath } from "@tauri-apps/plugin-opener"
|
||||
@@ -9,6 +16,7 @@ import { open as shellOpen } from "@tauri-apps/plugin-shell"
|
||||
import { type as ostype } from "@tauri-apps/plugin-os"
|
||||
import { check, Update } from "@tauri-apps/plugin-updater"
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window"
|
||||
import { invoke } from "@tauri-apps/api/core"
|
||||
import { isPermissionGranted, requestPermission } from "@tauri-apps/plugin-notification"
|
||||
import { relaunch } from "@tauri-apps/plugin-process"
|
||||
import { AsyncStorage } from "@solid-primitives/storage"
|
||||
@@ -338,6 +346,15 @@ const createPlatform = (password: Accessor<string | null>): Platform => ({
|
||||
await commands.setDefaultServerUrl(url)
|
||||
},
|
||||
|
||||
getDisplayBackend: async () => {
|
||||
const result = await invoke<DisplayBackend | null>("get_display_backend").catch(() => null)
|
||||
return result
|
||||
},
|
||||
|
||||
setDisplayBackend: async (backend) => {
|
||||
await invoke("set_display_backend", { backend }).catch(() => undefined)
|
||||
},
|
||||
|
||||
parseMarkdown: (markdown: string) => commands.parseMarkdownCommand(markdown),
|
||||
|
||||
webviewZoom,
|
||||
@@ -413,7 +430,7 @@ render(() => {
|
||||
}
|
||||
|
||||
return (
|
||||
<AppInterface defaultUrl={data().url}>
|
||||
<AppInterface defaultUrl={data().url} isSidecar>
|
||||
<Inner />
|
||||
</AppInterface>
|
||||
)
|
||||
|
||||
40
packages/enterprise/sst-env.d.ts
vendored
40
packages/enterprise/sst-env.d.ts
vendored
@@ -137,10 +137,50 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS11": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS12": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS13": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS14": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS15": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS16": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS17": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS18": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS19": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS2": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS20": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS3": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
|
||||
40
packages/function/sst-env.d.ts
vendored
40
packages/function/sst-env.d.ts
vendored
@@ -137,10 +137,50 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS11": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS12": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS13": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS14": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS15": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS16": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS17": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS18": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS19": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS2": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS20": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS3": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
|
||||
@@ -1,27 +1,10 @@
|
||||
# opencode agent guidelines
|
||||
# opencode database guide
|
||||
|
||||
## Build/Test Commands
|
||||
## Database
|
||||
|
||||
- **Install**: `bun install`
|
||||
- **Run**: `bun run --conditions=browser ./src/index.ts`
|
||||
- **Typecheck**: `bun run typecheck` (npm run typecheck)
|
||||
- **Test**: `bun test` (runs all tests)
|
||||
- **Single test**: `bun test test/tool/tool.test.ts` (specific test file)
|
||||
|
||||
## Code Style
|
||||
|
||||
- **Runtime**: Bun with TypeScript ESM modules
|
||||
- **Imports**: Use relative imports for local modules, named imports preferred
|
||||
- **Types**: Zod schemas for validation, TypeScript interfaces for structure
|
||||
- **Naming**: camelCase for variables/functions, PascalCase for classes/namespaces
|
||||
- **Error handling**: Use Result patterns, avoid throwing exceptions in tools
|
||||
- **File structure**: Namespace-based organization (e.g., `Tool.define()`, `Session.create()`)
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Tools**: Implement `Tool.Info` interface with `execute()` method
|
||||
- **Context**: Pass `sessionID` in tool context, use `App.provide()` for DI
|
||||
- **Validation**: All inputs validated with Zod schemas
|
||||
- **Logging**: Use `Log.create({ service: "name" })` pattern
|
||||
- **Storage**: Use `Storage` namespace for persistence
|
||||
- **API Client**: The TypeScript TUI (built with SolidJS + OpenTUI) communicates with the OpenCode server using `@opencode-ai/sdk`. When adding/modifying server endpoints in `packages/opencode/src/server/server.ts`, run `./script/generate.ts` to regenerate the SDK and related files.
|
||||
- **Schema**: Drizzle schema lives in `src/**/*.sql.ts`.
|
||||
- **Naming**: tables and columns use snake*case; join columns are `<entity>_id`; indexes are `<table>*<column>\_idx`.
|
||||
- **Migrations**: generated by Drizzle Kit using `drizzle.config.ts` (schema: `./src/**/*.sql.ts`, output: `./migration`).
|
||||
- **Command**: `bun run db generate --name <slug>`.
|
||||
- **Output**: creates `migration/<timestamp>_<slug>/migration.sql` and `snapshot.json`.
|
||||
- **Tests**: migration tests should read the per-folder layout (no `_journal.json`).
|
||||
|
||||
10
packages/opencode/drizzle.config.ts
Normal file
10
packages/opencode/drizzle.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from "drizzle-kit"
|
||||
|
||||
export default defineConfig({
|
||||
dialect: "sqlite",
|
||||
schema: "./src/**/*.sql.ts",
|
||||
out: "./migration",
|
||||
dbCredentials: {
|
||||
url: "/home/thdxr/.local/share/opencode/opencode.db",
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,90 @@
|
||||
CREATE TABLE `project` (
|
||||
`id` text PRIMARY KEY,
|
||||
`worktree` text NOT NULL,
|
||||
`vcs` text,
|
||||
`name` text,
|
||||
`icon_url` text,
|
||||
`icon_color` text,
|
||||
`time_created` integer NOT NULL,
|
||||
`time_updated` integer NOT NULL,
|
||||
`time_initialized` integer,
|
||||
`sandboxes` text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `message` (
|
||||
`id` text PRIMARY KEY,
|
||||
`session_id` text NOT NULL,
|
||||
`time_created` integer NOT NULL,
|
||||
`time_updated` integer NOT NULL,
|
||||
`data` text NOT NULL,
|
||||
CONSTRAINT `fk_message_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `part` (
|
||||
`id` text PRIMARY KEY,
|
||||
`message_id` text NOT NULL,
|
||||
`session_id` text NOT NULL,
|
||||
`time_created` integer NOT NULL,
|
||||
`time_updated` integer NOT NULL,
|
||||
`data` text NOT NULL,
|
||||
CONSTRAINT `fk_part_message_id_message_id_fk` FOREIGN KEY (`message_id`) REFERENCES `message`(`id`) ON DELETE CASCADE
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `permission` (
|
||||
`project_id` text PRIMARY KEY,
|
||||
`time_created` integer NOT NULL,
|
||||
`time_updated` integer NOT NULL,
|
||||
`data` text NOT NULL,
|
||||
CONSTRAINT `fk_permission_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `session` (
|
||||
`id` text PRIMARY KEY,
|
||||
`project_id` text NOT NULL,
|
||||
`parent_id` text,
|
||||
`slug` text NOT NULL,
|
||||
`directory` text NOT NULL,
|
||||
`title` text NOT NULL,
|
||||
`version` text NOT NULL,
|
||||
`share_url` text,
|
||||
`summary_additions` integer,
|
||||
`summary_deletions` integer,
|
||||
`summary_files` integer,
|
||||
`summary_diffs` text,
|
||||
`revert` text,
|
||||
`permission` text,
|
||||
`time_created` integer NOT NULL,
|
||||
`time_updated` integer NOT NULL,
|
||||
`time_compacting` integer,
|
||||
`time_archived` integer,
|
||||
CONSTRAINT `fk_session_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `todo` (
|
||||
`session_id` text NOT NULL,
|
||||
`content` text NOT NULL,
|
||||
`status` text NOT NULL,
|
||||
`priority` text NOT NULL,
|
||||
`position` integer NOT NULL,
|
||||
`time_created` integer NOT NULL,
|
||||
`time_updated` integer NOT NULL,
|
||||
CONSTRAINT `todo_pk` PRIMARY KEY(`session_id`, `position`),
|
||||
CONSTRAINT `fk_todo_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `session_share` (
|
||||
`session_id` text PRIMARY KEY,
|
||||
`id` text NOT NULL,
|
||||
`secret` text NOT NULL,
|
||||
`url` text NOT NULL,
|
||||
`time_created` integer NOT NULL,
|
||||
`time_updated` integer NOT NULL,
|
||||
CONSTRAINT `fk_session_share_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `message_session_idx` ON `message` (`session_id`);--> statement-breakpoint
|
||||
CREATE INDEX `part_message_idx` ON `part` (`message_id`);--> statement-breakpoint
|
||||
CREATE INDEX `part_session_idx` ON `part` (`session_id`);--> statement-breakpoint
|
||||
CREATE INDEX `session_project_idx` ON `session` (`project_id`);--> statement-breakpoint
|
||||
CREATE INDEX `session_parent_idx` ON `session` (`parent_id`);--> statement-breakpoint
|
||||
CREATE INDEX `todo_session_idx` ON `todo` (`session_id`);
|
||||
@@ -0,0 +1,796 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"id": "068758ed-a97a-46f6-8a59-6c639ae7c20c",
|
||||
"prevIds": ["00000000-0000-0000-0000-000000000000"],
|
||||
"ddl": [
|
||||
{
|
||||
"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": "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": "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": ["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": ["session_id", "position"],
|
||||
"nameExplicit": false,
|
||||
"name": "todo_pk",
|
||||
"entityType": "pks",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
@@ -15,7 +15,8 @@
|
||||
"lint": "echo 'Running lint checks...' && bun test --coverage",
|
||||
"format": "echo 'Formatting code...' && bun run --prettier --write src/**/*.ts",
|
||||
"docs": "echo 'Generating documentation...' && find src -name '*.ts' -exec echo 'Processing: {}' \\;",
|
||||
"deploy": "echo 'Deploying application...' && bun run build && echo 'Deployment completed successfully'"
|
||||
"deploy": "echo 'Deploying application...' && bun run build && echo 'Deployment completed successfully'",
|
||||
"db": "bun drizzle-kit"
|
||||
},
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode"
|
||||
@@ -42,6 +43,8 @@
|
||||
"@types/turndown": "5.0.5",
|
||||
"@types/yargs": "17.0.33",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"drizzle-kit": "1.0.0-beta.12-a5629fb",
|
||||
"drizzle-orm": "1.0.0-beta.12-a5629fb",
|
||||
"typescript": "catalog:",
|
||||
"vscode-languageserver-types": "3.17.5",
|
||||
"why-is-node-running": "3.2.2",
|
||||
@@ -100,6 +103,7 @@
|
||||
"clipboardy": "4.0.0",
|
||||
"decimal.js": "10.5.0",
|
||||
"diff": "catalog:",
|
||||
"drizzle-orm": "1.0.0-beta.12-a5629fb",
|
||||
"fuzzysort": "3.1.0",
|
||||
"gray-matter": "4.0.3",
|
||||
"hono": "catalog:",
|
||||
@@ -122,5 +126,8 @@
|
||||
"yargs": "18.0.0",
|
||||
"zod": "catalog:",
|
||||
"zod-to-json-schema": "3.24.5"
|
||||
},
|
||||
"overrides": {
|
||||
"drizzle-orm": "1.0.0-beta.12-a5629fb"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,32 @@ await Bun.write(
|
||||
)
|
||||
console.log("Generated models-snapshot.ts")
|
||||
|
||||
// Load migrations from migration directories
|
||||
const migrationDirs = (await fs.promises.readdir(path.join(dir, "migration"), { withFileTypes: true }))
|
||||
.filter((entry) => entry.isDirectory() && /^\d{4}\d{2}\d{2}\d{2}\d{2}\d{2}/.test(entry.name))
|
||||
.map((entry) => entry.name)
|
||||
.sort()
|
||||
|
||||
const migrations = await Promise.all(
|
||||
migrationDirs.map(async (name) => {
|
||||
const file = path.join(dir, "migration", name, "migration.sql")
|
||||
const sql = await Bun.file(file).text()
|
||||
const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(name)
|
||||
const timestamp = match
|
||||
? Date.UTC(
|
||||
Number(match[1]),
|
||||
Number(match[2]) - 1,
|
||||
Number(match[3]),
|
||||
Number(match[4]),
|
||||
Number(match[5]),
|
||||
Number(match[6]),
|
||||
)
|
||||
: 0
|
||||
return { sql, timestamp }
|
||||
}),
|
||||
)
|
||||
console.log(`Loaded ${migrations.length} migrations`)
|
||||
|
||||
const singleFlag = process.argv.includes("--single")
|
||||
const baselineFlag = process.argv.includes("--baseline")
|
||||
const skipInstall = process.argv.includes("--skip-install")
|
||||
@@ -156,6 +182,7 @@ for (const item of targets) {
|
||||
entrypoints: ["./src/index.ts", parserWorker, workerPath],
|
||||
define: {
|
||||
OPENCODE_VERSION: `'${Script.version}'`,
|
||||
OPENCODE_MIGRATIONS: JSON.stringify(migrations),
|
||||
OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath,
|
||||
OPENCODE_WORKER_PATH: workerPath,
|
||||
OPENCODE_CHANNEL: `'${Script.channel}'`,
|
||||
|
||||
16
packages/opencode/script/check-migrations.ts
Normal file
16
packages/opencode/script/check-migrations.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { $ } from "bun"
|
||||
|
||||
// drizzle-kit check compares schema to migrations, exits non-zero if drift
|
||||
const result = await $`bun drizzle-kit check`.quiet().nothrow()
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
console.error("Schema has changes not captured in migrations!")
|
||||
console.error("Run: bun drizzle-kit generate")
|
||||
console.error("")
|
||||
console.error(result.stderr.toString())
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log("Migrations are up to date")
|
||||
@@ -228,8 +228,8 @@ export namespace ACP {
|
||||
const metadata = permission.metadata || {}
|
||||
const filepath = typeof metadata["filepath"] === "string" ? metadata["filepath"] : ""
|
||||
const diff = typeof metadata["diff"] === "string" ? metadata["diff"] : ""
|
||||
|
||||
const content = await Bun.file(filepath).text()
|
||||
const file = Bun.file(filepath)
|
||||
const content = (await file.exists()) ? await file.text() : ""
|
||||
const newContent = getNewContent(content, diff)
|
||||
|
||||
if (newContent) {
|
||||
@@ -435,46 +435,68 @@ export namespace ACP {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (part.type === "text") {
|
||||
const delta = props.delta
|
||||
if (delta && part.ignored !== true) {
|
||||
await this.connection
|
||||
.sessionUpdate({
|
||||
sessionId,
|
||||
update: {
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: {
|
||||
type: "text",
|
||||
text: delta,
|
||||
},
|
||||
case "message.part.delta": {
|
||||
const props = event.properties
|
||||
const session = this.sessionManager.tryGet(props.sessionID)
|
||||
if (!session) return
|
||||
const sessionId = session.id
|
||||
|
||||
const message = await this.sdk.session
|
||||
.message(
|
||||
{
|
||||
sessionID: props.sessionID,
|
||||
messageID: props.messageID,
|
||||
directory: session.cwd,
|
||||
},
|
||||
{ throwOnError: true },
|
||||
)
|
||||
.then((x) => x.data)
|
||||
.catch((error) => {
|
||||
log.error("unexpected error when fetching message", { error })
|
||||
return undefined
|
||||
})
|
||||
|
||||
if (!message || message.info.role !== "assistant") return
|
||||
|
||||
const part = message.parts.find((p) => p.id === props.partID)
|
||||
if (!part) return
|
||||
|
||||
if (part.type === "text" && props.field === "text" && part.ignored !== true) {
|
||||
await this.connection
|
||||
.sessionUpdate({
|
||||
sessionId,
|
||||
update: {
|
||||
sessionUpdate: "agent_message_chunk",
|
||||
content: {
|
||||
type: "text",
|
||||
text: props.delta,
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error("failed to send text to ACP", { error })
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error("failed to send text delta to ACP", { error })
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (part.type === "reasoning") {
|
||||
const delta = props.delta
|
||||
if (delta) {
|
||||
await this.connection
|
||||
.sessionUpdate({
|
||||
sessionId,
|
||||
update: {
|
||||
sessionUpdate: "agent_thought_chunk",
|
||||
content: {
|
||||
type: "text",
|
||||
text: delta,
|
||||
},
|
||||
if (part.type === "reasoning" && props.field === "text") {
|
||||
await this.connection
|
||||
.sessionUpdate({
|
||||
sessionId,
|
||||
update: {
|
||||
sessionUpdate: "agent_thought_chunk",
|
||||
content: {
|
||||
type: "text",
|
||||
text: props.delta,
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error("failed to send reasoning to ACP", { error })
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error("failed to send reasoning delta to ACP", { error })
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ import type { Session as SDKSession, Message, Part } from "@opencode-ai/sdk/v2"
|
||||
import { Session } from "../../session"
|
||||
import { cmd } from "./cmd"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
import { Storage } from "../../storage/storage"
|
||||
import { Database } from "../../storage/db"
|
||||
import { SessionTable, MessageTable, PartTable } from "../../session/session.sql"
|
||||
import { Instance } from "../../project/instance"
|
||||
import { ShareNext } from "../../share/share-next"
|
||||
import { EOL } from "os"
|
||||
@@ -130,13 +131,35 @@ export const ImportCommand = cmd({
|
||||
return
|
||||
}
|
||||
|
||||
await Storage.write(["session", Instance.project.id, exportData.info.id], exportData.info)
|
||||
Database.use((db) => db.insert(SessionTable).values(Session.toRow(exportData.info)).onConflictDoNothing().run())
|
||||
|
||||
for (const msg of exportData.messages) {
|
||||
await Storage.write(["message", exportData.info.id, msg.info.id], msg.info)
|
||||
Database.use((db) =>
|
||||
db
|
||||
.insert(MessageTable)
|
||||
.values({
|
||||
id: msg.info.id,
|
||||
session_id: exportData.info.id,
|
||||
time_created: msg.info.time?.created ?? Date.now(),
|
||||
data: msg.info,
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
.run(),
|
||||
)
|
||||
|
||||
for (const part of msg.parts) {
|
||||
await Storage.write(["part", msg.info.id, part.id], part)
|
||||
Database.use((db) =>
|
||||
db
|
||||
.insert(PartTable)
|
||||
.values({
|
||||
id: part.id,
|
||||
message_id: msg.info.id,
|
||||
session_id: exportData.info.id,
|
||||
data: part,
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
.run(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ import type { Argv } from "yargs"
|
||||
import { cmd } from "./cmd"
|
||||
import { Session } from "../../session"
|
||||
import { bootstrap } from "../bootstrap"
|
||||
import { Storage } from "../../storage/storage"
|
||||
import { Database } from "../../storage/db"
|
||||
import { SessionTable } from "../../session/session.sql"
|
||||
import { Project } from "../../project/project"
|
||||
import { Instance } from "../../project/instance"
|
||||
|
||||
@@ -87,25 +88,8 @@ async function getCurrentProject(): Promise<Project.Info> {
|
||||
}
|
||||
|
||||
async function getAllSessions(): Promise<Session.Info[]> {
|
||||
const sessions: Session.Info[] = []
|
||||
|
||||
const projectKeys = await Storage.list(["project"])
|
||||
const projects = await Promise.all(projectKeys.map((key) => Storage.read<Project.Info>(key)))
|
||||
|
||||
for (const project of projects) {
|
||||
if (!project) continue
|
||||
|
||||
const sessionKeys = await Storage.list(["session", project.id])
|
||||
const projectSessions = await Promise.all(sessionKeys.map((key) => Storage.read<Session.Info>(key)))
|
||||
|
||||
for (const session of projectSessions) {
|
||||
if (session) {
|
||||
sessions.push(session)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sessions
|
||||
const rows = Database.use((db) => db.select().from(SessionTable).all())
|
||||
return rows.map((row) => Session.fromRow(row))
|
||||
}
|
||||
|
||||
export async function aggregateSessionStats(days?: number, projectFilter?: string): Promise<SessionStats> {
|
||||
|
||||
@@ -299,6 +299,24 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
break
|
||||
}
|
||||
|
||||
case "message.part.delta": {
|
||||
const parts = store.part[event.properties.messageID]
|
||||
if (!parts) break
|
||||
const result = Binary.search(parts, event.properties.partID, (p) => p.id)
|
||||
if (!result.found) break
|
||||
setStore(
|
||||
"part",
|
||||
event.properties.messageID,
|
||||
produce((draft) => {
|
||||
const part = draft[result.index]
|
||||
const field = event.properties.field as keyof typeof part
|
||||
const existing = part[field] as string | undefined
|
||||
;(part[field] as string) = (existing ?? "") + event.properties.delta
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
case "message.part.removed": {
|
||||
const parts = store.part[event.properties.messageID]
|
||||
const result = Binary.search(parts, event.properties.partID, (p) => p.id)
|
||||
|
||||
@@ -1099,9 +1099,6 @@ export function Session() {
|
||||
sessionID={route.sessionID}
|
||||
/>
|
||||
</box>
|
||||
<Show when={!sidebarVisible() || !wide()}>
|
||||
<Footer />
|
||||
</Show>
|
||||
</Show>
|
||||
<Toast />
|
||||
</box>
|
||||
@@ -2029,8 +2026,8 @@ function ApplyPatch(props: ToolProps<typeof ApplyPatchTool>) {
|
||||
</For>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<InlineTool icon="%" pending="Preparing apply_patch..." complete={false} part={props.part}>
|
||||
apply_patch
|
||||
<InlineTool icon="%" pending="Preparing patch..." complete={false} part={props.part}>
|
||||
Patch
|
||||
</InlineTool>
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
@@ -26,6 +26,10 @@ import { EOL } from "os"
|
||||
import { WebCommand } from "./cli/cmd/web"
|
||||
import { PrCommand } from "./cli/cmd/pr"
|
||||
import { SessionCommand } from "./cli/cmd/session"
|
||||
import path from "path"
|
||||
import { Global } from "./global"
|
||||
import { JsonMigration } from "./storage/json-migration"
|
||||
import { Database } from "./storage/db"
|
||||
|
||||
process.on("unhandledRejection", (e) => {
|
||||
Log.Default.error("rejection", {
|
||||
@@ -74,6 +78,37 @@ const cli = yargs(hideBin(process.argv))
|
||||
version: Installation.VERSION,
|
||||
args: process.argv.slice(2),
|
||||
})
|
||||
|
||||
const marker = path.join(Global.Path.data, "opencode.db")
|
||||
if (!(await Bun.file(marker).exists())) {
|
||||
console.log("Performing one time database migration, may take a few minutes...")
|
||||
const tty = process.stdout.isTTY
|
||||
const width = 36
|
||||
const orange = "\x1b[38;5;214m"
|
||||
const muted = "\x1b[0;2m"
|
||||
const reset = "\x1b[0m"
|
||||
let last = -1
|
||||
if (tty) process.stdout.write("\x1b[?25l")
|
||||
try {
|
||||
await JsonMigration.run(Database.Client().$client, {
|
||||
progress: (event) => {
|
||||
if (!tty) return
|
||||
const percent = Math.floor((event.current / event.total) * 100)
|
||||
if (percent === last && event.current !== event.total) return
|
||||
last = percent
|
||||
const fill = Math.round((percent / 100) * width)
|
||||
const bar = `${"■".repeat(fill)}${"・".repeat(width - fill)}`
|
||||
process.stdout.write(
|
||||
`\r${orange}${bar} ${percent.toString().padStart(3)}%${reset} ${muted}${event.label.padEnd(12)} ${event.current}/${event.total}${reset}`,
|
||||
)
|
||||
if (event.current === event.total) process.stdout.write("\n")
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
if (tty) process.stdout.write("\x1b[?25h")
|
||||
}
|
||||
console.log("Database migration complete.")
|
||||
}
|
||||
})
|
||||
.usage("\n" + UI.logo())
|
||||
.completion("completion", "generate shell completion script")
|
||||
|
||||
@@ -3,7 +3,8 @@ import { BusEvent } from "@/bus/bus-event"
|
||||
import { Config } from "@/config/config"
|
||||
import { Identifier } from "@/id/id"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { Storage } from "@/storage/storage"
|
||||
import { Database, eq } from "@/storage/db"
|
||||
import { PermissionTable } from "@/session/session.sql"
|
||||
import { fn } from "@/util/fn"
|
||||
import { Log } from "@/util/log"
|
||||
import { Wildcard } from "@/util/wildcard"
|
||||
@@ -105,9 +106,12 @@ export namespace PermissionNext {
|
||||
),
|
||||
}
|
||||
|
||||
const state = Instance.state(async () => {
|
||||
const state = Instance.state(() => {
|
||||
const projectID = Instance.project.id
|
||||
const stored = await Storage.read<Ruleset>(["permission", projectID]).catch(() => [] as Ruleset)
|
||||
const row = Database.use((db) =>
|
||||
db.select().from(PermissionTable).where(eq(PermissionTable.project_id, projectID)).get(),
|
||||
)
|
||||
const stored = row?.data ?? ([] as Ruleset)
|
||||
|
||||
const pending: Record<
|
||||
string,
|
||||
@@ -222,7 +226,8 @@ export namespace PermissionNext {
|
||||
|
||||
// TODO: we don't save the permission ruleset to disk yet until there's
|
||||
// UI to manage it
|
||||
// await Storage.write(["permission", Instance.project.id], s.approved)
|
||||
// db().insert(PermissionTable).values({ projectID: Instance.project.id, data: s.approved })
|
||||
// .onConflictDoUpdate({ target: PermissionTable.projectID, set: { data: s.approved } }).run()
|
||||
return
|
||||
}
|
||||
},
|
||||
@@ -275,6 +280,7 @@ export namespace PermissionNext {
|
||||
}
|
||||
|
||||
export async function list() {
|
||||
return state().then((x) => Object.values(x.pending).map((x) => x.info))
|
||||
const s = await state()
|
||||
return Object.values(s.pending).map((x) => x.info)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Plugin } from "../plugin"
|
||||
import { Share } from "../share/share"
|
||||
import { Format } from "../format"
|
||||
import { LSP } from "../lsp"
|
||||
import { FileWatcher } from "../file/watcher"
|
||||
@@ -17,7 +16,6 @@ import { Truncate } from "../tool/truncation"
|
||||
export async function InstanceBootstrap() {
|
||||
Log.Default.info("bootstrapping", { directory: Instance.directory })
|
||||
await Plugin.init()
|
||||
Share.init()
|
||||
ShareNext.init()
|
||||
Format.init()
|
||||
await LSP.init()
|
||||
|
||||
14
packages/opencode/src/project/project.sql.ts
Normal file
14
packages/opencode/src/project/project.sql.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"
|
||||
import { Timestamps } from "@/storage/schema.sql"
|
||||
|
||||
export const ProjectTable = sqliteTable("project", {
|
||||
id: text().primaryKey(),
|
||||
worktree: text().notNull(),
|
||||
vcs: text(),
|
||||
name: text(),
|
||||
icon_url: text(),
|
||||
icon_color: text(),
|
||||
...Timestamps,
|
||||
time_initialized: integer(),
|
||||
sandboxes: text({ mode: "json" }).notNull().$type<string[]>(),
|
||||
})
|
||||
@@ -1,18 +1,17 @@
|
||||
import z from "zod"
|
||||
import fs from "fs/promises"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import path from "path"
|
||||
import { $ } from "bun"
|
||||
import { Storage } from "../storage/storage"
|
||||
import { Database, eq } from "../storage/db"
|
||||
import { ProjectTable } from "./project.sql"
|
||||
import { SessionTable } from "../session/session.sql"
|
||||
import { Log } from "../util/log"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { Session } from "../session"
|
||||
import { work } from "../util/queue"
|
||||
import { fn } from "@opencode-ai/util/fn"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { iife } from "@/util/iife"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
import { existsSync } from "fs"
|
||||
|
||||
export namespace Project {
|
||||
const log = Log.create({ service: "project" })
|
||||
@@ -50,66 +49,85 @@ export namespace Project {
|
||||
Updated: BusEvent.define("project.updated", Info),
|
||||
}
|
||||
|
||||
type Row = typeof ProjectTable.$inferSelect
|
||||
|
||||
export function fromRow(row: Row): Info {
|
||||
const icon =
|
||||
row.icon_url || row.icon_color
|
||||
? { url: row.icon_url ?? undefined, color: row.icon_color ?? undefined }
|
||||
: undefined
|
||||
return {
|
||||
id: row.id,
|
||||
worktree: row.worktree,
|
||||
vcs: row.vcs ? Info.shape.vcs.parse(row.vcs) : undefined,
|
||||
name: row.name ?? undefined,
|
||||
icon,
|
||||
time: {
|
||||
created: row.time_created,
|
||||
updated: row.time_updated,
|
||||
initialized: row.time_initialized ?? undefined,
|
||||
},
|
||||
sandboxes: row.sandboxes,
|
||||
}
|
||||
}
|
||||
|
||||
export async function fromDirectory(directory: string) {
|
||||
log.info("fromDirectory", { directory })
|
||||
|
||||
const { id, sandbox, worktree, vcs } = await iife(async () => {
|
||||
const data = await iife(async () => {
|
||||
const matches = Filesystem.up({ targets: [".git"], start: directory })
|
||||
const git = await matches.next().then((x) => x.value)
|
||||
await matches.return()
|
||||
if (git) {
|
||||
let sandbox = path.dirname(git)
|
||||
const sandbox = path.dirname(git)
|
||||
const bin = Bun.which("git")
|
||||
|
||||
const gitBinary = Bun.which("git")
|
||||
|
||||
// cached id calculation
|
||||
let id = await Bun.file(path.join(git, "opencode"))
|
||||
const cached = await Bun.file(path.join(git, "opencode"))
|
||||
.text()
|
||||
.then((x) => x.trim())
|
||||
.catch(() => undefined)
|
||||
|
||||
if (!gitBinary) {
|
||||
if (!bin) {
|
||||
return {
|
||||
id: id ?? "global",
|
||||
id: cached ?? "global",
|
||||
worktree: sandbox,
|
||||
sandbox: sandbox,
|
||||
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
|
||||
}
|
||||
}
|
||||
|
||||
// generate id from root commit
|
||||
if (!id) {
|
||||
const roots = await $`git rev-list --max-parents=0 --all`
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.cwd(sandbox)
|
||||
.text()
|
||||
.then((x) =>
|
||||
x
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((x) => x.trim())
|
||||
.toSorted(),
|
||||
)
|
||||
.catch(() => undefined)
|
||||
|
||||
if (!roots) {
|
||||
return {
|
||||
id: "global",
|
||||
worktree: sandbox,
|
||||
sandbox: sandbox,
|
||||
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
|
||||
}
|
||||
}
|
||||
|
||||
id = roots[0]
|
||||
if (id) {
|
||||
void Bun.file(path.join(git, "opencode"))
|
||||
.write(id)
|
||||
const roots = cached
|
||||
? undefined
|
||||
: await $`git rev-list --max-parents=0 --all`
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.cwd(sandbox)
|
||||
.text()
|
||||
.then((x) =>
|
||||
x
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((x) => x.trim())
|
||||
.toSorted(),
|
||||
)
|
||||
.catch(() => undefined)
|
||||
|
||||
if (!cached && !roots) {
|
||||
return {
|
||||
id: "global",
|
||||
worktree: sandbox,
|
||||
sandbox: sandbox,
|
||||
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
|
||||
}
|
||||
}
|
||||
|
||||
const id = cached ?? roots?.[0]
|
||||
if (!cached && id) {
|
||||
void Bun.file(path.join(git, "opencode"))
|
||||
.write(id)
|
||||
.catch(() => undefined)
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
return {
|
||||
id: "global",
|
||||
@@ -136,33 +154,31 @@ export namespace Project {
|
||||
}
|
||||
}
|
||||
|
||||
sandbox = top
|
||||
|
||||
const worktree = await $`git rev-parse --git-common-dir`
|
||||
const tree = await $`git rev-parse --git-common-dir`
|
||||
.quiet()
|
||||
.nothrow()
|
||||
.cwd(sandbox)
|
||||
.cwd(top)
|
||||
.text()
|
||||
.then((x) => {
|
||||
const dirname = path.dirname(x.trim())
|
||||
if (dirname === ".") return sandbox
|
||||
if (dirname === ".") return top
|
||||
return dirname
|
||||
})
|
||||
.catch(() => undefined)
|
||||
|
||||
if (!worktree) {
|
||||
if (!tree) {
|
||||
return {
|
||||
id,
|
||||
sandbox,
|
||||
worktree: sandbox,
|
||||
sandbox: top,
|
||||
worktree: top,
|
||||
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
sandbox,
|
||||
worktree,
|
||||
sandbox: top,
|
||||
worktree: tree,
|
||||
vcs: "git",
|
||||
}
|
||||
}
|
||||
@@ -175,47 +191,78 @@ export namespace Project {
|
||||
}
|
||||
})
|
||||
|
||||
let existing = await Storage.read<Info>(["project", id]).catch(() => undefined)
|
||||
if (!existing) {
|
||||
existing = {
|
||||
id,
|
||||
worktree,
|
||||
vcs: vcs as Info["vcs"],
|
||||
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, data.id)).get())
|
||||
const existing = await iife(async () => {
|
||||
if (row) return fromRow(row)
|
||||
const fresh: Info = {
|
||||
id: data.id,
|
||||
worktree: data.worktree,
|
||||
vcs: data.vcs as Info["vcs"],
|
||||
sandboxes: [],
|
||||
time: {
|
||||
created: Date.now(),
|
||||
updated: Date.now(),
|
||||
},
|
||||
}
|
||||
if (id !== "global") {
|
||||
await migrateFromGlobal(id, worktree)
|
||||
if (data.id !== "global") {
|
||||
await migrateFromGlobal(data.id, data.worktree)
|
||||
}
|
||||
}
|
||||
|
||||
// migrate old projects before sandboxes
|
||||
if (!existing.sandboxes) existing.sandboxes = []
|
||||
return fresh
|
||||
})
|
||||
|
||||
if (Flag.OPENCODE_EXPERIMENTAL_ICON_DISCOVERY) discover(existing)
|
||||
|
||||
const result: Info = {
|
||||
...existing,
|
||||
worktree,
|
||||
vcs: vcs as Info["vcs"],
|
||||
worktree: data.worktree,
|
||||
vcs: data.vcs as Info["vcs"],
|
||||
time: {
|
||||
...existing.time,
|
||||
updated: Date.now(),
|
||||
},
|
||||
}
|
||||
if (sandbox !== result.worktree && !result.sandboxes.includes(sandbox)) result.sandboxes.push(sandbox)
|
||||
result.sandboxes = result.sandboxes.filter((x) => existsSync(x))
|
||||
await Storage.write<Info>(["project", id], result)
|
||||
if (data.sandbox !== result.worktree && !result.sandboxes.includes(data.sandbox))
|
||||
result.sandboxes.push(data.sandbox)
|
||||
const sandboxes: string[] = []
|
||||
for (const x of result.sandboxes) {
|
||||
const stat = await Bun.file(x)
|
||||
.stat()
|
||||
.catch(() => undefined)
|
||||
if (stat) sandboxes.push(x)
|
||||
}
|
||||
result.sandboxes = sandboxes
|
||||
const insert = {
|
||||
id: result.id,
|
||||
worktree: result.worktree,
|
||||
vcs: result.vcs ?? null,
|
||||
name: result.name,
|
||||
icon_url: result.icon?.url,
|
||||
icon_color: result.icon?.color,
|
||||
time_created: result.time.created,
|
||||
time_updated: result.time.updated,
|
||||
time_initialized: result.time.initialized,
|
||||
sandboxes: result.sandboxes,
|
||||
}
|
||||
const updateSet = {
|
||||
worktree: result.worktree,
|
||||
vcs: result.vcs ?? null,
|
||||
name: result.name,
|
||||
icon_url: result.icon?.url,
|
||||
icon_color: result.icon?.color,
|
||||
time_updated: result.time.updated,
|
||||
time_initialized: result.time.initialized,
|
||||
sandboxes: result.sandboxes,
|
||||
}
|
||||
Database.use((db) =>
|
||||
db.insert(ProjectTable).values(insert).onConflictDoUpdate({ target: ProjectTable.id, set: updateSet }).run(),
|
||||
)
|
||||
GlobalBus.emit("event", {
|
||||
payload: {
|
||||
type: Event.Updated.type,
|
||||
properties: result,
|
||||
},
|
||||
})
|
||||
return { project: result, sandbox }
|
||||
return { project: result, sandbox: data.sandbox }
|
||||
}
|
||||
|
||||
export async function discover(input: Info) {
|
||||
@@ -248,43 +295,54 @@ export namespace Project {
|
||||
return
|
||||
}
|
||||
|
||||
async function migrateFromGlobal(newProjectID: string, worktree: string) {
|
||||
const globalProject = await Storage.read<Info>(["project", "global"]).catch(() => undefined)
|
||||
if (!globalProject) return
|
||||
async function migrateFromGlobal(id: string, worktree: string) {
|
||||
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, "global")).get())
|
||||
if (!row) return
|
||||
|
||||
const globalSessions = await Storage.list(["session", "global"]).catch(() => [])
|
||||
if (globalSessions.length === 0) return
|
||||
const sessions = Database.use((db) =>
|
||||
db.select().from(SessionTable).where(eq(SessionTable.project_id, "global")).all(),
|
||||
)
|
||||
if (sessions.length === 0) return
|
||||
|
||||
log.info("migrating sessions from global", { newProjectID, worktree, count: globalSessions.length })
|
||||
log.info("migrating sessions from global", { newProjectID: id, worktree, count: sessions.length })
|
||||
|
||||
await work(10, globalSessions, async (key) => {
|
||||
const sessionID = key[key.length - 1]
|
||||
const session = await Storage.read<Session.Info>(key).catch(() => undefined)
|
||||
if (!session) return
|
||||
if (session.directory && session.directory !== worktree) return
|
||||
await work(10, sessions, async (row) => {
|
||||
// Skip sessions that belong to a different directory
|
||||
if (row.directory && row.directory !== worktree) return
|
||||
|
||||
session.projectID = newProjectID
|
||||
log.info("migrating session", { sessionID, from: "global", to: newProjectID })
|
||||
await Storage.write(["session", newProjectID, sessionID], session)
|
||||
await Storage.remove(key)
|
||||
log.info("migrating session", { sessionID: row.id, from: "global", to: id })
|
||||
Database.use((db) => db.update(SessionTable).set({ project_id: id }).where(eq(SessionTable.id, row.id)).run())
|
||||
}).catch((error) => {
|
||||
log.error("failed to migrate sessions from global to project", { error, projectId: newProjectID })
|
||||
log.error("failed to migrate sessions from global to project", { error, projectId: id })
|
||||
})
|
||||
}
|
||||
|
||||
export async function setInitialized(projectID: string) {
|
||||
await Storage.update<Info>(["project", projectID], (draft) => {
|
||||
draft.time.initialized = Date.now()
|
||||
})
|
||||
export function setInitialized(id: string) {
|
||||
Database.use((db) =>
|
||||
db
|
||||
.update(ProjectTable)
|
||||
.set({
|
||||
time_initialized: Date.now(),
|
||||
})
|
||||
.where(eq(ProjectTable.id, id))
|
||||
.run(),
|
||||
)
|
||||
}
|
||||
|
||||
export async function list() {
|
||||
const keys = await Storage.list(["project"])
|
||||
const projects = await Promise.all(keys.map((x) => Storage.read<Info>(x)))
|
||||
return projects.map((project) => ({
|
||||
...project,
|
||||
sandboxes: project.sandboxes?.filter((x) => existsSync(x)),
|
||||
}))
|
||||
export function list() {
|
||||
return Database.use((db) =>
|
||||
db
|
||||
.select()
|
||||
.from(ProjectTable)
|
||||
.all()
|
||||
.map((row) => fromRow(row)),
|
||||
)
|
||||
}
|
||||
|
||||
export function get(id: string): Info | undefined {
|
||||
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
|
||||
if (!row) return undefined
|
||||
return fromRow(row)
|
||||
}
|
||||
|
||||
export const update = fn(
|
||||
@@ -295,77 +353,89 @@ export namespace Project {
|
||||
commands: Info.shape.commands.optional(),
|
||||
}),
|
||||
async (input) => {
|
||||
const result = await Storage.update<Info>(["project", input.projectID], (draft) => {
|
||||
if (input.name !== undefined) draft.name = input.name
|
||||
if (input.icon !== undefined) {
|
||||
draft.icon = {
|
||||
...draft.icon,
|
||||
}
|
||||
if (input.icon.url !== undefined) draft.icon.url = input.icon.url
|
||||
if (input.icon.override !== undefined) draft.icon.override = input.icon.override || undefined
|
||||
if (input.icon.color !== undefined) draft.icon.color = input.icon.color
|
||||
}
|
||||
|
||||
if (input.commands?.start !== undefined) {
|
||||
const start = input.commands.start || undefined
|
||||
draft.commands = {
|
||||
...(draft.commands ?? {}),
|
||||
}
|
||||
draft.commands.start = start
|
||||
if (!draft.commands.start) draft.commands = undefined
|
||||
}
|
||||
|
||||
draft.time.updated = Date.now()
|
||||
})
|
||||
const result = Database.use((db) =>
|
||||
db
|
||||
.update(ProjectTable)
|
||||
.set({
|
||||
name: input.name,
|
||||
icon_url: input.icon?.url,
|
||||
icon_color: input.icon?.color,
|
||||
time_updated: Date.now(),
|
||||
})
|
||||
.where(eq(ProjectTable.id, input.projectID))
|
||||
.returning()
|
||||
.get(),
|
||||
)
|
||||
if (!result) throw new Error(`Project not found: ${input.projectID}`)
|
||||
const data = fromRow(result)
|
||||
GlobalBus.emit("event", {
|
||||
payload: {
|
||||
type: Event.Updated.type,
|
||||
properties: result,
|
||||
properties: data,
|
||||
},
|
||||
})
|
||||
return result
|
||||
return data
|
||||
},
|
||||
)
|
||||
|
||||
export async function sandboxes(projectID: string) {
|
||||
const project = await Storage.read<Info>(["project", projectID]).catch(() => undefined)
|
||||
if (!project?.sandboxes) return []
|
||||
export async function sandboxes(id: string) {
|
||||
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
|
||||
if (!row) return []
|
||||
const data = fromRow(row)
|
||||
const valid: string[] = []
|
||||
for (const dir of project.sandboxes) {
|
||||
const stat = await fs.stat(dir).catch(() => undefined)
|
||||
for (const dir of data.sandboxes) {
|
||||
const stat = await Bun.file(dir)
|
||||
.stat()
|
||||
.catch(() => undefined)
|
||||
if (stat?.isDirectory()) valid.push(dir)
|
||||
}
|
||||
return valid
|
||||
}
|
||||
|
||||
export async function addSandbox(projectID: string, directory: string) {
|
||||
const result = await Storage.update<Info>(["project", projectID], (draft) => {
|
||||
const sandboxes = draft.sandboxes ?? []
|
||||
if (!sandboxes.includes(directory)) sandboxes.push(directory)
|
||||
draft.sandboxes = sandboxes
|
||||
draft.time.updated = Date.now()
|
||||
})
|
||||
export async function addSandbox(id: string, directory: string) {
|
||||
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
|
||||
if (!row) throw new Error(`Project not found: ${id}`)
|
||||
const sandboxes = [...row.sandboxes]
|
||||
if (!sandboxes.includes(directory)) sandboxes.push(directory)
|
||||
const result = Database.use((db) =>
|
||||
db
|
||||
.update(ProjectTable)
|
||||
.set({ sandboxes, time_updated: Date.now() })
|
||||
.where(eq(ProjectTable.id, id))
|
||||
.returning()
|
||||
.get(),
|
||||
)
|
||||
if (!result) throw new Error(`Project not found: ${id}`)
|
||||
const data = fromRow(result)
|
||||
GlobalBus.emit("event", {
|
||||
payload: {
|
||||
type: Event.Updated.type,
|
||||
properties: result,
|
||||
properties: data,
|
||||
},
|
||||
})
|
||||
return result
|
||||
return data
|
||||
}
|
||||
|
||||
export async function removeSandbox(projectID: string, directory: string) {
|
||||
const result = await Storage.update<Info>(["project", projectID], (draft) => {
|
||||
const sandboxes = draft.sandboxes ?? []
|
||||
draft.sandboxes = sandboxes.filter((sandbox) => sandbox !== directory)
|
||||
draft.time.updated = Date.now()
|
||||
})
|
||||
export async function removeSandbox(id: string, directory: string) {
|
||||
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
|
||||
if (!row) throw new Error(`Project not found: ${id}`)
|
||||
const sandboxes = row.sandboxes.filter((s) => s !== directory)
|
||||
const result = Database.use((db) =>
|
||||
db
|
||||
.update(ProjectTable)
|
||||
.set({ sandboxes, time_updated: Date.now() })
|
||||
.where(eq(ProjectTable.id, id))
|
||||
.returning()
|
||||
.get(),
|
||||
)
|
||||
if (!result) throw new Error(`Project not found: ${id}`)
|
||||
const data = fromRow(result)
|
||||
GlobalBus.emit("event", {
|
||||
payload: {
|
||||
type: Event.Updated.type,
|
||||
properties: result,
|
||||
properties: data,
|
||||
},
|
||||
})
|
||||
return result
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
191
packages/opencode/src/provider/error.ts
Normal file
191
packages/opencode/src/provider/error.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import { APICallError } from "ai"
|
||||
import { STATUS_CODES } from "http"
|
||||
import { iife } from "@/util/iife"
|
||||
|
||||
export namespace ProviderError {
|
||||
// Adapted from overflow detection patterns in:
|
||||
// https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/overflow.ts
|
||||
const OVERFLOW_PATTERNS = [
|
||||
/prompt is too long/i, // Anthropic
|
||||
/input is too long for requested model/i, // Amazon Bedrock
|
||||
/exceeds the context window/i, // OpenAI (Completions + Responses API message text)
|
||||
/input token count.*exceeds the maximum/i, // Google (Gemini)
|
||||
/maximum prompt length is \d+/i, // xAI (Grok)
|
||||
/reduce the length of the messages/i, // Groq
|
||||
/maximum context length is \d+ tokens/i, // OpenRouter
|
||||
/exceeds the limit of \d+/i, // GitHub Copilot
|
||||
/exceeds the available context size/i, // llama.cpp server
|
||||
/greater than the context length/i, // LM Studio
|
||||
/context window exceeds limit/i, // MiniMax
|
||||
/exceeded model token limit/i, // Kimi For Coding
|
||||
/context[_ ]length[_ ]exceeded/i, // Generic fallback
|
||||
/too many tokens/i, // Generic fallback
|
||||
/token limit exceeded/i, // Generic fallback
|
||||
]
|
||||
|
||||
function isOpenAiErrorRetryable(e: APICallError) {
|
||||
const status = e.statusCode
|
||||
if (!status) return e.isRetryable
|
||||
// openai sometimes returns 404 for models that are actually available
|
||||
return status === 404 || e.isRetryable
|
||||
}
|
||||
|
||||
// Providers not reliably handled in this function:
|
||||
// - z.ai: can accept overflow silently (needs token-count/context-window checks)
|
||||
function isOverflow(message: string) {
|
||||
if (OVERFLOW_PATTERNS.some((p) => p.test(message))) return true
|
||||
|
||||
// Providers/status patterns handled outside of regex list:
|
||||
// - Cerebras: often returns "400 (no body)" / "413 (no body)"
|
||||
// - Mistral: often returns "400 (no body)" / "413 (no body)"
|
||||
return /^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message)
|
||||
}
|
||||
|
||||
function error(providerID: string, error: APICallError) {
|
||||
if (providerID.includes("github-copilot") && error.statusCode === 403) {
|
||||
return "Please reauthenticate with the copilot provider to ensure your credentials work properly with OpenCode."
|
||||
}
|
||||
|
||||
return error.message
|
||||
}
|
||||
|
||||
function message(providerID: string, e: APICallError) {
|
||||
return iife(() => {
|
||||
const msg = e.message
|
||||
if (msg === "") {
|
||||
if (e.responseBody) return e.responseBody
|
||||
if (e.statusCode) {
|
||||
const err = STATUS_CODES[e.statusCode]
|
||||
if (err) return err
|
||||
}
|
||||
return "Unknown error"
|
||||
}
|
||||
|
||||
const transformed = error(providerID, e)
|
||||
if (transformed !== msg) {
|
||||
return transformed
|
||||
}
|
||||
if (!e.responseBody || (e.statusCode && msg !== STATUS_CODES[e.statusCode])) {
|
||||
return msg
|
||||
}
|
||||
|
||||
try {
|
||||
const body = JSON.parse(e.responseBody)
|
||||
// try to extract common error message fields
|
||||
const errMsg = body.message || body.error || body.error?.message
|
||||
if (errMsg && typeof errMsg === "string") {
|
||||
return `${msg}: ${errMsg}`
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return `${msg}: ${e.responseBody}`
|
||||
}).trim()
|
||||
}
|
||||
|
||||
function json(input: unknown) {
|
||||
if (typeof input === "string") {
|
||||
try {
|
||||
const result = JSON.parse(input)
|
||||
if (result && typeof result === "object") return result
|
||||
return undefined
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
if (typeof input === "object" && input !== null) {
|
||||
return input
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export type ParsedStreamError =
|
||||
| {
|
||||
type: "context_overflow"
|
||||
message: string
|
||||
responseBody: string
|
||||
}
|
||||
| {
|
||||
type: "api_error"
|
||||
message: string
|
||||
isRetryable: false
|
||||
responseBody: string
|
||||
}
|
||||
|
||||
export function parseStreamError(input: unknown): ParsedStreamError | undefined {
|
||||
const body = json(input)
|
||||
if (!body) return
|
||||
|
||||
const responseBody = JSON.stringify(body)
|
||||
if (body.type !== "error") return
|
||||
|
||||
switch (body?.error?.code) {
|
||||
case "context_length_exceeded":
|
||||
return {
|
||||
type: "context_overflow",
|
||||
message: "Input exceeds context window of this model",
|
||||
responseBody,
|
||||
}
|
||||
case "insufficient_quota":
|
||||
return {
|
||||
type: "api_error",
|
||||
message: "Quota exceeded. Check your plan and billing details.",
|
||||
isRetryable: false,
|
||||
responseBody,
|
||||
}
|
||||
case "usage_not_included":
|
||||
return {
|
||||
type: "api_error",
|
||||
message: "To use Codex with your ChatGPT plan, upgrade to Plus: https://chatgpt.com/explore/plus.",
|
||||
isRetryable: false,
|
||||
responseBody,
|
||||
}
|
||||
case "invalid_prompt":
|
||||
return {
|
||||
type: "api_error",
|
||||
message: typeof body?.error?.message === "string" ? body?.error?.message : "Invalid prompt.",
|
||||
isRetryable: false,
|
||||
responseBody,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type ParsedAPICallError =
|
||||
| {
|
||||
type: "context_overflow"
|
||||
message: string
|
||||
responseBody?: string
|
||||
}
|
||||
| {
|
||||
type: "api_error"
|
||||
message: string
|
||||
statusCode?: number
|
||||
isRetryable: boolean
|
||||
responseHeaders?: Record<string, string>
|
||||
responseBody?: string
|
||||
metadata?: Record<string, string>
|
||||
}
|
||||
|
||||
export function parseAPICallError(input: { providerID: string; error: APICallError }): ParsedAPICallError {
|
||||
const m = message(input.providerID, input.error)
|
||||
if (isOverflow(m)) {
|
||||
return {
|
||||
type: "context_overflow",
|
||||
message: m,
|
||||
responseBody: input.error.responseBody,
|
||||
}
|
||||
}
|
||||
|
||||
const metadata = input.error.url ? { url: input.error.url } : undefined
|
||||
return {
|
||||
type: "api_error",
|
||||
message: m,
|
||||
statusCode: input.error.statusCode,
|
||||
isRetryable: input.providerID.startsWith("openai")
|
||||
? isOpenAiErrorRetryable(input.error)
|
||||
: input.error.isRetryable,
|
||||
responseHeaders: input.error.responseHeaders,
|
||||
responseBody: input.error.responseBody,
|
||||
metadata,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { APICallError, ModelMessage } from "ai"
|
||||
import type { ModelMessage } from "ai"
|
||||
import { mergeDeep, unique } from "remeda"
|
||||
import type { JSONSchema7 } from "@ai-sdk/provider"
|
||||
import type { JSONSchema } from "zod/v4/core"
|
||||
@@ -643,6 +643,20 @@ export namespace ProviderTransform {
|
||||
}
|
||||
}
|
||||
|
||||
// Enable thinking for reasoning models on alibaba-cn (DashScope).
|
||||
// DashScope's OpenAI-compatible API requires `enable_thinking: true` in the request body
|
||||
// to return reasoning_content. Without it, models like kimi-k2.5, qwen-plus, qwen3, qwq,
|
||||
// deepseek-r1, etc. never output thinking/reasoning tokens.
|
||||
// Note: kimi-k2-thinking is excluded as it returns reasoning_content by default.
|
||||
if (
|
||||
input.model.providerID === "alibaba-cn" &&
|
||||
input.model.capabilities.reasoning &&
|
||||
input.model.api.npm === "@ai-sdk/openai-compatible" &&
|
||||
!modelId.includes("kimi-k2-thinking")
|
||||
) {
|
||||
result["enable_thinking"] = true
|
||||
}
|
||||
|
||||
if (input.model.api.id.includes("gpt-5") && !input.model.api.id.includes("gpt-5-chat")) {
|
||||
if (!input.model.api.id.includes("gpt-5-pro")) {
|
||||
result["reasoningEffort"] = "medium"
|
||||
@@ -810,19 +824,4 @@ export namespace ProviderTransform {
|
||||
|
||||
return schema as JSONSchema7
|
||||
}
|
||||
|
||||
export function error(providerID: string, error: APICallError) {
|
||||
let message = error.message
|
||||
if (providerID.includes("github-copilot") && error.statusCode === 403) {
|
||||
return "Please reauthenticate with the copilot provider to ensure your credentials work properly with OpenCode."
|
||||
}
|
||||
if (providerID.includes("github-copilot") && message.includes("The requested model is not supported")) {
|
||||
return (
|
||||
message +
|
||||
"\n\nMake sure the model is enabled in your copilot settings: https://github.com/settings/copilot/features"
|
||||
)
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { resolver } from "hono-openapi"
|
||||
import z from "zod"
|
||||
import { Storage } from "../storage/storage"
|
||||
import { NotFoundError } from "../storage/db"
|
||||
|
||||
export const ERRORS = {
|
||||
400: {
|
||||
@@ -25,7 +25,7 @@ export const ERRORS = {
|
||||
description: "Not found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Storage.NotFoundError.Schema),
|
||||
schema: resolver(NotFoundError.Schema),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ import { describeRoute, validator, resolver } from "hono-openapi"
|
||||
import { upgradeWebSocket } from "hono/bun"
|
||||
import z from "zod"
|
||||
import { Pty } from "@/pty"
|
||||
import { Storage } from "../../storage/storage"
|
||||
import { NotFoundError } from "../../storage/db"
|
||||
import { errors } from "../error"
|
||||
import { lazy } from "../../util/lazy"
|
||||
|
||||
@@ -76,7 +76,7 @@ export const PtyRoutes = lazy(() =>
|
||||
async (c) => {
|
||||
const info = Pty.get(c.req.valid("param").ptyID)
|
||||
if (!info) {
|
||||
throw new Storage.NotFoundError({ message: "Session not found" })
|
||||
throw new NotFoundError({ message: "Session not found" })
|
||||
}
|
||||
return c.json(info)
|
||||
},
|
||||
|
||||
@@ -276,18 +276,15 @@ export const SessionRoutes = lazy(() =>
|
||||
const sessionID = c.req.valid("param").sessionID
|
||||
const updates = c.req.valid("json")
|
||||
|
||||
const updatedSession = await Session.update(
|
||||
sessionID,
|
||||
(session) => {
|
||||
if (updates.title !== undefined) {
|
||||
session.title = updates.title
|
||||
}
|
||||
if (updates.time?.archived !== undefined) session.time.archived = updates.time.archived
|
||||
},
|
||||
{ touch: false },
|
||||
)
|
||||
let session = await Session.get(sessionID)
|
||||
if (updates.title !== undefined) {
|
||||
session = await Session.setTitle({ sessionID, title: updates.title })
|
||||
}
|
||||
if (updates.time?.archived !== undefined) {
|
||||
session = await Session.setArchived({ sessionID, time: updates.time.archived })
|
||||
}
|
||||
|
||||
return c.json(updatedSession)
|
||||
return c.json(session)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
|
||||
@@ -31,7 +31,7 @@ import { ExperimentalRoutes } from "./routes/experimental"
|
||||
import { ProviderRoutes } from "./routes/provider"
|
||||
import { lazy } from "../util/lazy"
|
||||
import { InstanceBootstrap } from "../project/bootstrap"
|
||||
import { Storage } from "../storage/storage"
|
||||
import { NotFoundError } from "../storage/db"
|
||||
import type { ContentfulStatusCode } from "hono/utils/http-status"
|
||||
import { websocket } from "hono/bun"
|
||||
import { HTTPException } from "hono/http-exception"
|
||||
@@ -65,7 +65,7 @@ export namespace Server {
|
||||
})
|
||||
if (err instanceof NamedError) {
|
||||
let status: ContentfulStatusCode
|
||||
if (err instanceof Storage.NotFoundError) status = 404
|
||||
if (err instanceof NotFoundError) status = 404
|
||||
else if (err instanceof Provider.ModelNotFoundError) status = 400
|
||||
else if (err.name.startsWith("Worktree")) status = 400
|
||||
else status = 500
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user