mirror of
https://github.com/anomalyco/opencode.git
synced 2026-03-05 14:14:03 +00:00
Compare commits
217 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8fb97267b | ||
|
|
1d455854d8 | ||
|
|
7c675b9558 | ||
|
|
de33f6b58d | ||
|
|
82dfab6253 | ||
|
|
9aff85193d | ||
|
|
ae959d4926 | ||
|
|
e53e39605f | ||
|
|
5359e7c625 | ||
|
|
c999f5b64f | ||
|
|
0bc18a9cee | ||
|
|
1f6b098c07 | ||
|
|
67d60ff4b9 | ||
|
|
acf08857d9 | ||
|
|
2efe4d04c3 | ||
|
|
e4d6f38762 | ||
|
|
329fd941e9 | ||
|
|
3d7cd4a012 | ||
|
|
853eaf1f66 | ||
|
|
5f40bd42f8 | ||
|
|
0e5edef51e | ||
|
|
3448118be8 | ||
|
|
2bb3dc585b | ||
|
|
27baa2d65c | ||
|
|
62909e917a | ||
|
|
a60e715fc6 | ||
|
|
161734fb95 | ||
|
|
4e26b0aec7 | ||
|
|
6531cfc521 | ||
|
|
6ddd13c6ac | ||
|
|
7948de1612 | ||
|
|
f363904feb | ||
|
|
85ff05670a | ||
|
|
f03910cc4b | ||
|
|
78cd0c9d7a | ||
|
|
e67bedb0b2 | ||
|
|
37d3ad0475 | ||
|
|
7de99b6a94 | ||
|
|
6e123c5f2a | ||
|
|
2b5a95bb88 | ||
|
|
11522769fb | ||
|
|
1fca726266 | ||
|
|
3a654ccd56 | ||
|
|
6784f917c7 | ||
|
|
a2fb4ca322 | ||
|
|
6082393db3 | ||
|
|
b7be8787fb | ||
|
|
92534bbabd | ||
|
|
302f4bd863 | ||
|
|
3032fe445d | ||
|
|
1abb7efc28 | ||
|
|
7fe615247b | ||
|
|
6455ccb609 | ||
|
|
868fc1829a | ||
|
|
4c0f6cbc38 | ||
|
|
c2796fa803 | ||
|
|
8efadf9858 | ||
|
|
2dcb54c5f2 | ||
|
|
c0fd605d73 | ||
|
|
986a6b19e0 | ||
|
|
2c39a7fcb3 | ||
|
|
9939e735bb | ||
|
|
0461efa8ab | ||
|
|
bffda3716e | ||
|
|
bb8b850b5f | ||
|
|
4ef8ae9ddb | ||
|
|
abe4a2548f | ||
|
|
82a29ba9b9 | ||
|
|
7ebf1c3707 | ||
|
|
24bcc96511 | ||
|
|
7e5477c7c4 | ||
|
|
a0296094c5 | ||
|
|
7add2e67c3 | ||
|
|
a16d77b08d | ||
|
|
83de487dcd | ||
|
|
4e05d3487c | ||
|
|
9cba8f9349 | ||
|
|
45eb4be7f2 | ||
|
|
8851c619b4 | ||
|
|
418ecc7073 | ||
|
|
55762b2bbb | ||
|
|
0ce963bfb3 | ||
|
|
6f8f0d1830 | ||
|
|
969f434769 | ||
|
|
49f55ae426 | ||
|
|
28538bc65d | ||
|
|
1b9ca3da27 | ||
|
|
c96a3b15c5 | ||
|
|
21b6a5f5fd | ||
|
|
474e2e5165 | ||
|
|
eadd42bfe1 | ||
|
|
b939299a0f | ||
|
|
bc56419124 | ||
|
|
55ab1f094c | ||
|
|
14f88ae889 | ||
|
|
9d53b3a221 | ||
|
|
0ad0d84402 | ||
|
|
33b6ba68fc | ||
|
|
90345c57e1 | ||
|
|
324230806e | ||
|
|
7f7e622426 | ||
|
|
27447bab26 | ||
|
|
45ac20b8aa | ||
|
|
218330aec1 | ||
|
|
67fa7903c3 | ||
|
|
cd3a09c6a7 | ||
|
|
f8685a4d53 | ||
|
|
6cbb1ef1c2 | ||
|
|
0b825ca383 | ||
|
|
22a4c5a77e | ||
|
|
29dbfc25e5 | ||
|
|
40fc406424 | ||
|
|
6f23271741 | ||
|
|
b7198c28c8 | ||
|
|
de6a6af5ab | ||
|
|
0f1f55a24c | ||
|
|
744c38cc7c | ||
|
|
e9de2505f6 | ||
|
|
22fcde926f | ||
|
|
b42a63b882 | ||
|
|
ca5a7378de | ||
|
|
c6187ee40f | ||
|
|
d94c516402 | ||
|
|
61795d794e | ||
|
|
7c215c0d02 | ||
|
|
ad56338108 | ||
|
|
dd4ad5f2c5 | ||
|
|
eb71856733 | ||
|
|
0a2aa8688d | ||
|
|
e44cdaf887 | ||
|
|
5709561917 | ||
|
|
9909f94048 | ||
|
|
e8f67ddbcc | ||
|
|
a2d3d62db3 | ||
|
|
695a26a168 | ||
|
|
92aab78442 | ||
|
|
1bbd3a71fb | ||
|
|
12f4315d9d | ||
|
|
d80334b2bc | ||
|
|
c2f5abe759 | ||
|
|
b1c166edf4 | ||
|
|
3c8ce4ab90 | ||
|
|
bb2e9fffb1 | ||
|
|
e993acec31 | ||
|
|
611e616010 | ||
|
|
b286c0ae3f | ||
|
|
81a61f8dbd | ||
|
|
752e449e38 | ||
|
|
a44f78c34a | ||
|
|
a5d727e7f9 | ||
|
|
7b5b665b4a | ||
|
|
b5515dd2f7 | ||
|
|
d16e5b98dc | ||
|
|
9dbf3a2042 | ||
|
|
da34dfa80b | ||
|
|
5e9dd5dca3 | ||
|
|
1ac4a1a1fa | ||
|
|
8405c70993 | ||
|
|
d7b2a15959 | ||
|
|
5fee541d52 | ||
|
|
fe0f298293 | ||
|
|
29d90056e9 | ||
|
|
276d60e82a | ||
|
|
9ea36ccd9d | ||
|
|
b9ca79f3b6 | ||
|
|
323e7a36da | ||
|
|
9faaa6130d | ||
|
|
5d419a0211 | ||
|
|
8b168981aa | ||
|
|
724dd665ec | ||
|
|
d20698401b | ||
|
|
ab44597018 | ||
|
|
aec95c4d10 | ||
|
|
b2c82cb897 | ||
|
|
20905212f9 | ||
|
|
76cda30896 | ||
|
|
4f740306f0 | ||
|
|
c7e9851826 | ||
|
|
bf53e1c24b | ||
|
|
50004d1f94 | ||
|
|
acd7c5ad55 | ||
|
|
cf54b544e3 | ||
|
|
52b42258fa | ||
|
|
3026a005b6 | ||
|
|
a6f802d7fe | ||
|
|
9ef803be82 | ||
|
|
ce5c827a6e | ||
|
|
56decd79db | ||
|
|
fc258ea74f | ||
|
|
abd9e195ac | ||
|
|
9d78b69cd3 | ||
|
|
e31f00ad22 | ||
|
|
70b555472e | ||
|
|
e514919cc4 | ||
|
|
a90e8de050 | ||
|
|
ba5121ce0b | ||
|
|
eabf770053 | ||
|
|
86d7bdc542 | ||
|
|
d3ab78bba0 | ||
|
|
a531f3f36d | ||
|
|
bb3382311d | ||
|
|
ad545d0cc9 | ||
|
|
ac244b1458 | ||
|
|
f202536b65 | ||
|
|
405cc3f610 | ||
|
|
878c1b8c2d | ||
|
|
d8bcfd90d3 | ||
|
|
954d31903f | ||
|
|
1587d93b29 | ||
|
|
d364c43916 | ||
|
|
72eec20437 | ||
|
|
4503bde1cc | ||
|
|
1abc228e95 | ||
|
|
991e823039 | ||
|
|
62fa5c1314 | ||
|
|
93b9e47c05 | ||
|
|
bb4d978684 |
@@ -122,3 +122,7 @@ const table = sqliteTable("session", {
|
||||
- Avoid mocks as much as possible
|
||||
- Test actual implementation, do not duplicate logic into tests
|
||||
- Tests cannot run from repo root (guard: `do-not-run-tests-from-root`); run from package dirs like `packages/opencode`.
|
||||
|
||||
## Type Checking
|
||||
|
||||
- Always run `bun typecheck` from package directories (e.g., `packages/opencode`), never `tsc` directly.
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { Context as GitHubContext } from "@actions/github/lib/context"
|
||||
import type { IssueCommentEvent, PullRequestReviewCommentEvent } from "@octokit/webhooks-types"
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk"
|
||||
import { spawn } from "node:child_process"
|
||||
import { setTimeout as sleep } from "node:timers/promises"
|
||||
|
||||
type GitHubAuthor = {
|
||||
login: string
|
||||
@@ -281,7 +282,7 @@ async function assertOpencodeConnected() {
|
||||
connected = true
|
||||
break
|
||||
} catch (e) {}
|
||||
await Bun.sleep(300)
|
||||
await sleep(300)
|
||||
} while (retry++ < 30)
|
||||
|
||||
if (!connected) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-jtBYpfiE9g0otqZEtOksW1Nbg+O8CJP9OEOEhsa7sa8=",
|
||||
"aarch64-linux": "sha256-m+YNZIB7I7EMPyfqkKsvDvmBX9R1szmEKxXpxTNFLH8=",
|
||||
"aarch64-darwin": "sha256-1gVmtkC1/I8sdHZcaeSFJheySVlpCyKCjf9zbVsVqAQ=",
|
||||
"x86_64-darwin": "sha256-Tvk5YL6Z0xRul4jopbGme/997iHBylXC0Cq3RnjQb+I="
|
||||
"x86_64-linux": "sha256-TnrYykX8Mf/Ugtkix6V",
|
||||
"aarch64-linux": "sha256-TnrYykX8Mf/Ugtkix6V",
|
||||
"aarch64-darwin": "sha256-TnrYykX8Mf/Ugtkix6V",
|
||||
"x86_64-darwin": "sha256-TnrYykX8Mf/Ugtkix6V"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,12 +71,13 @@
|
||||
"@actions/artifact": "5.0.1",
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/mime-types": "3.0.1",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"glob": "13.0.5",
|
||||
"husky": "9.1.7",
|
||||
"prettier": "3.6.2",
|
||||
"semver": "^7.6.0",
|
||||
"sst": "3.18.10",
|
||||
"turbo": "2.5.6"
|
||||
"turbo": "2.8.13"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.933.0",
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
sessionItemSelector,
|
||||
dropdownMenuTriggerSelector,
|
||||
dropdownMenuContentSelector,
|
||||
sessionHeaderSelector,
|
||||
projectMenuTriggerSelector,
|
||||
projectWorkspacesToggleSelector,
|
||||
titlebarRightSelector,
|
||||
@@ -225,9 +226,9 @@ export async function hoverSessionItem(page: Page, sessionID: string) {
|
||||
export async function openSessionMoreMenu(page: Page, sessionID: string) {
|
||||
await expect(page).toHaveURL(new RegExp(`/session/${sessionID}(?:[/?#]|$)`))
|
||||
|
||||
const scroller = page.locator(".scroll-view__viewport").first()
|
||||
await expect(scroller).toBeVisible()
|
||||
await expect(scroller.getByRole("heading", { level: 1 }).first()).toBeVisible({ timeout: 30_000 })
|
||||
const header = page.locator(sessionHeaderSelector).first()
|
||||
await expect(header).toBeVisible()
|
||||
await expect(header.getByRole("heading", { level: 1 }).first()).toBeVisible({ timeout: 30_000 })
|
||||
|
||||
const menu = page
|
||||
.locator(dropdownMenuContentSelector)
|
||||
@@ -243,7 +244,7 @@ export async function openSessionMoreMenu(page: Page, sessionID: string) {
|
||||
|
||||
if (opened) return menu
|
||||
|
||||
const menuTrigger = scroller.getByRole("button", { name: /more options/i }).first()
|
||||
const menuTrigger = header.getByRole("button", { name: /more options/i }).first()
|
||||
await expect(menuTrigger).toBeVisible()
|
||||
await menuTrigger.click()
|
||||
|
||||
|
||||
@@ -101,3 +101,56 @@ test("cmd+f opens text viewer search while prompt is focused", async ({ page, go
|
||||
await expect(findInput).toBeVisible()
|
||||
await expect(findInput).toBeFocused()
|
||||
})
|
||||
|
||||
test("cmd+f opens text viewer search while prompt is not focused", async ({ page, gotoSession }) => {
|
||||
await gotoSession()
|
||||
|
||||
await page.locator(promptSelector).click()
|
||||
await page.keyboard.type("/open")
|
||||
|
||||
const command = page.locator('[data-slash-id="file.open"]').first()
|
||||
await expect(command).toBeVisible()
|
||||
await page.keyboard.press("Enter")
|
||||
|
||||
const dialog = page
|
||||
.getByRole("dialog")
|
||||
.filter({ has: page.getByPlaceholder(/search files/i) })
|
||||
.first()
|
||||
await expect(dialog).toBeVisible()
|
||||
|
||||
const input = dialog.getByRole("textbox").first()
|
||||
await input.fill("package.json")
|
||||
|
||||
const items = dialog.locator('[data-slot="list-item"][data-key^="file:"]')
|
||||
let index = -1
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const keys = await items.evaluateAll((nodes) => nodes.map((node) => node.getAttribute("data-key") ?? ""))
|
||||
index = keys.findIndex((key) => /packages[\\/]+app[\\/]+package\.json$/i.test(key.replace(/^file:/, "")))
|
||||
return index >= 0
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
)
|
||||
.toBe(true)
|
||||
|
||||
const item = items.nth(index)
|
||||
await expect(item).toBeVisible()
|
||||
await item.click()
|
||||
|
||||
await expect(dialog).toHaveCount(0)
|
||||
|
||||
const tab = page.getByRole("tab", { name: "package.json" })
|
||||
await expect(tab).toBeVisible()
|
||||
await tab.click()
|
||||
|
||||
const viewer = page.locator('[data-component="file"][data-mode="text"]').first()
|
||||
await expect(viewer).toBeVisible()
|
||||
|
||||
await viewer.click()
|
||||
await page.keyboard.press(`${modKey}+f`)
|
||||
|
||||
const findInput = page.getByPlaceholder("Find")
|
||||
await expect(findInput).toBeVisible()
|
||||
await expect(findInput).toBeFocused()
|
||||
})
|
||||
|
||||
@@ -53,6 +53,8 @@ export const dropdownMenuContentSelector = '[data-component="dropdown-menu-conte
|
||||
|
||||
export const inlineInputSelector = '[data-component="inline-input"]'
|
||||
|
||||
export const sessionHeaderSelector = "[data-session-title]"
|
||||
|
||||
export const sessionItemSelector = (sessionID: string) => `${sidebarNavSelector} [data-session-id="${sessionID}"]`
|
||||
|
||||
export const workspaceItemSelector = (slug: string) =>
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
openSharePopover,
|
||||
withSession,
|
||||
} from "../actions"
|
||||
import { sessionItemSelector, inlineInputSelector } from "../selectors"
|
||||
import { sessionHeaderSelector, sessionItemSelector, inlineInputSelector } from "../selectors"
|
||||
|
||||
const shareDisabled = process.env.OPENCODE_DISABLE_SHARE === "true" || process.env.OPENCODE_DISABLE_SHARE === "1"
|
||||
|
||||
@@ -44,7 +44,7 @@ test("session can be renamed via header menu", async ({ page, sdk, gotoSession }
|
||||
const menu = await openSessionMoreMenu(page, session.id)
|
||||
await clickMenuItem(menu, /rename/i)
|
||||
|
||||
const input = page.locator(".scroll-view__viewport").locator(inlineInputSelector).first()
|
||||
const input = page.locator(sessionHeaderSelector).locator(inlineInputSelector).first()
|
||||
await expect(input).toBeVisible()
|
||||
await expect(input).toBeFocused()
|
||||
await input.fill(renamedTitle)
|
||||
|
||||
50
packages/app/src/api/releases.ts
Normal file
50
packages/app/src/api/releases.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { Platform } from "@/context/platform"
|
||||
|
||||
const REPO = "anomalyco/opencode"
|
||||
const GITHUB_API_URL = `https://api.github.com/repos/${REPO}/releases`
|
||||
const PER_PAGE = 30
|
||||
const CACHE_TTL = 1000 * 60 * 30
|
||||
const CACHE_KEY = "opencode.releases"
|
||||
|
||||
type Release = {
|
||||
tag: string
|
||||
body: string
|
||||
date: string
|
||||
}
|
||||
|
||||
function loadCache() {
|
||||
const raw = localStorage.getItem(CACHE_KEY)
|
||||
return raw ? JSON.parse(raw) : null
|
||||
}
|
||||
|
||||
function saveCache(data: { releases: Release[]; timestamp: number }) {
|
||||
localStorage.setItem(CACHE_KEY, JSON.stringify(data))
|
||||
}
|
||||
|
||||
export async function fetchReleases(platform: Platform): Promise<{ releases: Release[] }> {
|
||||
const now = Date.now()
|
||||
const cached = loadCache()
|
||||
|
||||
if (cached && now - cached.timestamp < CACHE_TTL) {
|
||||
return { releases: cached.releases }
|
||||
}
|
||||
|
||||
const fetcher = platform.fetch ?? fetch
|
||||
const res = await fetcher(`${GITHUB_API_URL}?per_page=${PER_PAGE}`, {
|
||||
headers: { Accept: "application/vnd.github.v3+json" },
|
||||
}).then((r) => (r.ok ? r.json() : Promise.reject(new Error("Failed to load"))))
|
||||
|
||||
const releases = (Array.isArray(res) ? res : []).map((r) => ({
|
||||
tag: r.tag_name ?? "Unknown",
|
||||
body: (r.body ?? "")
|
||||
.replace(/#(\d+)/g, (_: string, id: string) => `[#${id}](https://github.com/anomalyco/opencode/pull/${id})`)
|
||||
.replace(/@([a-zA-Z0-9_-]+)/g, (_: string, u: string) => `[@${u}](https://github.com/${u})`),
|
||||
date: r.published_at ?? "",
|
||||
}))
|
||||
|
||||
saveCache({ releases, timestamp: now })
|
||||
|
||||
return { releases }
|
||||
}
|
||||
|
||||
export type { Release }
|
||||
150
packages/app/src/components/dialog-changelog.css
Normal file
150
packages/app/src/components/dialog-changelog.css
Normal file
@@ -0,0 +1,150 @@
|
||||
.dialog-changelog {
|
||||
min-height: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dialog-changelog [data-slot="dialog-body"] {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.dialog-changelog-list {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.dialog-changelog-list [data-slot="list-scroll"] {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--border-weak-base) transparent;
|
||||
}
|
||||
|
||||
.dialog-changelog-list [data-slot="list-scroll"]::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.dialog-changelog-list [data-slot="list-scroll"]::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.dialog-changelog-list [data-slot="list-scroll"]::-webkit-scrollbar-thumb {
|
||||
background: var(--border-weak-base);
|
||||
border-radius: 5px;
|
||||
border: 3px solid transparent;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.dialog-changelog-list [data-slot="list-scroll"]::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--border-weak-base);
|
||||
}
|
||||
|
||||
.dialog-changelog-header {
|
||||
padding: 8px 12px 8px 8px;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background: var(--surface-raised-stronger-non-alpha);
|
||||
}
|
||||
|
||||
.dialog-changelog-header::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 16px;
|
||||
background: linear-gradient(to bottom, var(--surface-raised-stronger-non-alpha), transparent);
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.dialog-changelog-header[data-stuck="true"]::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.dialog-changelog-version {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dialog-changelog-date {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--text-weak);
|
||||
}
|
||||
|
||||
.dialog-changelog-list [data-slot="list-item"] {
|
||||
margin-bottom: 32px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: default;
|
||||
display: block;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.dialog-changelog-list [data-slot="list-item"]:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.dialog-changelog-list [data-slot="list-item"]:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dialog-changelog-list [data-slot="list-item"]:focus-visible {
|
||||
outline: 2px solid var(--focus-base);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.dialog-changelog-content {
|
||||
padding: 0 8px 24px;
|
||||
}
|
||||
|
||||
.dialog-changelog-markdown h2 {
|
||||
border-bottom: 1px solid var(--border-weak-base);
|
||||
padding-bottom: 4px;
|
||||
margin: 32px 0 12px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.dialog-changelog-markdown h2:first-child {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.dialog-changelog-markdown a.external-link {
|
||||
color: var(--text-interactive-base);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dialog-changelog-markdown a.external-link[href^="https://github.com/anomalyco/opencode/pull/"],
|
||||
.dialog-changelog-markdown a.external-link[href^="https://github.com/anomalyco/opencode/issues/"],
|
||||
.dialog-changelog-markdown a.external-link[href^="https://github.com/"]
|
||||
{
|
||||
border-radius: 3px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.dialog-changelog-markdown a.external-link[href^="https://github.com/anomalyco/opencode/pull/"]:hover,
|
||||
.dialog-changelog-markdown a.external-link[href^="https://github.com/anomalyco/opencode/issues/"]:hover,
|
||||
.dialog-changelog-markdown a.external-link[href^="https://github.com/"]:hover
|
||||
{
|
||||
background: var(--surface-weak-base);
|
||||
}
|
||||
43
packages/app/src/components/dialog-changelog.tsx
Normal file
43
packages/app/src/components/dialog-changelog.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { createResource, Suspense, ErrorBoundary, Show } from "solid-js"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { DataProvider } from "@opencode-ai/ui/context"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { fetchReleases } from "@/api/releases"
|
||||
import { ReleaseList } from "@/components/release-list"
|
||||
|
||||
export function DialogChangelog() {
|
||||
const language = useLanguage()
|
||||
const platform = usePlatform()
|
||||
const [data] = createResource(() => fetchReleases(platform))
|
||||
|
||||
return (
|
||||
<Dialog size="x-large" transition title="Changelog">
|
||||
<DataProvider data={{ session: [], session_status: {}, session_diff: {}, message: {}, part: {} }} directory="">
|
||||
<div class="flex-1 min-h-0 flex flex-col">
|
||||
<ErrorBoundary
|
||||
fallback={(e) => (
|
||||
<p class="text-text-weak p-6">
|
||||
{e instanceof Error ? e.message : "Failed to load changelog"}
|
||||
</p>
|
||||
)}
|
||||
>
|
||||
<Suspense fallback={<p class="text-text-weak p-6">{language.t("common.loading")}...</p>}>
|
||||
<Show
|
||||
when={(data()?.releases.length ?? 0) > 0}
|
||||
fallback={<p class="text-text-weak p-6">{language.t("common.noReleasesFound")}</p>}
|
||||
>
|
||||
<ReleaseList
|
||||
releases={data()!.releases}
|
||||
hasMore={false}
|
||||
loadingMore={false}
|
||||
onLoadMore={() => {}}
|
||||
/>
|
||||
</Show>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</DataProvider>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -459,4 +459,4 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
||||
</List>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,25 @@ import { Component } from "solid-js"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { SettingsGeneral } from "./settings-general"
|
||||
import { SettingsKeybinds } from "./settings-keybinds"
|
||||
import { SettingsProviders } from "./settings-providers"
|
||||
import { SettingsModels } from "./settings-models"
|
||||
import { SettingsArchive } from "./settings-archive"
|
||||
import { DialogChangelog } from "@/components/dialog-changelog"
|
||||
|
||||
export const DialogSettings: Component = () => {
|
||||
const language = useLanguage()
|
||||
const platform = usePlatform()
|
||||
const dialog = useDialog()
|
||||
|
||||
function handleShowChangelog() {
|
||||
dialog.show(() => <DialogChangelog />)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog size="x-large" transition>
|
||||
@@ -47,11 +56,27 @@ export const DialogSettings: Component = () => {
|
||||
</Tabs.Trigger>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<Tabs.SectionTitle>{language.t("settings.section.data")}</Tabs.SectionTitle>
|
||||
<div class="flex flex-col gap-1.5 w-full">
|
||||
<Tabs.Trigger value="archive">
|
||||
<Icon name="archive" />
|
||||
{language.t("settings.archive.title")}
|
||||
</Tabs.Trigger>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 pl-1 py-1 text-12-medium text-text-weak">
|
||||
<span>{language.t("app.name.desktop")}</span>
|
||||
<span class="text-11-regular">v{platform.version}</span>
|
||||
<button
|
||||
class="text-11-regular text-text-weak hover:text-text-base self-start"
|
||||
onClick={handleShowChangelog}
|
||||
>
|
||||
Changelog
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.List>
|
||||
@@ -67,6 +92,9 @@ export const DialogSettings: Component = () => {
|
||||
<Tabs.Content value="models" class="no-scrollbar">
|
||||
<SettingsModels />
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="archive" class="no-scrollbar">
|
||||
<SettingsArchive />
|
||||
</Tabs.Content>
|
||||
</Tabs>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
@@ -256,7 +256,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
pendingAutoAccept: false,
|
||||
})
|
||||
|
||||
const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 })
|
||||
const buttonsSpring = useSpring(
|
||||
() => (store.mode === "normal" ? 1 : 0),
|
||||
{ visualDuration: 0.2, bounce: 0 },
|
||||
)
|
||||
|
||||
const springFade = (t: number): Record<string, string> => ({
|
||||
opacity: `${t}`,
|
||||
transform: `scale(${0.95 + t * 0.05})`,
|
||||
filter: `blur(${(1 - t) * 2}px)`,
|
||||
"pointer-events": t > 0.5 ? "auto" : "none",
|
||||
})
|
||||
|
||||
const commentCount = createMemo(() => {
|
||||
if (store.mode === "shell") return 0
|
||||
@@ -1254,9 +1264,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<div
|
||||
aria-hidden={store.mode !== "normal"}
|
||||
class="flex items-center gap-1"
|
||||
style={{
|
||||
"pointer-events": buttonsSpring() > 0.5 ? "auto" : "none",
|
||||
}}
|
||||
style={{ "pointer-events": buttonsSpring() > 0.5 ? "auto" : "none" }}
|
||||
>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
@@ -1268,11 +1276,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
type="button"
|
||||
variant="ghost"
|
||||
class="size-8 p-0"
|
||||
style={{
|
||||
opacity: buttonsSpring(),
|
||||
transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
|
||||
filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
|
||||
}}
|
||||
style={springFade(buttonsSpring())}
|
||||
onClick={pick}
|
||||
disabled={store.mode !== "normal"}
|
||||
tabIndex={store.mode === "normal" ? undefined : -1}
|
||||
@@ -1310,11 +1314,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
icon={working() ? "stop" : "arrow-up"}
|
||||
variant="primary"
|
||||
class="size-8"
|
||||
style={{
|
||||
opacity: buttonsSpring(),
|
||||
transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
|
||||
filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
|
||||
}}
|
||||
style={springFade(buttonsSpring())}
|
||||
aria-label={working() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -1370,13 +1370,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
<div class="flex items-center gap-1.5 min-w-0 flex-1 relative">
|
||||
<div
|
||||
class="h-7 flex items-center gap-1.5 max-w-[160px] min-w-0 absolute inset-y-0 left-0"
|
||||
style={{
|
||||
padding: "0 4px 0 8px",
|
||||
opacity: 1 - buttonsSpring(),
|
||||
transform: `scale(${0.95 + (1 - buttonsSpring()) * 0.05})`,
|
||||
filter: `blur(${buttonsSpring() * 2}px)`,
|
||||
"pointer-events": buttonsSpring() < 0.5 ? "auto" : "none",
|
||||
}}
|
||||
style={{ padding: "0 4px 0 8px", ...springFade(1 - buttonsSpring()) }}
|
||||
>
|
||||
<span class="truncate text-13-medium text-text-strong">{language.t("prompt.mode.shell")}</span>
|
||||
<div class="size-4 shrink-0" />
|
||||
@@ -1395,13 +1389,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
onSelect={local.agent.set}
|
||||
class="capitalize max-w-[160px]"
|
||||
valueClass="truncate text-13-regular"
|
||||
triggerStyle={{
|
||||
height: "28px",
|
||||
opacity: buttonsSpring(),
|
||||
transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
|
||||
filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
|
||||
"pointer-events": buttonsSpring() > 0.5 ? "auto" : "none",
|
||||
}}
|
||||
triggerStyle={{ height: "28px", ...springFade(buttonsSpring()) }}
|
||||
variant="ghost"
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
@@ -1419,13 +1407,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
variant="ghost"
|
||||
size="normal"
|
||||
class="min-w-0 max-w-[320px] text-13-regular group"
|
||||
style={{
|
||||
height: "28px",
|
||||
opacity: buttonsSpring(),
|
||||
transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
|
||||
filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
|
||||
"pointer-events": buttonsSpring() > 0.5 ? "auto" : "none",
|
||||
}}
|
||||
style={{ height: "28px", ...springFade(buttonsSpring()) }}
|
||||
onClick={() => dialog.show(() => <DialogSelectModelUnpaid />)}
|
||||
>
|
||||
<Show when={local.model.current()?.provider?.id}>
|
||||
@@ -1454,13 +1436,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
triggerProps={{
|
||||
variant: "ghost",
|
||||
size: "normal",
|
||||
style: {
|
||||
height: "28px",
|
||||
opacity: buttonsSpring(),
|
||||
transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
|
||||
filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
|
||||
"pointer-events": buttonsSpring() > 0.5 ? "auto" : "none",
|
||||
},
|
||||
style: { height: "28px", ...springFade(buttonsSpring()) },
|
||||
class: "min-w-0 max-w-[320px] text-13-regular group",
|
||||
}}
|
||||
>
|
||||
@@ -1492,13 +1468,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
onSelect={(x) => local.model.variant.set(x === "default" ? undefined : x)}
|
||||
class="capitalize max-w-[160px]"
|
||||
valueClass="truncate text-13-regular"
|
||||
triggerStyle={{
|
||||
height: "28px",
|
||||
opacity: buttonsSpring(),
|
||||
transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
|
||||
filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
|
||||
"pointer-events": buttonsSpring() > 0.5 ? "auto" : "none",
|
||||
}}
|
||||
triggerStyle={{ height: "28px", ...springFade(buttonsSpring()) }}
|
||||
variant="ghost"
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { Message } from "@opencode-ai/sdk/v2/client"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
import { errorMessage } from "@/pages/layout/helpers"
|
||||
import { useNavigate, useParams } from "@solidjs/router"
|
||||
import type { Accessor } from "solid-js"
|
||||
import { batch, type Accessor } from "solid-js"
|
||||
import type { FileSelection } from "@/context/file"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
@@ -16,6 +17,7 @@ import { Identifier } from "@/utils/id"
|
||||
import { Worktree as WorktreeState } from "@/utils/worktree"
|
||||
import { buildRequestParts } from "./build-request-parts"
|
||||
import { setCursorPosition } from "./editor-dom"
|
||||
import { formatServerError } from "@/utils/server-errors"
|
||||
|
||||
type PendingPrompt = {
|
||||
abort: AbortController
|
||||
@@ -64,14 +66,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
const language = useLanguage()
|
||||
const params = useParams()
|
||||
|
||||
const errorMessage = (err: unknown) => {
|
||||
if (err && typeof err === "object" && "data" in err) {
|
||||
const data = (err as { data?: { message?: string } }).data
|
||||
if (data?.message) return data.message
|
||||
}
|
||||
if (err instanceof Error) return err.message
|
||||
return language.t("common.requestFailed")
|
||||
}
|
||||
const toastError = (err: unknown) => errorMessage(err, language.t("common.requestFailed"))
|
||||
|
||||
const abort = async () => {
|
||||
const sessionID = params.id
|
||||
@@ -157,7 +152,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
title: language.t("prompt.toast.worktreeCreateFailed.title"),
|
||||
description: errorMessage(err),
|
||||
description: toastError(err),
|
||||
})
|
||||
return undefined
|
||||
})
|
||||
@@ -196,7 +191,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
title: language.t("prompt.toast.sessionCreateFailed.title"),
|
||||
description: errorMessage(err),
|
||||
description: toastError(err),
|
||||
})
|
||||
return undefined
|
||||
})
|
||||
@@ -254,7 +249,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
title: language.t("prompt.toast.shellSendFailed.title"),
|
||||
description: errorMessage(err),
|
||||
description: toastError(err),
|
||||
})
|
||||
restoreInput()
|
||||
})
|
||||
@@ -286,7 +281,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
title: language.t("prompt.toast.commandSendFailed.title"),
|
||||
description: errorMessage(err),
|
||||
description: formatServerError(err, language.t, language.t("common.requestFailed")),
|
||||
})
|
||||
restoreInput()
|
||||
})
|
||||
@@ -332,9 +327,14 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
messageID,
|
||||
})
|
||||
|
||||
removeCommentItems(commentItems)
|
||||
clearInput()
|
||||
addOptimisticMessage()
|
||||
batch(() => {
|
||||
removeCommentItems(commentItems)
|
||||
clearInput()
|
||||
if (sessionDirectory === projectDirectory) {
|
||||
sync.set("session_status", session.id, { type: "busy" })
|
||||
}
|
||||
addOptimisticMessage()
|
||||
})
|
||||
|
||||
const waitForWorktree = async () => {
|
||||
const worktree = WorktreeState.get(sessionDirectory)
|
||||
@@ -411,7 +411,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
||||
}
|
||||
showToast({
|
||||
title: language.t("prompt.toast.promptSendFailed.title"),
|
||||
description: errorMessage(err),
|
||||
description: toastError(err),
|
||||
})
|
||||
removeOptimisticMessage()
|
||||
restoreCommentItems(commentItems)
|
||||
|
||||
61
packages/app/src/components/release-list.tsx
Normal file
61
packages/app/src/components/release-list.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Component } from "solid-js"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { Markdown } from "@opencode-ai/ui/markdown"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Tag } from "@opencode-ai/ui/tag"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { getRelativeTime } from "@/utils/time"
|
||||
|
||||
type Release = {
|
||||
tag: string
|
||||
body: string
|
||||
date: string
|
||||
}
|
||||
|
||||
interface ReleaseListProps {
|
||||
releases: Release[]
|
||||
hasMore: boolean
|
||||
loadingMore: boolean
|
||||
onLoadMore: () => void
|
||||
}
|
||||
|
||||
export const ReleaseList: Component<ReleaseListProps> = (props) => {
|
||||
const language = useLanguage()
|
||||
|
||||
return (
|
||||
<List
|
||||
items={props.releases}
|
||||
key={(x) => x.tag}
|
||||
search={false}
|
||||
emptyMessage="No releases found"
|
||||
loadingMessage={language.t("common.loading")}
|
||||
class="flex-1 min-h-0 overflow-hidden flex flex-col [&_[data-slot=list-scroll]]:session-scroller [&_[data-slot=list-item]]:block [&_[data-slot=list-item]]:p-0 [&_[data-slot=list-item]]:border-0 [&_[data-slot=list-item]]:bg-transparent [&_[data-slot=list-item]]:text-left [&_[data-slot=list-item]]:cursor-default [&_[data-slot=list-item]]:hover:bg-transparent [&_[data-slot=list-item]]:focus:outline-none"
|
||||
add={{
|
||||
render: () =>
|
||||
props.hasMore ? (
|
||||
<div class="p-4 flex justify-center">
|
||||
<Button variant="secondary" size="small" onClick={props.onLoadMore} loading={props.loadingMore}>
|
||||
{language.t("common.loadMore")}
|
||||
</Button>
|
||||
</div>
|
||||
) : null,
|
||||
}}
|
||||
>
|
||||
{(item) => (
|
||||
<div class="mb-8">
|
||||
<div class="py-2 pr-3 pl-2 flex items-baseline gap-2 sticky top-0 z-10 bg-surface-raised-stronger-non-alpha">
|
||||
<span class="text-[20px] font-semibold">{item.tag}</span>
|
||||
<span class="text-xs text-text-weak">{item.date ? getRelativeTime(item.date, language.t) : ""}</span>
|
||||
{item.tag === props.releases[0]?.tag && <Tag>{language.t("changelog.tag.latest")}</Tag>}
|
||||
</div>
|
||||
<div class="px-2 pb-2">
|
||||
<Markdown
|
||||
text={item.body}
|
||||
class="prose prose-sm max-w-none text-text-base [&_h2]:border-b [&_h2]:border-border-weak-base [&_h2]:pb-1 [&_h2]:mt-8 [&_h2]:mb-3 [&_h2]:text-sm [&_h2]:font-medium [&_h2]:capitalize [&_h2:first-child]:mt-4 [&_a.external-link]:text-text-interactive-base [&_a.external-link]:font-medium"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { Message } from "@opencode-ai/sdk/v2/client"
|
||||
import { findAssistantMessages } from "@opencode-ai/ui/find-assistant-messages"
|
||||
|
||||
function user(id: string): Message {
|
||||
return {
|
||||
id,
|
||||
role: "user",
|
||||
sessionID: "session-1",
|
||||
time: { created: 1 },
|
||||
} as unknown as Message
|
||||
}
|
||||
|
||||
function assistant(id: string, parentID: string): Message {
|
||||
return {
|
||||
id,
|
||||
role: "assistant",
|
||||
sessionID: "session-1",
|
||||
parentID,
|
||||
time: { created: 1 },
|
||||
} as unknown as Message
|
||||
}
|
||||
|
||||
describe("findAssistantMessages", () => {
|
||||
test("normal ordering: assistant after user in array → found via forward scan", () => {
|
||||
const messages = [user("u1"), assistant("a1", "u1")]
|
||||
const result = findAssistantMessages(messages, 0, "u1")
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].id).toBe("a1")
|
||||
})
|
||||
|
||||
test("clock skew: assistant before user in array → found via backward scan", () => {
|
||||
// When client clock is ahead, user ID sorts after assistant ID,
|
||||
// so assistant appears earlier in the ID-sorted message array
|
||||
const messages = [assistant("a1", "u1"), user("u1")]
|
||||
const result = findAssistantMessages(messages, 1, "u1")
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].id).toBe("a1")
|
||||
})
|
||||
|
||||
test("no assistant messages → returns empty array", () => {
|
||||
const messages = [user("u1"), user("u2")]
|
||||
const result = findAssistantMessages(messages, 0, "u1")
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
test("multiple assistant messages with matching parentID → all found", () => {
|
||||
const messages = [user("u1"), assistant("a1", "u1"), assistant("a2", "u1")]
|
||||
const result = findAssistantMessages(messages, 0, "u1")
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0].id).toBe("a1")
|
||||
expect(result[1].id).toBe("a2")
|
||||
})
|
||||
|
||||
test("does not return assistant messages with different parentID", () => {
|
||||
const messages = [user("u1"), assistant("a1", "u1"), assistant("a2", "other")]
|
||||
const result = findAssistantMessages(messages, 0, "u1")
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].id).toBe("a1")
|
||||
})
|
||||
|
||||
test("stops forward scan at next user message", () => {
|
||||
const messages = [user("u1"), assistant("a1", "u1"), user("u2"), assistant("a2", "u1")]
|
||||
const result = findAssistantMessages(messages, 0, "u1")
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].id).toBe("a1")
|
||||
})
|
||||
|
||||
test("stops backward scan at previous user message", () => {
|
||||
const messages = [assistant("a0", "u1"), user("u0"), assistant("a1", "u1"), user("u1")]
|
||||
const result = findAssistantMessages(messages, 3, "u1")
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].id).toBe("a1")
|
||||
})
|
||||
|
||||
test("invalid index returns empty array", () => {
|
||||
const messages = [user("u1")]
|
||||
expect(findAssistantMessages(messages, -1, "u1")).toHaveLength(0)
|
||||
expect(findAssistantMessages(messages, 5, "u1")).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
@@ -4,8 +4,7 @@ import { useParams } from "@solidjs/router"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { checksum } from "@opencode-ai/util/encode"
|
||||
import { findLast } from "@opencode-ai/util/array"
|
||||
import { same } from "@/utils/same"
|
||||
import { findLast, same } from "@opencode-ai/util/array"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { Accordion } from "@opencode-ai/ui/accordion"
|
||||
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
|
||||
|
||||
@@ -51,22 +51,22 @@ export function NewSessionView(props: NewSessionViewProps) {
|
||||
return (
|
||||
<div class={ROOT_CLASS}>
|
||||
<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" />
|
||||
<div class="text-12-medium text-text-weak select-text">
|
||||
<div class="flex justify-center items-start gap-3 min-h-5">
|
||||
<Icon name="folder" size="small" class="mt-0.5 shrink-0" />
|
||||
<div class="text-12-medium text-text-weak select-text leading-5">
|
||||
{getDirectory(projectRoot())}
|
||||
<span class="text-text-strong">{getFilename(projectRoot())}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center items-center gap-1">
|
||||
<Icon name="branch" size="small" />
|
||||
<div class="text-12-medium text-text-weak select-text ml-2">{label(current())}</div>
|
||||
<div class="flex justify-center items-start gap-3 min-h-5">
|
||||
<Icon name="branch" size="small" class="mt-0.5 shrink-0" />
|
||||
<div class="text-12-medium text-text-weak select-text leading-5">{label(current())}</div>
|
||||
</div>
|
||||
<Show when={sync.project}>
|
||||
{(project) => (
|
||||
<div class="flex justify-center items-center gap-3">
|
||||
<Icon name="pencil-line" size="small" />
|
||||
<div class="text-12-medium text-text-weak">
|
||||
<div class="flex justify-center items-start gap-3 min-h-5">
|
||||
<Icon name="pencil-line" size="small" class="mt-0.5 shrink-0" />
|
||||
<div class="text-12-medium text-text-weak leading-5">
|
||||
{language.t("session.new.lastModified")}
|
||||
<span class="text-text-strong">
|
||||
{DateTime.fromMillis(project().time.updated ?? project().time.created)
|
||||
|
||||
188
packages/app/src/components/settings-archive.tsx
Normal file
188
packages/app/src/components/settings-archive.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { RadioGroup } from "@opencode-ai/ui/radio-group"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { Component, For, Show, createMemo, createResource, createSignal } from "solid-js"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { getRelativeTime } from "@/utils/time"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
import type { Session } from "@opencode-ai/sdk/v2/client"
|
||||
import { SessionSkeleton } from "@/pages/layout/sidebar-items"
|
||||
|
||||
type FilterScope = "all" | "current"
|
||||
|
||||
type ScopeOption = { value: FilterScope; label: "settings.archive.scope.all" | "settings.archive.scope.current" }
|
||||
|
||||
const scopeOptions: ScopeOption[] = [
|
||||
{ value: "all", label: "settings.archive.scope.all" },
|
||||
{ value: "current", label: "settings.archive.scope.current" },
|
||||
]
|
||||
|
||||
export const SettingsArchive: Component = () => {
|
||||
const language = useLanguage()
|
||||
const globalSDK = useGlobalSDK()
|
||||
const globalSync = useGlobalSync()
|
||||
const layout = useLayout()
|
||||
const params = useParams()
|
||||
const [removedIds, setRemovedIds] = createSignal<Set<string>>(new Set())
|
||||
|
||||
const projects = createMemo(() => globalSync.data.project)
|
||||
const layoutProjects = createMemo(() => layout.projects.list())
|
||||
const hasMultipleProjects = createMemo(() => projects().length > 1)
|
||||
const homedir = createMemo(() => globalSync.data.path.home)
|
||||
|
||||
const defaultScope = () => (hasMultipleProjects() ? "current" : "all")
|
||||
const [filterScope, setFilterScope] = createSignal<FilterScope>(defaultScope())
|
||||
|
||||
const currentDirectory = createMemo(() => decode64(params.dir) ?? "")
|
||||
|
||||
const currentProject = createMemo(() => {
|
||||
const dir = currentDirectory()
|
||||
if (!dir) return null
|
||||
return layoutProjects().find((p) => p.worktree === dir || p.sandboxes?.includes(dir)) ?? null
|
||||
})
|
||||
|
||||
const filteredProjects = createMemo(() => {
|
||||
if (filterScope() === "current" && currentProject()) {
|
||||
return [currentProject()!]
|
||||
}
|
||||
return layoutProjects()
|
||||
})
|
||||
|
||||
const getSessionLabel = (session: Session) => {
|
||||
const directory = session.directory
|
||||
const home = homedir()
|
||||
const path = home ? directory.replace(home, "~") : directory
|
||||
|
||||
if (filterScope() === "current" && currentProject()) {
|
||||
const current = currentProject()
|
||||
const kind =
|
||||
current && directory === current.worktree
|
||||
? language.t("workspace.type.local")
|
||||
: language.t("workspace.type.sandbox")
|
||||
const [store] = globalSync.child(directory, { bootstrap: false })
|
||||
const name = store.vcs?.branch ?? getFilename(directory)
|
||||
return `${kind} : ${name || path}`
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
const [archivedSessions] = createResource(
|
||||
() => ({ scope: filterScope(), projects: filteredProjects() }),
|
||||
async ({ projects }) => {
|
||||
const allSessions: Session[] = []
|
||||
for (const project of projects) {
|
||||
const directories = [project.worktree, ...(project.sandboxes ?? [])]
|
||||
for (const directory of directories) {
|
||||
const result = await globalSDK.client.experimental.session.list({ directory, archived: true })
|
||||
const sessions = result.data ?? []
|
||||
for (const session of sessions) {
|
||||
allSessions.push(session)
|
||||
}
|
||||
}
|
||||
}
|
||||
return allSessions.sort((a, b) => (b.time?.updated ?? 0) - (a.time?.updated ?? 0))
|
||||
},
|
||||
{ initialValue: [] },
|
||||
)
|
||||
|
||||
const displayedSessions = () => {
|
||||
const sessions = archivedSessions() ?? []
|
||||
const removed = removedIds()
|
||||
return sessions.filter((s) => !removed.has(s.id))
|
||||
}
|
||||
|
||||
const currentScopeOption = () => scopeOptions.find((o) => o.value === filterScope())
|
||||
|
||||
const unarchiveSession = async (session: Session) => {
|
||||
setRemovedIds((prev) => new Set(prev).add(session.id))
|
||||
await globalSDK.client.session.update({
|
||||
directory: session.directory,
|
||||
sessionID: session.id,
|
||||
time: { archived: null as any },
|
||||
})
|
||||
}
|
||||
|
||||
const handleScopeChange = (option: ScopeOption | undefined) => {
|
||||
if (!option) return
|
||||
setRemovedIds(new Set<string>())
|
||||
setFilterScope(option.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
||||
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
||||
<div class="flex flex-col gap-1 pt-6 pb-8 max-w-[720px]">
|
||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.archive.title")}</h2>
|
||||
<p class="text-14-regular text-text-weak">{language.t("settings.archive.description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4 max-w-[720px]">
|
||||
<Show when={hasMultipleProjects()}>
|
||||
<RadioGroup
|
||||
options={scopeOptions}
|
||||
current={currentScopeOption() ?? undefined}
|
||||
value={(o) => o.value}
|
||||
size="small"
|
||||
label={(o) => language.t(o.label)}
|
||||
onSelect={handleScopeChange}
|
||||
/>
|
||||
</Show>
|
||||
<Show
|
||||
when={!archivedSessions.loading}
|
||||
fallback={
|
||||
<div class="min-h-[700px]">
|
||||
<SessionSkeleton count={4} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Show
|
||||
when={displayedSessions().length}
|
||||
fallback={
|
||||
<div class="min-h-[700px]">
|
||||
<div class="text-14-regular text-text-weak">{language.t("settings.archive.none")}</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="min-h-[700px] flex flex-col gap-2">
|
||||
<For each={displayedSessions()}>
|
||||
{(session) => (
|
||||
<div class="flex items-center justify-between gap-4 px-3 py-1 rounded-md hover:bg-surface-raised-base-hover">
|
||||
<div class="flex items-center gap-x-3 grow min-w-0">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<span class="text-14-regular text-text-strong truncate">{session.title}</span>
|
||||
<span class="text-14-regular text-text-weak truncate">{getSessionLabel(session)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 shrink-0">
|
||||
<Show when={session.time?.updated}>
|
||||
{(updated) => (
|
||||
<span class="text-12-regular text-text-weak whitespace-nowrap">
|
||||
{getRelativeTime(new Date(updated()).toISOString(), language.t)}
|
||||
</span>
|
||||
)}
|
||||
</Show>
|
||||
<Button
|
||||
size="normal"
|
||||
variant="secondary"
|
||||
onClick={() => unarchiveSession(session)}
|
||||
>
|
||||
{language.t("common.unarchive")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -266,6 +266,9 @@ export function Titlebar() {
|
||||
</div>
|
||||
</div>
|
||||
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" />
|
||||
<div class="bg-icon-interactive-base text-background-base font-medium px-2 rounded-sm uppercase font-mono">
|
||||
BETA
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 flex items-center justify-center pointer-events-none">
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { isEditableTarget } from "@/utils/dom"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
|
||||
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
||||
@@ -177,14 +178,6 @@ export function formatKeybind(config: string): string {
|
||||
return IS_MAC ? parts.join("") : parts.join("+")
|
||||
}
|
||||
|
||||
function isEditableTarget(target: EventTarget | null) {
|
||||
if (!(target instanceof HTMLElement)) return false
|
||||
if (target.isContentEditable) return true
|
||||
if (target.closest("[contenteditable='true']")) return true
|
||||
if (target.closest("input, textarea, select")) return true
|
||||
return false
|
||||
}
|
||||
|
||||
export const { use: useCommand, provider: CommandProvider } = createSimpleContext({
|
||||
name: "Command",
|
||||
init: () => {
|
||||
|
||||
@@ -228,10 +228,7 @@ function createGlobalSync() {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("toast.session.listFailed.title", { project }),
|
||||
description: formatServerError(err, {
|
||||
unknown: language.t("error.chain.unknown"),
|
||||
invalidConfiguration: language.t("error.server.invalidConfiguration"),
|
||||
}),
|
||||
description: formatServerError(err, language.t),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -261,8 +258,7 @@ function createGlobalSync() {
|
||||
setStore: child[1],
|
||||
vcsCache: cache,
|
||||
loadSessions,
|
||||
unknownError: language.t("error.chain.unknown"),
|
||||
invalidConfigurationError: language.t("error.server.invalidConfiguration"),
|
||||
translate: language.t,
|
||||
})
|
||||
})()
|
||||
|
||||
@@ -331,8 +327,7 @@ function createGlobalSync() {
|
||||
url: globalSDK.url,
|
||||
}),
|
||||
requestFailedTitle: language.t("common.requestFailed"),
|
||||
unknownError: language.t("error.chain.unknown"),
|
||||
invalidConfigurationError: language.t("error.server.invalidConfiguration"),
|
||||
translate: language.t,
|
||||
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
|
||||
setGlobalStore: setBootStore,
|
||||
})
|
||||
@@ -358,7 +353,6 @@ function createGlobalSync() {
|
||||
.update({ config })
|
||||
.then(bootstrap)
|
||||
.then(() => {
|
||||
queue.refresh()
|
||||
setGlobalStore("reload", undefined)
|
||||
queue.refresh()
|
||||
})
|
||||
|
||||
@@ -36,8 +36,7 @@ export async function bootstrapGlobal(input: {
|
||||
connectErrorTitle: string
|
||||
connectErrorDescription: string
|
||||
requestFailedTitle: string
|
||||
unknownError: string
|
||||
invalidConfigurationError: string
|
||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||
formatMoreCount: (count: number) => string
|
||||
setGlobalStore: SetStoreFunction<GlobalStore>
|
||||
}) {
|
||||
@@ -91,10 +90,7 @@ export async function bootstrapGlobal(input: {
|
||||
const results = await Promise.allSettled(tasks)
|
||||
const errors = results.filter((r): r is PromiseRejectedResult => r.status === "rejected").map((r) => r.reason)
|
||||
if (errors.length) {
|
||||
const message = formatServerError(errors[0], {
|
||||
unknown: input.unknownError,
|
||||
invalidConfiguration: input.invalidConfigurationError,
|
||||
})
|
||||
const message = formatServerError(errors[0], input.translate)
|
||||
const more = errors.length > 1 ? input.formatMoreCount(errors.length - 1) : ""
|
||||
showToast({
|
||||
variant: "error",
|
||||
@@ -122,8 +118,7 @@ export async function bootstrapDirectory(input: {
|
||||
setStore: SetStoreFunction<State>
|
||||
vcsCache: VcsCache
|
||||
loadSessions: (directory: string) => Promise<void> | void
|
||||
unknownError: string
|
||||
invalidConfigurationError: string
|
||||
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||
}) {
|
||||
if (input.store.status !== "complete") input.setStore("status", "loading")
|
||||
|
||||
@@ -145,10 +140,7 @@ export async function bootstrapDirectory(input: {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: `Failed to reload ${project}`,
|
||||
description: formatServerError(err, {
|
||||
unknown: input.unknownError,
|
||||
invalidConfiguration: input.invalidConfigurationError,
|
||||
}),
|
||||
description: formatServerError(err, input.translate),
|
||||
})
|
||||
input.setStore("status", "partial")
|
||||
return
|
||||
|
||||
@@ -8,7 +8,7 @@ import { usePlatform } from "./platform"
|
||||
import { Project } from "@opencode-ai/sdk/v2"
|
||||
import { Persist, persisted, removePersisted } from "@/utils/persist"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
import { same } from "@/utils/same"
|
||||
import { same } from "@opencode-ai/util/array"
|
||||
import { createScrollPersistence, type SessionScroll } from "./layout-scroll"
|
||||
import { createPathHelpers } from "./file/path"
|
||||
|
||||
|
||||
@@ -35,6 +35,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
|
||||
const agent = (() => {
|
||||
const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden))
|
||||
const models = useModels()
|
||||
|
||||
const [store, setStore] = createStore<{
|
||||
current?: string
|
||||
}>({
|
||||
@@ -53,11 +55,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
setStore("current", undefined)
|
||||
return
|
||||
}
|
||||
if (name && available.some((x) => x.name === name)) {
|
||||
setStore("current", name)
|
||||
return
|
||||
}
|
||||
setStore("current", available[0].name)
|
||||
const match = name ? available.find((x) => x.name === name) : undefined
|
||||
const value = match ?? available[0]
|
||||
if (!value) return
|
||||
setStore("current", value.name)
|
||||
if (!value.model) return
|
||||
setModel({
|
||||
providerID: value.model.providerID,
|
||||
modelID: value.model.modelID,
|
||||
})
|
||||
if (value.variant)
|
||||
models.variant.set({ providerID: value.model.providerID, modelID: value.model.modelID }, value.variant)
|
||||
},
|
||||
move(direction: 1 | -1) {
|
||||
const available = list()
|
||||
@@ -71,11 +79,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
const value = available[next]
|
||||
if (!value) return
|
||||
setStore("current", value.name)
|
||||
if (value.model)
|
||||
setModel({
|
||||
providerID: value.model.providerID,
|
||||
modelID: value.model.modelID,
|
||||
})
|
||||
if (!value.model) return
|
||||
setModel({
|
||||
providerID: value.model.providerID,
|
||||
modelID: value.model.modelID,
|
||||
})
|
||||
if (value.variant)
|
||||
models.variant.set({ providerID: value.model.providerID, modelID: value.model.modelID }, value.variant)
|
||||
},
|
||||
}
|
||||
})()
|
||||
|
||||
@@ -506,6 +506,10 @@ export const dict = {
|
||||
"common.close": "إغلاق",
|
||||
"common.edit": "تحرير",
|
||||
"common.loadMore": "تحميل المزيد",
|
||||
"common.changelog": "التغييرات",
|
||||
"common.noReleasesFound": "لم يتم العثور على إصدارات",
|
||||
"changelog.tag.latest": "الأحدث",
|
||||
|
||||
"common.key.esc": "ESC",
|
||||
"sidebar.menu.toggle": "تبديل القائمة",
|
||||
"sidebar.nav.projectsAndSessions": "المشاريع والجلسات",
|
||||
@@ -734,6 +738,11 @@ export const dict = {
|
||||
"workspace.reset.archived.one": "ستتم أرشفة جلسة واحدة.",
|
||||
"workspace.reset.archived.many": "ستتم أرشفة {{count}} جلسات.",
|
||||
"workspace.reset.note": "سيؤدي هذا إلى إعادة تعيين مساحة العمل لتتطابق مع الفرع الافتراضي.",
|
||||
"settings.archive.title": "الجلسات المؤرشفة",
|
||||
"settings.archive.description": "استعادة الجلسات المؤرشفة لجعلها مرئية في الشريط الجانبي.",
|
||||
"settings.archive.none": "لا توجد جلسات مؤرشفة.",
|
||||
"settings.archive.scope.all": "جميع المشاريع",
|
||||
"settings.archive.scope.current": "المشروع الحالي",
|
||||
"common.open": "فتح",
|
||||
"dialog.releaseNotes.action.getStarted": "البدء",
|
||||
"dialog.releaseNotes.action.next": "التالي",
|
||||
@@ -748,4 +757,4 @@ export const dict = {
|
||||
"common.time.daysAgo.short": "قبل {{count}} ي",
|
||||
"settings.providers.connected.environmentDescription": "متصل من متغيرات البيئة الخاصة بك",
|
||||
"settings.providers.custom.description": "أضف مزود متوافق مع OpenAI بواسطة عنوان URL الأساسي.",
|
||||
}
|
||||
}
|
||||
@@ -512,6 +512,9 @@ export const dict = {
|
||||
"common.close": "Fechar",
|
||||
"common.edit": "Editar",
|
||||
"common.loadMore": "Carregar mais",
|
||||
"common.changelog": "Novidades",
|
||||
"common.noReleasesFound": "Nenhuma release encontrada",
|
||||
"changelog.tag.latest": "Mais recente",
|
||||
"common.key.esc": "ESC",
|
||||
"sidebar.menu.toggle": "Alternar menu",
|
||||
"sidebar.nav.projectsAndSessions": "Projetos e sessões",
|
||||
@@ -742,6 +745,11 @@ export const dict = {
|
||||
"workspace.reset.archived.one": "1 sessão será arquivada.",
|
||||
"workspace.reset.archived.many": "{{count}} sessões serão arquivadas.",
|
||||
"workspace.reset.note": "Isso redefinirá o espaço de trabalho para corresponder ao branch padrão.",
|
||||
"settings.archive.title": "Sessões arquivadas",
|
||||
"settings.archive.description": "Restaure sessões arquivadas para torná-las visíveis na barra lateral.",
|
||||
"settings.archive.none": "Nenhuma sessão arquivada.",
|
||||
"settings.archive.scope.all": "Todos os projetos",
|
||||
"settings.archive.scope.current": "Projeto atual",
|
||||
"common.open": "Abrir",
|
||||
"dialog.releaseNotes.action.getStarted": "Começar",
|
||||
"dialog.releaseNotes.action.next": "Próximo",
|
||||
|
||||
@@ -572,6 +572,9 @@ export const dict = {
|
||||
"common.close": "Zatvori",
|
||||
"common.edit": "Uredi",
|
||||
"common.loadMore": "Učitaj još",
|
||||
"common.changelog": "Novosti",
|
||||
"common.noReleasesFound": "Nema pronađenih verzija",
|
||||
"changelog.tag.latest": "Najnovije",
|
||||
"common.key.esc": "ESC",
|
||||
|
||||
"sidebar.menu.toggle": "Prikaži/sakrij meni",
|
||||
@@ -819,6 +822,11 @@ export const dict = {
|
||||
"workspace.reset.archived.one": "1 sesija će biti arhivirana.",
|
||||
"workspace.reset.archived.many": "Biće arhivirano {{count}} sesija.",
|
||||
"workspace.reset.note": "Ovo će resetovati radni prostor da odgovara podrazumijevanoj grani.",
|
||||
"settings.archive.title": "Arhivirane sesije",
|
||||
"settings.archive.description": "Vrati arhivirane sesije da bi bile vidljive u bočnoj traci.",
|
||||
"settings.archive.none": "Nema arhiviranih sesija.",
|
||||
"settings.archive.scope.all": "Svi projekti",
|
||||
"settings.archive.scope.current": "Trenutni projekt",
|
||||
"common.open": "Otvori",
|
||||
"dialog.releaseNotes.action.getStarted": "Započni",
|
||||
"dialog.releaseNotes.action.next": "Sljedeće",
|
||||
|
||||
@@ -568,6 +568,9 @@ export const dict = {
|
||||
"common.close": "Luk",
|
||||
"common.edit": "Rediger",
|
||||
"common.loadMore": "Indlæs flere",
|
||||
"common.changelog": "Nyheder",
|
||||
"common.noReleasesFound": "Ingen versioner fundet",
|
||||
"changelog.tag.latest": "Seneste",
|
||||
|
||||
"common.key.esc": "ESC",
|
||||
"sidebar.menu.toggle": "Skift menu",
|
||||
@@ -813,6 +816,11 @@ export const dict = {
|
||||
"workspace.reset.archived.one": "1 session vil blive arkiveret.",
|
||||
"workspace.reset.archived.many": "{{count}} sessioner vil blive arkiveret.",
|
||||
"workspace.reset.note": "Dette vil nulstille arbejdsområdet til at matche hovedgrenen.",
|
||||
"settings.archive.title": "Arkiverede sessioner",
|
||||
"settings.archive.description": "Gendan arkiverede sessioner for at gøre dem synlige i sidebjælken.",
|
||||
"settings.archive.none": "Ingen arkiverede sessioner.",
|
||||
"settings.archive.scope.all": "Alle projekter",
|
||||
"settings.archive.scope.current": "Nuværende projekt",
|
||||
"common.open": "Åbn",
|
||||
"dialog.releaseNotes.action.getStarted": "Kom i gang",
|
||||
"dialog.releaseNotes.action.next": "Næste",
|
||||
|
||||
@@ -520,6 +520,10 @@ export const dict = {
|
||||
"common.close": "Schließen",
|
||||
"common.edit": "Bearbeiten",
|
||||
"common.loadMore": "Mehr laden",
|
||||
"common.changelog": "Neuerungen",
|
||||
"common.noReleasesFound": "Keine Versionen gefunden",
|
||||
"changelog.tag.latest": "Neueste",
|
||||
|
||||
"common.key.esc": "ESC",
|
||||
"sidebar.menu.toggle": "Menü umschalten",
|
||||
"sidebar.nav.projectsAndSessions": "Projekte und Sitzungen",
|
||||
@@ -751,6 +755,12 @@ export const dict = {
|
||||
"workspace.reset.archived.one": "1 Sitzung wird archiviert.",
|
||||
"workspace.reset.archived.many": "{{count}} Sitzungen werden archiviert.",
|
||||
"workspace.reset.note": "Dadurch wird der Arbeitsbereich auf den Standard-Branch zurückgesetzt.",
|
||||
|
||||
"settings.archive.title": "Archivierte Sitzungen",
|
||||
"settings.archive.description": "Archivierte Sitzungen wiederherstellen, um sie in der Seitenleiste anzuzeigen.",
|
||||
"settings.archive.none": "Keine archivierten Sitzungen.",
|
||||
"settings.archive.scope.all": "Alle Projekte",
|
||||
"settings.archive.scope.current": "Aktuelles Projekt",
|
||||
"common.open": "Öffnen",
|
||||
"dialog.releaseNotes.action.getStarted": "Loslegen",
|
||||
"dialog.releaseNotes.action.next": "Weiter",
|
||||
|
||||
@@ -585,16 +585,19 @@ export const dict = {
|
||||
"common.rename": "Rename",
|
||||
"common.reset": "Reset",
|
||||
"common.archive": "Archive",
|
||||
"common.unarchive": "Unarchive",
|
||||
"common.delete": "Delete",
|
||||
"common.close": "Close",
|
||||
"common.edit": "Edit",
|
||||
"common.loadMore": "Load more",
|
||||
"common.key.esc": "ESC",
|
||||
|
||||
"common.changelog": "Changelog",
|
||||
"common.noReleasesFound": "No releases found",
|
||||
"common.time.justNow": "Just now",
|
||||
"common.time.minutesAgo.short": "{{count}}m ago",
|
||||
"common.time.hoursAgo.short": "{{count}}h ago",
|
||||
"common.time.daysAgo.short": "{{count}}d ago",
|
||||
"changelog.tag.latest": "Latest",
|
||||
"common.key.esc": "ESC",
|
||||
|
||||
"sidebar.menu.toggle": "Toggle menu",
|
||||
"sidebar.nav.projectsAndSessions": "Projects and sessions",
|
||||
@@ -613,6 +616,7 @@ export const dict = {
|
||||
|
||||
"settings.section.desktop": "Desktop",
|
||||
"settings.section.server": "Server",
|
||||
"settings.section.data": "Data",
|
||||
"settings.tab.general": "General",
|
||||
"settings.tab.shortcuts": "Shortcuts",
|
||||
"settings.desktop.section.wsl": "WSL",
|
||||
@@ -844,4 +848,10 @@ export const dict = {
|
||||
"workspace.reset.archived.one": "1 session will be archived.",
|
||||
"workspace.reset.archived.many": "{{count}} sessions will be archived.",
|
||||
"workspace.reset.note": "This will reset the workspace to match the default branch.",
|
||||
|
||||
"settings.archive.title": "Archived Sessions",
|
||||
"settings.archive.description": "Restore archived sessions to make them visible in the sidebar.",
|
||||
"settings.archive.none": "No archived sessions.",
|
||||
"settings.archive.scope.all": "All projects",
|
||||
"settings.archive.scope.current": "Current project",
|
||||
}
|
||||
|
||||
@@ -575,6 +575,10 @@ export const dict = {
|
||||
"common.close": "Cerrar",
|
||||
"common.edit": "Editar",
|
||||
"common.loadMore": "Cargar más",
|
||||
"common.changelog": "Novedades",
|
||||
"common.noReleasesFound": "No se encontraron versiones",
|
||||
"changelog.tag.latest": "Último",
|
||||
|
||||
"common.key.esc": "ESC",
|
||||
|
||||
"sidebar.menu.toggle": "Alternar menú",
|
||||
@@ -825,6 +829,12 @@ export const dict = {
|
||||
"workspace.reset.archived.one": "1 sesión será archivada.",
|
||||
"workspace.reset.archived.many": "{{count}} sesiones serán archivadas.",
|
||||
"workspace.reset.note": "Esto restablecerá el espacio de trabajo para coincidir con la rama predeterminada.",
|
||||
|
||||
"settings.archive.title": "Sesiones archivadas",
|
||||
"settings.archive.description": "Restaura las sesiones archivadas para hacerlas visibles en la barra lateral.",
|
||||
"settings.archive.none": "No hay sesiones archivadas.",
|
||||
"settings.archive.scope.all": "Todos los proyectos",
|
||||
"settings.archive.scope.current": "Proyecto actual",
|
||||
"common.open": "Abrir",
|
||||
"dialog.releaseNotes.action.getStarted": "Comenzar",
|
||||
"dialog.releaseNotes.action.next": "Siguiente",
|
||||
|
||||
@@ -516,6 +516,10 @@ export const dict = {
|
||||
"common.close": "Fermer",
|
||||
"common.edit": "Modifier",
|
||||
"common.loadMore": "Charger plus",
|
||||
"common.changelog": "Nouveautés",
|
||||
"common.noReleasesFound": "Aucune version trouvée",
|
||||
"changelog.tag.latest": "Dernier",
|
||||
|
||||
"common.key.esc": "ESC",
|
||||
"sidebar.menu.toggle": "Basculer le menu",
|
||||
"sidebar.nav.projectsAndSessions": "Projets et sessions",
|
||||
@@ -749,6 +753,11 @@ export const dict = {
|
||||
"workspace.reset.archived.one": "1 session sera archivée.",
|
||||
"workspace.reset.archived.many": "{{count}} sessions seront archivées.",
|
||||
"workspace.reset.note": "Cela réinitialisera l'espace de travail pour correspondre à la branche par défaut.",
|
||||
"settings.archive.title": "Sessions archivées",
|
||||
"settings.archive.description": "Restaurez les sessions archivées pour les rendre visibles dans la barre latérale.",
|
||||
"settings.archive.none": "Aucune session archivée.",
|
||||
"settings.archive.scope.all": "Tous les Projets",
|
||||
"settings.archive.scope.current": "Projet actuel",
|
||||
"common.open": "Ouvrir",
|
||||
"dialog.releaseNotes.action.getStarted": "Commencer",
|
||||
"dialog.releaseNotes.action.next": "Suivant",
|
||||
|
||||
@@ -510,6 +510,10 @@ export const dict = {
|
||||
"common.close": "閉じる",
|
||||
"common.edit": "編集",
|
||||
"common.loadMore": "さらに読み込む",
|
||||
"common.changelog": "更新履歴",
|
||||
"common.noReleasesFound": "バージョンが見つかりません",
|
||||
"changelog.tag.latest": "最新",
|
||||
|
||||
"common.key.esc": "ESC",
|
||||
"sidebar.menu.toggle": "メニューを切り替え",
|
||||
"sidebar.nav.projectsAndSessions": "プロジェクトとセッション",
|
||||
@@ -738,6 +742,12 @@ export const dict = {
|
||||
"workspace.reset.archived.one": "1つのセッションがアーカイブされます。",
|
||||
"workspace.reset.archived.many": "{{count}}個のセッションがアーカイブされます。",
|
||||
"workspace.reset.note": "これにより、ワークスペースはデフォルトブランチと一致するようにリセットされます。",
|
||||
|
||||
"settings.archive.title": "アーカイブされたセッション",
|
||||
"settings.archive.description": "アーカイブされたセッションを復元してサイドバーに表示します。",
|
||||
"settings.archive.none": "アーカイブされたセッションはありません。",
|
||||
"settings.archive.scope.all": "すべてのプロジェクト",
|
||||
"settings.archive.scope.current": "現在のプロジェクト",
|
||||
"common.open": "開く",
|
||||
"dialog.releaseNotes.action.getStarted": "始める",
|
||||
"dialog.releaseNotes.action.next": "次へ",
|
||||
|
||||
@@ -511,6 +511,10 @@ export const dict = {
|
||||
"common.close": "닫기",
|
||||
"common.edit": "편집",
|
||||
"common.loadMore": "더 불러오기",
|
||||
"common.changelog": "새로운 기능",
|
||||
"common.noReleasesFound": "버전을 찾을 수 없음",
|
||||
"changelog.tag.latest": "최신",
|
||||
|
||||
"common.key.esc": "ESC",
|
||||
"sidebar.menu.toggle": "메뉴 토글",
|
||||
"sidebar.nav.projectsAndSessions": "프로젝트 및 세션",
|
||||
@@ -738,6 +742,12 @@ export const dict = {
|
||||
"workspace.reset.archived.one": "1개의 세션이 보관됩니다.",
|
||||
"workspace.reset.archived.many": "{{count}}개의 세션이 보관됩니다.",
|
||||
"workspace.reset.note": "이 작업은 작업 공간을 기본 브랜치와 일치하도록 재설정합니다.",
|
||||
|
||||
"settings.archive.title": "보관된 세션",
|
||||
"settings.archive.description": "보관된 세션을 복원하여 사이드바에 표시합니다.",
|
||||
"settings.archive.none": "보관된 세션이 없습니다.",
|
||||
"settings.archive.scope.all": "모든 프로젝트",
|
||||
"settings.archive.scope.current": "현재 프로젝트",
|
||||
"common.open": "열기",
|
||||
"dialog.releaseNotes.action.getStarted": "시작하기",
|
||||
"dialog.releaseNotes.action.next": "다음",
|
||||
|
||||
@@ -575,6 +575,9 @@ export const dict = {
|
||||
"common.close": "Lukk",
|
||||
"common.edit": "Rediger",
|
||||
"common.loadMore": "Last flere",
|
||||
"common.changelog": "Nyheter",
|
||||
"common.noReleasesFound": "Ingen versjoner funnet",
|
||||
"changelog.tag.latest": "Siste",
|
||||
"common.key.esc": "ESC",
|
||||
|
||||
"sidebar.menu.toggle": "Veksle meny",
|
||||
@@ -821,6 +824,12 @@ export const dict = {
|
||||
"workspace.reset.archived.one": "1 sesjon vil bli arkivert.",
|
||||
"workspace.reset.archived.many": "{{count}} sesjoner vil bli arkivert.",
|
||||
"workspace.reset.note": "Dette vil tilbakestille arbeidsområdet til å samsvare med standardgrenen.",
|
||||
|
||||
"settings.archive.title": "Arkiverte økter",
|
||||
"settings.archive.description": "Gjenopprett arkiverte økter for å gjøre dem synlige i sidefeltet.",
|
||||
"settings.archive.none": "Ingen arkiverte økter.",
|
||||
"settings.archive.scope.all": "Alle prosjekter",
|
||||
"settings.archive.scope.current": "Nåværende prosjekt",
|
||||
"common.open": "Åpne",
|
||||
"dialog.releaseNotes.action.getStarted": "Kom i gang",
|
||||
"dialog.releaseNotes.action.next": "Neste",
|
||||
|
||||
@@ -511,6 +511,9 @@ export const dict = {
|
||||
"common.close": "Zamknij",
|
||||
"common.edit": "Edytuj",
|
||||
"common.loadMore": "Załaduj więcej",
|
||||
"common.changelog": "Nowości",
|
||||
"common.noReleasesFound": "Nie znaleziono wersji",
|
||||
"changelog.tag.latest": "Najnowszy",
|
||||
"common.key.esc": "ESC",
|
||||
"sidebar.menu.toggle": "Przełącz menu",
|
||||
"sidebar.nav.projectsAndSessions": "Projekty i sesje",
|
||||
@@ -740,6 +743,11 @@ export const dict = {
|
||||
"workspace.reset.archived.one": "1 sesja zostanie zarchiwizowana.",
|
||||
"workspace.reset.archived.many": "{{count}} sesji zostanie zarchiwizowanych.",
|
||||
"workspace.reset.note": "To zresetuje przestrzeń roboczą, aby odpowiadała domyślnej gałęzi.",
|
||||
"settings.archive.title": "Zarchiwizowane sesje",
|
||||
"settings.archive.description": "Przywróć zarchiwizowane sesje, aby były widoczne na pasku bocznym.",
|
||||
"settings.archive.none": "Brak zarchiwizowanych sesji.",
|
||||
"settings.archive.scope.all": "Wszystkie projekty",
|
||||
"settings.archive.scope.current": "Bieżący projekt",
|
||||
"common.open": "Otwórz",
|
||||
"dialog.releaseNotes.action.getStarted": "Rozpocznij",
|
||||
"dialog.releaseNotes.action.next": "Dalej",
|
||||
|
||||
@@ -573,6 +573,9 @@ export const dict = {
|
||||
"common.close": "Закрыть",
|
||||
"common.edit": "Редактировать",
|
||||
"common.loadMore": "Загрузить ещё",
|
||||
"common.changelog": "Что нового",
|
||||
"common.noReleasesFound": "Версии не найдены",
|
||||
"changelog.tag.latest": "Последний",
|
||||
"common.key.esc": "ESC",
|
||||
|
||||
"sidebar.menu.toggle": "Переключить меню",
|
||||
@@ -821,6 +824,11 @@ export const dict = {
|
||||
"workspace.reset.archived.one": "1 сессия будет архивирована.",
|
||||
"workspace.reset.archived.many": "{{count}} сессий будет архивировано.",
|
||||
"workspace.reset.note": "Рабочее пространство будет сброшено в соответствие с веткой по умолчанию.",
|
||||
"settings.archive.title": "Архивированные сессии",
|
||||
"settings.archive.description": "Восстановите архивированные сессии, чтобы они отображались на боковой панели.",
|
||||
"settings.archive.none": "Нет архивированных сессий.",
|
||||
"settings.archive.scope.all": "Все проекты",
|
||||
"settings.archive.scope.current": "Текущий проект",
|
||||
"common.open": "Открыть",
|
||||
"dialog.releaseNotes.action.getStarted": "Начать",
|
||||
"dialog.releaseNotes.action.next": "Далее",
|
||||
|
||||
@@ -567,6 +567,9 @@ export const dict = {
|
||||
"common.close": "ปิด",
|
||||
"common.edit": "แก้ไข",
|
||||
"common.loadMore": "โหลดเพิ่มเติม",
|
||||
"common.changelog": "อัปเดต",
|
||||
"common.noReleasesFound": "ไม่พบเวอร์ชัน",
|
||||
"changelog.tag.latest": "ล่าสุด",
|
||||
"common.key.esc": "ESC",
|
||||
|
||||
"sidebar.menu.toggle": "สลับเมนู",
|
||||
@@ -811,6 +814,12 @@ export const dict = {
|
||||
"workspace.reset.archived.one": "1 เซสชันจะถูกจัดเก็บ",
|
||||
"workspace.reset.archived.many": "{{count}} เซสชันจะถูกจัดเก็บ",
|
||||
"workspace.reset.note": "สิ่งนี้จะรีเซ็ตพื้นที่ทำงานให้ตรงกับสาขาเริ่มต้น",
|
||||
|
||||
"settings.archive.title": "เซสชันที่จัดเก็บ",
|
||||
"settings.archive.description": "กู้คืนเซสชันที่จัดเก็บเพื่อให้แสดงในแถบด้านข้าง",
|
||||
"settings.archive.none": "ไม่มีเซสชันที่จัดเก็บ",
|
||||
"settings.archive.scope.all": "โปรเจกต์ทั้งหมด",
|
||||
"settings.archive.scope.current": "โปรเจกต์ปัจจุบัน",
|
||||
"common.open": "เปิด",
|
||||
"dialog.releaseNotes.action.getStarted": "เริ่มต้น",
|
||||
"dialog.releaseNotes.action.next": "ถัดไป",
|
||||
|
||||
@@ -566,6 +566,10 @@ export const dict = {
|
||||
"common.close": "关闭",
|
||||
"common.edit": "编辑",
|
||||
"common.loadMore": "加载更多",
|
||||
"common.changelog": "更新日志",
|
||||
"common.noReleasesFound": "未找到版本",
|
||||
"changelog.tag.latest": "最新",
|
||||
|
||||
"common.key.esc": "ESC",
|
||||
|
||||
"sidebar.menu.toggle": "切换菜单",
|
||||
@@ -809,6 +813,12 @@ export const dict = {
|
||||
"workspace.reset.archived.one": "将归档 1 个会话。",
|
||||
"workspace.reset.archived.many": "将归档 {{count}} 个会话。",
|
||||
"workspace.reset.note": "这将把工作区重置为与默认分支一致。",
|
||||
|
||||
"settings.archive.title": "归档会话",
|
||||
"settings.archive.description": "恢复归档会话以使其在侧边栏中可见。",
|
||||
"settings.archive.none": "没有归档会话。",
|
||||
"settings.archive.scope.all": "所有项目",
|
||||
"settings.archive.scope.current": "当前项目",
|
||||
"common.open": "打开",
|
||||
"dialog.releaseNotes.action.getStarted": "开始",
|
||||
"dialog.releaseNotes.action.next": "下一步",
|
||||
|
||||
@@ -563,6 +563,9 @@ export const dict = {
|
||||
"common.close": "關閉",
|
||||
"common.edit": "編輯",
|
||||
"common.loadMore": "載入更多",
|
||||
"common.changelog": "更新日誌",
|
||||
"common.noReleasesFound": "未找到版本",
|
||||
"changelog.tag.latest": "最新",
|
||||
|
||||
"common.key.esc": "ESC",
|
||||
"sidebar.menu.toggle": "切換選單",
|
||||
@@ -804,6 +807,12 @@ export const dict = {
|
||||
"workspace.reset.archived.one": "將封存 1 個工作階段。",
|
||||
"workspace.reset.archived.many": "將封存 {{count}} 個工作階段。",
|
||||
"workspace.reset.note": "這將把工作區重設為與預設分支一致。",
|
||||
|
||||
"settings.archive.title": "封存工作階段",
|
||||
"settings.archive.description": "恢復封存的工作階段以使其在側邊欄中可見。",
|
||||
"settings.archive.none": "沒有封存的工作階段。",
|
||||
"settings.archive.scope.all": "所有專案",
|
||||
"settings.archive.scope.current": "目前專案",
|
||||
"common.open": "打開",
|
||||
"dialog.releaseNotes.action.getStarted": "開始",
|
||||
"dialog.releaseNotes.action.next": "下一步",
|
||||
|
||||
@@ -9,11 +9,13 @@ import { DataProvider } from "@opencode-ai/ui/context"
|
||||
import { decode64 } from "@/utils/base64"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
|
||||
function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
|
||||
const params = useParams()
|
||||
const navigate = useNavigate()
|
||||
const sync = useSync()
|
||||
const platform = usePlatform()
|
||||
|
||||
return (
|
||||
<DataProvider
|
||||
@@ -21,6 +23,34 @@ function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
|
||||
directory={props.directory}
|
||||
onNavigateToSession={(sessionID: string) => navigate(`/${params.dir}/session/${sessionID}`)}
|
||||
onSessionHref={(sessionID: string) => `/${params.dir}/session/${sessionID}`}
|
||||
onOpenFilePath={async (input) => {
|
||||
const file = input.path.replace(/^[\\/]+/, "")
|
||||
const separator = props.directory.includes("\\") ? "\\" : "/"
|
||||
const path = props.directory.endsWith(separator) ? props.directory + file : props.directory + separator + file
|
||||
|
||||
if (platform.platform === "desktop" && platform.openPath) {
|
||||
await platform.openPath(path).catch((error) => {
|
||||
const description = error instanceof Error ? error.message : String(error)
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: "Open failed",
|
||||
description,
|
||||
})
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("opencode:open-file-path", {
|
||||
detail: input,
|
||||
}),
|
||||
)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("opencode:open-file-path", {
|
||||
detail: input,
|
||||
}),
|
||||
)
|
||||
}}
|
||||
>
|
||||
<LocalProvider>{props.children}</LocalProvider>
|
||||
</DataProvider>
|
||||
|
||||
@@ -44,6 +44,7 @@ import { playSound, soundSrc } from "@/utils/sound"
|
||||
import { createAim } from "@/utils/aim"
|
||||
import { setNavigate } from "@/utils/notification-click"
|
||||
import { Worktree as WorktreeState } from "@/utils/worktree"
|
||||
import { setSessionHandoff } from "@/pages/session/handoff"
|
||||
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
|
||||
@@ -67,7 +68,12 @@ import {
|
||||
sortedRootSessions,
|
||||
workspaceKey,
|
||||
} from "./layout/helpers"
|
||||
import { collectOpenProjectDeepLinks, deepLinkEvent, drainPendingDeepLinks } from "./layout/deep-links"
|
||||
import {
|
||||
collectNewSessionDeepLinks,
|
||||
collectOpenProjectDeepLinks,
|
||||
deepLinkEvent,
|
||||
drainPendingDeepLinks,
|
||||
} from "./layout/deep-links"
|
||||
import { createInlineEditorController } from "./layout/inline-editor"
|
||||
import {
|
||||
LocalWorkspace,
|
||||
@@ -1177,9 +1183,20 @@ export default function Layout(props: ParentProps) {
|
||||
|
||||
const handleDeepLinks = (urls: string[]) => {
|
||||
if (!server.isLocal()) return
|
||||
|
||||
for (const directory of collectOpenProjectDeepLinks(urls)) {
|
||||
openProject(directory)
|
||||
}
|
||||
|
||||
for (const link of collectNewSessionDeepLinks(urls)) {
|
||||
openProject(link.directory, false)
|
||||
const slug = base64Encode(link.directory)
|
||||
if (link.prompt) {
|
||||
setSessionHandoff(slug, { prompt: link.prompt })
|
||||
}
|
||||
const href = link.prompt ? `/${slug}/session?prompt=${encodeURIComponent(link.prompt)}` : `/${slug}/session`
|
||||
navigateWithSidebarReset(href)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
@@ -1919,45 +1936,34 @@ export default function Layout(props: ParentProps) {
|
||||
when={workspacesEnabled()}
|
||||
fallback={
|
||||
<>
|
||||
<div class="shrink-0 py-4 px-3">
|
||||
<TooltipKeybind
|
||||
title={language.t("command.session.new")}
|
||||
keybind={command.keybind("session.new")}
|
||||
placement="top"
|
||||
>
|
||||
<Button
|
||||
size="large"
|
||||
icon="plus-small"
|
||||
class="w-full"
|
||||
onClick={() => navigateWithSidebarReset(`/${base64Encode(p().worktree)}/session`)}
|
||||
>
|
||||
{language.t("command.session.new")}
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
<div class="flex-1 min-h-0">
|
||||
<LocalWorkspace
|
||||
ctx={workspaceSidebarCtx}
|
||||
project={p()}
|
||||
sortNow={sortNow}
|
||||
mobile={panelProps.mobile}
|
||||
header={
|
||||
<TooltipKeybind
|
||||
title={language.t("command.session.new")}
|
||||
keybind={command.keybind("session.new")}
|
||||
placement="top"
|
||||
>
|
||||
<Button
|
||||
size="large"
|
||||
icon="plus-small"
|
||||
class="w-full"
|
||||
onClick={() => navigateWithSidebarReset(`/${base64Encode(p().worktree)}/session`)}
|
||||
>
|
||||
{language.t("command.session.new")}
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<>
|
||||
<div class="shrink-0 py-4 px-3">
|
||||
<TooltipKeybind
|
||||
title={language.t("workspace.new")}
|
||||
keybind={command.keybind("workspace.new")}
|
||||
placement="top"
|
||||
>
|
||||
<Button size="large" icon="plus-small" class="w-full" onClick={() => createWorkspace(p())}>
|
||||
{language.t("workspace.new")}
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
<div class="relative flex-1 min-h-0">
|
||||
<DragDropProvider
|
||||
onDragStart={handleWorkspaceDragStart}
|
||||
@@ -1971,21 +1977,41 @@ export default function Layout(props: ParentProps) {
|
||||
ref={(el) => {
|
||||
if (!panelProps.mobile) scrollContainerRef = el
|
||||
}}
|
||||
class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar [overflow-anchor:none]"
|
||||
class="size-full flex flex-col overflow-y-auto no-scrollbar [overflow-anchor:none]"
|
||||
>
|
||||
<SortableProvider ids={workspaces()}>
|
||||
<For each={workspaces()}>
|
||||
{(directory) => (
|
||||
<SortableWorkspace
|
||||
ctx={workspaceSidebarCtx}
|
||||
directory={directory}
|
||||
project={p()}
|
||||
sortNow={sortNow}
|
||||
mobile={panelProps.mobile}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</SortableProvider>
|
||||
<div class="sticky top-0 z-20 pointer-events-none bg-[linear-gradient(to_bottom,var(--background-stronger)_calc(100%_-_24px),transparent)] pt-4 pb-6 px-3">
|
||||
<div class="pointer-events-auto">
|
||||
<TooltipKeybind
|
||||
title={language.t("workspace.new")}
|
||||
keybind={command.keybind("workspace.new")}
|
||||
placement="top"
|
||||
>
|
||||
<Button
|
||||
size="large"
|
||||
icon="plus-small"
|
||||
class="w-full"
|
||||
onClick={() => createWorkspace(p())}
|
||||
>
|
||||
{language.t("workspace.new")}
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 py-2">
|
||||
<SortableProvider ids={workspaces()}>
|
||||
<For each={workspaces()}>
|
||||
{(directory) => (
|
||||
<SortableWorkspace
|
||||
ctx={workspaceSidebarCtx}
|
||||
directory={directory}
|
||||
project={p()}
|
||||
sortNow={sortNow}
|
||||
mobile={panelProps.mobile}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</SortableProvider>
|
||||
</div>
|
||||
</div>
|
||||
<DragOverlay>
|
||||
<WorkspaceDragOverlay
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
export const deepLinkEvent = "opencode:deep-link"
|
||||
|
||||
export const parseDeepLink = (input: string) => {
|
||||
const parseUrl = (input: string) => {
|
||||
if (!input.startsWith("opencode://")) return
|
||||
if (typeof URL.canParse === "function" && !URL.canParse(input)) return
|
||||
const url = (() => {
|
||||
try {
|
||||
return new URL(input)
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
})()
|
||||
try {
|
||||
return new URL(input)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
export const parseDeepLink = (input: string) => {
|
||||
const url = parseUrl(input)
|
||||
if (!url) return
|
||||
if (url.hostname !== "open-project") return
|
||||
const directory = url.searchParams.get("directory")
|
||||
@@ -17,9 +19,23 @@ export const parseDeepLink = (input: string) => {
|
||||
return directory
|
||||
}
|
||||
|
||||
export const parseNewSessionDeepLink = (input: string) => {
|
||||
const url = parseUrl(input)
|
||||
if (!url) return
|
||||
if (url.hostname !== "new-session") return
|
||||
const directory = url.searchParams.get("directory")
|
||||
if (!directory) return
|
||||
const prompt = url.searchParams.get("prompt") || undefined
|
||||
if (!prompt) return { directory }
|
||||
return { directory, prompt }
|
||||
}
|
||||
|
||||
export const collectOpenProjectDeepLinks = (urls: string[]) =>
|
||||
urls.map(parseDeepLink).filter((directory): directory is string => !!directory)
|
||||
|
||||
export const collectNewSessionDeepLinks = (urls: string[]) =>
|
||||
urls.map(parseNewSessionDeepLink).filter((link): link is { directory: string; prompt?: string } => !!link)
|
||||
|
||||
type OpenCodeWindow = Window & {
|
||||
__OPENCODE__?: {
|
||||
deepLinks?: string[]
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { type Session } from "@opencode-ai/sdk/v2/client"
|
||||
import { collectOpenProjectDeepLinks, drainPendingDeepLinks, parseDeepLink } from "./deep-links"
|
||||
import {
|
||||
displayName,
|
||||
errorMessage,
|
||||
getDraggableId,
|
||||
hasProjectPermissions,
|
||||
latestRootSession,
|
||||
syncWorkspaceOrder,
|
||||
workspaceKey,
|
||||
} from "./helpers"
|
||||
collectNewSessionDeepLinks,
|
||||
collectOpenProjectDeepLinks,
|
||||
drainPendingDeepLinks,
|
||||
parseDeepLink,
|
||||
parseNewSessionDeepLink,
|
||||
} from "./deep-links"
|
||||
import { displayName, errorMessage, getDraggableId, syncWorkspaceOrder, workspaceKey } from "./helpers"
|
||||
import { type Session } from "@opencode-ai/sdk/v2/client"
|
||||
import { hasProjectPermissions, latestRootSession } from "./helpers"
|
||||
|
||||
const session = (input: Partial<Session> & Pick<Session, "id" | "directory">) =>
|
||||
({
|
||||
@@ -62,6 +61,28 @@ describe("layout deep links", () => {
|
||||
expect(result).toEqual(["/a", "/c"])
|
||||
})
|
||||
|
||||
test("parses new-session deep links with optional prompt", () => {
|
||||
expect(parseNewSessionDeepLink("opencode://new-session?directory=/tmp/demo")).toEqual({ directory: "/tmp/demo" })
|
||||
expect(parseNewSessionDeepLink("opencode://new-session?directory=/tmp/demo&prompt=hello%20world")).toEqual({
|
||||
directory: "/tmp/demo",
|
||||
prompt: "hello world",
|
||||
})
|
||||
})
|
||||
|
||||
test("ignores new-session deep links without directory", () => {
|
||||
expect(parseNewSessionDeepLink("opencode://new-session")).toBeUndefined()
|
||||
expect(parseNewSessionDeepLink("opencode://new-session?directory=")).toBeUndefined()
|
||||
})
|
||||
|
||||
test("collects only valid new-session deep links", () => {
|
||||
const result = collectNewSessionDeepLinks([
|
||||
"opencode://new-session?directory=/a",
|
||||
"opencode://open-project?directory=/b",
|
||||
"opencode://new-session?directory=/c&prompt=ship%20it",
|
||||
])
|
||||
expect(result).toEqual([{ directory: "/a" }, { directory: "/c", prompt: "ship it" }])
|
||||
})
|
||||
|
||||
test("drains global deep links once", () => {
|
||||
const target = {
|
||||
__OPENCODE__: {
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import { A, useNavigate, useParams } from "@solidjs/router"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useLayout, type LocalProject, getAvatarColors } from "@/context/layout"
|
||||
import { useNotification } from "@/context/notification"
|
||||
import { usePermission } from "@/context/permission"
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
import type { Message, Session, TextPart, UserMessage } from "@opencode-ai/sdk/v2/client"
|
||||
import { Avatar } from "@opencode-ai/ui/avatar"
|
||||
import { HoverCard } from "@opencode-ai/ui/hover-card"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
@@ -12,12 +6,18 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { MessageNav } from "@opencode-ai/ui/message-nav"
|
||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { type Message, type Session, type TextPart, type UserMessage } from "@opencode-ai/sdk/v2/client"
|
||||
import { For, Match, Show, Switch, createMemo, onCleanup, type Accessor, type JSX } from "solid-js"
|
||||
import { A, useNavigate, useParams } from "@solidjs/router"
|
||||
import { type Accessor, createMemo, For, type JSX, Match, onCleanup, Show, Switch } from "solid-js"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout"
|
||||
import { useNotification } from "@/context/notification"
|
||||
import { usePermission } from "@/context/permission"
|
||||
import { agentColor } from "@/utils/agent"
|
||||
import { hasProjectPermissions } from "./helpers"
|
||||
import { sessionPermissionRequest } from "../session/composer/session-request-tree"
|
||||
import { hasProjectPermissions } from "./helpers"
|
||||
|
||||
const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
|
||||
|
||||
@@ -231,7 +231,9 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
||||
const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed())
|
||||
const isActive = createMemo(() => props.session.id === params.id)
|
||||
|
||||
const hoverPrefetch = { current: undefined as ReturnType<typeof setTimeout> | undefined }
|
||||
const hoverPrefetch = {
|
||||
current: undefined as ReturnType<typeof setTimeout> | undefined,
|
||||
}
|
||||
const cancelHoverPrefetch = () => {
|
||||
if (hoverPrefetch.current === undefined) return
|
||||
clearTimeout(hoverPrefetch.current)
|
||||
@@ -275,7 +277,7 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
||||
return (
|
||||
<div
|
||||
data-session-id={props.session.id}
|
||||
class="group/session relative w-full rounded-md cursor-default transition-colors pl-2 pr-3
|
||||
class="group/session relative w-full rounded-md cursor-default transition-colors pl-2 pr-3 scroll-mt-24
|
||||
hover:bg-surface-raised-base-hover [&:has(:focus-visible)]:bg-surface-raised-base-hover has-[[data-expanded]]:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active"
|
||||
>
|
||||
<Show
|
||||
@@ -300,17 +302,15 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
||||
setHoverSession={props.setHoverSession}
|
||||
messageLabel={messageLabel}
|
||||
onMessageSelect={(message) => {
|
||||
if (!isActive()) {
|
||||
if (!isActive())
|
||||
layout.pendingMessage.set(`${base64Encode(props.session.directory)}/${props.session.id}`, message.id)
|
||||
navigate(`${props.slug}/session/${props.session.id}`)
|
||||
return
|
||||
}
|
||||
window.history.replaceState(null, "", `#message-${message.id}`)
|
||||
window.dispatchEvent(new HashChangeEvent("hashchange"))
|
||||
|
||||
navigate(`${props.slug}/session/${props.session.id}#message-${message.id}`)
|
||||
}}
|
||||
trigger={item}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<div
|
||||
class={`absolute ${props.dense ? "top-0.5 right-0.5" : "top-1 right-1"} flex items-center gap-0.5 transition-opacity`}
|
||||
classList={{
|
||||
|
||||
@@ -467,6 +467,7 @@ export const LocalWorkspace = (props: {
|
||||
project: LocalProject
|
||||
sortNow: Accessor<number>
|
||||
mobile?: boolean
|
||||
header?: JSX.Element
|
||||
}): JSX.Element => {
|
||||
const globalSync = useGlobalSync()
|
||||
const language = useLanguage()
|
||||
@@ -488,9 +489,14 @@ export const LocalWorkspace = (props: {
|
||||
return (
|
||||
<div
|
||||
ref={(el) => props.ctx.setScrollContainerRef(el, props.mobile)}
|
||||
class="size-full flex flex-col py-2 overflow-y-auto no-scrollbar [overflow-anchor:none]"
|
||||
class="size-full flex flex-col overflow-y-auto no-scrollbar [overflow-anchor:none]"
|
||||
>
|
||||
<nav class="flex flex-col gap-1 px-2">
|
||||
<Show when={props.header}>
|
||||
<div class="sticky top-0 z-20 pointer-events-none bg-[linear-gradient(to_bottom,var(--background-stronger)_calc(100%_-_24px),transparent)] pt-4 pb-6 px-3">
|
||||
<div class="pointer-events-auto">{props.header}</div>
|
||||
</div>
|
||||
</Show>
|
||||
<nav class="flex flex-col gap-1 px-2 pb-2" classList={{ "pt-2": !props.header }}>
|
||||
<Show when={loading()}>
|
||||
<SessionSkeleton />
|
||||
</Show>
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
import {
|
||||
onCleanup,
|
||||
Show,
|
||||
Match,
|
||||
Switch,
|
||||
createMemo,
|
||||
createEffect,
|
||||
createComputed,
|
||||
on,
|
||||
onMount,
|
||||
untrack,
|
||||
createSignal,
|
||||
} from "solid-js"
|
||||
import type { UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { onCleanup, Show, Match, Switch, createMemo, createEffect, on, onMount, untrack } from "solid-js"
|
||||
import { createMediaQuery } from "@solid-primitives/media"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
import { useLocal } from "@/context/local"
|
||||
@@ -20,32 +10,37 @@ import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||
import { Select } from "@opencode-ai/ui/select"
|
||||
import { createAutoScroll } from "@opencode-ai/ui/hooks"
|
||||
import { Mark } from "@opencode-ai/ui/logo"
|
||||
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { checksum, base64Encode } from "@opencode-ai/util/encode"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useNavigate, useParams } from "@solidjs/router"
|
||||
import { UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { usePrompt } from "@/context/prompt"
|
||||
import { base64Encode, checksum } from "@opencode-ai/util/encode"
|
||||
import { useNavigate, useParams, useSearchParams } from "@solidjs/router"
|
||||
import { NewSessionView, SessionHeader } from "@/components/session"
|
||||
import { useComments } from "@/context/comments"
|
||||
import { SessionHeader, NewSessionView } from "@/components/session"
|
||||
import { same } from "@/utils/same"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { usePrompt } from "@/context/prompt"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { isEditableTarget } from "@/utils/dom"
|
||||
import { createOpenReviewFile } from "@/pages/session/helpers"
|
||||
import { createScrollSpy } from "@/pages/session/scroll-spy"
|
||||
import { SessionReviewTab, type DiffStyle, type SessionReviewTabProps } from "@/pages/session/review-tab"
|
||||
import { TerminalPanel } from "@/pages/session/terminal-panel"
|
||||
import { MessageTimeline } from "@/pages/session/message-timeline"
|
||||
import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab"
|
||||
import { createScrollSpy } from "@/pages/session/scroll-spy"
|
||||
import { AnimationDebugPanel } from "@opencode-ai/ui/animation-debug-panel"
|
||||
import { useSessionCommands } from "@/pages/session/use-session-commands"
|
||||
import { SessionComposerRegion, createSessionComposerState } from "@/pages/session/composer"
|
||||
import { SessionMobileTabs } from "@/pages/session/session-mobile-tabs"
|
||||
import { SessionSidePanel } from "@/pages/session/session-side-panel"
|
||||
import { TerminalPanel } from "@/pages/session/terminal-panel"
|
||||
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
|
||||
|
||||
const emptyUserMessages: UserMessage[] = []
|
||||
|
||||
const same = <T,>(a: readonly T[] | undefined, b: readonly T[] | undefined) => {
|
||||
if (a === b) return true
|
||||
if (!a || !b) return false
|
||||
if (a.length !== b.length) return false
|
||||
return a.every((x, i) => x === b[i])
|
||||
}
|
||||
|
||||
type SessionHistoryWindowInput = {
|
||||
sessionID: () => string | undefined
|
||||
messagesReady: () => boolean
|
||||
@@ -120,13 +115,13 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
||||
return
|
||||
}
|
||||
const beforeTop = el.scrollTop
|
||||
const beforeHeight = el.scrollHeight
|
||||
fn()
|
||||
requestAnimationFrame(() => {
|
||||
const delta = el.scrollHeight - beforeHeight
|
||||
if (!delta) return
|
||||
el.scrollTop = beforeTop + delta
|
||||
})
|
||||
// SolidJS updates the DOM synchronously. Force reflow so the browser
|
||||
// processes the new layout, then restore scrollTop before paint.
|
||||
// With column-reverse + overflow-anchor:none the same scrollTop value
|
||||
// keeps the same distance from the bottom — no delta math needed.
|
||||
void el.scrollHeight
|
||||
el.scrollTop = beforeTop
|
||||
}
|
||||
|
||||
const backfillTurns = () => {
|
||||
@@ -209,7 +204,8 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
||||
if (!input.userScrolled()) return
|
||||
const el = input.scroller()
|
||||
if (!el) return
|
||||
if (el.scrollTop >= turnScrollThreshold) return
|
||||
// With column-reverse, distance from top = scrollHeight - clientHeight + scrollTop
|
||||
if (el.scrollHeight - el.clientHeight + el.scrollTop >= turnScrollThreshold) return
|
||||
|
||||
const start = turnStart()
|
||||
if (start > 0) {
|
||||
@@ -265,6 +261,19 @@ export default function Page() {
|
||||
const sdk = useSDK()
|
||||
const prompt = usePrompt()
|
||||
const comments = useComments()
|
||||
const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>()
|
||||
|
||||
createEffect(() => {
|
||||
if (!untrack(() => prompt.ready())) return
|
||||
prompt.ready()
|
||||
untrack(() => {
|
||||
if (params.id || !prompt.ready()) return
|
||||
const text = searchParams.prompt
|
||||
if (!text) return
|
||||
prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length)
|
||||
setSearchParams({ ...searchParams, prompt: undefined })
|
||||
})
|
||||
})
|
||||
|
||||
const [ui, setUi] = createStore({
|
||||
pendingMessage: undefined as string | undefined,
|
||||
@@ -274,7 +283,6 @@ export default function Page() {
|
||||
bottom: true,
|
||||
},
|
||||
})
|
||||
|
||||
const composer = createSessionComposerState()
|
||||
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
@@ -405,7 +413,10 @@ export default function Page() {
|
||||
() => {
|
||||
const msg = lastUserMessage()
|
||||
if (!msg) return
|
||||
if (msg.agent) local.agent.set(msg.agent)
|
||||
if (msg.agent) {
|
||||
local.agent.set(msg.agent)
|
||||
if (local.agent.current()?.model) return
|
||||
}
|
||||
if (msg.model) local.model.set(msg.model)
|
||||
},
|
||||
),
|
||||
@@ -416,20 +427,8 @@ export default function Page() {
|
||||
mobileTab: "session" as "session" | "changes",
|
||||
changes: "session" as "session" | "turn",
|
||||
newSessionWorktree: "main",
|
||||
deferRender: false,
|
||||
})
|
||||
|
||||
createComputed((prev) => {
|
||||
const key = sessionKey()
|
||||
if (key !== prev) {
|
||||
setStore("deferRender", true)
|
||||
requestAnimationFrame(() => {
|
||||
setTimeout(() => setStore("deferRender", false), 0)
|
||||
})
|
||||
}
|
||||
return key
|
||||
}, sessionKey())
|
||||
|
||||
const turnDiffs = createMemo(() => lastUserMessage()?.summary?.diffs ?? [])
|
||||
const reviewDiffs = createMemo(() => (store.changes === "session" ? diffs() : turnDiffs()))
|
||||
|
||||
@@ -440,11 +439,6 @@ export default function Page() {
|
||||
return "main"
|
||||
})
|
||||
|
||||
const activeMessage = createMemo(() => {
|
||||
if (!store.messageId) return lastUserMessage()
|
||||
const found = visibleUserMessages()?.find((m) => m.id === store.messageId)
|
||||
return found ?? lastUserMessage()
|
||||
})
|
||||
const setActiveMessage = (message: UserMessage | undefined) => {
|
||||
setStore("messageId", message?.id)
|
||||
}
|
||||
@@ -606,11 +600,6 @@ export default function Page() {
|
||||
saveLabel: language.t("common.save"),
|
||||
}))
|
||||
|
||||
const isEditableTarget = (target: EventTarget | null | undefined) => {
|
||||
if (!(target instanceof HTMLElement)) return false
|
||||
return /^(INPUT|TEXTAREA|SELECT|BUTTON)$/.test(target.tagName) || target.isContentEditable
|
||||
}
|
||||
|
||||
const deepActiveElement = () => {
|
||||
let current: Element | null = document.activeElement
|
||||
while (current instanceof HTMLElement && current.shadowRoot?.activeElement) {
|
||||
@@ -679,7 +668,11 @@ export default function Page() {
|
||||
on(
|
||||
sessionKey,
|
||||
() => {
|
||||
setTree({ reviewScroll: undefined, pendingDiff: undefined, activeDiff: undefined })
|
||||
setTree({
|
||||
reviewScroll: undefined,
|
||||
pendingDiff: undefined,
|
||||
activeDiff: undefined,
|
||||
})
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
@@ -700,11 +693,26 @@ export default function Page() {
|
||||
|
||||
const openReviewFile = createOpenReviewFile({
|
||||
showAllFiles,
|
||||
openReviewPanel,
|
||||
tabForPath: file.tab,
|
||||
openTab: tabs().open,
|
||||
setActive: tabs().setActive,
|
||||
setSelectedLines: file.setSelectedLines,
|
||||
loadFile: file.load,
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
const open = (event: Event) => {
|
||||
const detail = (event as CustomEvent<{ path?: string; line?: number }>).detail
|
||||
const path = detail?.path
|
||||
if (!path) return
|
||||
openReviewFile(path, detail?.line)
|
||||
}
|
||||
|
||||
window.addEventListener("opencode:open-file-path", open)
|
||||
onCleanup(() => window.removeEventListener("opencode:open-file-path", open))
|
||||
})
|
||||
|
||||
const changesOptions = ["session", "turn"] as const
|
||||
const changesOptionsList = [...changesOptions]
|
||||
|
||||
@@ -736,12 +744,35 @@ export default function Page() {
|
||||
loadingClass: string
|
||||
emptyClass: string
|
||||
}) => (
|
||||
<Show when={!store.deferRender}>
|
||||
<Switch>
|
||||
<Match when={store.changes === "turn" && !!params.id}>
|
||||
<Switch>
|
||||
<Match when={store.changes === "turn" && !!params.id}>
|
||||
<SessionReviewTab
|
||||
title={changesTitle()}
|
||||
empty={emptyTurn()}
|
||||
diffs={reviewDiffs}
|
||||
view={view}
|
||||
diffStyle={input.diffStyle}
|
||||
onDiffStyleChange={input.onDiffStyleChange}
|
||||
onScrollRef={(el) => setTree("reviewScroll", el)}
|
||||
focusedFile={tree.activeDiff}
|
||||
onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
|
||||
onLineCommentUpdate={updateCommentInContext}
|
||||
onLineCommentDelete={removeCommentFromContext}
|
||||
lineCommentActions={reviewCommentActions()}
|
||||
comments={comments.all()}
|
||||
focusedComment={comments.focus()}
|
||||
onFocusedCommentChange={comments.setFocus}
|
||||
onViewFile={openReviewFile}
|
||||
classes={input.classes}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={hasReview()}>
|
||||
<Show
|
||||
when={diffsReady()}
|
||||
fallback={<div class={input.loadingClass}>{language.t("session.review.loadingChanges")}</div>}
|
||||
>
|
||||
<SessionReviewTab
|
||||
title={changesTitle()}
|
||||
empty={emptyTurn()}
|
||||
diffs={reviewDiffs}
|
||||
view={view}
|
||||
diffStyle={input.diffStyle}
|
||||
@@ -758,64 +789,39 @@ export default function Page() {
|
||||
onViewFile={openReviewFile}
|
||||
classes={input.classes}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={hasReview()}>
|
||||
<Show
|
||||
when={diffsReady()}
|
||||
fallback={<div class={input.loadingClass}>{language.t("session.review.loadingChanges")}</div>}
|
||||
>
|
||||
<SessionReviewTab
|
||||
title={changesTitle()}
|
||||
diffs={reviewDiffs}
|
||||
view={view}
|
||||
diffStyle={input.diffStyle}
|
||||
onDiffStyleChange={input.onDiffStyleChange}
|
||||
onScrollRef={(el) => setTree("reviewScroll", el)}
|
||||
focusedFile={tree.activeDiff}
|
||||
onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
|
||||
onLineCommentUpdate={updateCommentInContext}
|
||||
onLineCommentDelete={removeCommentFromContext}
|
||||
lineCommentActions={reviewCommentActions()}
|
||||
comments={comments.all()}
|
||||
focusedComment={comments.focus()}
|
||||
onFocusedCommentChange={comments.setFocus}
|
||||
onViewFile={openReviewFile}
|
||||
classes={input.classes}
|
||||
/>
|
||||
</Show>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<SessionReviewTab
|
||||
title={changesTitle()}
|
||||
empty={
|
||||
store.changes === "turn" ? (
|
||||
emptyTurn()
|
||||
) : (
|
||||
<div class={input.emptyClass}>
|
||||
<Mark class="w-14 opacity-10" />
|
||||
<div class="text-14-regular text-text-weak max-w-56">{language.t(reviewEmptyKey())}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
diffs={reviewDiffs}
|
||||
view={view}
|
||||
diffStyle={input.diffStyle}
|
||||
onDiffStyleChange={input.onDiffStyleChange}
|
||||
onScrollRef={(el) => setTree("reviewScroll", el)}
|
||||
focusedFile={tree.activeDiff}
|
||||
onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
|
||||
onLineCommentUpdate={updateCommentInContext}
|
||||
onLineCommentDelete={removeCommentFromContext}
|
||||
lineCommentActions={reviewCommentActions()}
|
||||
comments={comments.all()}
|
||||
focusedComment={comments.focus()}
|
||||
onFocusedCommentChange={comments.setFocus}
|
||||
onViewFile={openReviewFile}
|
||||
classes={input.classes}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Show>
|
||||
</Show>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<SessionReviewTab
|
||||
title={changesTitle()}
|
||||
empty={
|
||||
store.changes === "turn" ? (
|
||||
emptyTurn()
|
||||
) : (
|
||||
<div class={input.emptyClass}>
|
||||
<Mark class="w-14 opacity-10" />
|
||||
<div class="text-14-regular text-text-weak max-w-56">{language.t(reviewEmptyKey())}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
diffs={reviewDiffs}
|
||||
view={view}
|
||||
diffStyle={input.diffStyle}
|
||||
onDiffStyleChange={input.onDiffStyleChange}
|
||||
onScrollRef={(el) => setTree("reviewScroll", el)}
|
||||
focusedFile={tree.activeDiff}
|
||||
onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
|
||||
onLineCommentUpdate={updateCommentInContext}
|
||||
onLineCommentDelete={removeCommentFromContext}
|
||||
lineCommentActions={reviewCommentActions()}
|
||||
comments={comments.all()}
|
||||
focusedComment={comments.focus()}
|
||||
onFocusedCommentChange={comments.setFocus}
|
||||
onViewFile={openReviewFile}
|
||||
classes={input.classes}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
|
||||
const reviewPanel = () => (
|
||||
@@ -1026,7 +1032,10 @@ export default function Page() {
|
||||
const updateScrollState = (el: HTMLDivElement) => {
|
||||
const max = el.scrollHeight - el.clientHeight
|
||||
const overflow = max > 1
|
||||
const bottom = !overflow || el.scrollTop >= max - 2
|
||||
// If auto-scroll is tracking the bottom, always report bottom: true
|
||||
// to prevent the scroll-down arrow from flashing during height animations
|
||||
// With column-reverse, scrollTop=0 is at the bottom
|
||||
const bottom = !overflow || Math.abs(el.scrollTop) <= 2 || !autoScroll.userScrolled()
|
||||
|
||||
if (ui.scroll.overflow === overflow && ui.scroll.bottom === bottom) return
|
||||
setUi("scroll", { overflow, bottom })
|
||||
@@ -1049,7 +1058,7 @@ export default function Page() {
|
||||
|
||||
const resumeScroll = () => {
|
||||
setStore("messageId", undefined)
|
||||
autoScroll.forceScrollToBottom()
|
||||
autoScroll.smoothScrollToBottom()
|
||||
clearMessageHash()
|
||||
|
||||
const el = scroller
|
||||
@@ -1117,9 +1126,8 @@ export default function Page() {
|
||||
|
||||
const el = scroller
|
||||
const delta = next - dockHeight
|
||||
const stick = el
|
||||
? !autoScroll.userScrolled() || el.scrollHeight - el.clientHeight - el.scrollTop < 10 + Math.max(0, delta)
|
||||
: false
|
||||
// With column-reverse, near bottom = scrollTop near 0
|
||||
const stick = el ? Math.abs(el.scrollTop) < 10 + Math.max(0, delta) : false
|
||||
|
||||
dockHeight = next
|
||||
|
||||
@@ -1160,6 +1168,7 @@ export default function Page() {
|
||||
|
||||
return (
|
||||
<div class="relative bg-background-base size-full overflow-hidden flex flex-col">
|
||||
{import.meta.env.DEV && <AnimationDebugPanel />}
|
||||
<SessionHeader />
|
||||
<div class="flex-1 min-h-0 flex flex-col md:flex-row">
|
||||
<SessionMobileTabs
|
||||
@@ -1185,50 +1194,49 @@ export default function Page() {
|
||||
<div class="flex-1 min-h-0 overflow-hidden">
|
||||
<Switch>
|
||||
<Match when={params.id}>
|
||||
<Show when={activeMessage()}>
|
||||
<MessageTimeline
|
||||
mobileChanges={mobileChanges()}
|
||||
mobileFallback={reviewContent({
|
||||
diffStyle: "unified",
|
||||
classes: {
|
||||
root: "pb-8",
|
||||
header: "px-4",
|
||||
container: "px-4",
|
||||
},
|
||||
loadingClass: "px-4 py-4 text-text-weak",
|
||||
emptyClass: "h-full pb-30 flex flex-col items-center justify-center text-center gap-6",
|
||||
})}
|
||||
scroll={ui.scroll}
|
||||
onResumeScroll={resumeScroll}
|
||||
setScrollRef={setScrollRef}
|
||||
onScheduleScrollState={scheduleScrollState}
|
||||
onAutoScrollHandleScroll={autoScroll.handleScroll}
|
||||
onMarkScrollGesture={markScrollGesture}
|
||||
hasScrollGesture={hasScrollGesture}
|
||||
isDesktop={isDesktop()}
|
||||
onScrollSpyScroll={scrollSpy.onScroll}
|
||||
onTurnBackfillScroll={historyWindow.onScrollerScroll}
|
||||
onAutoScrollInteraction={autoScroll.handleInteraction}
|
||||
centered={centered()}
|
||||
setContentRef={(el) => {
|
||||
content = el
|
||||
autoScroll.contentRef(el)
|
||||
<MessageTimeline
|
||||
mobileChanges={mobileChanges()}
|
||||
mobileFallback={reviewContent({
|
||||
diffStyle: "unified",
|
||||
classes: {
|
||||
root: "pb-8",
|
||||
header: "px-4",
|
||||
container: "px-4",
|
||||
},
|
||||
loadingClass: "px-4 py-4 text-text-weak",
|
||||
emptyClass: "h-full pb-30 flex flex-col items-center justify-center text-center gap-6",
|
||||
})}
|
||||
scroll={ui.scroll}
|
||||
onResumeScroll={resumeScroll}
|
||||
setScrollRef={setScrollRef}
|
||||
onScheduleScrollState={scheduleScrollState}
|
||||
onAutoScrollHandleScroll={autoScroll.handleScroll}
|
||||
onMarkScrollGesture={markScrollGesture}
|
||||
hasScrollGesture={hasScrollGesture}
|
||||
isDesktop={isDesktop()}
|
||||
onScrollSpyScroll={scrollSpy.onScroll}
|
||||
onTurnBackfillScroll={historyWindow.onScrollerScroll}
|
||||
onAutoScrollInteraction={autoScroll.handleInteraction}
|
||||
onPreserveScrollAnchor={autoScroll.preserve}
|
||||
centered={centered()}
|
||||
setContentRef={(el) => {
|
||||
content = el
|
||||
autoScroll.contentRef(el)
|
||||
|
||||
const root = scroller
|
||||
if (root) scheduleScrollState(root)
|
||||
}}
|
||||
turnStart={historyWindow.turnStart()}
|
||||
historyMore={historyMore()}
|
||||
historyLoading={historyLoading()}
|
||||
onLoadEarlier={() => {
|
||||
void historyWindow.loadAndReveal()
|
||||
}}
|
||||
renderedUserMessages={historyWindow.renderedUserMessages()}
|
||||
anchor={anchor}
|
||||
onRegisterMessage={scrollSpy.register}
|
||||
onUnregisterMessage={scrollSpy.unregister}
|
||||
/>
|
||||
</Show>
|
||||
const root = scroller
|
||||
if (root) scheduleScrollState(root)
|
||||
}}
|
||||
turnStart={historyWindow.turnStart()}
|
||||
historyMore={historyMore()}
|
||||
historyLoading={historyLoading()}
|
||||
onLoadEarlier={() => {
|
||||
void historyWindow.loadAndReveal()
|
||||
}}
|
||||
renderedUserMessages={historyWindow.renderedUserMessages()}
|
||||
anchor={anchor}
|
||||
onRegisterMessage={scrollSpy.register}
|
||||
onUnregisterMessage={scrollSpy.unregister}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<NewSessionView
|
||||
@@ -1254,7 +1262,6 @@ export default function Page() {
|
||||
|
||||
<SessionComposerRegion
|
||||
state={composer}
|
||||
ready={!store.deferRender && messagesReady()}
|
||||
centered={centered()}
|
||||
inputRef={(el) => {
|
||||
inputRef = el
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { Show, createMemo, createSignal, createEffect } from "solid-js"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
||||
import { useElementHeight } from "@opencode-ai/ui/hooks"
|
||||
import { PromptInput } from "@/components/prompt-input"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePrompt } from "@/context/prompt"
|
||||
@@ -9,11 +9,12 @@ import { getSessionHandoff, setSessionHandoff } from "@/pages/session/handoff"
|
||||
import { SessionPermissionDock } from "@/pages/session/composer/session-permission-dock"
|
||||
import { SessionQuestionDock } from "@/pages/session/composer/session-question-dock"
|
||||
import type { SessionComposerState } from "@/pages/session/composer/session-composer-state"
|
||||
import { SessionTodoDock } from "@/pages/session/composer/session-todo-dock"
|
||||
import { SessionTodoDock, COLLAPSED_HEIGHT } from "@/pages/session/composer/session-todo-dock"
|
||||
|
||||
const DOCK_SPRING = { visualDuration: 0.3, bounce: 0 }
|
||||
|
||||
export function SessionComposerRegion(props: {
|
||||
state: SessionComposerState
|
||||
ready: boolean
|
||||
centered: boolean
|
||||
inputRef: (el: HTMLDivElement) => void
|
||||
newSessionWorktree: string
|
||||
@@ -21,23 +22,6 @@ export function SessionComposerRegion(props: {
|
||||
onSubmit: () => void
|
||||
onResponseSubmit: () => void
|
||||
setPromptDockRef: (el: HTMLDivElement) => void
|
||||
visualDuration?: number
|
||||
bounce?: number
|
||||
dockOpenVisualDuration?: number
|
||||
dockOpenBounce?: number
|
||||
dockCloseVisualDuration?: number
|
||||
dockCloseBounce?: number
|
||||
drawerExpandVisualDuration?: number
|
||||
drawerExpandBounce?: number
|
||||
drawerCollapseVisualDuration?: number
|
||||
drawerCollapseBounce?: number
|
||||
subtitleDuration?: number
|
||||
subtitleTravel?: number
|
||||
subtitleEdge?: number
|
||||
countDuration?: number
|
||||
countMask?: number
|
||||
countMaskHeight?: number
|
||||
countWidthDuration?: number
|
||||
}) {
|
||||
const params = useParams()
|
||||
const prompt = usePrompt()
|
||||
@@ -63,73 +47,15 @@ export function SessionComposerRegion(props: {
|
||||
setSessionHandoff(sessionKey(), { prompt: previewPrompt() })
|
||||
})
|
||||
|
||||
const [gate, setGate] = createStore({
|
||||
ready: false,
|
||||
})
|
||||
let timer: number | undefined
|
||||
let frame: number | undefined
|
||||
|
||||
const clear = () => {
|
||||
if (timer !== undefined) {
|
||||
window.clearTimeout(timer)
|
||||
timer = undefined
|
||||
}
|
||||
if (frame !== undefined) {
|
||||
cancelAnimationFrame(frame)
|
||||
frame = undefined
|
||||
}
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
sessionKey()
|
||||
const ready = props.ready
|
||||
const delay = 140
|
||||
|
||||
clear()
|
||||
setGate("ready", false)
|
||||
if (!ready) return
|
||||
|
||||
frame = requestAnimationFrame(() => {
|
||||
frame = undefined
|
||||
timer = window.setTimeout(() => {
|
||||
setGate("ready", true)
|
||||
timer = undefined
|
||||
}, delay)
|
||||
})
|
||||
})
|
||||
|
||||
onCleanup(clear)
|
||||
|
||||
const open = createMemo(() => gate.ready && props.state.dock() && !props.state.closing())
|
||||
const config = createMemo(() =>
|
||||
open()
|
||||
? {
|
||||
visualDuration: props.dockOpenVisualDuration ?? props.visualDuration ?? 0.3,
|
||||
bounce: props.dockOpenBounce ?? props.bounce ?? 0,
|
||||
}
|
||||
: {
|
||||
visualDuration: props.dockCloseVisualDuration ?? props.visualDuration ?? 0.3,
|
||||
bounce: props.dockCloseBounce ?? props.bounce ?? 0,
|
||||
},
|
||||
const open = createMemo(() => props.state.dock() && !props.state.closing())
|
||||
const progress = useSpring(
|
||||
() => (open() ? 1 : 0),
|
||||
DOCK_SPRING,
|
||||
)
|
||||
const progress = useSpring(() => (open() ? 1 : 0), config)
|
||||
const value = createMemo(() => Math.max(0, Math.min(1, progress())))
|
||||
const [height, setHeight] = createSignal(320)
|
||||
const dock = createMemo(() => (gate.ready && props.state.dock()) || value() > 0.001)
|
||||
const full = createMemo(() => Math.max(78, height()))
|
||||
const dock = createMemo(() => props.state.dock() || progress() > 0.001)
|
||||
const [contentRef, setContentRef] = createSignal<HTMLDivElement>()
|
||||
|
||||
createEffect(() => {
|
||||
const el = contentRef()
|
||||
if (!el) return
|
||||
const update = () => {
|
||||
setHeight(el.getBoundingClientRect().height)
|
||||
}
|
||||
update()
|
||||
const observer = new ResizeObserver(update)
|
||||
observer.observe(el)
|
||||
onCleanup(() => observer.disconnect())
|
||||
})
|
||||
const height = useElementHeight(contentRef, 320)
|
||||
const full = createMemo(() => Math.max(COLLAPSED_HEIGHT, height()))
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -179,10 +105,10 @@ export function SessionComposerRegion(props: {
|
||||
<div
|
||||
classList={{
|
||||
"overflow-hidden": true,
|
||||
"pointer-events-none": value() < 0.98,
|
||||
"pointer-events-none": progress() < 0.98,
|
||||
}}
|
||||
style={{
|
||||
"max-height": `${full() * value()}px`,
|
||||
"max-height": `${full() * progress()}px`,
|
||||
}}
|
||||
>
|
||||
<div ref={setContentRef}>
|
||||
@@ -191,20 +117,7 @@ export function SessionComposerRegion(props: {
|
||||
title={language.t("session.todo.title")}
|
||||
collapseLabel={language.t("session.todo.collapse")}
|
||||
expandLabel={language.t("session.todo.expand")}
|
||||
dockProgress={value()}
|
||||
visualDuration={props.visualDuration}
|
||||
bounce={props.bounce}
|
||||
expandVisualDuration={props.drawerExpandVisualDuration}
|
||||
expandBounce={props.drawerExpandBounce}
|
||||
collapseVisualDuration={props.drawerCollapseVisualDuration}
|
||||
collapseBounce={props.drawerCollapseBounce}
|
||||
subtitleDuration={props.subtitleDuration}
|
||||
subtitleTravel={props.subtitleTravel}
|
||||
subtitleEdge={props.subtitleEdge}
|
||||
countDuration={props.countDuration}
|
||||
countMask={props.countMask}
|
||||
countMaskHeight={props.countMaskHeight}
|
||||
countWidthDuration={props.countWidthDuration}
|
||||
dockProgress={progress()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -214,7 +127,7 @@ export function SessionComposerRegion(props: {
|
||||
"relative z-10": true,
|
||||
}}
|
||||
style={{
|
||||
"margin-top": `${-36 * value()}px`,
|
||||
"margin-top": `${-36 * progress()}px`,
|
||||
}}
|
||||
>
|
||||
<PromptInput
|
||||
|
||||
@@ -29,7 +29,11 @@ export function createSessionComposerBlocked() {
|
||||
})
|
||||
}
|
||||
|
||||
export function createSessionComposerState(options?: { closeMs?: number | (() => number) }) {
|
||||
export function createSessionComposerState(
|
||||
options?: {
|
||||
closeMs?: number | (() => number)
|
||||
},
|
||||
) {
|
||||
const params = useParams()
|
||||
const sdk = useSDK()
|
||||
const sync = useSync()
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createStore } from "solid-js/store"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { DockPrompt } from "@opencode-ai/ui/dock-prompt"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
|
||||
import { useLanguage } from "@/context/language"
|
||||
@@ -22,6 +23,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
||||
customOn: [] as boolean[],
|
||||
editing: false,
|
||||
sending: false,
|
||||
collapsed: false,
|
||||
})
|
||||
|
||||
let root: HTMLDivElement | undefined
|
||||
@@ -31,6 +33,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
||||
const input = createMemo(() => store.custom[store.tab] ?? "")
|
||||
const on = createMemo(() => store.customOn[store.tab] === true)
|
||||
const multi = createMemo(() => question()?.multiple === true)
|
||||
const picked = createMemo(() => store.answers[store.tab]?.length ?? 0)
|
||||
|
||||
const summary = createMemo(() => {
|
||||
const n = Math.min(store.tab + 1, total())
|
||||
@@ -39,6 +42,8 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
||||
|
||||
const last = createMemo(() => store.tab >= total() - 1)
|
||||
|
||||
const fold = () => setStore("collapsed", (value) => !value)
|
||||
|
||||
const customUpdate = (value: string, selected: boolean = on()) => {
|
||||
const prev = input().trim()
|
||||
const next = value.trim()
|
||||
@@ -239,9 +244,21 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
||||
kind="question"
|
||||
ref={(el) => (root = el)}
|
||||
header={
|
||||
<>
|
||||
<div
|
||||
data-action="session-question-toggle"
|
||||
class="flex flex-1 min-w-0 items-center gap-2 cursor-default select-none"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
style={{ margin: "0 -10px", padding: "0 0 0 10px" }}
|
||||
onClick={fold}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return
|
||||
event.preventDefault()
|
||||
fold()
|
||||
}}
|
||||
>
|
||||
<div data-slot="question-header-title">{summary()}</div>
|
||||
<div data-slot="question-progress">
|
||||
<div data-slot="question-progress" class="ml-auto mr-1">
|
||||
<For each={questions()}>
|
||||
{(_, i) => (
|
||||
<button
|
||||
@@ -253,13 +270,38 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
||||
(store.customOn[i()] === true && (store.custom[i()] ?? "").trim().length > 0)
|
||||
}
|
||||
disabled={store.sending}
|
||||
onClick={() => jump(i())}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
jump(i())
|
||||
}}
|
||||
aria-label={`${language.t("ui.tool.questions")} ${i() + 1}`}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</>
|
||||
<div>
|
||||
<IconButton
|
||||
data-action="session-question-toggle-button"
|
||||
icon="chevron-down"
|
||||
size="normal"
|
||||
variant="ghost"
|
||||
classList={{ "rotate-180": store.collapsed }}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
fold()
|
||||
}}
|
||||
aria-label={store.collapsed ? language.t("session.todo.expand") : language.t("session.todo.collapse")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
footer={
|
||||
<>
|
||||
@@ -279,56 +321,121 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div data-slot="question-text">{question()?.question}</div>
|
||||
<Show when={multi()} fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</div>}>
|
||||
<div data-slot="question-hint">{language.t("ui.question.multiHint")}</div>
|
||||
<div
|
||||
data-slot="question-text"
|
||||
class="cursor-default"
|
||||
classList={{
|
||||
"mb-6": store.collapsed && picked() === 0,
|
||||
}}
|
||||
role={store.collapsed ? "button" : undefined}
|
||||
tabIndex={store.collapsed ? 0 : undefined}
|
||||
onClick={fold}
|
||||
onKeyDown={(event) => {
|
||||
if (!store.collapsed) return
|
||||
if (event.key !== "Enter" && event.key !== " ") return
|
||||
event.preventDefault()
|
||||
fold()
|
||||
}}
|
||||
>
|
||||
{question()?.question}
|
||||
</div>
|
||||
<Show when={store.collapsed && picked() > 0}>
|
||||
<div data-slot="question-hint" class="cursor-default mb-6">
|
||||
{picked()} answer{picked() === 1 ? "" : "s"} selected
|
||||
</div>
|
||||
</Show>
|
||||
<div data-slot="question-options">
|
||||
<For each={options()}>
|
||||
{(opt, i) => {
|
||||
const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
|
||||
return (
|
||||
<div data-slot="question-answers" hidden={store.collapsed} aria-hidden={store.collapsed}>
|
||||
<Show when={multi()} fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</div>}>
|
||||
<div data-slot="question-hint">{language.t("ui.question.multiHint")}</div>
|
||||
</Show>
|
||||
<div data-slot="question-options">
|
||||
<For each={options()}>
|
||||
{(opt, i) => {
|
||||
const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
|
||||
return (
|
||||
<button
|
||||
data-slot="question-option"
|
||||
data-picked={picked()}
|
||||
role={multi() ? "checkbox" : "radio"}
|
||||
aria-checked={picked()}
|
||||
disabled={store.sending}
|
||||
onClick={() => selectOption(i())}
|
||||
>
|
||||
<span data-slot="question-option-check" aria-hidden="true">
|
||||
<span
|
||||
data-slot="question-option-box"
|
||||
data-type={multi() ? "checkbox" : "radio"}
|
||||
data-picked={picked()}
|
||||
>
|
||||
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
||||
<Icon name="check-small" size="small" />
|
||||
</Show>
|
||||
</span>
|
||||
</span>
|
||||
<span data-slot="question-option-main">
|
||||
<span data-slot="option-label">{opt.label}</span>
|
||||
<Show when={opt.description}>
|
||||
<span data-slot="option-description">{opt.description}</span>
|
||||
</Show>
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
|
||||
<Show
|
||||
when={store.editing}
|
||||
fallback={
|
||||
<button
|
||||
data-slot="question-option"
|
||||
data-picked={picked()}
|
||||
data-custom="true"
|
||||
data-picked={on()}
|
||||
role={multi() ? "checkbox" : "radio"}
|
||||
aria-checked={picked()}
|
||||
aria-checked={on()}
|
||||
disabled={store.sending}
|
||||
onClick={() => selectOption(i())}
|
||||
onClick={customOpen}
|
||||
>
|
||||
<span data-slot="question-option-check" aria-hidden="true">
|
||||
<span
|
||||
data-slot="question-option-box"
|
||||
data-type={multi() ? "checkbox" : "radio"}
|
||||
data-picked={picked()}
|
||||
>
|
||||
<span
|
||||
data-slot="question-option-check"
|
||||
aria-hidden="true"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
customToggle()
|
||||
}}
|
||||
>
|
||||
<span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
|
||||
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
||||
<Icon name="check-small" size="small" />
|
||||
</Show>
|
||||
</span>
|
||||
</span>
|
||||
<span data-slot="question-option-main">
|
||||
<span data-slot="option-label">{opt.label}</span>
|
||||
<Show when={opt.description}>
|
||||
<span data-slot="option-description">{opt.description}</span>
|
||||
</Show>
|
||||
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
|
||||
<span data-slot="option-description">{input() || language.t("ui.question.custom.placeholder")}</span>
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
|
||||
<Show
|
||||
when={store.editing}
|
||||
fallback={
|
||||
<button
|
||||
}
|
||||
>
|
||||
<form
|
||||
data-slot="question-option"
|
||||
data-custom="true"
|
||||
data-picked={on()}
|
||||
role={multi() ? "checkbox" : "radio"}
|
||||
aria-checked={on()}
|
||||
disabled={store.sending}
|
||||
onClick={customOpen}
|
||||
onMouseDown={(e) => {
|
||||
if (store.sending) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
if (e.target instanceof HTMLTextAreaElement) return
|
||||
const input = e.currentTarget.querySelector('[data-slot="question-custom-input"]')
|
||||
if (input instanceof HTMLTextAreaElement) input.focus()
|
||||
}}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
commitCustom()
|
||||
}}
|
||||
>
|
||||
<span
|
||||
data-slot="question-option-check"
|
||||
@@ -347,80 +454,39 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
||||
</span>
|
||||
<span data-slot="question-option-main">
|
||||
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
|
||||
<span data-slot="option-description">{input() || language.t("ui.question.custom.placeholder")}</span>
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<form
|
||||
data-slot="question-option"
|
||||
data-custom="true"
|
||||
data-picked={on()}
|
||||
role={multi() ? "checkbox" : "radio"}
|
||||
aria-checked={on()}
|
||||
onMouseDown={(e) => {
|
||||
if (store.sending) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
if (e.target instanceof HTMLTextAreaElement) return
|
||||
const input = e.currentTarget.querySelector('[data-slot="question-custom-input"]')
|
||||
if (input instanceof HTMLTextAreaElement) input.focus()
|
||||
}}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
commitCustom()
|
||||
}}
|
||||
>
|
||||
<span
|
||||
data-slot="question-option-check"
|
||||
aria-hidden="true"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
customToggle()
|
||||
}}
|
||||
>
|
||||
<span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
|
||||
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
||||
<Icon name="check-small" size="small" />
|
||||
</Show>
|
||||
</span>
|
||||
</span>
|
||||
<span data-slot="question-option-main">
|
||||
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
|
||||
<textarea
|
||||
ref={(el) =>
|
||||
setTimeout(() => {
|
||||
el.focus()
|
||||
el.style.height = "0px"
|
||||
el.style.height = `${el.scrollHeight}px`
|
||||
}, 0)
|
||||
}
|
||||
data-slot="question-custom-input"
|
||||
placeholder={language.t("ui.question.custom.placeholder")}
|
||||
value={input()}
|
||||
rows={1}
|
||||
disabled={store.sending}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault()
|
||||
setStore("editing", false)
|
||||
return
|
||||
<textarea
|
||||
ref={(el) =>
|
||||
setTimeout(() => {
|
||||
el.focus()
|
||||
el.style.height = "0px"
|
||||
el.style.height = `${el.scrollHeight}px`
|
||||
}, 0)
|
||||
}
|
||||
if (e.key !== "Enter" || e.shiftKey) return
|
||||
e.preventDefault()
|
||||
commitCustom()
|
||||
}}
|
||||
onInput={(e) => {
|
||||
customUpdate(e.currentTarget.value)
|
||||
e.currentTarget.style.height = "0px"
|
||||
e.currentTarget.style.height = `${e.currentTarget.scrollHeight}px`
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</form>
|
||||
</Show>
|
||||
data-slot="question-custom-input"
|
||||
placeholder={language.t("ui.question.custom.placeholder")}
|
||||
value={input()}
|
||||
rows={1}
|
||||
disabled={store.sending}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault()
|
||||
setStore("editing", false)
|
||||
return
|
||||
}
|
||||
if (e.key !== "Enter" || e.shiftKey) return
|
||||
e.preventDefault()
|
||||
commitCustom()
|
||||
}}
|
||||
onInput={(e) => {
|
||||
customUpdate(e.currentTarget.value)
|
||||
e.currentTarget.style.height = "0px"
|
||||
e.currentTarget.style.height = `${e.currentTarget.scrollHeight}px`
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</form>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</DockPrompt>
|
||||
)
|
||||
|
||||
@@ -6,9 +6,15 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
||||
import { TextReveal } from "@opencode-ai/ui/text-reveal"
|
||||
import { TextStrikethrough } from "@opencode-ai/ui/text-strikethrough"
|
||||
import { useElementHeight } from "@opencode-ai/ui/hooks"
|
||||
import { Index, createEffect, createMemo, createSignal, on, onCleanup } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
|
||||
const COLLAPSE_SPRING = { visualDuration: 0.3, bounce: 0 }
|
||||
export const COLLAPSED_HEIGHT = 78
|
||||
const SUBTITLE = { duration: 600, travel: 25, edge: 17 }
|
||||
const COUNT = { duration: 600, mask: 18, maskHeight: 0, widthDuration: 560 }
|
||||
|
||||
function dot(status: Todo["status"]) {
|
||||
if (status !== "in_progress") return undefined
|
||||
return (
|
||||
@@ -40,19 +46,6 @@ export function SessionTodoDock(props: {
|
||||
collapseLabel: string
|
||||
expandLabel: string
|
||||
dockProgress?: number
|
||||
visualDuration?: number
|
||||
bounce?: number
|
||||
expandVisualDuration?: number
|
||||
expandBounce?: number
|
||||
collapseVisualDuration?: number
|
||||
collapseBounce?: number
|
||||
subtitleDuration?: number
|
||||
subtitleTravel?: number
|
||||
subtitleEdge?: number
|
||||
countDuration?: number
|
||||
countMask?: number
|
||||
countMaskHeight?: number
|
||||
countWidthDuration?: number
|
||||
}) {
|
||||
const [store, setStore] = createStore({
|
||||
collapsed: false,
|
||||
@@ -73,39 +66,12 @@ export function SessionTodoDock(props: {
|
||||
)
|
||||
|
||||
const preview = createMemo(() => active()?.content ?? "")
|
||||
const config = createMemo(() =>
|
||||
store.collapsed
|
||||
? {
|
||||
visualDuration: props.collapseVisualDuration ?? props.visualDuration ?? 0.3,
|
||||
bounce: props.collapseBounce ?? props.bounce ?? 0,
|
||||
}
|
||||
: {
|
||||
visualDuration: props.expandVisualDuration ?? props.visualDuration ?? 0.3,
|
||||
bounce: props.expandBounce ?? props.bounce ?? 0,
|
||||
},
|
||||
)
|
||||
const collapse = useSpring(() => (store.collapsed ? 1 : 0), config)
|
||||
const dock = createMemo(() => Math.max(0, Math.min(1, props.dockProgress ?? 1)))
|
||||
const shut = createMemo(() => 1 - dock())
|
||||
const value = createMemo(() => Math.max(0, Math.min(1, collapse())))
|
||||
const hide = createMemo(() => Math.max(value(), shut()))
|
||||
const off = createMemo(() => hide() > 0.98)
|
||||
const turn = createMemo(() => Math.max(0, Math.min(1, value())))
|
||||
const [height, setHeight] = createSignal(320)
|
||||
const full = createMemo(() => Math.max(78, height()))
|
||||
const collapse = useSpring(() => (store.collapsed ? 1 : 0), COLLAPSE_SPRING)
|
||||
const shut = createMemo(() => 1 - (props.dockProgress ?? 1))
|
||||
const hide = createMemo(() => Math.max(collapse(), shut()))
|
||||
let contentRef: HTMLDivElement | undefined
|
||||
|
||||
createEffect(() => {
|
||||
const el = contentRef
|
||||
if (!el) return
|
||||
const update = () => {
|
||||
setHeight(el.getBoundingClientRect().height)
|
||||
}
|
||||
update()
|
||||
const observer = new ResizeObserver(update)
|
||||
observer.observe(el)
|
||||
onCleanup(() => observer.disconnect())
|
||||
})
|
||||
const height = useElementHeight(() => contentRef, 320)
|
||||
const full = createMemo(() => Math.max(COLLAPSED_HEIGHT, height()))
|
||||
|
||||
return (
|
||||
<DockTray
|
||||
@@ -113,7 +79,7 @@ export function SessionTodoDock(props: {
|
||||
style={{
|
||||
"overflow-x": "visible",
|
||||
"overflow-y": "hidden",
|
||||
"max-height": `${Math.max(78, full() - value() * (full() - 78))}px`,
|
||||
"max-height": `${Math.max(COLLAPSED_HEIGHT, full() - collapse() * (full() - COLLAPSED_HEIGHT))}px`,
|
||||
}}
|
||||
>
|
||||
<div ref={contentRef}>
|
||||
@@ -133,12 +99,12 @@ export function SessionTodoDock(props: {
|
||||
class="text-14-regular text-text-strong cursor-default inline-flex items-baseline shrink-0 whitespace-nowrap overflow-visible"
|
||||
aria-label={label()}
|
||||
style={{
|
||||
"--tool-motion-odometer-ms": `${props.countDuration ?? 600}ms`,
|
||||
"--tool-motion-mask": `${props.countMask ?? 18}%`,
|
||||
"--tool-motion-mask-height": `${props.countMaskHeight ?? 0}px`,
|
||||
"--tool-motion-spring-ms": `${props.countWidthDuration ?? 560}ms`,
|
||||
opacity: `${Math.max(0, Math.min(1, 1 - shut()))}`,
|
||||
filter: `blur(${Math.max(0, Math.min(1, shut())) * 2}px)`,
|
||||
"--tool-motion-odometer-ms": `${COUNT.duration}ms`,
|
||||
"--tool-motion-mask": `${COUNT.mask}%`,
|
||||
"--tool-motion-mask-height": `${COUNT.maskHeight}px`,
|
||||
"--tool-motion-spring-ms": `${COUNT.widthDuration}ms`,
|
||||
opacity: `${1 - shut()}`,
|
||||
filter: shut() > 0.01 ? `blur(${shut() * 2}px)` : "none",
|
||||
}}
|
||||
>
|
||||
<AnimatedNumber value={done()} />
|
||||
@@ -157,9 +123,9 @@ export function SessionTodoDock(props: {
|
||||
<TextReveal
|
||||
class="text-14-regular text-text-base cursor-default"
|
||||
text={store.collapsed ? preview() : undefined}
|
||||
duration={props.subtitleDuration ?? 600}
|
||||
travel={props.subtitleTravel ?? 25}
|
||||
edge={props.subtitleEdge ?? 17}
|
||||
duration={SUBTITLE.duration}
|
||||
travel={SUBTITLE.travel}
|
||||
edge={SUBTITLE.edge}
|
||||
spring="cubic-bezier(0.34, 1, 0.64, 1)"
|
||||
springSoft="cubic-bezier(0.34, 1, 0.64, 1)"
|
||||
growOnly
|
||||
@@ -173,7 +139,7 @@ export function SessionTodoDock(props: {
|
||||
icon="chevron-down"
|
||||
size="normal"
|
||||
variant="ghost"
|
||||
style={{ transform: `rotate(${turn() * 180}deg)` }}
|
||||
style={{ transform: `rotate(${collapse() * 180}deg)` }}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
@@ -189,14 +155,15 @@ export function SessionTodoDock(props: {
|
||||
|
||||
<div
|
||||
data-slot="session-todo-list"
|
||||
aria-hidden={store.collapsed || off()}
|
||||
class="pb-2"
|
||||
aria-hidden={store.collapsed}
|
||||
classList={{
|
||||
"pointer-events-none": hide() > 0.1,
|
||||
}}
|
||||
style={{
|
||||
visibility: off() ? "hidden" : "visible",
|
||||
opacity: `${Math.max(0, Math.min(1, 1 - hide()))}`,
|
||||
filter: `blur(${Math.max(0, Math.min(1, hide())) * 2}px)`,
|
||||
opacity: `${1 - hide()}`,
|
||||
filter: hide() > 0.01 ? `blur(${hide() * 2}px)` : "none",
|
||||
visibility: hide() > 0.98 ? "hidden" : "visible",
|
||||
}}
|
||||
>
|
||||
<TodoList todos={props.todos} open={!store.collapsed} />
|
||||
@@ -282,7 +249,7 @@ function TodoList(props: { todos: Todo[]; open: boolean }) {
|
||||
"--checkbox-align": "flex-start",
|
||||
"--checkbox-offset": "1px",
|
||||
transition: "opacity 220ms var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1))",
|
||||
opacity: todo().status === "pending" ? "0.94" : "1",
|
||||
opacity: todo().status === "pending" ? "0.5" : "1",
|
||||
}}
|
||||
>
|
||||
<TextStrikethrough
|
||||
@@ -292,12 +259,11 @@ function TodoList(props: { todos: Todo[]; open: boolean }) {
|
||||
style={{
|
||||
"line-height": "var(--line-height-normal)",
|
||||
transition:
|
||||
"color 220ms var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1)), opacity 220ms var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1))",
|
||||
"color 220ms var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1))",
|
||||
color:
|
||||
todo().status === "completed" || todo().status === "cancelled"
|
||||
? "var(--text-weak)"
|
||||
: "var(--text-strong)",
|
||||
opacity: todo().status === "pending" ? "0.92" : "1",
|
||||
}}
|
||||
/>
|
||||
</Checkbox>
|
||||
|
||||
@@ -234,7 +234,6 @@ export function FileTabContent(props: { tab: string }) {
|
||||
if (typeof window === "undefined") return
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.defaultPrevented) return
|
||||
if (tabs().active() !== props.tab) return
|
||||
if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey) return
|
||||
if (event.key.toLowerCase() !== "f") return
|
||||
|
||||
@@ -6,17 +6,45 @@ describe("createOpenReviewFile", () => {
|
||||
const calls: string[] = []
|
||||
const openReviewFile = createOpenReviewFile({
|
||||
showAllFiles: () => calls.push("show"),
|
||||
openReviewPanel: () => calls.push("review"),
|
||||
tabForPath: (path) => {
|
||||
calls.push(`tab:${path}`)
|
||||
return `file://${path}`
|
||||
},
|
||||
openTab: (tab) => calls.push(`open:${tab}`),
|
||||
setActive: (tab) => calls.push(`active:${tab}`),
|
||||
setSelectedLines: (path, range) => calls.push(`select:${path}:${range ? `${range.start}-${range.end}` : "none"}`),
|
||||
loadFile: (path) => calls.push(`load:${path}`),
|
||||
})
|
||||
|
||||
openReviewFile("src/a.ts")
|
||||
|
||||
expect(calls).toEqual(["show", "load:src/a.ts", "tab:src/a.ts", "open:file://src/a.ts"])
|
||||
expect(calls).toEqual([
|
||||
"tab:src/a.ts",
|
||||
"show",
|
||||
"review",
|
||||
"load:src/a.ts",
|
||||
"open:file://src/a.ts",
|
||||
"active:file://src/a.ts",
|
||||
"select:src/a.ts:none",
|
||||
])
|
||||
})
|
||||
|
||||
test("selects the requested line when provided", () => {
|
||||
const calls: string[] = []
|
||||
const openReviewFile = createOpenReviewFile({
|
||||
showAllFiles: () => calls.push("show"),
|
||||
openReviewPanel: () => calls.push("review"),
|
||||
tabForPath: (path) => `file://${path}`,
|
||||
openTab: () => calls.push("open"),
|
||||
setActive: () => calls.push("active"),
|
||||
setSelectedLines: (_path, range) => calls.push(`select:${range?.start}-${range?.end}`),
|
||||
loadFile: () => calls.push("load"),
|
||||
})
|
||||
|
||||
openReviewFile("src/a.ts", 12)
|
||||
|
||||
expect(calls).toContain("select:12-12")
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -24,13 +24,22 @@ export const createOpenReviewFile = (input: {
|
||||
showAllFiles: () => void
|
||||
tabForPath: (path: string) => string
|
||||
openTab: (tab: string) => void
|
||||
setActive: (tab: string) => void
|
||||
openReviewPanel: () => void
|
||||
setSelectedLines: (path: string, range: { start: number; end: number } | null) => void
|
||||
loadFile: (path: string) => any | Promise<void>
|
||||
}) => {
|
||||
return (path: string) => {
|
||||
return (path: string, line?: number) => {
|
||||
const tab = input.tabForPath(path)
|
||||
batch(() => {
|
||||
input.showAllFiles()
|
||||
input.openReviewPanel()
|
||||
const maybePromise = input.loadFile(path)
|
||||
const openTab = () => input.openTab(input.tabForPath(path))
|
||||
const openTab = () => {
|
||||
input.openTab(tab)
|
||||
input.setActive(tab)
|
||||
input.setSelectedLines(path, line ? { start: line, end: line } : null)
|
||||
}
|
||||
if (maybePromise instanceof Promise) maybePromise.then(openTab)
|
||||
else openTab()
|
||||
})
|
||||
|
||||
@@ -49,11 +49,11 @@ describe("shouldMarkBoundaryGesture", () => {
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test("does not mark when nested scroller can consume movement", () => {
|
||||
test("does not mark when scroller can consume movement", () => {
|
||||
expect(
|
||||
shouldMarkBoundaryGesture({
|
||||
delta: 20,
|
||||
scrollTop: 200,
|
||||
scrollTop: 300,
|
||||
scrollHeight: 1000,
|
||||
clientHeight: 400,
|
||||
}),
|
||||
|
||||
@@ -14,8 +14,8 @@ export const shouldMarkBoundaryGesture = (input: {
|
||||
if (max <= 1) return true
|
||||
if (!input.delta) return false
|
||||
|
||||
if (input.delta < 0) return input.scrollTop + input.delta <= 0
|
||||
|
||||
const remaining = max - input.scrollTop
|
||||
return input.delta > remaining
|
||||
const top = Math.max(0, Math.min(max, input.scrollTop))
|
||||
if (input.delta < 0) return -input.delta > top
|
||||
const bottom = max - top
|
||||
return input.delta > bottom
|
||||
}
|
||||
|
||||
6
packages/app/src/pages/session/message-id-from-hash.ts
Normal file
6
packages/app/src/pages/session/message-id-from-hash.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const messageIdFromHash = (hash: string) => {
|
||||
const value = hash.startsWith("#") ? hash.slice(1) : hash
|
||||
const match = value.match(/^message-(.+)$/)
|
||||
if (!match) return
|
||||
return match[1]
|
||||
}
|
||||
@@ -1,27 +1,31 @@
|
||||
import { For, createEffect, createMemo, on, onCleanup, Show, startTransition, Index, type JSX } from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { useNavigate, useParams } from "@solidjs/router"
|
||||
import {
|
||||
For,
|
||||
Index,
|
||||
createEffect,
|
||||
createMemo,
|
||||
createSignal,
|
||||
on,
|
||||
onCleanup,
|
||||
Show,
|
||||
startTransition,
|
||||
type JSX,
|
||||
} from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { InlineInput } from "@opencode-ai/ui/inline-input"
|
||||
import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||
import type { AssistantMessage, Message as MessageType, Part, TextPart, UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { Binary } from "@opencode-ai/util/binary"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture"
|
||||
import { SessionContextUsage } from "@/components/session-context-usage"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useSettings } from "@/context/settings"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note"
|
||||
import { SessionTimelineHeader } from "@/pages/session/session-timeline-header"
|
||||
|
||||
type MessageComment = {
|
||||
path: string
|
||||
@@ -33,7 +37,9 @@ type MessageComment = {
|
||||
}
|
||||
|
||||
const emptyMessages: MessageType[] = []
|
||||
const idle = { type: "idle" as const }
|
||||
|
||||
const isDefaultSessionTitle = (title?: string) =>
|
||||
!!title && /^(New session - |Child session - )\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(title)
|
||||
|
||||
const messageComments = (parts: Part[]): MessageComment[] =>
|
||||
parts.flatMap((part) => {
|
||||
@@ -110,6 +116,8 @@ function createTimelineStaging(input: TimelineStageInput) {
|
||||
completedSession: "",
|
||||
count: 0,
|
||||
})
|
||||
const [readySession, setReadySession] = createSignal("")
|
||||
let active = ""
|
||||
|
||||
const stagedCount = createMemo(() => {
|
||||
const total = input.messages().length
|
||||
@@ -134,23 +142,46 @@ function createTimelineStaging(input: TimelineStageInput) {
|
||||
cancelAnimationFrame(frame)
|
||||
frame = undefined
|
||||
}
|
||||
const scheduleReady = (sessionKey: string) => {
|
||||
if (input.sessionKey() !== sessionKey) return
|
||||
if (readySession() === sessionKey) return
|
||||
setReadySession(sessionKey)
|
||||
}
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => [input.sessionKey(), input.turnStart() > 0, input.messages().length] as const,
|
||||
([sessionKey, isWindowed, total]) => {
|
||||
const switched = active !== sessionKey
|
||||
if (switched) {
|
||||
active = sessionKey
|
||||
setReadySession("")
|
||||
}
|
||||
|
||||
const staging = state.activeSession === sessionKey && state.completedSession !== sessionKey
|
||||
const shouldStage = isWindowed && total > input.config.init && state.completedSession !== sessionKey
|
||||
|
||||
if (staging && !switched && shouldStage && frame !== undefined) return
|
||||
|
||||
cancel()
|
||||
const shouldStage =
|
||||
isWindowed &&
|
||||
total > input.config.init &&
|
||||
state.completedSession !== sessionKey &&
|
||||
state.activeSession !== sessionKey
|
||||
|
||||
if (shouldStage) setReadySession("")
|
||||
if (!shouldStage) {
|
||||
setState({ activeSession: "", count: total })
|
||||
setState({
|
||||
activeSession: "",
|
||||
completedSession: isWindowed ? sessionKey : state.completedSession,
|
||||
count: total,
|
||||
})
|
||||
if (total <= 0) {
|
||||
setReadySession("")
|
||||
return
|
||||
}
|
||||
if (readySession() !== sessionKey) scheduleReady(sessionKey)
|
||||
return
|
||||
}
|
||||
|
||||
let count = Math.min(total, input.config.init)
|
||||
if (staging) count = Math.min(total, Math.max(count, state.count))
|
||||
setState({ activeSession: sessionKey, count })
|
||||
|
||||
const step = () => {
|
||||
@@ -164,6 +195,7 @@ function createTimelineStaging(input: TimelineStageInput) {
|
||||
if (count >= currentTotal) {
|
||||
setState({ completedSession: sessionKey, activeSession: "" })
|
||||
frame = undefined
|
||||
scheduleReady(sessionKey)
|
||||
return
|
||||
}
|
||||
frame = requestAnimationFrame(step)
|
||||
@@ -177,9 +209,12 @@ function createTimelineStaging(input: TimelineStageInput) {
|
||||
const key = input.sessionKey()
|
||||
return state.activeSession === key && state.completedSession !== key
|
||||
})
|
||||
const ready = createMemo(() => readySession() === input.sessionKey())
|
||||
|
||||
onCleanup(cancel)
|
||||
return { messages: stagedUserMessages, isStaging }
|
||||
onCleanup(() => {
|
||||
cancel()
|
||||
})
|
||||
return { messages: stagedUserMessages, isStaging, ready }
|
||||
}
|
||||
|
||||
export function MessageTimeline(props: {
|
||||
@@ -196,6 +231,7 @@ export function MessageTimeline(props: {
|
||||
onScrollSpyScroll: () => void
|
||||
onTurnBackfillScroll: () => void
|
||||
onAutoScrollInteraction: (event: MouseEvent) => void
|
||||
onPreserveScrollAnchor: (target: HTMLElement) => void
|
||||
centered: boolean
|
||||
setContentRef: (el: HTMLDivElement) => void
|
||||
turnStart: number
|
||||
@@ -210,14 +246,19 @@ export function MessageTimeline(props: {
|
||||
let touchGesture: number | undefined
|
||||
|
||||
const params = useParams()
|
||||
const navigate = useNavigate()
|
||||
const sdk = useSDK()
|
||||
const sync = useSync()
|
||||
const settings = useSettings()
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
|
||||
const rendered = createMemo(() => props.renderedUserMessages.map((message) => message.id))
|
||||
const trigger = (target: EventTarget | null) => {
|
||||
const next =
|
||||
target instanceof Element
|
||||
? target.closest('[data-slot="collapsible-trigger"], [data-slot="accordion-trigger"]')
|
||||
: undefined
|
||||
if (!(next instanceof HTMLElement)) return
|
||||
return next
|
||||
}
|
||||
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const sessionID = createMemo(() => params.id)
|
||||
const sessionMessages = createMemo(() => {
|
||||
@@ -230,28 +271,20 @@ export function MessageTimeline(props: {
|
||||
(item): item is AssistantMessage => item.role === "assistant" && typeof item.time.completed !== "number",
|
||||
),
|
||||
)
|
||||
const sessionStatus = createMemo(() => {
|
||||
const id = sessionID()
|
||||
if (!id) return idle
|
||||
return sync.data.session_status[id] ?? idle
|
||||
})
|
||||
const sessionStatus = createMemo(() => sync.data.session_status[sessionID() ?? ""]?.type ?? "idle")
|
||||
const activeMessageID = createMemo(() => {
|
||||
const parentID = pending()?.parentID
|
||||
if (parentID) {
|
||||
const messages = sessionMessages()
|
||||
const result = Binary.search(messages, parentID, (message) => message.id)
|
||||
const message = result.found ? messages[result.index] : messages.find((item) => item.id === parentID)
|
||||
if (message && message.role === "user") return message.id
|
||||
const messages = sessionMessages()
|
||||
const message = pending()
|
||||
if (message?.parentID) {
|
||||
const result = Binary.search(messages, message.parentID, (item) => item.id)
|
||||
const parent = result.found ? messages[result.index] : messages.find((item) => item.id === message.parentID)
|
||||
if (parent?.role === "user") return parent.id
|
||||
}
|
||||
|
||||
const status = sessionStatus()
|
||||
if (status.type !== "idle") {
|
||||
const messages = sessionMessages()
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].role === "user") return messages[i].id
|
||||
}
|
||||
if (sessionStatus() === "idle") return undefined
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].role === "user") return messages[i].id
|
||||
}
|
||||
|
||||
return undefined
|
||||
})
|
||||
const info = createMemo(() => {
|
||||
@@ -259,9 +292,19 @@ export function MessageTimeline(props: {
|
||||
if (!id) return
|
||||
return sync.session.get(id)
|
||||
})
|
||||
const titleValue = createMemo(() => info()?.title)
|
||||
const titleValue = createMemo(() => {
|
||||
const title = info()?.title
|
||||
if (!title) return
|
||||
if (isDefaultSessionTitle(title)) return language.t("command.session.new")
|
||||
return title
|
||||
})
|
||||
const defaultTitle = createMemo(() => isDefaultSessionTitle(info()?.title))
|
||||
const headerTitle = createMemo(
|
||||
() => titleValue() ?? (props.renderedUserMessages.length ? language.t("command.session.new") : undefined),
|
||||
)
|
||||
const placeholderTitle = createMemo(() => defaultTitle() || (!info()?.title && props.renderedUserMessages.length > 0))
|
||||
const parentID = createMemo(() => info()?.parentID)
|
||||
const showHeader = createMemo(() => !!(titleValue() || parentID()))
|
||||
const showHeader = createMemo(() => !!(headerTitle() || parentID()))
|
||||
const stageCfg = { init: 1, batch: 3 }
|
||||
const staging = createTimelineStaging({
|
||||
sessionKey,
|
||||
@@ -269,212 +312,7 @@ export function MessageTimeline(props: {
|
||||
messages: () => props.renderedUserMessages,
|
||||
config: stageCfg,
|
||||
})
|
||||
|
||||
const [title, setTitle] = createStore({
|
||||
draft: "",
|
||||
editing: false,
|
||||
saving: false,
|
||||
menuOpen: false,
|
||||
pendingRename: false,
|
||||
})
|
||||
let titleRef: HTMLInputElement | undefined
|
||||
|
||||
const errorMessage = (err: unknown) => {
|
||||
if (err && typeof err === "object" && "data" in err) {
|
||||
const data = (err as { data?: { message?: string } }).data
|
||||
if (data?.message) return data.message
|
||||
}
|
||||
if (err instanceof Error) return err.message
|
||||
return language.t("common.requestFailed")
|
||||
}
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
sessionKey,
|
||||
() => setTitle({ draft: "", editing: false, saving: false, menuOpen: false, pendingRename: false }),
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
const openTitleEditor = () => {
|
||||
if (!sessionID()) return
|
||||
setTitle({ editing: true, draft: titleValue() ?? "" })
|
||||
requestAnimationFrame(() => {
|
||||
titleRef?.focus()
|
||||
titleRef?.select()
|
||||
})
|
||||
}
|
||||
|
||||
const closeTitleEditor = () => {
|
||||
if (title.saving) return
|
||||
setTitle({ editing: false, saving: false })
|
||||
}
|
||||
|
||||
const saveTitleEditor = async () => {
|
||||
const id = sessionID()
|
||||
if (!id) return
|
||||
if (title.saving) return
|
||||
|
||||
const next = title.draft.trim()
|
||||
if (!next || next === (titleValue() ?? "")) {
|
||||
setTitle({ editing: false, saving: false })
|
||||
return
|
||||
}
|
||||
|
||||
setTitle("saving", true)
|
||||
await sdk.client.session
|
||||
.update({ sessionID: id, title: next })
|
||||
.then(() => {
|
||||
sync.set(
|
||||
produce((draft) => {
|
||||
const index = draft.session.findIndex((s) => s.id === id)
|
||||
if (index !== -1) draft.session[index].title = next
|
||||
}),
|
||||
)
|
||||
setTitle({ editing: false, saving: false })
|
||||
})
|
||||
.catch((err) => {
|
||||
setTitle("saving", false)
|
||||
showToast({
|
||||
title: language.t("common.requestFailed"),
|
||||
description: errorMessage(err),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const navigateAfterSessionRemoval = (sessionID: string, parentID?: string, nextSessionID?: string) => {
|
||||
if (params.id !== sessionID) return
|
||||
if (parentID) {
|
||||
navigate(`/${params.dir}/session/${parentID}`)
|
||||
return
|
||||
}
|
||||
if (nextSessionID) {
|
||||
navigate(`/${params.dir}/session/${nextSessionID}`)
|
||||
return
|
||||
}
|
||||
navigate(`/${params.dir}/session`)
|
||||
}
|
||||
|
||||
const archiveSession = async (sessionID: string) => {
|
||||
const session = sync.session.get(sessionID)
|
||||
if (!session) return
|
||||
|
||||
const sessions = sync.data.session ?? []
|
||||
const index = sessions.findIndex((s) => s.id === sessionID)
|
||||
const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
|
||||
|
||||
await sdk.client.session
|
||||
.update({ sessionID, time: { archived: Date.now() } })
|
||||
.then(() => {
|
||||
sync.set(
|
||||
produce((draft) => {
|
||||
const index = draft.session.findIndex((s) => s.id === sessionID)
|
||||
if (index !== -1) draft.session.splice(index, 1)
|
||||
}),
|
||||
)
|
||||
navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
|
||||
})
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
title: language.t("common.requestFailed"),
|
||||
description: errorMessage(err),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const deleteSession = async (sessionID: string) => {
|
||||
const session = sync.session.get(sessionID)
|
||||
if (!session) return false
|
||||
|
||||
const sessions = (sync.data.session ?? []).filter((s) => !s.parentID && !s.time?.archived)
|
||||
const index = sessions.findIndex((s) => s.id === sessionID)
|
||||
const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
|
||||
|
||||
const result = await sdk.client.session
|
||||
.delete({ sessionID })
|
||||
.then((x) => x.data)
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
title: language.t("session.delete.failed.title"),
|
||||
description: errorMessage(err),
|
||||
})
|
||||
return false
|
||||
})
|
||||
|
||||
if (!result) return false
|
||||
|
||||
sync.set(
|
||||
produce((draft) => {
|
||||
const removed = new Set<string>([sessionID])
|
||||
|
||||
const byParent = new Map<string, string[]>()
|
||||
for (const item of draft.session) {
|
||||
const parentID = item.parentID
|
||||
if (!parentID) continue
|
||||
const existing = byParent.get(parentID)
|
||||
if (existing) {
|
||||
existing.push(item.id)
|
||||
continue
|
||||
}
|
||||
byParent.set(parentID, [item.id])
|
||||
}
|
||||
|
||||
const stack = [sessionID]
|
||||
while (stack.length) {
|
||||
const parentID = stack.pop()
|
||||
if (!parentID) continue
|
||||
|
||||
const children = byParent.get(parentID)
|
||||
if (!children) continue
|
||||
|
||||
for (const child of children) {
|
||||
if (removed.has(child)) continue
|
||||
removed.add(child)
|
||||
stack.push(child)
|
||||
}
|
||||
}
|
||||
|
||||
draft.session = draft.session.filter((s) => !removed.has(s.id))
|
||||
}),
|
||||
)
|
||||
|
||||
navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
|
||||
return true
|
||||
}
|
||||
|
||||
const navigateParent = () => {
|
||||
const id = parentID()
|
||||
if (!id) return
|
||||
navigate(`/${params.dir}/session/${id}`)
|
||||
}
|
||||
|
||||
function DialogDeleteSession(props: { sessionID: string }) {
|
||||
const name = createMemo(() => sync.session.get(props.sessionID)?.title ?? language.t("command.session.new"))
|
||||
const handleDelete = async () => {
|
||||
await deleteSession(props.sessionID)
|
||||
dialog.close()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog title={language.t("session.delete.title")} fit>
|
||||
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-14-regular text-text-strong">
|
||||
{language.t("session.delete.confirm", { name: name() })}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="ghost" size="large" onClick={() => dialog.close()}>
|
||||
{language.t("common.cancel")}
|
||||
</Button>
|
||||
<Button variant="primary" size="large" onClick={handleDelete}>
|
||||
{language.t("session.delete.button")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
const rendered = createMemo(() => staging.messages().map((message) => message.id))
|
||||
|
||||
return (
|
||||
<Show
|
||||
@@ -498,6 +336,16 @@ export function MessageTimeline(props: {
|
||||
<Icon name="arrow-down-to-line" />
|
||||
</button>
|
||||
</div>
|
||||
<SessionTimelineHeader
|
||||
centered={props.centered}
|
||||
showHeader={showHeader}
|
||||
sessionKey={sessionKey}
|
||||
sessionID={sessionID}
|
||||
parentID={parentID}
|
||||
titleValue={titleValue}
|
||||
headerTitle={headerTitle}
|
||||
placeholderTitle={placeholderTitle}
|
||||
/>
|
||||
<ScrollView
|
||||
viewportRef={props.setScrollRef}
|
||||
onWheel={(e) => {
|
||||
@@ -532,9 +380,18 @@ export function MessageTimeline(props: {
|
||||
touchGesture = undefined
|
||||
}}
|
||||
onPointerDown={(e) => {
|
||||
const next = trigger(e.target)
|
||||
if (next) props.onPreserveScrollAnchor(next)
|
||||
|
||||
if (e.target !== e.currentTarget) return
|
||||
props.onMarkScrollGesture(e.currentTarget)
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key !== "Enter" && e.key !== " ") return
|
||||
const next = trigger(e.target)
|
||||
if (!next) return
|
||||
props.onPreserveScrollAnchor(next)
|
||||
}}
|
||||
onScroll={(e) => {
|
||||
props.onScheduleScrollState(e.currentTarget)
|
||||
props.onTurnBackfillScroll()
|
||||
@@ -543,131 +400,22 @@ export function MessageTimeline(props: {
|
||||
props.onMarkScrollGesture(e.currentTarget)
|
||||
if (props.isDesktop) props.onScrollSpyScroll()
|
||||
}}
|
||||
onClick={props.onAutoScrollInteraction}
|
||||
onClick={(e) => {
|
||||
const next = trigger(e.target)
|
||||
if (next) props.onPreserveScrollAnchor(next)
|
||||
props.onAutoScrollInteraction(e)
|
||||
}}
|
||||
class="relative min-w-0 w-full h-full"
|
||||
style={{
|
||||
"--session-title-height": showHeader() ? "40px" : "0px",
|
||||
"--sticky-accordion-top": showHeader() ? "48px" : "0px",
|
||||
}}
|
||||
>
|
||||
<div ref={props.setContentRef} class="min-w-0 w-full">
|
||||
<Show when={showHeader()}>
|
||||
<div
|
||||
data-session-title
|
||||
classList={{
|
||||
"sticky top-0 z-30 bg-[linear-gradient(to_bottom,var(--background-stronger)_48px,transparent)]": true,
|
||||
"w-full": true,
|
||||
"pb-4": true,
|
||||
"pl-2 pr-3 md:pl-4 md:pr-3": true,
|
||||
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
||||
}}
|
||||
>
|
||||
<div class="h-12 w-full flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-1 min-w-0 flex-1 pr-3">
|
||||
<Show when={parentID()}>
|
||||
<IconButton
|
||||
tabIndex={-1}
|
||||
icon="arrow-left"
|
||||
variant="ghost"
|
||||
onClick={navigateParent}
|
||||
aria-label={language.t("common.goBack")}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={titleValue() || title.editing}>
|
||||
<Show
|
||||
when={title.editing}
|
||||
fallback={
|
||||
<h1
|
||||
class="text-14-medium text-text-strong truncate grow-1 min-w-0 pl-2"
|
||||
onDblClick={openTitleEditor}
|
||||
>
|
||||
{titleValue()}
|
||||
</h1>
|
||||
}
|
||||
>
|
||||
<InlineInput
|
||||
ref={(el) => {
|
||||
titleRef = el
|
||||
}}
|
||||
value={title.draft}
|
||||
disabled={title.saving}
|
||||
class="text-14-medium text-text-strong grow-1 min-w-0 pl-2 rounded-[6px]"
|
||||
style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }}
|
||||
onInput={(event) => setTitle("draft", event.currentTarget.value)}
|
||||
onKeyDown={(event) => {
|
||||
event.stopPropagation()
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault()
|
||||
void saveTitleEditor()
|
||||
return
|
||||
}
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault()
|
||||
closeTitleEditor()
|
||||
}
|
||||
}}
|
||||
onBlur={closeTitleEditor}
|
||||
/>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={sessionID()}>
|
||||
{(id) => (
|
||||
<div class="shrink-0 flex items-center gap-3">
|
||||
<SessionContextUsage placement="bottom" />
|
||||
<DropdownMenu
|
||||
gutter={4}
|
||||
placement="bottom-end"
|
||||
open={title.menuOpen}
|
||||
onOpenChange={(open) => setTitle("menuOpen", open)}
|
||||
>
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
icon="dot-grid"
|
||||
variant="ghost"
|
||||
class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
|
||||
aria-label={language.t("common.moreOptions")}
|
||||
/>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
style={{ "min-width": "104px" }}
|
||||
onCloseAutoFocus={(event) => {
|
||||
if (!title.pendingRename) return
|
||||
event.preventDefault()
|
||||
setTitle("pendingRename", false)
|
||||
openTitleEditor()
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
setTitle("pendingRename", true)
|
||||
setTitle("menuOpen", false)
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onSelect={() => void archiveSession(id())}>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id()} />)}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div>
|
||||
<div
|
||||
ref={props.setContentRef}
|
||||
role="log"
|
||||
class="flex flex-col gap-12 items-start justify-start pb-16 transition-[margin]"
|
||||
class="flex flex-col gap-0 items-start justify-start pb-16 transition-[margin]"
|
||||
classList={{
|
||||
"w-full": true,
|
||||
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
||||
@@ -692,6 +440,15 @@ export function MessageTimeline(props: {
|
||||
</Show>
|
||||
<For each={rendered()}>
|
||||
{(messageID) => {
|
||||
// Capture at creation time: animate only messages added after the
|
||||
// timeline finishes its initial backfill staging, plus the first
|
||||
// turn while a brand new session is still using its default title.
|
||||
const isNew =
|
||||
staging.ready() ||
|
||||
(defaultTitle() &&
|
||||
sessionStatus() !== "idle" &&
|
||||
props.renderedUserMessages.length === 1 &&
|
||||
messageID === props.renderedUserMessages[0]?.id)
|
||||
const active = createMemo(() => activeMessageID() === messageID)
|
||||
const queued = createMemo(() => {
|
||||
if (active()) return false
|
||||
@@ -700,7 +457,10 @@ export function MessageTimeline(props: {
|
||||
return false
|
||||
})
|
||||
const comments = createMemo(() => messageComments(sync.data.part[messageID] ?? []), [], {
|
||||
equals: (a, b) => JSON.stringify(a) === JSON.stringify(b),
|
||||
equals: (a, b) => {
|
||||
if (a.length !== b.length) return false
|
||||
return a.every((x, i) => x.path === b[i].path && x.comment === b[i].comment)
|
||||
},
|
||||
})
|
||||
const commentCount = createMemo(() => comments().length)
|
||||
return (
|
||||
@@ -757,7 +517,7 @@ export function MessageTimeline(props: {
|
||||
messageID={messageID}
|
||||
active={active()}
|
||||
queued={queued()}
|
||||
status={active() ? sessionStatus() : undefined}
|
||||
animate={isNew || active()}
|
||||
showReasoningSummaries={settings.general.showReasoningSummaries()}
|
||||
shellToolDefaultOpen={settings.general.shellToolPartsExpanded()}
|
||||
editToolDefaultOpen={settings.general.editToolPartsExpanded()}
|
||||
|
||||
509
packages/app/src/pages/session/session-timeline-header.tsx
Normal file
509
packages/app/src/pages/session/session-timeline-header.tsx
Normal file
@@ -0,0 +1,509 @@
|
||||
import { createEffect, createMemo, on, onCleanup, Show } from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { useNavigate, useParams } from "@solidjs/router"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { InlineInput } from "@opencode-ai/ui/inline-input"
|
||||
import { animate, type AnimationPlaybackControls, clearFadeStyles, FAST_SPRING } from "@opencode-ai/ui/motion"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { errorMessage } from "@/pages/layout/helpers"
|
||||
import { SessionContextUsage } from "@/components/session-context-usage"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { useSync } from "@/context/sync"
|
||||
|
||||
export function SessionTimelineHeader(props: {
|
||||
centered: boolean
|
||||
showHeader: () => boolean
|
||||
sessionKey: () => string
|
||||
sessionID: () => string | undefined
|
||||
parentID: () => string | undefined
|
||||
titleValue: () => string | undefined
|
||||
headerTitle: () => string | undefined
|
||||
placeholderTitle: () => boolean
|
||||
}) {
|
||||
const navigate = useNavigate()
|
||||
const params = useParams()
|
||||
const sdk = useSDK()
|
||||
const sync = useSync()
|
||||
const dialog = useDialog()
|
||||
const language = useLanguage()
|
||||
|
||||
const [title, setTitle] = createStore({
|
||||
draft: "",
|
||||
editing: false,
|
||||
saving: false,
|
||||
menuOpen: false,
|
||||
pendingRename: false,
|
||||
})
|
||||
const [headerText, setHeaderText] = createStore({
|
||||
session: props.sessionKey(),
|
||||
value: props.headerTitle(),
|
||||
prev: undefined as string | undefined,
|
||||
muted: props.placeholderTitle(),
|
||||
prevMuted: false,
|
||||
})
|
||||
let headerAnim: AnimationPlaybackControls | undefined
|
||||
let enterAnim: AnimationPlaybackControls | undefined
|
||||
let leaveAnim: AnimationPlaybackControls | undefined
|
||||
let titleRef: HTMLInputElement | undefined
|
||||
let headerRef: HTMLDivElement | undefined
|
||||
let enterRef: HTMLSpanElement | undefined
|
||||
let leaveRef: HTMLSpanElement | undefined
|
||||
|
||||
const clearHeaderAnim = () => {
|
||||
headerAnim?.stop()
|
||||
headerAnim = undefined
|
||||
}
|
||||
|
||||
const animateHeader = () => {
|
||||
const el = headerRef
|
||||
if (!el) return
|
||||
|
||||
clearHeaderAnim()
|
||||
if (!headerText.muted) {
|
||||
el.style.opacity = "1"
|
||||
return
|
||||
}
|
||||
|
||||
headerAnim = animate(el, { opacity: [0, 1] }, { type: "spring", visualDuration: 1.0, bounce: 0 })
|
||||
headerAnim.finished.then(() => {
|
||||
if (headerRef !== el) return
|
||||
clearFadeStyles(el)
|
||||
})
|
||||
}
|
||||
|
||||
const clearTitleAnims = () => {
|
||||
enterAnim?.stop()
|
||||
enterAnim = undefined
|
||||
leaveAnim?.stop()
|
||||
leaveAnim = undefined
|
||||
}
|
||||
|
||||
const settleTitleEnter = () => {
|
||||
if (enterRef) clearFadeStyles(enterRef)
|
||||
}
|
||||
|
||||
const hideLeave = () => {
|
||||
if (!leaveRef) return
|
||||
leaveRef.style.opacity = "0"
|
||||
leaveRef.style.filter = ""
|
||||
leaveRef.style.transform = ""
|
||||
}
|
||||
|
||||
const animateEnterSpan = () => {
|
||||
if (!enterRef) return
|
||||
enterAnim = animate(
|
||||
enterRef,
|
||||
{ opacity: [0, 1], filter: ["blur(2px)", "blur(0px)"], transform: ["translateY(-2px)", "translateY(0)"] },
|
||||
FAST_SPRING,
|
||||
)
|
||||
enterAnim.finished.then(() => settleTitleEnter())
|
||||
}
|
||||
|
||||
const crossfadeTitle = (nextTitle: string, nextMuted: boolean) => {
|
||||
clearTitleAnims()
|
||||
setHeaderText({ prev: headerText.value, prevMuted: headerText.muted })
|
||||
setHeaderText({ value: nextTitle, muted: nextMuted })
|
||||
|
||||
if (leaveRef) {
|
||||
leaveAnim = animate(
|
||||
leaveRef,
|
||||
{ opacity: [1, 0], filter: ["blur(0px)", "blur(2px)"], transform: ["translateY(0)", "translateY(2px)"] },
|
||||
FAST_SPRING,
|
||||
)
|
||||
leaveAnim.finished.then(() => {
|
||||
setHeaderText({ prev: undefined, prevMuted: false })
|
||||
hideLeave()
|
||||
})
|
||||
}
|
||||
|
||||
animateEnterSpan()
|
||||
}
|
||||
|
||||
const fadeInTitle = (nextTitle: string, nextMuted: boolean) => {
|
||||
clearTitleAnims()
|
||||
setHeaderText({ value: nextTitle, muted: nextMuted, prev: undefined, prevMuted: false })
|
||||
animateEnterSpan()
|
||||
}
|
||||
|
||||
const snapTitle = (nextTitle: string | undefined, nextMuted: boolean) => {
|
||||
clearTitleAnims()
|
||||
setHeaderText({ value: nextTitle, muted: nextMuted, prev: undefined, prevMuted: false })
|
||||
settleTitleEnter()
|
||||
}
|
||||
|
||||
createEffect(
|
||||
on(props.showHeader, (show, prev) => {
|
||||
if (!show) {
|
||||
clearHeaderAnim()
|
||||
return
|
||||
}
|
||||
if (show === prev) return
|
||||
animateHeader()
|
||||
}),
|
||||
)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => [props.sessionKey(), props.headerTitle(), props.placeholderTitle()] as const,
|
||||
([nextSession, nextTitle, nextMuted]) => {
|
||||
if (nextSession !== headerText.session) {
|
||||
setHeaderText("session", nextSession)
|
||||
if (nextTitle && nextMuted) {
|
||||
fadeInTitle(nextTitle, nextMuted)
|
||||
return
|
||||
}
|
||||
snapTitle(nextTitle, nextMuted)
|
||||
return
|
||||
}
|
||||
if (nextTitle === headerText.value && nextMuted === headerText.muted) return
|
||||
if (!nextTitle) {
|
||||
snapTitle(undefined, false)
|
||||
return
|
||||
}
|
||||
if (!headerText.value) {
|
||||
fadeInTitle(nextTitle, nextMuted)
|
||||
return
|
||||
}
|
||||
if (title.saving || title.editing) {
|
||||
snapTitle(nextTitle, nextMuted)
|
||||
return
|
||||
}
|
||||
crossfadeTitle(nextTitle, nextMuted)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
onCleanup(() => {
|
||||
clearHeaderAnim()
|
||||
clearTitleAnims()
|
||||
})
|
||||
|
||||
const toastError = (err: unknown) => errorMessage(err, language.t("common.requestFailed"))
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
props.sessionKey,
|
||||
() => setTitle({ draft: "", editing: false, saving: false, menuOpen: false, pendingRename: false }),
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
const openTitleEditor = () => {
|
||||
if (!props.sessionID()) return
|
||||
setTitle({ editing: true, draft: props.titleValue() ?? "" })
|
||||
requestAnimationFrame(() => {
|
||||
titleRef?.focus()
|
||||
titleRef?.select()
|
||||
})
|
||||
}
|
||||
|
||||
const closeTitleEditor = () => {
|
||||
if (title.saving) return
|
||||
setTitle({ editing: false, saving: false })
|
||||
}
|
||||
|
||||
const saveTitleEditor = async () => {
|
||||
const id = props.sessionID()
|
||||
if (!id) return
|
||||
if (title.saving) return
|
||||
|
||||
const next = title.draft.trim()
|
||||
if (!next || next === (props.titleValue() ?? "")) {
|
||||
setTitle({ editing: false, saving: false })
|
||||
return
|
||||
}
|
||||
|
||||
setTitle("saving", true)
|
||||
await sdk.client.session
|
||||
.update({ sessionID: id, title: next })
|
||||
.then(() => {
|
||||
sync.set(
|
||||
produce((draft) => {
|
||||
const index = draft.session.findIndex((session) => session.id === id)
|
||||
if (index !== -1) draft.session[index].title = next
|
||||
}),
|
||||
)
|
||||
setTitle({ editing: false, saving: false })
|
||||
})
|
||||
.catch((err) => {
|
||||
setTitle("saving", false)
|
||||
showToast({
|
||||
title: language.t("common.requestFailed"),
|
||||
description: toastError(err),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const navigateAfterSessionRemoval = (sessionID: string, parentID?: string, nextSessionID?: string) => {
|
||||
if (params.id !== sessionID) return
|
||||
if (parentID) {
|
||||
navigate(`/${params.dir}/session/${parentID}`)
|
||||
return
|
||||
}
|
||||
if (nextSessionID) {
|
||||
navigate(`/${params.dir}/session/${nextSessionID}`)
|
||||
return
|
||||
}
|
||||
navigate(`/${params.dir}/session`)
|
||||
}
|
||||
|
||||
const archiveSession = async (sessionID: string) => {
|
||||
const session = sync.session.get(sessionID)
|
||||
if (!session) return
|
||||
|
||||
const sessions = sync.data.session ?? []
|
||||
const index = sessions.findIndex((item) => item.id === sessionID)
|
||||
const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
|
||||
|
||||
await sdk.client.session
|
||||
.update({ sessionID, time: { archived: Date.now() } })
|
||||
.then(() => {
|
||||
sync.set(
|
||||
produce((draft) => {
|
||||
const index = draft.session.findIndex((item) => item.id === sessionID)
|
||||
if (index !== -1) draft.session.splice(index, 1)
|
||||
}),
|
||||
)
|
||||
navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
|
||||
})
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
title: language.t("common.requestFailed"),
|
||||
description: toastError(err),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const deleteSession = async (sessionID: string) => {
|
||||
const session = sync.session.get(sessionID)
|
||||
if (!session) return false
|
||||
|
||||
const sessions = (sync.data.session ?? []).filter((item) => !item.parentID && !item.time?.archived)
|
||||
const index = sessions.findIndex((item) => item.id === sessionID)
|
||||
const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
|
||||
|
||||
const result = await sdk.client.session
|
||||
.delete({ sessionID })
|
||||
.then((x) => x.data)
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
title: language.t("session.delete.failed.title"),
|
||||
description: toastError(err),
|
||||
})
|
||||
return false
|
||||
})
|
||||
|
||||
if (!result) return false
|
||||
|
||||
sync.set(
|
||||
produce((draft) => {
|
||||
const removed = new Set<string>([sessionID])
|
||||
const byParent = new Map<string, string[]>()
|
||||
|
||||
for (const item of draft.session) {
|
||||
const parentID = item.parentID
|
||||
if (!parentID) continue
|
||||
|
||||
const existing = byParent.get(parentID)
|
||||
if (existing) {
|
||||
existing.push(item.id)
|
||||
continue
|
||||
}
|
||||
byParent.set(parentID, [item.id])
|
||||
}
|
||||
|
||||
const stack = [sessionID]
|
||||
while (stack.length) {
|
||||
const parentID = stack.pop()
|
||||
if (!parentID) continue
|
||||
|
||||
const children = byParent.get(parentID)
|
||||
if (!children) continue
|
||||
|
||||
for (const child of children) {
|
||||
if (removed.has(child)) continue
|
||||
removed.add(child)
|
||||
stack.push(child)
|
||||
}
|
||||
}
|
||||
|
||||
draft.session = draft.session.filter((item) => !removed.has(item.id))
|
||||
}),
|
||||
)
|
||||
|
||||
navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
|
||||
return true
|
||||
}
|
||||
|
||||
const navigateParent = () => {
|
||||
const id = props.parentID()
|
||||
if (!id) return
|
||||
navigate(`/${params.dir}/session/${id}`)
|
||||
}
|
||||
|
||||
function DialogDeleteSession(input: { sessionID: string }) {
|
||||
const name = createMemo(() => sync.session.get(input.sessionID)?.title ?? language.t("command.session.new"))
|
||||
|
||||
const handleDelete = async () => {
|
||||
await deleteSession(input.sessionID)
|
||||
dialog.close()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog title={language.t("session.delete.title")} fit>
|
||||
<div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-14-regular text-text-strong">
|
||||
{language.t("session.delete.confirm", { name: name() })}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="ghost" size="large" onClick={() => dialog.close()}>
|
||||
{language.t("common.cancel")}
|
||||
</Button>
|
||||
<Button variant="primary" size="large" onClick={handleDelete}>
|
||||
{language.t("session.delete.button")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Show when={props.showHeader()}>
|
||||
<div
|
||||
data-session-title
|
||||
ref={(el) => {
|
||||
headerRef = el
|
||||
el.style.opacity = "0"
|
||||
}}
|
||||
class="pointer-events-none absolute inset-x-0 top-0 z-30"
|
||||
>
|
||||
<div
|
||||
classList={{
|
||||
"bg-[linear-gradient(to_bottom,var(--background-stronger)_38px,transparent)]": true,
|
||||
"w-full": true,
|
||||
"pb-10": true,
|
||||
"pl-2 pr-3 md:pl-4 md:pr-3": true,
|
||||
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
||||
}}
|
||||
>
|
||||
<div class="pointer-events-auto h-12 w-full flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-1 min-w-0 flex-1 pr-3">
|
||||
<Show when={props.parentID()}>
|
||||
<div>
|
||||
<IconButton
|
||||
tabIndex={-1}
|
||||
icon="arrow-left"
|
||||
variant="ghost"
|
||||
onClick={navigateParent}
|
||||
aria-label={language.t("common.goBack")}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={!!headerText.value || title.editing}>
|
||||
<Show
|
||||
when={title.editing}
|
||||
fallback={
|
||||
<h1 class="text-14-medium text-text-strong grow-1 min-w-0 pl-2" onDblClick={openTitleEditor}>
|
||||
<span class="grid min-w-0" style={{ overflow: "clip" }}>
|
||||
<span ref={enterRef} class="col-start-1 row-start-1 min-w-0 truncate">
|
||||
<span classList={{ "opacity-60": headerText.muted }}>{headerText.value}</span>
|
||||
</span>
|
||||
<span
|
||||
ref={leaveRef}
|
||||
class="col-start-1 row-start-1 min-w-0 truncate pointer-events-none"
|
||||
style={{ opacity: "0" }}
|
||||
>
|
||||
<span classList={{ "opacity-60": headerText.prevMuted }}>{headerText.prev}</span>
|
||||
</span>
|
||||
</span>
|
||||
</h1>
|
||||
}
|
||||
>
|
||||
<InlineInput
|
||||
ref={(el) => {
|
||||
titleRef = el
|
||||
}}
|
||||
value={title.draft}
|
||||
disabled={title.saving}
|
||||
class="text-14-medium text-text-strong grow-1 min-w-0 pl-2 rounded-[6px]"
|
||||
style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }}
|
||||
onInput={(event) => setTitle("draft", event.currentTarget.value)}
|
||||
onKeyDown={(event) => {
|
||||
event.stopPropagation()
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault()
|
||||
void saveTitleEditor()
|
||||
return
|
||||
}
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault()
|
||||
closeTitleEditor()
|
||||
}
|
||||
}}
|
||||
onBlur={closeTitleEditor}
|
||||
/>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={props.sessionID()}>
|
||||
{(id) => (
|
||||
<div class="shrink-0 flex items-center gap-3">
|
||||
<SessionContextUsage placement="bottom" />
|
||||
<DropdownMenu
|
||||
gutter={4}
|
||||
placement="bottom-end"
|
||||
open={title.menuOpen}
|
||||
onOpenChange={(open) => setTitle("menuOpen", open)}
|
||||
>
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
icon="dot-grid"
|
||||
variant="ghost"
|
||||
class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
|
||||
aria-label={language.t("common.moreOptions")}
|
||||
/>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
style={{ "min-width": "104px" }}
|
||||
onCloseAutoFocus={(event) => {
|
||||
if (!title.pendingRename) return
|
||||
event.preventDefault()
|
||||
setTitle("pendingRename", false)
|
||||
openTitleEditor()
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
setTitle("pendingRename", true)
|
||||
setTitle("menuOpen", false)
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onSelect={() => void archiveSession(id())}>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id()} />)}>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { messageIdFromHash } from "./use-session-hash-scroll"
|
||||
import { messageIdFromHash } from "./message-id-from-hash"
|
||||
|
||||
describe("messageIdFromHash", () => {
|
||||
test("parses hash with leading #", () => {
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { createEffect, createMemo, onCleanup, onMount } from "solid-js"
|
||||
import { UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import type { UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { useLocation, useNavigate } from "@solidjs/router"
|
||||
import { createEffect, createMemo, onMount } from "solid-js"
|
||||
import { messageIdFromHash } from "./message-id-from-hash"
|
||||
|
||||
export const messageIdFromHash = (hash: string) => {
|
||||
const value = hash.startsWith("#") ? hash.slice(1) : hash
|
||||
const match = value.match(/^message-(.+)$/)
|
||||
if (!match) return
|
||||
return match[1]
|
||||
}
|
||||
export { messageIdFromHash } from "./message-id-from-hash"
|
||||
|
||||
export const useSessionHashScroll = (input: {
|
||||
sessionKey: () => string
|
||||
@@ -19,7 +16,7 @@ export const useSessionHashScroll = (input: {
|
||||
setPendingMessage: (value: string | undefined) => void
|
||||
setActiveMessage: (message: UserMessage | undefined) => void
|
||||
setTurnStart: (value: number) => void
|
||||
autoScroll: { pause: () => void; forceScrollToBottom: () => void }
|
||||
autoScroll: { pause: () => void; snapToBottom: () => void }
|
||||
scroller: () => HTMLDivElement | undefined
|
||||
anchor: (id: string) => string
|
||||
scheduleScrollState: (el: HTMLDivElement) => void
|
||||
@@ -30,13 +27,18 @@ export const useSessionHashScroll = (input: {
|
||||
const messageIndex = createMemo(() => new Map(visibleUserMessages().map((m, i) => [m.id, i])))
|
||||
let pendingKey = ""
|
||||
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const clearMessageHash = () => {
|
||||
if (!window.location.hash) return
|
||||
window.history.replaceState(null, "", window.location.href.replace(/#.*$/, ""))
|
||||
if (!location.hash) return
|
||||
navigate(location.pathname + location.search, { replace: true })
|
||||
}
|
||||
|
||||
const updateHash = (id: string) => {
|
||||
window.history.replaceState(null, "", `#${input.anchor(id)}`)
|
||||
navigate(location.pathname + location.search + `#${input.anchor(id)}`, {
|
||||
replace: true,
|
||||
})
|
||||
}
|
||||
|
||||
const scrollToElement = (el: HTMLElement, behavior: ScrollBehavior) => {
|
||||
@@ -45,14 +47,16 @@ export const useSessionHashScroll = (input: {
|
||||
|
||||
const a = el.getBoundingClientRect()
|
||||
const b = root.getBoundingClientRect()
|
||||
const sticky = root.querySelector("[data-session-title]")
|
||||
const inset = sticky instanceof HTMLElement ? sticky.offsetHeight : 0
|
||||
const top = Math.max(0, a.top - b.top + root.scrollTop - inset)
|
||||
const title = parseFloat(getComputedStyle(root).getPropertyValue("--session-title-height"))
|
||||
const inset = Number.isNaN(title) ? 0 : title
|
||||
// With column-reverse, scrollTop is negative — don't clamp to 0
|
||||
const top = a.top - b.top + root.scrollTop - inset
|
||||
root.scrollTo({ top, behavior })
|
||||
return true
|
||||
}
|
||||
|
||||
const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => {
|
||||
console.log({ message, behavior })
|
||||
if (input.currentMessageId() !== message.id) input.setActiveMessage(message)
|
||||
|
||||
const index = messageIndex().get(message.id) ?? -1
|
||||
@@ -100,9 +104,9 @@ export const useSessionHashScroll = (input: {
|
||||
}
|
||||
|
||||
const applyHash = (behavior: ScrollBehavior) => {
|
||||
const hash = window.location.hash.slice(1)
|
||||
const hash = location.hash.slice(1)
|
||||
if (!hash) {
|
||||
input.autoScroll.forceScrollToBottom()
|
||||
input.autoScroll.snapToBottom()
|
||||
const el = input.scroller()
|
||||
if (el) input.scheduleScrollState(el)
|
||||
return
|
||||
@@ -126,12 +130,13 @@ export const useSessionHashScroll = (input: {
|
||||
return
|
||||
}
|
||||
|
||||
input.autoScroll.forceScrollToBottom()
|
||||
input.autoScroll.snapToBottom()
|
||||
const el = input.scroller()
|
||||
if (el) input.scheduleScrollState(el)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
location.hash
|
||||
if (!input.sessionID() || !input.messagesReady()) return
|
||||
requestAnimationFrame(() => applyHash("auto"))
|
||||
})
|
||||
@@ -155,7 +160,7 @@ export const useSessionHashScroll = (input: {
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetId) targetId = messageIdFromHash(window.location.hash)
|
||||
if (!targetId) targetId = messageIdFromHash(location.hash)
|
||||
if (!targetId) return
|
||||
if (input.currentMessageId() === targetId) return
|
||||
|
||||
@@ -171,14 +176,6 @@ export const useSessionHashScroll = (input: {
|
||||
if (typeof window !== "undefined" && "scrollRestoration" in window.history) {
|
||||
window.history.scrollRestoration = "manual"
|
||||
}
|
||||
|
||||
const handler = () => {
|
||||
if (!input.sessionID() || !input.messagesReady()) return
|
||||
requestAnimationFrame(() => applyHash("auto"))
|
||||
}
|
||||
|
||||
window.addEventListener("hashchange", handler)
|
||||
onCleanup(() => window.removeEventListener("hashchange", handler))
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
export function isEditableTarget(target: EventTarget | null | undefined) {
|
||||
if (!(target instanceof HTMLElement)) return false
|
||||
if (/^(INPUT|TEXTAREA|SELECT|BUTTON)$/.test(target.tagName)) return true
|
||||
if (target.isContentEditable) return true
|
||||
if (target.closest("[contenteditable='true']")) return true
|
||||
if (target.closest("input, textarea, select")) return true
|
||||
return false
|
||||
}
|
||||
|
||||
export function getCharacterOffsetInLine(lineElement: Element, targetNode: Node, offset: number): number {
|
||||
const r = document.createRange()
|
||||
r.selectNodeContents(lineElement)
|
||||
|
||||
@@ -7,6 +7,7 @@ export const setNavigate = (fn: (href: string) => void) => {
|
||||
export const handleNotificationClick = (href?: string) => {
|
||||
window.focus()
|
||||
if (!href) return
|
||||
if (nav) nav(href)
|
||||
else window.location.assign(href)
|
||||
if (nav) return nav(href)
|
||||
console.warn("notification-click: navigate function not set, falling back to window.location.assign")
|
||||
window.location.assign(href)
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export function same<T>(a: readonly T[] | undefined, b: readonly T[] | undefined) {
|
||||
if (a === b) return true
|
||||
if (!a || !b) return false
|
||||
if (a.length !== b.length) return false
|
||||
return a.every((x, i) => x === b[i])
|
||||
}
|
||||
@@ -1,8 +1,37 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { ConfigInvalidError } from "./server-errors"
|
||||
import { formatServerError, parseReabaleConfigInvalidError } from "./server-errors"
|
||||
import type { ConfigInvalidError, ProviderModelNotFoundError } from "./server-errors"
|
||||
import { formatServerError, parseReadableConfigInvalidError } from "./server-errors"
|
||||
|
||||
describe("parseReabaleConfigInvalidError", () => {
|
||||
function fill(text: string, vars?: Record<string, string | number>) {
|
||||
if (!vars) return text
|
||||
return text.replace(/{{\s*(\w+)\s*}}/g, (_, key: string) => {
|
||||
const value = vars[key]
|
||||
if (value === undefined) return ""
|
||||
return String(value)
|
||||
})
|
||||
}
|
||||
|
||||
function useLanguageMock() {
|
||||
const dict: Record<string, string> = {
|
||||
"error.chain.unknown": "Erro desconhecido",
|
||||
"error.chain.configInvalid": "Arquivo de config em {{path}} invalido",
|
||||
"error.chain.configInvalidWithMessage": "Arquivo de config em {{path}} invalido: {{message}}",
|
||||
"error.chain.modelNotFound": "Modelo nao encontrado: {{provider}}/{{model}}",
|
||||
"error.chain.didYouMean": "Voce quis dizer: {{suggestions}}",
|
||||
"error.chain.checkConfig": "Revise provider/model no config",
|
||||
}
|
||||
return {
|
||||
t(key: string, vars?: Record<string, string | number>) {
|
||||
const text = dict[key]
|
||||
if (!text) return key
|
||||
return fill(text, vars)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const language = useLanguageMock()
|
||||
|
||||
describe("parseReadableConfigInvalidError", () => {
|
||||
test("formats issues with file path", () => {
|
||||
const error = {
|
||||
name: "ConfigInvalidError",
|
||||
@@ -15,10 +44,10 @@ describe("parseReabaleConfigInvalidError", () => {
|
||||
},
|
||||
} satisfies ConfigInvalidError
|
||||
|
||||
const result = parseReabaleConfigInvalidError(error)
|
||||
const result = parseReadableConfigInvalidError(error, language.t)
|
||||
|
||||
expect(result).toBe(
|
||||
["Invalid configuration", "opencode.config.ts", "settings.host: Required", "mode: Invalid"].join("\n"),
|
||||
["Arquivo de config em opencode.config.ts invalido: settings.host: Required", "mode: Invalid"].join("\n"),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -31,9 +60,9 @@ describe("parseReabaleConfigInvalidError", () => {
|
||||
},
|
||||
} satisfies ConfigInvalidError
|
||||
|
||||
const result = parseReabaleConfigInvalidError(error)
|
||||
const result = parseReadableConfigInvalidError(error, language.t)
|
||||
|
||||
expect(result).toBe(["Invalid configuration", "Bad value"].join("\n"))
|
||||
expect(result).toBe("Arquivo de config em config invalido: Bad value")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -46,24 +75,57 @@ describe("formatServerError", () => {
|
||||
},
|
||||
} satisfies ConfigInvalidError
|
||||
|
||||
const result = formatServerError(error)
|
||||
const result = formatServerError(error, language.t)
|
||||
|
||||
expect(result).toBe(["Invalid configuration", "Missing host"].join("\n"))
|
||||
expect(result).toBe("Arquivo de config em config invalido: Missing host")
|
||||
})
|
||||
|
||||
test("returns error messages", () => {
|
||||
expect(formatServerError(new Error("Request failed with status 503"))).toBe("Request failed with status 503")
|
||||
expect(formatServerError(new Error("Request failed with status 503"), language.t)).toBe(
|
||||
"Request failed with status 503",
|
||||
)
|
||||
})
|
||||
|
||||
test("returns provided string errors", () => {
|
||||
expect(formatServerError("Failed to connect to server")).toBe("Failed to connect to server")
|
||||
expect(formatServerError("Failed to connect to server", language.t)).toBe("Failed to connect to server")
|
||||
})
|
||||
|
||||
test("falls back to unknown", () => {
|
||||
expect(formatServerError(0)).toBe("Unknown error")
|
||||
test("uses translated unknown fallback", () => {
|
||||
expect(formatServerError(0, language.t)).toBe("Erro desconhecido")
|
||||
})
|
||||
|
||||
test("falls back for unknown error objects and names", () => {
|
||||
expect(formatServerError({ name: "ServerTimeoutError", data: { seconds: 30 } })).toBe("Unknown error")
|
||||
expect(formatServerError({ name: "ServerTimeoutError", data: { seconds: 30 } }, language.t)).toBe(
|
||||
"Erro desconhecido",
|
||||
)
|
||||
})
|
||||
|
||||
test("formats provider model errors using provider/model", () => {
|
||||
const error = {
|
||||
name: "ProviderModelNotFoundError",
|
||||
data: {
|
||||
providerID: "openai",
|
||||
modelID: "gpt-4.1",
|
||||
},
|
||||
} satisfies ProviderModelNotFoundError
|
||||
|
||||
expect(formatServerError(error, language.t)).toBe(
|
||||
["Modelo nao encontrado: openai/gpt-4.1", "Revise provider/model no config"].join("\n"),
|
||||
)
|
||||
})
|
||||
|
||||
test("formats provider model suggestions", () => {
|
||||
const error = {
|
||||
name: "ProviderModelNotFoundError",
|
||||
data: {
|
||||
providerID: "x",
|
||||
modelID: "y",
|
||||
suggestions: ["x/y2", "x/y3"],
|
||||
},
|
||||
} satisfies ProviderModelNotFoundError
|
||||
|
||||
expect(formatServerError(error, language.t)).toBe(
|
||||
["Modelo nao encontrado: x/y", "Voce quis dizer: x/y2, x/y3", "Revise provider/model no config"].join("\n"),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,28 +7,31 @@ export type ConfigInvalidError = {
|
||||
}
|
||||
}
|
||||
|
||||
type Label = {
|
||||
unknown: string
|
||||
invalidConfiguration: string
|
||||
}
|
||||
|
||||
const fallback: Label = {
|
||||
unknown: "Unknown error",
|
||||
invalidConfiguration: "Invalid configuration",
|
||||
}
|
||||
|
||||
function resolveLabel(labels: Partial<Label> | undefined): Label {
|
||||
return {
|
||||
unknown: labels?.unknown ?? fallback.unknown,
|
||||
invalidConfiguration: labels?.invalidConfiguration ?? fallback.invalidConfiguration,
|
||||
export type ProviderModelNotFoundError = {
|
||||
name: "ProviderModelNotFoundError"
|
||||
data: {
|
||||
providerID: string
|
||||
modelID: string
|
||||
suggestions?: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export function formatServerError(error: unknown, labels?: Partial<Label>) {
|
||||
if (isConfigInvalidErrorLike(error)) return parseReabaleConfigInvalidError(error, labels)
|
||||
type Translator = (key: string, vars?: Record<string, string | number>) => string
|
||||
|
||||
function tr(translator: Translator | undefined, key: string, text: string, vars?: Record<string, string | number>) {
|
||||
if (!translator) return text
|
||||
const out = translator(key, vars)
|
||||
if (!out || out === key) return text
|
||||
return out
|
||||
}
|
||||
|
||||
export function formatServerError(error: unknown, translate?: Translator, fallback?: string) {
|
||||
if (isConfigInvalidErrorLike(error)) return parseReadableConfigInvalidError(error, translate)
|
||||
if (isProviderModelNotFoundErrorLike(error)) return parseReadableProviderModelNotFoundError(error, translate)
|
||||
if (error instanceof Error && error.message) return error.message
|
||||
if (typeof error === "string" && error) return error
|
||||
return resolveLabel(labels).unknown
|
||||
if (fallback) return fallback
|
||||
return tr(translate, "error.chain.unknown", "Unknown error")
|
||||
}
|
||||
|
||||
function isConfigInvalidErrorLike(error: unknown): error is ConfigInvalidError {
|
||||
@@ -37,13 +40,41 @@ function isConfigInvalidErrorLike(error: unknown): error is ConfigInvalidError {
|
||||
return o.name === "ConfigInvalidError" && typeof o.data === "object" && o.data !== null
|
||||
}
|
||||
|
||||
export function parseReabaleConfigInvalidError(errorInput: ConfigInvalidError, labels?: Partial<Label>) {
|
||||
const head = resolveLabel(labels).invalidConfiguration
|
||||
const file = errorInput.data.path && errorInput.data.path !== "config" ? errorInput.data.path : ""
|
||||
const detail = errorInput.data.message?.trim() ?? ""
|
||||
const issues = (errorInput.data.issues ?? []).map((issue) => {
|
||||
return `${issue.path.join(".")}: ${issue.message}`
|
||||
})
|
||||
if (issues.length) return [head, file, "", ...issues].filter(Boolean).join("\n")
|
||||
return [head, file, detail].filter(Boolean).join("\n")
|
||||
function isProviderModelNotFoundErrorLike(error: unknown): error is ProviderModelNotFoundError {
|
||||
if (typeof error !== "object" || error === null) return false
|
||||
const o = error as Record<string, unknown>
|
||||
return o.name === "ProviderModelNotFoundError" && typeof o.data === "object" && o.data !== null
|
||||
}
|
||||
|
||||
export function parseReadableConfigInvalidError(errorInput: ConfigInvalidError, translator?: Translator) {
|
||||
const file = errorInput.data.path && errorInput.data.path !== "config" ? errorInput.data.path : "config"
|
||||
const detail = errorInput.data.message?.trim() ?? ""
|
||||
const issues = (errorInput.data.issues ?? [])
|
||||
.map((issue) => {
|
||||
const msg = issue.message.trim()
|
||||
if (!issue.path.length) return msg
|
||||
return `${issue.path.join(".")}: ${msg}`
|
||||
})
|
||||
.filter(Boolean)
|
||||
const msg = issues.length ? issues.join("\n") : detail
|
||||
if (!msg) return tr(translator, "error.chain.configInvalid", `Config file at ${file} is invalid`, { path: file })
|
||||
return tr(translator, "error.chain.configInvalidWithMessage", `Config file at ${file} is invalid: ${msg}`, {
|
||||
path: file,
|
||||
message: msg,
|
||||
})
|
||||
}
|
||||
|
||||
function parseReadableProviderModelNotFoundError(errorInput: ProviderModelNotFoundError, translator?: Translator) {
|
||||
const p = errorInput.data.providerID.trim()
|
||||
const m = errorInput.data.modelID.trim()
|
||||
const list = (errorInput.data.suggestions ?? []).map((v) => v.trim()).filter(Boolean)
|
||||
const body = tr(translator, "error.chain.modelNotFound", `Model not found: ${p}/${m}`, { provider: p, model: m })
|
||||
const tail = tr(translator, "error.chain.checkConfig", "Check your config (opencode.json) provider/model names")
|
||||
if (list.length) {
|
||||
const suggestions = list.slice(0, 5).join(", ")
|
||||
return [body, tr(translator, "error.chain.didYouMean", `Did you mean: ${suggestions}`, { suggestions }), tail].join(
|
||||
"\n",
|
||||
)
|
||||
}
|
||||
return [body, tail].join("\n")
|
||||
}
|
||||
|
||||
@@ -19,4 +19,4 @@ export function getRelativeTime(dateString: string, t: Translate): string {
|
||||
if (diffMinutes < 60) return t("common.time.minutesAgo.short", { count: diffMinutes })
|
||||
if (diffHours < 24) return t("common.time.hoursAgo.short", { count: diffHours })
|
||||
return t("common.time.daysAgo.short", { count: diffDays })
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ async function getMainRoutes(): Promise<SitemapEntry[]> {
|
||||
{ path: "/enterprise", priority: 0.8, changefreq: "weekly" },
|
||||
{ path: "/brand", priority: 0.6, changefreq: "monthly" },
|
||||
{ path: "/zen", priority: 0.8, changefreq: "weekly" },
|
||||
{ path: "/go", priority: 0.8, changefreq: "weekly" },
|
||||
]
|
||||
|
||||
for (const item of staticRoutes) {
|
||||
|
||||
6
packages/console/app/src/asset/go-ornate-dark.svg
Normal file
6
packages/console/app/src/asset/go-ornate-dark.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="54" height="30" viewBox="0 0 54 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M24 30H0V0H24V6H6V24H18V18H12V12H24V30Z" fill="#F1ECEC"/>
|
||||
<path d="M12 18H18V24H6V12H12V18Z" fill="#4B4646"/>
|
||||
<path d="M48 12V24H36V12H48Z" fill="#4B4646"/>
|
||||
<path d="M54 30H30V0H54V30ZM36 24H48V6H36V24Z" fill="#F1ECEC"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 333 B |
6
packages/console/app/src/asset/go-ornate-light.svg
Normal file
6
packages/console/app/src/asset/go-ornate-light.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="54" height="30" viewBox="0 0 54 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M24 30H0V0H24V6H6V24H18V18H12V12H24V30Z" fill="#211E1E"/>
|
||||
<path d="M12 18H18V24H6V12H12V18Z" fill="#CFCECD"/>
|
||||
<path d="M48 12V24H36V12H48Z" fill="#CFCECD"/>
|
||||
<path d="M54 30H30V0H54V30ZM36 24H48V6H36V24Z" fill="#211E1E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 333 B |
@@ -36,7 +36,7 @@ const fetchSvgContent = async (svgPath: string): Promise<string> => {
|
||||
}
|
||||
}
|
||||
|
||||
export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) {
|
||||
export function Header(props: { zen?: boolean; go?: boolean; hideGetStarted?: boolean }) {
|
||||
const navigate = useNavigate()
|
||||
const i18n = useI18n()
|
||||
const language = useLanguage()
|
||||
@@ -161,19 +161,24 @@ export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) {
|
||||
<li>
|
||||
<a href={language.route("/docs")}>{i18n.t("nav.docs")}</a>
|
||||
</li>
|
||||
<Show when={!props.zen}>
|
||||
<li>
|
||||
<A href={language.route("/zen")}>{i18n.t("nav.zen")}</A>
|
||||
</li>
|
||||
</Show>
|
||||
<Show when={!props.go}>
|
||||
<li>
|
||||
<A href={language.route("/go")}>{i18n.t("nav.go")}</A>
|
||||
</li>
|
||||
</Show>
|
||||
<li>
|
||||
<A href={language.route("/enterprise")}>{i18n.t("nav.enterprise")}</A>
|
||||
</li>
|
||||
<li>
|
||||
<Switch>
|
||||
<Match when={props.zen}>
|
||||
<a href="/auth">{i18n.t("nav.login")}</a>
|
||||
</Match>
|
||||
<Match when={!props.zen}>
|
||||
<A href={language.route("/zen")}>{i18n.t("nav.zen")}</A>
|
||||
</Match>
|
||||
</Switch>
|
||||
</li>
|
||||
<Show when={props.zen || props.go}>
|
||||
<li>
|
||||
<a href="/auth">{i18n.t("nav.login")}</a>
|
||||
</li>
|
||||
</Show>
|
||||
<Show when={!props.hideGetStarted}>
|
||||
<li>
|
||||
<A href={language.route("/download")} data-slot="cta-button">
|
||||
@@ -257,19 +262,24 @@ export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) {
|
||||
<li>
|
||||
<a href={language.route("/docs")}>{i18n.t("nav.docs")}</a>
|
||||
</li>
|
||||
<Show when={!props.zen}>
|
||||
<li>
|
||||
<A href={language.route("/zen")}>{i18n.t("nav.zen")}</A>
|
||||
</li>
|
||||
</Show>
|
||||
<Show when={!props.go}>
|
||||
<li>
|
||||
<A href={language.route("/go")}>{i18n.t("nav.go")}</A>
|
||||
</li>
|
||||
</Show>
|
||||
<li>
|
||||
<A href={language.route("/enterprise")}>{i18n.t("nav.enterprise")}</A>
|
||||
</li>
|
||||
<li>
|
||||
<Switch>
|
||||
<Match when={props.zen}>
|
||||
<a href="/auth">{i18n.t("nav.login")}</a>
|
||||
</Match>
|
||||
<Match when={!props.zen}>
|
||||
<A href={language.route("/zen")}>{i18n.t("nav.zen")}</A>
|
||||
</Match>
|
||||
</Switch>
|
||||
</li>
|
||||
<Show when={props.zen || props.go}>
|
||||
<li>
|
||||
<a href="/auth">{i18n.t("nav.login")}</a>
|
||||
</li>
|
||||
</Show>
|
||||
<Show when={!props.hideGetStarted}>
|
||||
<li>
|
||||
<A href={language.route("/download")} data-slot="cta-button">
|
||||
|
||||
@@ -247,6 +247,104 @@ export const dict = {
|
||||
"تتم استضافة جميع نماذج Zen في الولايات المتحدة. يتبع المزودون سياسة عدم الاحتفاظ بالبيانات ولا يستخدمون بياناتك لتدريب النماذج، مع",
|
||||
"zen.privacy.exceptionsLink": "الاستثناءات التالية",
|
||||
|
||||
"go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع",
|
||||
"go.meta.description":
|
||||
"Go هو اشتراك بقيمة 10 دولارات شهريًا مع حدود سخية تبلغ 5 ساعات للطلبات لنماذج GLM-5 وKimi K2.5 وMiniMax M2.5.",
|
||||
"go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع",
|
||||
"go.hero.body":
|
||||
"يجلب Go البرمجة الوكيلة للمبرمجين حول العالم. يوفر حدودًا سخية ووصولًا موثوقًا إلى أقوى النماذج مفتوحة المصدر، حتى تتمكن من البناء باستخدام وكلاء أقوياء دون القلق بشأن التكلفة أو التوفر.",
|
||||
|
||||
"go.cta.start": "اشترك في Go",
|
||||
"go.cta.template": "{{text}} {{price}}",
|
||||
"go.cta.text": "اشترك في Go",
|
||||
"go.cta.price": "$10/شهر",
|
||||
"go.pricing.body": "استخدمه مع أي وكيل. اشحن الرصيد إذا لزم الأمر. ألغِ في أي وقت.",
|
||||
"go.graph.free": "مجاني",
|
||||
"go.graph.freePill": "Big Pickle ونماذج مجانية",
|
||||
"go.graph.go": "Go",
|
||||
"go.graph.label": "الطلبات كل 5 ساعات",
|
||||
"go.graph.usageLimits": "حدود الاستخدام",
|
||||
"go.graph.tick": "{{n}}x",
|
||||
"go.graph.aria": "الطلبات كل 5 ساعات: {{free}} مقابل {{go}}",
|
||||
|
||||
"go.testimonials.brand.zen": "Zen",
|
||||
"go.testimonials.brand.go": "Go",
|
||||
"go.testimonials.handle": "@OpenCode",
|
||||
"go.testimonials.dax.name": "Dax Raad",
|
||||
"go.testimonials.dax.title": "الرئيس التنفيذي السابق، منتجات Terminal",
|
||||
"go.testimonials.dax.quoteAfter": "كان تغييرًا جذريًا في الحياة، إنه قرار لا يحتاج لتفكير.",
|
||||
"go.testimonials.jay.name": "Jay V",
|
||||
"go.testimonials.jay.title": "مؤسس سابق، SEED، PM، Melt، Pop، Dapt، Cadmus، وViewPoint",
|
||||
"go.testimonials.jay.quoteBefore": "4 من كل 5 أشخاص في فريقنا يحبون استخدام",
|
||||
"go.testimonials.jay.quoteAfter": ".",
|
||||
"go.testimonials.adam.name": "Adam Elmore",
|
||||
"go.testimonials.adam.title": "بطل سابق، AWS",
|
||||
"go.testimonials.adam.quoteBefore": "لا أستطيع التوصية بـ",
|
||||
"go.testimonials.adam.quoteAfter": "بما فيه الكفاية. بجدية، إنه جيد حقًا.",
|
||||
"go.testimonials.david.name": "David Hill",
|
||||
"go.testimonials.david.title": "رئيس التصميم السابق، Laravel",
|
||||
"go.testimonials.david.quoteBefore": "مع",
|
||||
"go.testimonials.david.quoteAfter": "أعلم أن جميع النماذج مختبرة ومثالية لوكلاء البرمجة.",
|
||||
"go.testimonials.frank.name": "Frank Wang",
|
||||
"go.testimonials.frank.title": "متدرب سابق، Nvidia (4 مرات)",
|
||||
"go.testimonials.frank.quote": "أتمنى لو كنت لا أزال في Nvidia.",
|
||||
"go.problem.title": "ما المشكلة التي يحلها Go؟",
|
||||
"go.problem.body":
|
||||
"نحن نركز على جلب تجربة OpenCode لأكبر عدد ممكن من الناس. OpenCode Go هو اشتراك منخفض التكلفة (10 دولارات شهريًا) مصمم لجلب البرمجة الوكيلة للمبرمجين حول العالم. يوفر حدودًا سخية ووصولًا موثوقًا إلى أقوى النماذج مفتوحة المصدر.",
|
||||
"go.problem.subtitle": " ",
|
||||
"go.problem.item1": "أسعار اشتراك منخفضة التكلفة",
|
||||
"go.problem.item2": "حدود سخية ووصول موثوق",
|
||||
"go.problem.item3": "مصمم لأكبر عدد ممكن من المبرمجين",
|
||||
"go.problem.item4": "يتضمن GLM-5 وKimi K2.5 وMiniMax M2.5",
|
||||
"go.how.title": "كيف يعمل Go",
|
||||
"go.how.body": "Go هو اشتراك بقيمة 10 دولارات شهريًا يمكنك استخدامه مع OpenCode أو أي وكيل.",
|
||||
"go.how.step1.title": "أنشئ حسابًا",
|
||||
"go.how.step1.beforeLink": "اتبع",
|
||||
"go.how.step1.link": "تعليمات الإعداد",
|
||||
"go.how.step2.title": "اشترك في Go",
|
||||
"go.how.step2.link": "$10/شهر",
|
||||
"go.how.step2.afterLink": "مع حدود سخية",
|
||||
"go.how.step3.title": "ابدأ البرمجة",
|
||||
"go.how.step3.body": "مع وصول موثوق لنماذج مفتوحة المصدر",
|
||||
"go.privacy.title": "خصوصيتك مهمة بالنسبة لنا",
|
||||
"go.privacy.body":
|
||||
"تم تصميم الخطة بشكل أساسي للمستخدمين الدوليين، مع استضافة النماذج في الولايات المتحدة والاتحاد الأوروبي وسنغافورة للحصول على وصول عالمي مستقر.",
|
||||
"go.privacy.contactAfter": "إذا كان لديك أي أسئلة.",
|
||||
"go.privacy.beforeExceptions":
|
||||
"تتم استضافة نماذج Go في الولايات المتحدة. يتبع المزودون سياسة عدم الاحتفاظ بالبيانات ولا يستخدمون بياناتك لتدريب النماذج، مع",
|
||||
"go.privacy.exceptionsLink": "الاستثناءات التالية",
|
||||
"go.faq.q1": "ما هو OpenCode Go؟",
|
||||
"go.faq.a1": "Go هو اشتراك منخفض التكلفة يمنحك وصولًا موثوقًا إلى نماذج مفتوحة المصدر قادرة على البرمجة الوكيلة.",
|
||||
"go.faq.q2": "ما النماذج التي يتضمنها Go؟",
|
||||
"go.faq.a2": "يتضمن Go نماذج GLM-5 وKimi K2.5 وMiniMax M2.5، مع حدود سخية ووصول موثوق.",
|
||||
"go.faq.q3": "هل Go هو نفسه Zen؟",
|
||||
"go.faq.a3":
|
||||
"لا. Zen هو نظام الدفع حسب الاستخدام، بينما Go هو اشتراك بقيمة 10 دولارات شهريًا مع حدود سخية ووصول موثوق لنماذج مفتوحة المصدر GLM-5 وKimi K2.5 وMiniMax M2.5.",
|
||||
"go.faq.q4": "كم تكلفة Go؟",
|
||||
"go.faq.a4.p1.beforePricing": "تكلفة Go",
|
||||
"go.faq.a4.p1.pricingLink": "$10/شهر",
|
||||
"go.faq.a4.p1.afterPricing": "مع حدود سخية.",
|
||||
"go.faq.a4.p2.beforeAccount": "يمكنك إدارة اشتراكك في",
|
||||
"go.faq.a4.p2.accountLink": "حسابك",
|
||||
"go.faq.a4.p3": "ألغِ في أي وقت.",
|
||||
"go.faq.q5": "ماذا عن البيانات والخصوصية؟",
|
||||
"go.faq.a5.body":
|
||||
"تم تصميم الخطة بشكل أساسي للمستخدمين الدوليين، مع استضافة النماذج في الولايات المتحدة والاتحاد الأوروبي وسنغافورة للحصول على وصول عالمي مستقر.",
|
||||
"go.faq.a5.contactAfter": "إذا كان لديك أي أسئلة.",
|
||||
"go.faq.a5.beforeExceptions":
|
||||
"تتم استضافة نماذج Go في الولايات المتحدة. يتبع المزودون سياسة عدم الاحتفاظ بالبيانات ولا يستخدمون بياناتك لتدريب النماذج، مع",
|
||||
"go.faq.a5.exceptionsLink": "الاستثناءات التالية",
|
||||
"go.faq.q6": "هل يمكنني شحن رصيد إضافي؟",
|
||||
"go.faq.a6": "إذا كنت بحاجة إلى مزيد من الاستخدام، يمكنك شحن رصيد في حسابك.",
|
||||
"go.faq.q7": "هل يمكنني الإلغاء؟",
|
||||
"go.faq.a7": "نعم، يمكنك الإلغاء في أي وقت.",
|
||||
"go.faq.q8": "هل يمكنني استخدام Go مع وكلاء برمجة آخرين؟",
|
||||
"go.faq.a8": "نعم، يمكنك استخدام Go مع أي وكيل. اتبع تعليمات الإعداد في وكيل البرمجة المفضل لديك.",
|
||||
|
||||
"go.faq.q9": "ما الفرق بين النماذج المجانية وGo؟",
|
||||
"go.faq.a9":
|
||||
"تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة 200 طلب/يوم. يتضمن Go نماذج GLM-5 وKimi K2.5 وMiniMax M2.5 مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، أسبوعيًا، وشهريًا)، تعادل تقريبًا 12 دولارًا كل 5 ساعات، و30 دولارًا في الأسبوع، و60 دولارًا في الشهر (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "تم تجاوز حد الطلبات. يرجى المحاولة مرة أخرى لاحقًا.",
|
||||
"zen.api.error.modelNotSupported": "النموذج {{model}} غير مدعوم",
|
||||
"zen.api.error.modelFormatNotSupported": "النموذج {{model}} غير مدعوم للتنسيق {{format}}",
|
||||
|
||||
@@ -251,6 +251,107 @@ export const dict = {
|
||||
"Todos os modelos Zen são hospedados nos EUA. Os provedores seguem uma política de retenção zero e não usam seus dados para treinamento de modelo, com as",
|
||||
"zen.privacy.exceptionsLink": "seguintes exceções",
|
||||
|
||||
"go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos",
|
||||
"go.meta.description":
|
||||
"O Go é uma assinatura de $10/mês com limites generosos de 5 horas de requisição para GLM-5, Kimi K2.5 e MiniMax M2.5.",
|
||||
"go.hero.title": "Modelos de codificação de baixo custo para todos",
|
||||
"go.hero.body":
|
||||
"O Go traz a codificação com agentes para programadores em todo o mundo. Oferecendo limites generosos e acesso confiável aos modelos de código aberto mais capazes, para que você possa construir com agentes poderosos sem se preocupar com custos ou disponibilidade.",
|
||||
|
||||
"go.cta.start": "Assinar o Go",
|
||||
"go.cta.template": "{{text}} {{price}}",
|
||||
"go.cta.text": "Assinar o Go",
|
||||
"go.cta.price": "$10/mês",
|
||||
"go.pricing.body": "Use com qualquer agente. Recarregue crédito se necessário. Cancele a qualquer momento.",
|
||||
"go.graph.free": "Grátis",
|
||||
"go.graph.freePill": "Big Pickle e modelos gratuitos",
|
||||
"go.graph.go": "Go",
|
||||
"go.graph.label": "Requisições por 5 horas",
|
||||
"go.graph.usageLimits": "Limites de uso",
|
||||
"go.graph.tick": "{{n}}x",
|
||||
"go.graph.aria": "Requisições por 5h: {{free}} vs {{go}}",
|
||||
|
||||
"go.testimonials.brand.zen": "Zen",
|
||||
"go.testimonials.brand.go": "Go",
|
||||
"go.testimonials.handle": "@OpenCode",
|
||||
"go.testimonials.dax.name": "Dax Raad",
|
||||
"go.testimonials.dax.title": "ex-CEO, Terminal Products",
|
||||
"go.testimonials.dax.quoteAfter": "mudou minha vida, é realmente uma escolha óbvia.",
|
||||
"go.testimonials.jay.name": "Jay V",
|
||||
"go.testimonials.jay.title": "ex-Fundador, SEED, PM, Melt, Pop, Dapt, Cadmus e ViewPoint",
|
||||
"go.testimonials.jay.quoteBefore": "4 de 5 pessoas em nossa equipe adoram usar",
|
||||
"go.testimonials.jay.quoteAfter": ".",
|
||||
"go.testimonials.adam.name": "Adam Elmore",
|
||||
"go.testimonials.adam.title": "ex-Hero, AWS",
|
||||
"go.testimonials.adam.quoteBefore": "Eu não consigo recomendar o",
|
||||
"go.testimonials.adam.quoteAfter": "o suficiente. Sério, é muito bom.",
|
||||
"go.testimonials.david.name": "David Hill",
|
||||
"go.testimonials.david.title": "ex-Head de Design, Laravel",
|
||||
"go.testimonials.david.quoteBefore": "Com o",
|
||||
"go.testimonials.david.quoteAfter":
|
||||
"eu sei que todos os modelos são testados e perfeitos para agentes de codificação.",
|
||||
"go.testimonials.frank.name": "Frank Wang",
|
||||
"go.testimonials.frank.title": "ex-Estagiário, Nvidia (4 vezes)",
|
||||
"go.testimonials.frank.quote": "Eu queria ainda estar na Nvidia.",
|
||||
"go.problem.title": "Que problema o Go resolve?",
|
||||
"go.problem.body":
|
||||
"Estamos focados em levar a experiência OpenCode para o maior número possível de pessoas. OpenCode Go é uma assinatura de baixo custo ($10/mês) projetada para levar a codificação com agentes para programadores em todo o mundo. Fornece limites generosos e acesso confiável aos modelos de código aberto mais capazes.",
|
||||
"go.problem.subtitle": " ",
|
||||
"go.problem.item1": "Preço de assinatura de baixo custo",
|
||||
"go.problem.item2": "Limites generosos e acesso confiável",
|
||||
"go.problem.item3": "Feito para o maior número possível de programadores",
|
||||
"go.problem.item4": "Inclui GLM-5, Kimi K2.5 e MiniMax M2.5",
|
||||
"go.how.title": "Como o Go funciona",
|
||||
"go.how.body": "Go é uma assinatura de $10/mês que você pode usar com OpenCode ou qualquer agente.",
|
||||
"go.how.step1.title": "Crie uma conta",
|
||||
"go.how.step1.beforeLink": "siga as",
|
||||
"go.how.step1.link": "instruções de configuração",
|
||||
"go.how.step2.title": "Assinar o Go",
|
||||
"go.how.step2.link": "$10/mês",
|
||||
"go.how.step2.afterLink": "com limites generosos",
|
||||
"go.how.step3.title": "Comece a codificar",
|
||||
"go.how.step3.body": "com acesso confiável a modelos de código aberto",
|
||||
"go.privacy.title": "Sua privacidade é importante para nós",
|
||||
"go.privacy.body":
|
||||
"O plano é projetado principalmente para usuários internacionais, com modelos hospedados nos EUA, UE e Singapura para acesso global estável.",
|
||||
"go.privacy.contactAfter": "se você tiver alguma dúvida.",
|
||||
"go.privacy.beforeExceptions":
|
||||
"Os modelos Go são hospedados nos EUA. Os provedores seguem uma política de retenção zero e não usam seus dados para treinamento de modelos, com as",
|
||||
"go.privacy.exceptionsLink": "seguintes exceções",
|
||||
"go.faq.q1": "O que é OpenCode Go?",
|
||||
"go.faq.a1":
|
||||
"Go é uma assinatura de baixo custo que oferece acesso confiável a modelos de código aberto capazes para codificação com agentes.",
|
||||
"go.faq.q2": "Quais modelos o Go inclui?",
|
||||
"go.faq.a2": "Go inclui GLM-5, Kimi K2.5 e MiniMax M2.5, com limites generosos e acesso confiável.",
|
||||
"go.faq.q3": "O Go é o mesmo que o Zen?",
|
||||
"go.faq.a3":
|
||||
"Não. O Zen é pago por uso (pay-as-you-go), enquanto o Go é uma assinatura de $10/mês com limites generosos e acesso confiável aos modelos de código aberto GLM-5, Kimi K2.5 e MiniMax M2.5.",
|
||||
"go.faq.q4": "Quanto custa o Go?",
|
||||
"go.faq.a4.p1.beforePricing": "O Go custa",
|
||||
"go.faq.a4.p1.pricingLink": "$10/mês",
|
||||
"go.faq.a4.p1.afterPricing": "com limites generosos.",
|
||||
"go.faq.a4.p2.beforeAccount": "Você pode gerenciar sua assinatura em sua",
|
||||
"go.faq.a4.p2.accountLink": "conta",
|
||||
"go.faq.a4.p3": "Cancele a qualquer momento.",
|
||||
"go.faq.q5": "E sobre dados e privacidade?",
|
||||
"go.faq.a5.body":
|
||||
"O plano é projetado principalmente para usuários internacionais, com modelos hospedados nos EUA, UE e Singapura para acesso global estável.",
|
||||
"go.faq.a5.contactAfter": "se você tiver alguma dúvida.",
|
||||
"go.faq.a5.beforeExceptions":
|
||||
"Os modelos Go são hospedados nos EUA. Os provedores seguem uma política de retenção zero e não usam seus dados para treinamento de modelos, com as",
|
||||
"go.faq.a5.exceptionsLink": "seguintes exceções",
|
||||
"go.faq.q6": "Posso recarregar crédito?",
|
||||
"go.faq.a6": "Se você precisar de mais uso, pode recarregar crédito em sua conta.",
|
||||
"go.faq.q7": "Posso cancelar?",
|
||||
"go.faq.a7": "Sim, você pode cancelar a qualquer momento.",
|
||||
"go.faq.q8": "Posso usar o Go com outros agentes de codificação?",
|
||||
"go.faq.a8":
|
||||
"Sim, você pode usar o Go com qualquer agente. Siga as instruções de configuração no seu agente de codificação preferido.",
|
||||
|
||||
"go.faq.q9": "Qual a diferença entre os modelos gratuitos e o Go?",
|
||||
"go.faq.a9":
|
||||
"Os modelos gratuitos incluem Big Pickle e modelos promocionais disponíveis no momento, com uma cota de 200 requisições/dia. O Go inclui GLM-5, Kimi K2.5 e MiniMax M2.5 com cotas de requisição mais altas aplicadas em janelas móveis (5 horas, semanal e mensal), aproximadamente equivalentes a $12 por 5 horas, $30 por semana e $60 por mês (as contagens reais de requisições variam de acordo com o modelo e o uso).",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Limite de taxa excedido. Por favor, tente novamente mais tarde.",
|
||||
"zen.api.error.modelNotSupported": "Modelo {{model}} não suportado",
|
||||
"zen.api.error.modelFormatNotSupported": "Modelo {{model}} não suportado para o formato {{format}}",
|
||||
|
||||
@@ -249,6 +249,105 @@ export const dict = {
|
||||
"Alle Zen-modeller er hostet i USA. Udbydere følger en nulopbevaringspolitik og bruger ikke dine data til modeltræning med",
|
||||
"zen.privacy.exceptionsLink": "følgende undtagelser",
|
||||
|
||||
"go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle",
|
||||
"go.meta.description":
|
||||
"Go er et abonnement til $10/måned med generøse grænser på 5 timers forespørgsler for GLM-5, Kimi K2.5 og MiniMax M2.5.",
|
||||
"go.hero.title": "Kodningsmodeller til lav pris for alle",
|
||||
"go.hero.body":
|
||||
"Go bringer agentisk kodning til programmører over hele verden. Med generøse grænser og pålidelig adgang til de mest kapable open source-modeller, så du kan bygge med kraftfulde agenter uden at bekymre dig om omkostninger eller tilgængelighed.",
|
||||
|
||||
"go.cta.start": "Abonner på Go",
|
||||
"go.cta.template": "{{text}} {{price}}",
|
||||
"go.cta.text": "Abonner på Go",
|
||||
"go.cta.price": "$10/måned",
|
||||
"go.pricing.body": "Brug med enhver agent. Genopfyld kredit om nødvendigt. Annuller til enhver tid.",
|
||||
"go.graph.free": "Gratis",
|
||||
"go.graph.freePill": "Big Pickle og gratis modeller",
|
||||
"go.graph.go": "Go",
|
||||
"go.graph.label": "Forespørgsler pr. 5 timer",
|
||||
"go.graph.usageLimits": "Brugsgrænser",
|
||||
"go.graph.tick": "{{n}}x",
|
||||
"go.graph.aria": "Forespørgsler pr. 5t: {{free}} vs {{go}}",
|
||||
|
||||
"go.testimonials.brand.zen": "Zen",
|
||||
"go.testimonials.brand.go": "Go",
|
||||
"go.testimonials.handle": "@OpenCode",
|
||||
"go.testimonials.dax.name": "Dax Raad",
|
||||
"go.testimonials.dax.title": "ex-CEO, Terminal Products",
|
||||
"go.testimonials.dax.quoteAfter": "har været livsændrende, det er virkelig en no-brainer.",
|
||||
"go.testimonials.jay.name": "Jay V",
|
||||
"go.testimonials.jay.title": "ex-Founder, SEED, PM, Melt, Pop, Dapt, Cadmus, og ViewPoint",
|
||||
"go.testimonials.jay.quoteBefore": "4 ud af 5 personer på vores team elsker at bruge",
|
||||
"go.testimonials.jay.quoteAfter": ".",
|
||||
"go.testimonials.adam.name": "Adam Elmore",
|
||||
"go.testimonials.adam.title": "ex-Hero, AWS",
|
||||
"go.testimonials.adam.quoteBefore": "Jeg kan ikke anbefale",
|
||||
"go.testimonials.adam.quoteAfter": "nok. Seriøst, det er virkelig godt.",
|
||||
"go.testimonials.david.name": "David Hill",
|
||||
"go.testimonials.david.title": "ex-Head of Design, Laravel",
|
||||
"go.testimonials.david.quoteBefore": "Med",
|
||||
"go.testimonials.david.quoteAfter": "ved jeg, at alle modellerne er testede og perfekte til kodningsagenter.",
|
||||
"go.testimonials.frank.name": "Frank Wang",
|
||||
"go.testimonials.frank.title": "ex-Intern, Nvidia (4 gange)",
|
||||
"go.testimonials.frank.quote": "Jeg ville ønske, jeg stadig var hos Nvidia.",
|
||||
"go.problem.title": "Hvilket problem løser Go?",
|
||||
"go.problem.body":
|
||||
"Vi fokuserer på at bringe OpenCode-oplevelsen til så mange mennesker som muligt. OpenCode Go er et lavprisabonnement ($10/måned) designet til at bringe agentisk kodning til programmører over hele verden. Det giver generøse grænser og pålidelig adgang til de mest kapable open source-modeller.",
|
||||
"go.problem.subtitle": " ",
|
||||
"go.problem.item1": "Lavpris abonnementspriser",
|
||||
"go.problem.item2": "Generøse grænser og pålidelig adgang",
|
||||
"go.problem.item3": "Bygget til så mange programmører som muligt",
|
||||
"go.problem.item4": "Inkluderer GLM-5, Kimi K2.5 og MiniMax M2.5",
|
||||
"go.how.title": "Hvordan Go virker",
|
||||
"go.how.body": "Go er et abonnement til $10/måned, som du kan bruge med OpenCode eller enhver anden agent.",
|
||||
"go.how.step1.title": "Opret en konto",
|
||||
"go.how.step1.beforeLink": "følg",
|
||||
"go.how.step1.link": "opsætningsinstruktionerne",
|
||||
"go.how.step2.title": "Abonner på Go",
|
||||
"go.how.step2.link": "$10/måned",
|
||||
"go.how.step2.afterLink": "med generøse grænser",
|
||||
"go.how.step3.title": "Start kodning",
|
||||
"go.how.step3.body": "med pålidelig adgang til open source-modeller",
|
||||
"go.privacy.title": "Dit privatliv er vigtigt for os",
|
||||
"go.privacy.body":
|
||||
"Planen er primært designet til internationale brugere, med modeller hostet i USA, EU og Singapore for stabil global adgang.",
|
||||
"go.privacy.contactAfter": "hvis du har spørgsmål.",
|
||||
"go.privacy.beforeExceptions":
|
||||
"Go-modeller hostes i USA. Udbydere følger en nulopbevaringspolitik og bruger ikke dine data til modeltræning, med de",
|
||||
"go.privacy.exceptionsLink": "følgende undtagelser",
|
||||
"go.faq.q1": "Hvad er OpenCode Go?",
|
||||
"go.faq.a1":
|
||||
"Go er et lavprisabonnement, der giver dig pålidelig adgang til kapable open source-modeller til agentisk kodning.",
|
||||
"go.faq.q2": "Hvilke modeller inkluderer Go?",
|
||||
"go.faq.a2": "Go inkluderer GLM-5, Kimi K2.5 og MiniMax M2.5, med generøse grænser og pålidelig adgang.",
|
||||
"go.faq.q3": "Er Go det samme som Zen?",
|
||||
"go.faq.a3":
|
||||
"Nej. Zen er pay-as-you-go, mens Go er et abonnement til $10/måned med generøse grænser og pålidelig adgang til open source-modellerne GLM-5, Kimi K2.5 og MiniMax M2.5.",
|
||||
"go.faq.q4": "Hvad koster Go?",
|
||||
"go.faq.a4.p1.beforePricing": "Go koster",
|
||||
"go.faq.a4.p1.pricingLink": "$10/måned",
|
||||
"go.faq.a4.p1.afterPricing": "med generøse grænser.",
|
||||
"go.faq.a4.p2.beforeAccount": "Du kan administrere dit abonnement i din",
|
||||
"go.faq.a4.p2.accountLink": "konto",
|
||||
"go.faq.a4.p3": "Annuller til enhver tid.",
|
||||
"go.faq.q5": "Hvad med data og privatliv?",
|
||||
"go.faq.a5.body":
|
||||
"Planen er primært designet til internationale brugere, med modeller hostet i USA, EU og Singapore for stabil global adgang.",
|
||||
"go.faq.a5.contactAfter": "hvis du har spørgsmål.",
|
||||
"go.faq.a5.beforeExceptions":
|
||||
"Go-modeller hostes i USA. Udbydere følger en nulopbevaringspolitik og bruger ikke dine data til modeltræning, med de",
|
||||
"go.faq.a5.exceptionsLink": "følgende undtagelser",
|
||||
"go.faq.q6": "Kan jeg tanke kredit op?",
|
||||
"go.faq.a6": "Hvis du har brug for mere forbrug, kan du tanke kredit op på din konto.",
|
||||
"go.faq.q7": "Kan jeg annullere?",
|
||||
"go.faq.a7": "Ja, du kan annullere til enhver tid.",
|
||||
"go.faq.q8": "Kan jeg bruge Go med andre kodningsagenter?",
|
||||
"go.faq.a8": "Ja, du kan bruge Go med enhver agent. Følg opsætningsinstruktionerne i din foretrukne kodningsagent.",
|
||||
|
||||
"go.faq.q9": "Hvad er forskellen på gratis modeller og Go?",
|
||||
"go.faq.a9":
|
||||
"Gratis modeller inkluderer Big Pickle plus salgsfremmende modeller tilgængelige på det tidspunkt, med en kvote på 200 forespørgsler/dag. Go inkluderer GLM-5, Kimi K2.5 og MiniMax M2.5 med højere anmodningskvoter håndhævet over rullende vinduer (5-timers, ugentlig og månedlig), nogenlunde svarende til $12 pr. 5 timer, $30 pr. uge og $60 pr. måned (faktiske anmodningstal varierer efter model og brug).",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Hastighedsgrænse overskredet. Prøv venligst igen senere.",
|
||||
"zen.api.error.modelNotSupported": "Model {{model}} understøttes ikke",
|
||||
"zen.api.error.modelFormatNotSupported": "Model {{model}} understøttes ikke for format {{format}}",
|
||||
|
||||
@@ -251,6 +251,106 @@ export const dict = {
|
||||
"Alle Zen-Modelle werden in den USA gehostet. Anbieter folgen einer Zero-Retention-Policy und nutzen deine Daten nicht für Modelltraining, mit den",
|
||||
"zen.privacy.exceptionsLink": "folgenden Ausnahmen",
|
||||
|
||||
"go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle",
|
||||
"go.meta.description":
|
||||
"Go ist ein Abonnement für $10/Monat mit großzügigen 5-Stunden-Limits für GLM-5, Kimi K2.5 und MiniMax M2.5.",
|
||||
"go.hero.title": "Kostengünstige Coding-Modelle für alle",
|
||||
"go.hero.body":
|
||||
"Go bringt Agentic Coding zu Programmierern auf der ganzen Welt. Mit großzügigen Limits und zuverlässigem Zugang zu den leistungsfähigsten Open-Source-Modellen, damit du mit leistungsstarken Agenten entwickeln kannst, ohne dir Gedanken über Kosten oder Verfügbarkeit zu machen.",
|
||||
|
||||
"go.cta.start": "Go abonnieren",
|
||||
"go.cta.template": "{{text}} {{price}}",
|
||||
"go.cta.text": "Go abonnieren",
|
||||
"go.cta.price": "$10/Monat",
|
||||
"go.pricing.body": "Nutzung mit jedem Agenten. Guthaben bei Bedarf aufladen. Jederzeit kündbar.",
|
||||
"go.graph.free": "Kostenlos",
|
||||
"go.graph.freePill": "Big Pickle und kostenlose Modelle",
|
||||
"go.graph.go": "Go",
|
||||
"go.graph.label": "Anfragen pro 5 Stunden",
|
||||
"go.graph.usageLimits": "Nutzungslimits",
|
||||
"go.graph.tick": "{{n}}x",
|
||||
"go.graph.aria": "Anfragen pro 5h: {{free}} vs {{go}}",
|
||||
|
||||
"go.testimonials.brand.zen": "Zen",
|
||||
"go.testimonials.brand.go": "Go",
|
||||
"go.testimonials.handle": "@OpenCode",
|
||||
"go.testimonials.dax.name": "Dax Raad",
|
||||
"go.testimonials.dax.title": "ex-CEO, Terminal Products",
|
||||
"go.testimonials.dax.quoteAfter": "hat mein Leben verändert, es ist wirklich ein No-Brainer.",
|
||||
"go.testimonials.jay.name": "Jay V",
|
||||
"go.testimonials.jay.title": "ex-Gründer, SEED, PM, Melt, Pop, Dapt, Cadmus und ViewPoint",
|
||||
"go.testimonials.jay.quoteBefore": "4 von 5 Leuten in unserem Team lieben die Nutzung von",
|
||||
"go.testimonials.jay.quoteAfter": ".",
|
||||
"go.testimonials.adam.name": "Adam Elmore",
|
||||
"go.testimonials.adam.title": "ex-Hero, AWS",
|
||||
"go.testimonials.adam.quoteBefore": "Ich kann",
|
||||
"go.testimonials.adam.quoteAfter": "nicht genug empfehlen. Ernsthaft, es ist wirklich gut.",
|
||||
"go.testimonials.david.name": "David Hill",
|
||||
"go.testimonials.david.title": "ex-Head of Design, Laravel",
|
||||
"go.testimonials.david.quoteBefore": "Mit",
|
||||
"go.testimonials.david.quoteAfter": "weiß ich, dass alle Modelle getestet und perfekt für Coding-Agenten sind.",
|
||||
"go.testimonials.frank.name": "Frank Wang",
|
||||
"go.testimonials.frank.title": "ex-Praktikant, Nvidia (4 mal)",
|
||||
"go.testimonials.frank.quote": "Ich wünschte, ich wäre noch bei Nvidia.",
|
||||
"go.problem.title": "Welches Problem löst Go?",
|
||||
"go.problem.body":
|
||||
"Wir konzentrieren uns darauf, die OpenCode-Erfahrung so vielen Menschen wie möglich zugänglich zu machen. OpenCode Go ist ein kostengünstiges ($10/Monat) Abonnement, das entwickelt wurde, um Agentic Coding zu Programmierern auf der ganzen Welt zu bringen. Es bietet großzügige Limits und zuverlässigen Zugang zu den leistungsfähigsten Open-Source-Modellen.",
|
||||
"go.problem.subtitle": " ",
|
||||
"go.problem.item1": "Kostengünstiges Abonnement",
|
||||
"go.problem.item2": "Großzügige Limits und zuverlässiger Zugang",
|
||||
"go.problem.item3": "Für so viele Programmierer wie möglich gebaut",
|
||||
"go.problem.item4": "Beinhaltet GLM-5, Kimi K2.5 und MiniMax M2.5",
|
||||
"go.how.title": "Wie Go funktioniert",
|
||||
"go.how.body": "Go ist ein Abonnement für $10/Monat, das du mit OpenCode oder jedem anderen Agenten nutzen kannst.",
|
||||
"go.how.step1.title": "Konto erstellen",
|
||||
"go.how.step1.beforeLink": "folge den",
|
||||
"go.how.step1.link": "Einrichtungsanweisungen",
|
||||
"go.how.step2.title": "Go abonnieren",
|
||||
"go.how.step2.link": "$10/Monat",
|
||||
"go.how.step2.afterLink": "mit großzügigen Limits",
|
||||
"go.how.step3.title": "Loslegen mit Coding",
|
||||
"go.how.step3.body": "mit zuverlässigem Zugang zu Open-Source-Modellen",
|
||||
"go.privacy.title": "Deine Privatsphäre ist uns wichtig",
|
||||
"go.privacy.body":
|
||||
"Der Plan ist primär für internationale Nutzer konzipiert, mit Modellen gehostet in den USA, der EU und Singapur für stabilen globalen Zugang.",
|
||||
"go.privacy.contactAfter": "wenn du Fragen hast.",
|
||||
"go.privacy.beforeExceptions":
|
||||
"Go-Modelle werden in den USA gehostet. Anbieter verfolgen eine Zero-Retention-Politik und nutzen deine Daten nicht für das Training von Modellen, mit den",
|
||||
"go.privacy.exceptionsLink": "folgenden Ausnahmen",
|
||||
"go.faq.q1": "Was ist OpenCode Go?",
|
||||
"go.faq.a1":
|
||||
"Go ist ein kostengünstiges Abonnement, das dir zuverlässigen Zugang zu leistungsfähigen Open-Source-Modellen für Agentic Coding bietet.",
|
||||
"go.faq.q2": "Welche Modelle beinhaltet Go?",
|
||||
"go.faq.a2": "Go beinhaltet GLM-5, Kimi K2.5 und MiniMax M2.5, mit großzügigen Limits und zuverlässigem Zugang.",
|
||||
"go.faq.q3": "Ist Go dasselbe wie Zen?",
|
||||
"go.faq.a3":
|
||||
"Nein. Zen ist Pay-as-you-go, während Go ein Abonnement für $10/Monat mit großzügigen Limits und zuverlässigem Zugang zu den Open-Source-Modellen GLM-5, Kimi K2.5 und MiniMax M2.5 ist.",
|
||||
"go.faq.q4": "Wie viel kostet Go?",
|
||||
"go.faq.a4.p1.beforePricing": "Go kostet",
|
||||
"go.faq.a4.p1.pricingLink": "$10/Monat",
|
||||
"go.faq.a4.p1.afterPricing": "mit großzügigen Limits.",
|
||||
"go.faq.a4.p2.beforeAccount": "Du kannst dein Abonnement in deinem",
|
||||
"go.faq.a4.p2.accountLink": "Konto verwalten",
|
||||
"go.faq.a4.p3": "Jederzeit kündbar.",
|
||||
"go.faq.q5": "Was ist mit Daten und Privatsphäre?",
|
||||
"go.faq.a5.body":
|
||||
"Der Plan ist primär für internationale Nutzer konzipiert, mit Modellen gehostet in den USA, der EU und Singapur für stabilen globalen Zugang.",
|
||||
"go.faq.a5.contactAfter": "wenn du Fragen hast.",
|
||||
"go.faq.a5.beforeExceptions":
|
||||
"Go-Modelle werden in den USA gehostet. Anbieter verfolgen eine Zero-Retention-Politik und nutzen deine Daten nicht für das Training von Modellen, mit den",
|
||||
"go.faq.a5.exceptionsLink": "folgenden Ausnahmen",
|
||||
"go.faq.q6": "Kann ich Guthaben aufladen?",
|
||||
"go.faq.a6": "Wenn du mehr Nutzung benötigst, kannst du Guthaben in deinem Konto aufladen.",
|
||||
"go.faq.q7": "Kann ich kündigen?",
|
||||
"go.faq.a7": "Ja, du kannst jederzeit kündigen.",
|
||||
"go.faq.q8": "Kann ich Go mit anderen Coding-Agenten nutzen?",
|
||||
"go.faq.a8":
|
||||
"Ja, du kannst Go mit jedem Agenten nutzen. Folge den Einrichtungsanweisungen in deinem bevorzugten Coding-Agenten.",
|
||||
|
||||
"go.faq.q9": "Was ist der Unterschied zwischen kostenlosen Modellen und Go?",
|
||||
"go.faq.a9":
|
||||
"Kostenlose Modelle beinhalten Big Pickle sowie Werbemodelle, die zum jeweiligen Zeitpunkt verfügbar sind, mit einem Kontingent von 200 Anfragen/Tag. Go beinhaltet GLM-5, Kimi K2.5 und MiniMax M2.5 mit höheren Anfragekontingenten, die über rollierende Zeitfenster (5 Stunden, wöchentlich und monatlich) durchgesetzt werden, grob äquivalent zu $12 pro 5 Stunden, $30 pro Woche und $60 pro Monat (tatsächliche Anfragezahlen variieren je nach Modell und Nutzung).",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Ratenlimit überschritten. Bitte versuche es später erneut.",
|
||||
"zen.api.error.modelNotSupported": "Modell {{model}} wird nicht unterstützt",
|
||||
"zen.api.error.modelFormatNotSupported": "Modell {{model}} wird für das Format {{format}} nicht unterstützt",
|
||||
|
||||
@@ -6,6 +6,7 @@ export const dict = {
|
||||
"nav.x": "X",
|
||||
"nav.enterprise": "Enterprise",
|
||||
"nav.zen": "Zen",
|
||||
"nav.go": "Go",
|
||||
"nav.login": "Login",
|
||||
"nav.free": "Free",
|
||||
"nav.home": "Home",
|
||||
@@ -54,6 +55,7 @@ export const dict = {
|
||||
"common.cancel": "Cancel",
|
||||
"common.creating": "Creating...",
|
||||
"common.create": "Create",
|
||||
"common.contactUs": "Contact us",
|
||||
|
||||
"common.videoUnsupported": "Your browser does not support the video tag.",
|
||||
"common.figure": "Fig {{n}}.",
|
||||
@@ -243,6 +245,105 @@ export const dict = {
|
||||
"All Zen models are hosted in the US. Providers follow a zero-retention policy and do not use your data for model training, with the",
|
||||
"zen.privacy.exceptionsLink": "following exceptions",
|
||||
|
||||
"go.title": "OpenCode Go | Low cost coding models for everyone",
|
||||
"go.meta.description":
|
||||
"Go is a $10/month subscription with generous 5-hour request limits for GLM-5, Kimi K2.5, and MiniMax M2.5.",
|
||||
"go.hero.title": "Low cost coding models for everyone",
|
||||
"go.hero.body":
|
||||
"Go brings agentic coding to programmers around the world. Offering generous limits and reliable access to the most capable open-source models, so you can build with powerful agents without worrying about cost or availability.",
|
||||
|
||||
"go.cta.start": "Subscribe to Go",
|
||||
"go.cta.template": "{{text}} {{price}}",
|
||||
"go.cta.text": "Subscribe to Go",
|
||||
"go.cta.price": "$10/month",
|
||||
"go.pricing.body": "Use with any agent. Top up credit if needed. Cancel any time.",
|
||||
"go.graph.free": "Free",
|
||||
"go.graph.freePill": "Big Pickle and free models",
|
||||
"go.graph.go": "Go",
|
||||
"go.graph.label": "Requests per 5 hour",
|
||||
"go.graph.usageLimits": "Usage limits",
|
||||
"go.graph.tick": "{{n}}x",
|
||||
"go.graph.aria": "Requests per 5h: {{free}} vs {{go}}",
|
||||
|
||||
"go.testimonials.brand.zen": "Zen",
|
||||
"go.testimonials.brand.go": "Go",
|
||||
"go.testimonials.handle": "@OpenCode",
|
||||
"go.testimonials.dax.name": "Dax Raad",
|
||||
"go.testimonials.dax.title": "ex-CEO, Terminal Products",
|
||||
"go.testimonials.dax.quoteAfter": "has been life changing, it's truly a no-brainer.",
|
||||
"go.testimonials.jay.name": "Jay V",
|
||||
"go.testimonials.jay.title": "ex-Founder, SEED, PM, Melt, Pop, Dapt, Cadmus, and ViewPoint",
|
||||
"go.testimonials.jay.quoteBefore": "4 out of 5 people on our team love using",
|
||||
"go.testimonials.jay.quoteAfter": ".",
|
||||
"go.testimonials.adam.name": "Adam Elmore",
|
||||
"go.testimonials.adam.title": "ex-Hero, AWS",
|
||||
"go.testimonials.adam.quoteBefore": "I can't recommend",
|
||||
"go.testimonials.adam.quoteAfter": "enough. Seriously, it's really good.",
|
||||
"go.testimonials.david.name": "David Hill",
|
||||
"go.testimonials.david.title": "ex-Head of Design, Laravel",
|
||||
"go.testimonials.david.quoteBefore": "With",
|
||||
"go.testimonials.david.quoteAfter": "I know all the models are tested and perfect for coding agents.",
|
||||
"go.testimonials.frank.name": "Frank Wang",
|
||||
"go.testimonials.frank.title": "ex-Intern, Nvidia (4 times)",
|
||||
"go.testimonials.frank.quote": "I wish I was still at Nvidia.",
|
||||
"go.problem.title": "What problem is Go solving?",
|
||||
"go.problem.body":
|
||||
"We're focused on bringing the OpenCode experience to as many people as possible. OpenCode Go is a low cost ($10/month) subscription designed to bring agentic coding to programmers around the world. It provides generous limits and reliable access to the most capable open source models.",
|
||||
"go.problem.subtitle": " ",
|
||||
"go.problem.item1": "Low cost subscription pricing",
|
||||
"go.problem.item2": "Generous limits and reliable access",
|
||||
"go.problem.item3": "Built for as many programmers as possible",
|
||||
"go.problem.item4": "Includes GLM-5, Kimi K2.5, and MiniMax M2.5",
|
||||
"go.how.title": "How Go works",
|
||||
"go.how.body": "Go is a $10/month subscription you can use with OpenCode or any agent.",
|
||||
"go.how.step1.title": "Create an account",
|
||||
"go.how.step1.beforeLink": "follow the",
|
||||
"go.how.step1.link": "setup instructions",
|
||||
"go.how.step2.title": "Subscribe to Go",
|
||||
"go.how.step2.link": "$10/month",
|
||||
"go.how.step2.afterLink": "with generous limits",
|
||||
"go.how.step3.title": "Start coding",
|
||||
"go.how.step3.body": "with reliable access to open-source models",
|
||||
"go.privacy.title": "Your privacy is important to us",
|
||||
"go.privacy.body":
|
||||
"The plan is designed primarily for international users, with models hosted in the US, EU, and Singapore for stable global access.",
|
||||
"go.privacy.contactAfter": "if you have any questions.",
|
||||
"go.privacy.beforeExceptions":
|
||||
"Go models are hosted in the US. Providers follow a zero-retention policy and do not use your data for model training, with the",
|
||||
"go.privacy.exceptionsLink": "following exceptions",
|
||||
"go.faq.q1": "What is OpenCode Go?",
|
||||
"go.faq.a1":
|
||||
"Go is a low-cost subscription that gives you reliable access to capable open-source models for agentic coding.",
|
||||
"go.faq.q2": "What models does Go include?",
|
||||
"go.faq.a2": "Go includes GLM-5, Kimi K2.5, and MiniMax M2.5, with generous limits and reliable access.",
|
||||
"go.faq.q3": "Is Go the same as Zen?",
|
||||
"go.faq.a3":
|
||||
"No. Zen is pay-as-you-go, while Go is a $10/month subscription with generous limits and reliable access to open-source models GLM-5, Kimi K2.5, and MiniMax M2.5.",
|
||||
"go.faq.q4": "How much does Go cost?",
|
||||
"go.faq.a4.p1.beforePricing": "Go costs",
|
||||
"go.faq.a4.p1.pricingLink": "$10/month",
|
||||
"go.faq.a4.p1.afterPricing": "with generous limits.",
|
||||
"go.faq.a4.p2.beforeAccount": "You can manage your subscription in your",
|
||||
"go.faq.a4.p2.accountLink": "account",
|
||||
"go.faq.a4.p3": "Cancel any time.",
|
||||
"go.faq.q5": "What about data and privacy?",
|
||||
"go.faq.a5.body":
|
||||
"The plan is designed primarily for international users, with models hosted in the US, EU, and Singapore for stable global access.",
|
||||
"go.faq.a5.contactAfter": "if you have any questions.",
|
||||
"go.faq.a5.beforeExceptions":
|
||||
"Go models are hosted in the US. Providers follow a zero-retention policy and do not use your data for model training, with the",
|
||||
"go.faq.a5.exceptionsLink": "following exceptions",
|
||||
"go.faq.q6": "Can I top up credit?",
|
||||
"go.faq.a6": "If you need more usage, you can top up credit in your account.",
|
||||
"go.faq.q7": "Can I cancel?",
|
||||
"go.faq.a7": "Yes, you can cancel any time.",
|
||||
"go.faq.q8": "Can I use Go with other coding agents?",
|
||||
"go.faq.a8": "Yes, you can use Go with any agent. Follow the setup instructions in your preferred coding agent.",
|
||||
|
||||
"go.faq.q9": "What is the difference between free models and Go?",
|
||||
"go.faq.a9":
|
||||
"Free models include Big Pickle plus promotional models available at the time, with a quota of 200 requests/day. Go includes GLM-5, Kimi K2.5, and MiniMax M2.5 with higher request quotas enforced across rolling windows (5-hour, weekly, and monthly), roughly equivalent to $12 per 5 hours, $30 per week, and $60 per month (actual request counts vary by model and usage).",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Rate limit exceeded. Please try again later.",
|
||||
"zen.api.error.modelNotSupported": "Model {{model}} not supported",
|
||||
"zen.api.error.modelFormatNotSupported": "Model {{model}} not supported for format {{format}}",
|
||||
|
||||
@@ -252,6 +252,107 @@ export const dict = {
|
||||
"Todos los modelos Zen están alojados en EE. UU. Los proveedores siguen una política de cero retención y no usan tus datos para entrenamiento de modelos, con las",
|
||||
"zen.privacy.exceptionsLink": "siguientes excepciones",
|
||||
|
||||
"go.title": "OpenCode Go | Modelos de programación de bajo coste para todos",
|
||||
"go.meta.description":
|
||||
"Go es una suscripción de 10 $/mes con generosos límites de solicitudes de 5 horas para GLM-5, Kimi K2.5 y MiniMax M2.5.",
|
||||
"go.hero.title": "Modelos de programación de bajo coste para todos",
|
||||
"go.hero.body":
|
||||
"Go lleva la programación agéntica a programadores de todo el mundo. Ofrece límites generosos y acceso fiable a los modelos de código abierto más capaces, para que puedas crear con agentes potentes sin preocuparte por el coste o la disponibilidad.",
|
||||
|
||||
"go.cta.start": "Suscribirse a Go",
|
||||
"go.cta.template": "{{text}} {{price}}",
|
||||
"go.cta.text": "Suscribirse a Go",
|
||||
"go.cta.price": "10 $/mes",
|
||||
"go.pricing.body": "Úsalo con cualquier agente. Recarga crédito si es necesario. Cancela en cualquier momento.",
|
||||
"go.graph.free": "Gratis",
|
||||
"go.graph.freePill": "Big Pickle y modelos gratuitos",
|
||||
"go.graph.go": "Go",
|
||||
"go.graph.label": "Solicitudes por 5 horas",
|
||||
"go.graph.usageLimits": "Límites de uso",
|
||||
"go.graph.tick": "{{n}}x",
|
||||
"go.graph.aria": "Solicitudes por 5h: {{free}} vs {{go}}",
|
||||
|
||||
"go.testimonials.brand.zen": "Zen",
|
||||
"go.testimonials.brand.go": "Go",
|
||||
"go.testimonials.handle": "@OpenCode",
|
||||
"go.testimonials.dax.name": "Dax Raad",
|
||||
"go.testimonials.dax.title": "ex-CEO, Terminal Products",
|
||||
"go.testimonials.dax.quoteAfter": "ha cambiado mi vida, es realmente una obviedad.",
|
||||
"go.testimonials.jay.name": "Jay V",
|
||||
"go.testimonials.jay.title": "ex-Founder, SEED, PM, Melt, Pop, Dapt, Cadmus, and ViewPoint",
|
||||
"go.testimonials.jay.quoteBefore": "A 4 de cada 5 personas en nuestro equipo les encanta usar",
|
||||
"go.testimonials.jay.quoteAfter": ".",
|
||||
"go.testimonials.adam.name": "Adam Elmore",
|
||||
"go.testimonials.adam.title": "ex-Hero, AWS",
|
||||
"go.testimonials.adam.quoteBefore": "No puedo recomendar",
|
||||
"go.testimonials.adam.quoteAfter": "lo suficiente. En serio, es realmente bueno.",
|
||||
"go.testimonials.david.name": "David Hill",
|
||||
"go.testimonials.david.title": "ex-Head of Design, Laravel",
|
||||
"go.testimonials.david.quoteBefore": "Con",
|
||||
"go.testimonials.david.quoteAfter":
|
||||
"sé que todos los modelos están probados y son perfectos para agentes de programación.",
|
||||
"go.testimonials.frank.name": "Frank Wang",
|
||||
"go.testimonials.frank.title": "ex-Intern, Nvidia (4 times)",
|
||||
"go.testimonials.frank.quote": "Ojalá siguiera en Nvidia.",
|
||||
"go.problem.title": "¿Qué problema resuelve Go?",
|
||||
"go.problem.body":
|
||||
"Estamos enfocados en llevar la experiencia de OpenCode a tanta gente como sea posible. OpenCode Go es una suscripción de bajo coste (10 $/mes) diseñada para llevar la programación agéntica a programadores de todo el mundo. Proporciona límites generosos y acceso fiable a los modelos de código abierto más capaces.",
|
||||
"go.problem.subtitle": " ",
|
||||
"go.problem.item1": "Precios de suscripción de bajo coste",
|
||||
"go.problem.item2": "Límites generosos y acceso fiable",
|
||||
"go.problem.item3": "Creado para tantos programadores como sea posible",
|
||||
"go.problem.item4": "Incluye GLM-5, Kimi K2.5 y MiniMax M2.5",
|
||||
"go.how.title": "Cómo funciona Go",
|
||||
"go.how.body": "Go es una suscripción de 10 $/mes que puedes usar con OpenCode o cualquier agente.",
|
||||
"go.how.step1.title": "Crear una cuenta",
|
||||
"go.how.step1.beforeLink": "sigue las",
|
||||
"go.how.step1.link": "instrucciones de configuración",
|
||||
"go.how.step2.title": "Suscribirse a Go",
|
||||
"go.how.step2.link": "10 $/mes",
|
||||
"go.how.step2.afterLink": "con límites generosos",
|
||||
"go.how.step3.title": "Empezar a programar",
|
||||
"go.how.step3.body": "con acceso fiable a modelos de código abierto",
|
||||
"go.privacy.title": "Tu privacidad es importante para nosotros",
|
||||
"go.privacy.body":
|
||||
"El plan está diseñado principalmente para usuarios internacionales, con modelos alojados en EE. UU., UE y Singapur para un acceso global estable.",
|
||||
"go.privacy.contactAfter": "si tienes alguna pregunta.",
|
||||
"go.privacy.beforeExceptions":
|
||||
"Los modelos de Go están alojados en EE. UU. Los proveedores siguen una política de retención cero y no utilizan tus datos para el entrenamiento de modelos, con las",
|
||||
"go.privacy.exceptionsLink": "siguientes excepciones",
|
||||
"go.faq.q1": "¿Qué es OpenCode Go?",
|
||||
"go.faq.a1":
|
||||
"Go es una suscripción de bajo coste que te da acceso fiable a modelos de código abierto capaces para programación agéntica.",
|
||||
"go.faq.q2": "¿Qué modelos incluye Go?",
|
||||
"go.faq.a2": "Go incluye GLM-5, Kimi K2.5 y MiniMax M2.5, con límites generosos y acceso fiable.",
|
||||
"go.faq.q3": "¿Es Go lo mismo que Zen?",
|
||||
"go.faq.a3":
|
||||
"No. Zen es pago por uso, mientras que Go es una suscripción de 10 $/mes con límites generosos y acceso fiable a modelos de código abierto GLM-5, Kimi K2.5 y MiniMax M2.5.",
|
||||
"go.faq.q4": "¿Cuánto cuesta Go?",
|
||||
"go.faq.a4.p1.beforePricing": "Go cuesta",
|
||||
"go.faq.a4.p1.pricingLink": "10 $/mes",
|
||||
"go.faq.a4.p1.afterPricing": "con límites generosos.",
|
||||
"go.faq.a4.p2.beforeAccount": "Puedes gestionar tu suscripción en tu",
|
||||
"go.faq.a4.p2.accountLink": "cuenta",
|
||||
"go.faq.a4.p3": "Cancela en cualquier momento.",
|
||||
"go.faq.q5": "¿Qué pasa con los datos y la privacidad?",
|
||||
"go.faq.a5.body":
|
||||
"El plan está diseñado principalmente para usuarios internacionales, con modelos alojados en EE. UU., UE y Singapur para un acceso global estable.",
|
||||
"go.faq.a5.contactAfter": "si tienes alguna pregunta.",
|
||||
"go.faq.a5.beforeExceptions":
|
||||
"Los modelos de Go están alojados en EE. UU. Los proveedores siguen una política de retención cero y no utilizan tus datos para el entrenamiento de modelos, con las",
|
||||
"go.faq.a5.exceptionsLink": "siguientes excepciones",
|
||||
"go.faq.q6": "¿Puedo recargar crédito?",
|
||||
"go.faq.a6": "Si necesitas más uso, puedes recargar crédito en tu cuenta.",
|
||||
"go.faq.q7": "¿Puedo cancelar?",
|
||||
"go.faq.a7": "Sí, puedes cancelar en cualquier momento.",
|
||||
"go.faq.q8": "¿Puedo usar Go con otros agentes de programación?",
|
||||
"go.faq.a8":
|
||||
"Sí, puedes usar Go con cualquier agente. Sigue las instrucciones de configuración en tu agente de programación preferido.",
|
||||
|
||||
"go.faq.q9": "¿Cuál es la diferencia entre los modelos gratuitos y Go?",
|
||||
"go.faq.a9":
|
||||
"Los modelos gratuitos incluyen Big Pickle más modelos promocionales disponibles en el momento, con una cuota de 200 solicitudes/día. Go incluye GLM-5, Kimi K2.5 y MiniMax M2.5 con cuotas de solicitud más altas aplicadas a través de ventanas móviles (5 horas, semanal y mensual), aproximadamente equivalente a 12 $ por 5 horas, 30 $ por semana y 60 $ por mes (los recuentos reales de solicitudes varían según el modelo y el uso).",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Límite de tasa excedido. Por favor, inténtalo de nuevo más tarde.",
|
||||
"zen.api.error.modelNotSupported": "Modelo {{model}} no soportado",
|
||||
"zen.api.error.modelFormatNotSupported": "Modelo {{model}} no soportado para el formato {{format}}",
|
||||
|
||||
@@ -253,6 +253,105 @@ export const dict = {
|
||||
"Tous les modèles Zen sont hébergés aux États-Unis. Les fournisseurs suivent une politique de rétention zéro et n'utilisent pas vos données pour l'entraînement des modèles, avec les",
|
||||
"zen.privacy.exceptionsLink": "exceptions suivantes",
|
||||
|
||||
"go.title": "OpenCode Go | Modèles de code à faible coût pour tous",
|
||||
"go.meta.description":
|
||||
"Go est un abonnement à 10 $/mois avec des limites généreuses de 5 heures de requêtes pour GLM-5, Kimi K2.5 et MiniMax M2.5.",
|
||||
"go.hero.title": "Modèles de code à faible coût pour tous",
|
||||
"go.hero.body":
|
||||
"Go apporte le codage agentique aux programmeurs du monde entier. Offrant des limites généreuses et un accès fiable aux modèles open source les plus capables, pour que vous puissiez construire avec des agents puissants sans vous soucier du coût ou de la disponibilité.",
|
||||
|
||||
"go.cta.start": "S'abonner à Go",
|
||||
"go.cta.template": "{{text}} {{price}}",
|
||||
"go.cta.text": "S'abonner à Go",
|
||||
"go.cta.price": "10 $/mois",
|
||||
"go.pricing.body": "Utilisez avec n'importe quel agent. Rechargez du crédit si nécessaire. Annulez à tout moment.",
|
||||
"go.graph.free": "Gratuit",
|
||||
"go.graph.freePill": "Big Pickle et modèles gratuits",
|
||||
"go.graph.go": "Go",
|
||||
"go.graph.label": "Requêtes par tranche de 5 heures",
|
||||
"go.graph.usageLimits": "Limites d'utilisation",
|
||||
"go.graph.tick": "{{n}}x",
|
||||
"go.graph.aria": "Requêtes par 5h : {{free}} vs {{go}}",
|
||||
|
||||
"go.testimonials.brand.zen": "Zen",
|
||||
"go.testimonials.brand.go": "Go",
|
||||
"go.testimonials.handle": "@OpenCode",
|
||||
"go.testimonials.dax.name": "Dax Raad",
|
||||
"go.testimonials.dax.title": "ex-PDG, Terminal Products",
|
||||
"go.testimonials.dax.quoteAfter": "a changé ma vie, c'est vraiment une évidence.",
|
||||
"go.testimonials.jay.name": "Jay V",
|
||||
"go.testimonials.jay.title": "ex-Fondateur, SEED, PM, Melt, Pop, Dapt, Cadmus, et ViewPoint",
|
||||
"go.testimonials.jay.quoteBefore": "4 personnes sur 5 dans notre équipe adorent utiliser",
|
||||
"go.testimonials.jay.quoteAfter": ".",
|
||||
"go.testimonials.adam.name": "Adam Elmore",
|
||||
"go.testimonials.adam.title": "ex-Hero, AWS",
|
||||
"go.testimonials.adam.quoteBefore": "Je ne peux pas recommander",
|
||||
"go.testimonials.adam.quoteAfter": "assez. Sérieusement, c'est vraiment bien.",
|
||||
"go.testimonials.david.name": "David Hill",
|
||||
"go.testimonials.david.title": "ex-Directeur du Design, Laravel",
|
||||
"go.testimonials.david.quoteBefore": "Avec",
|
||||
"go.testimonials.david.quoteAfter": "je sais que tous les modèles sont testés et parfaits pour les agents de code.",
|
||||
"go.testimonials.frank.name": "Frank Wang",
|
||||
"go.testimonials.frank.title": "ex-Stagiaire, Nvidia (4 fois)",
|
||||
"go.testimonials.frank.quote": "J'aimerais être encore chez Nvidia.",
|
||||
"go.problem.title": "Quel problème Go résout-il ?",
|
||||
"go.problem.body":
|
||||
"Nous nous concentrons sur le fait d'apporter l'expérience OpenCode à autant de personnes que possible. OpenCode Go est un abonnement à faible coût (10 $/mois) conçu pour apporter le codage agentique aux programmeurs du monde entier. Il offre des limites généreuses et un accès fiable aux modèles open source les plus capables.",
|
||||
"go.problem.subtitle": " ",
|
||||
"go.problem.item1": "Prix d'abonnement bas",
|
||||
"go.problem.item2": "Limites généreuses et accès fiable",
|
||||
"go.problem.item3": "Conçu pour autant de programmeurs que possible",
|
||||
"go.problem.item4": "Inclut GLM-5, Kimi K2.5 et MiniMax M2.5",
|
||||
"go.how.title": "Comment fonctionne Go",
|
||||
"go.how.body": "Go est un abonnement à 10 $/mois que vous pouvez utiliser avec OpenCode ou n'importe quel agent.",
|
||||
"go.how.step1.title": "Créez un compte",
|
||||
"go.how.step1.beforeLink": "suivez les",
|
||||
"go.how.step1.link": "instructions de configuration",
|
||||
"go.how.step2.title": "Abonnez-vous à Go",
|
||||
"go.how.step2.link": "10 $/mois",
|
||||
"go.how.step2.afterLink": "avec des limites généreuses",
|
||||
"go.how.step3.title": "Commencez à coder",
|
||||
"go.how.step3.body": "avec un accès fiable aux modèles open source",
|
||||
"go.privacy.title": "Votre vie privée est importante pour nous",
|
||||
"go.privacy.body":
|
||||
"Le plan est conçu principalement pour les utilisateurs internationaux, avec des modèles hébergés aux États-Unis, dans l'UE et à Singapour pour un accès mondial stable.",
|
||||
"go.privacy.contactAfter": "si vous avez des questions.",
|
||||
"go.privacy.beforeExceptions":
|
||||
"Les modèles Go sont hébergés aux États-Unis. Les fournisseurs suivent une politique de rétention zéro et n'utilisent pas vos données pour l'entraînement des modèles, avec les",
|
||||
"go.privacy.exceptionsLink": "exceptions suivantes",
|
||||
"go.faq.q1": "Qu'est-ce que OpenCode Go ?",
|
||||
"go.faq.a1":
|
||||
"Go est un abonnement à faible coût qui vous donne un accès fiable à des modèles open source performants pour le codage agentique.",
|
||||
"go.faq.q2": "Quels modèles Go inclut-il ?",
|
||||
"go.faq.a2": "Go inclut GLM-5, Kimi K2.5 et MiniMax M2.5, avec des limites généreuses et un accès fiable.",
|
||||
"go.faq.q3": "Est-ce que Go est la même chose que Zen ?",
|
||||
"go.faq.a3":
|
||||
"Non. Zen est payé à l'usage (pay-as-you-go), tandis que Go est un abonnement à 10 $/mois avec des limites généreuses et un accès fiable aux modèles open source GLM-5, Kimi K2.5 et MiniMax M2.5.",
|
||||
"go.faq.q4": "Combien coûte Go ?",
|
||||
"go.faq.a4.p1.beforePricing": "Go coûte",
|
||||
"go.faq.a4.p1.pricingLink": "10 $/mois",
|
||||
"go.faq.a4.p1.afterPricing": "avec des limites généreuses.",
|
||||
"go.faq.a4.p2.beforeAccount": "Vous pouvez gérer votre abonnement dans votre",
|
||||
"go.faq.a4.p2.accountLink": "compte",
|
||||
"go.faq.a4.p3": "Annulez à tout moment.",
|
||||
"go.faq.q5": "Et pour les données et la confidentialité ?",
|
||||
"go.faq.a5.body":
|
||||
"Le plan est conçu principalement pour les utilisateurs internationaux, avec des modèles hébergés aux États-Unis, dans l'UE et à Singapour pour un accès mondial stable.",
|
||||
"go.faq.a5.contactAfter": "si vous avez des questions.",
|
||||
"go.faq.a5.beforeExceptions":
|
||||
"Les modèles Go sont hébergés aux États-Unis. Les fournisseurs suivent une politique de rétention zéro et n'utilisent pas vos données pour l'entraînement des modèles, avec les",
|
||||
"go.faq.a5.exceptionsLink": "exceptions suivantes",
|
||||
"go.faq.q6": "Puis-je recharger mon crédit ?",
|
||||
"go.faq.a6": "Si vous avez besoin de plus d'utilisation, vous pouvez recharger du crédit dans votre compte.",
|
||||
"go.faq.q7": "Puis-je annuler ?",
|
||||
"go.faq.a7": "Oui, vous pouvez annuler à tout moment.",
|
||||
"go.faq.q8": "Puis-je utiliser Go avec d'autres agents de code ?",
|
||||
"go.faq.a8":
|
||||
"Oui, vous pouvez utiliser Go avec n'importe quel agent. Suivez les instructions de configuration dans votre agent de code préféré.",
|
||||
"go.faq.q9": "Quelle est la différence entre les modèles gratuits et Go ?",
|
||||
"go.faq.a9":
|
||||
"Les modèles gratuits incluent Big Pickle ainsi que des modèles promotionnels disponibles à ce moment-là, avec un quota de 200 requêtes/jour. Go inclut GLM-5, Kimi K2.5 et MiniMax M2.5 avec des quotas de requêtes plus élevés appliqués sur des fenêtres glissantes (5 heures, hebdomadaire et mensuelle), à peu près équivalent à 12 $ par 5 heures, 30 $ par semaine et 60 $ par mois (le nombre réel de requêtes varie selon le modèle et l'utilisation).",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Limite de débit dépassée. Veuillez réessayer plus tard.",
|
||||
"zen.api.error.modelNotSupported": "Modèle {{model}} non pris en charge",
|
||||
"zen.api.error.modelFormatNotSupported": "Modèle {{model}} non pris en charge pour le format {{format}}",
|
||||
|
||||
@@ -249,6 +249,106 @@ export const dict = {
|
||||
"Tutti i modelli Zen sono ospitati negli Stati Uniti. I provider seguono una policy di zero-retention e non usano i tuoi dati per l'addestramento dei modelli, con le",
|
||||
"zen.privacy.exceptionsLink": "seguenti eccezioni",
|
||||
|
||||
"go.title": "OpenCode Go | Modelli di coding a basso costo per tutti",
|
||||
"go.meta.description":
|
||||
"Go è un abbonamento da $10/mese con generosi limiti di richieste di 5 ore per GLM-5, Kimi K2.5 e MiniMax M2.5.",
|
||||
"go.hero.title": "Modelli di coding a basso costo per tutti",
|
||||
"go.hero.body":
|
||||
"Go porta il coding agentico ai programmatori di tutto il mondo. Offrendo limiti generosi e un accesso affidabile ai modelli open source più capaci, in modo da poter costruire con agenti potenti senza preoccuparsi dei costi o della disponibilità.",
|
||||
|
||||
"go.cta.start": "Abbonati a Go",
|
||||
"go.cta.template": "{{text}} {{price}}",
|
||||
"go.cta.text": "Abbonati a Go",
|
||||
"go.cta.price": "$10/mese",
|
||||
"go.pricing.body": "Usa con qualsiasi agente. Ricarica credito se necessario. Annulla in qualsiasi momento.",
|
||||
"go.graph.free": "Gratis",
|
||||
"go.graph.freePill": "Big Pickle e modelli gratuiti",
|
||||
"go.graph.go": "Go",
|
||||
"go.graph.label": "Richieste ogni 5 ore",
|
||||
"go.graph.usageLimits": "Limiti di utilizzo",
|
||||
"go.graph.tick": "{{n}}x",
|
||||
"go.graph.aria": "Richieste ogni 5h: {{free}} vs {{go}}",
|
||||
|
||||
"go.testimonials.brand.zen": "Zen",
|
||||
"go.testimonials.brand.go": "Go",
|
||||
"go.testimonials.handle": "@OpenCode",
|
||||
"go.testimonials.dax.name": "Dax Raad",
|
||||
"go.testimonials.dax.title": "ex-CEO, Terminal Products",
|
||||
"go.testimonials.dax.quoteAfter": "ha cambiato la vita, è davvero una scelta ovvia.",
|
||||
"go.testimonials.jay.name": "Jay V",
|
||||
"go.testimonials.jay.title": "ex-Founder, SEED, PM, Melt, Pop, Dapt, Cadmus, e ViewPoint",
|
||||
"go.testimonials.jay.quoteBefore": "4 persone su 5 nel nostro team amano usare",
|
||||
"go.testimonials.jay.quoteAfter": ".",
|
||||
"go.testimonials.adam.name": "Adam Elmore",
|
||||
"go.testimonials.adam.title": "ex-Hero, AWS",
|
||||
"go.testimonials.adam.quoteBefore": "Non posso raccomandare",
|
||||
"go.testimonials.adam.quoteAfter": "abbastanza. Seriamente, è davvero buono.",
|
||||
"go.testimonials.david.name": "David Hill",
|
||||
"go.testimonials.david.title": "ex-Head of Design, Laravel",
|
||||
"go.testimonials.david.quoteBefore": "Con",
|
||||
"go.testimonials.david.quoteAfter": "so che tutti i modelli sono testati e perfetti per gli agenti di coding.",
|
||||
"go.testimonials.frank.name": "Frank Wang",
|
||||
"go.testimonials.frank.title": "ex-Intern, Nvidia (4 volte)",
|
||||
"go.testimonials.frank.quote": "Vorrei essere ancora a Nvidia.",
|
||||
"go.problem.title": "Quale problema risolve Go?",
|
||||
"go.problem.body":
|
||||
"Ci concentriamo nel portare l'esperienza OpenCode a quante più persone possibile. OpenCode Go è un abbonamento a basso costo ($10/mese) progettato per portare il coding agentico ai programmatori di tutto il mondo. Fornisce limiti generosi e accesso affidabile ai modelli open source più capaci.",
|
||||
"go.problem.subtitle": " ",
|
||||
"go.problem.item1": "Prezzo di abbonamento a basso costo",
|
||||
"go.problem.item2": "Limiti generosi e accesso affidabile",
|
||||
"go.problem.item3": "Costruito per il maggior numero possibile di programmatori",
|
||||
"go.problem.item4": "Include GLM-5, Kimi K2.5 e MiniMax M2.5",
|
||||
"go.how.title": "Come funziona Go",
|
||||
"go.how.body": "Go è un abbonamento da $10/mese che puoi usare con OpenCode o qualsiasi agente.",
|
||||
"go.how.step1.title": "Crea un account",
|
||||
"go.how.step1.beforeLink": "segui le",
|
||||
"go.how.step1.link": "istruzioni di configurazione",
|
||||
"go.how.step2.title": "Abbonati a Go",
|
||||
"go.how.step2.link": "$10/mese",
|
||||
"go.how.step2.afterLink": "con limiti generosi",
|
||||
"go.how.step3.title": "Inizia a programmare",
|
||||
"go.how.step3.body": "con accesso affidabile ai modelli open source",
|
||||
"go.privacy.title": "La tua privacy è importante per noi",
|
||||
"go.privacy.body":
|
||||
"Il piano è progettato principalmente per gli utenti internazionali, con modelli ospitati negli Stati Uniti, UE e Singapore per un accesso globale stabile.",
|
||||
"go.privacy.contactAfter": "se hai domande.",
|
||||
"go.privacy.beforeExceptions":
|
||||
"I modelli Go sono ospitati negli Stati Uniti. I provider seguono una policy di zero-retention e non usano i tuoi dati per l'addestramento dei modelli, con le",
|
||||
"go.privacy.exceptionsLink": "seguenti eccezioni",
|
||||
"go.faq.q1": "Che cos'è OpenCode Go?",
|
||||
"go.faq.a1":
|
||||
"Go è un abbonamento a basso costo che ti dà un accesso affidabile a modelli open source capaci per il coding agentico.",
|
||||
"go.faq.q2": "Quali modelli include Go?",
|
||||
"go.faq.a2": "Go include GLM-5, Kimi K2.5 e MiniMax M2.5, con limiti generosi e accesso affidabile.",
|
||||
"go.faq.q3": "Go è lo stesso di Zen?",
|
||||
"go.faq.a3":
|
||||
"No. Zen è a consumo, mentre Go è un abbonamento da $10/mese con limiti generosi e accesso affidabile ai modelli open source GLM-5, Kimi K2.5 e MiniMax M2.5.",
|
||||
"go.faq.q4": "Quanto costa Go?",
|
||||
"go.faq.a4.p1.beforePricing": "Go costa",
|
||||
"go.faq.a4.p1.pricingLink": "$10/mese",
|
||||
"go.faq.a4.p1.afterPricing": "con limiti generosi.",
|
||||
"go.faq.a4.p2.beforeAccount": "Puoi gestire il tuo abbonamento nel tuo",
|
||||
"go.faq.a4.p2.accountLink": "account",
|
||||
"go.faq.a4.p3": "Annulla in qualsiasi momento.",
|
||||
"go.faq.q5": "E per quanto riguarda dati e privacy?",
|
||||
"go.faq.a5.body":
|
||||
"Il piano è progettato principalmente per gli utenti internazionali, con modelli ospitati negli Stati Uniti, UE e Singapore per un accesso globale stabile.",
|
||||
"go.faq.a5.contactAfter": "se hai domande.",
|
||||
"go.faq.a5.beforeExceptions":
|
||||
"I modelli Go sono ospitati negli Stati Uniti. I provider seguono una policy di zero-retention e non usano i tuoi dati per l'addestramento dei modelli, con le",
|
||||
"go.faq.a5.exceptionsLink": "seguenti eccezioni",
|
||||
"go.faq.q6": "Posso ricaricare il credito?",
|
||||
"go.faq.a6": "Se hai bisogno di più utilizzo, puoi ricaricare il credito nel tuo account.",
|
||||
"go.faq.q7": "Posso annullare?",
|
||||
"go.faq.a7": "Sì, puoi annullare in qualsiasi momento.",
|
||||
"go.faq.q8": "Posso usare Go con altri agenti di coding?",
|
||||
"go.faq.a8":
|
||||
"Sì, puoi usare Go con qualsiasi agente. Segui le istruzioni di configurazione nel tuo agente di coding preferito.",
|
||||
|
||||
"go.faq.q9": "Qual è la differenza tra i modelli gratuiti e Go?",
|
||||
"go.faq.a9":
|
||||
"I modelli gratuiti includono Big Pickle più modelli promozionali disponibili al momento, con una quota di 200 richieste/giorno. Go include GLM-5, Kimi K2.5 e MiniMax M2.5 con quote di richiesta più elevate applicate su finestre mobili (5 ore, settimanale e mensile), approssimativamente equivalenti a $12 ogni 5 ore, $30 a settimana e $60 al mese (il conteggio effettivo delle richieste varia in base al modello e all'utilizzo).",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Limite di richieste superato. Riprova più tardi.",
|
||||
"zen.api.error.modelNotSupported": "Modello {{model}} non supportato",
|
||||
"zen.api.error.modelFormatNotSupported": "Modello {{model}} non supportato per il formato {{format}}",
|
||||
|
||||
@@ -248,6 +248,107 @@ export const dict = {
|
||||
"すべてのZenモデルは米国でホストされています。プロバイダーはゼロ保持ポリシーに従い、モデルのトレーニングにデータを使用しません(",
|
||||
"zen.privacy.exceptionsLink": "以下の例外",
|
||||
|
||||
"go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル",
|
||||
"go.meta.description":
|
||||
"Goは、GLM-5、Kimi K2.5、MiniMax M2.5を5時間ごとの十分なリクエスト制限で利用できる月額$10のサブスクリプションです。",
|
||||
"go.hero.title": "すべての人のための低価格なコーディングモデル",
|
||||
"go.hero.body":
|
||||
"Goは、世界中のプログラマーにエージェント型コーディングをもたらします。最も高性能なオープンソースモデルへの十分な制限と安定したアクセスを提供し、コストや可用性を気にすることなく強力なエージェントで構築できます。",
|
||||
|
||||
"go.cta.start": "Goを購読する",
|
||||
"go.cta.template": "{{text}} {{price}}",
|
||||
"go.cta.text": "Goを購読する",
|
||||
"go.cta.price": "$10/月",
|
||||
"go.pricing.body": "任意のエージェントで利用可能。必要に応じてクレジットを追加。いつでもキャンセル可能。",
|
||||
"go.graph.free": "無料",
|
||||
"go.graph.freePill": "Big Pickleと無料モデル",
|
||||
"go.graph.go": "Go",
|
||||
"go.graph.label": "5時間あたりのリクエスト数",
|
||||
"go.graph.usageLimits": "利用制限",
|
||||
"go.graph.tick": "{{n}}倍",
|
||||
"go.graph.aria": "5時間あたりのリクエスト数: {{free}} 対 {{go}}",
|
||||
|
||||
"go.testimonials.brand.zen": "Zen",
|
||||
"go.testimonials.brand.go": "Go",
|
||||
"go.testimonials.handle": "@OpenCode",
|
||||
"go.testimonials.dax.name": "Dax Raad",
|
||||
"go.testimonials.dax.title": "元CEO, Terminal Products",
|
||||
"go.testimonials.dax.quoteAfter": "は人生を変えるものでした。本当に迷う必要はありません。",
|
||||
"go.testimonials.jay.name": "Jay V",
|
||||
"go.testimonials.jay.title": "元創業者, SEED, PM, Melt, Pop, Dapt, Cadmus, ViewPoint",
|
||||
"go.testimonials.jay.quoteBefore": "チームの5人中4人が",
|
||||
"go.testimonials.jay.quoteAfter": "の使用を気に入っています。",
|
||||
"go.testimonials.adam.name": "Adam Elmore",
|
||||
"go.testimonials.adam.title": "元Hero, AWS",
|
||||
"go.testimonials.adam.quoteBefore": "私は",
|
||||
"go.testimonials.adam.quoteAfter": "をどれだけ推薦してもしきれません。真剣に、本当に良いです。",
|
||||
"go.testimonials.david.name": "David Hill",
|
||||
"go.testimonials.david.title": "元デザイン責任者, Laravel",
|
||||
"go.testimonials.david.quoteBefore": "",
|
||||
"go.testimonials.david.quoteAfter":
|
||||
"を使えば、すべてのモデルがテスト済みでコーディングエージェントに最適だと確信できます。",
|
||||
"go.testimonials.frank.name": "Frank Wang",
|
||||
"go.testimonials.frank.title": "元インターン, Nvidia (4回)",
|
||||
"go.testimonials.frank.quote": "まだNvidiaにいられたらよかったのに。",
|
||||
"go.problem.title": "Goはどのような問題を解決していますか?",
|
||||
"go.problem.body":
|
||||
"私たちは、OpenCodeの体験をできるだけ多くの人々に届けることに注力しています。OpenCode Goは、世界中のプログラマーにエージェント型コーディングをもたらすために設計された低価格($10/月)のサブスクリプションです。最も高性能なオープンソースモデルへの十分な制限と安定したアクセスを提供します。",
|
||||
"go.problem.subtitle": " ",
|
||||
"go.problem.item1": "低価格なサブスクリプション料金",
|
||||
"go.problem.item2": "十分な制限と安定したアクセス",
|
||||
"go.problem.item3": "できるだけ多くのプログラマーのために構築",
|
||||
"go.problem.item4": "GLM-5、Kimi K2.5、MiniMax M2.5を含む",
|
||||
"go.how.title": "Goの仕組み",
|
||||
"go.how.body": "Goは、OpenCodeまたは任意のエージェントで使用できる月額$10のサブスクリプションです。",
|
||||
"go.how.step1.title": "アカウントを作成",
|
||||
"go.how.step1.beforeLink": "",
|
||||
"go.how.step1.link": "セットアップ手順はこちら",
|
||||
"go.how.step2.title": "Goを購読する",
|
||||
"go.how.step2.link": "$10/月",
|
||||
"go.how.step2.afterLink": "(十分な制限付き)",
|
||||
"go.how.step3.title": "コーディングを開始",
|
||||
"go.how.step3.body": "オープンソースモデルへの安定したアクセスで",
|
||||
"go.privacy.title": "あなたのプライバシーは私たちにとって重要です",
|
||||
"go.privacy.body":
|
||||
"このプランは主に海外ユーザー向けに設計されており、米国、EU、シンガポールでホストされたモデルにより安定したグローバルアクセスを提供します。",
|
||||
"go.privacy.contactAfter": "ご質問がございましたら。",
|
||||
"go.privacy.beforeExceptions":
|
||||
"Goのモデルは米国でホストされています。プロバイダーはゼロ保持ポリシーに従い、モデルのトレーニングにデータを使用しません(",
|
||||
"go.privacy.exceptionsLink": "以下の例外",
|
||||
"go.faq.q1": "OpenCode Goとは?",
|
||||
"go.faq.a1":
|
||||
"Goは、エージェント型コーディングのための有能なオープンソースモデルへの安定したアクセスを提供する低価格なサブスクリプションです。",
|
||||
"go.faq.q2": "Goにはどのモデルが含まれますか?",
|
||||
"go.faq.a2": "Goには、GLM-5、Kimi K2.5、MiniMax M2.5が含まれており、十分な制限と安定したアクセスが提供されます。",
|
||||
"go.faq.q3": "GoはZenと同じですか?",
|
||||
"go.faq.a3":
|
||||
"いいえ。Zenは従量課金制ですが、Goは月額$10のサブスクリプションで、GLM-5、Kimi K2.5、MiniMax M2.5といったオープンソースモデルへの十分な制限と安定したアクセスを提供します。",
|
||||
"go.faq.q4": "Goの料金は?",
|
||||
"go.faq.a4.p1.beforePricing": "Goは",
|
||||
"go.faq.a4.p1.pricingLink": "月額$10",
|
||||
"go.faq.a4.p1.afterPricing": "で、十分な制限が含まれます。",
|
||||
"go.faq.a4.p2.beforeAccount": "管理画面:",
|
||||
"go.faq.a4.p2.accountLink": "アカウント",
|
||||
"go.faq.a4.p3": "いつでもキャンセル可能です。",
|
||||
"go.faq.q5": "データとプライバシーは?",
|
||||
"go.faq.a5.body":
|
||||
"このプランは主に海外ユーザー向けに設計されており、米国、EU、シンガポールでホストされたモデルにより安定したグローバルアクセスを提供します。",
|
||||
"go.faq.a5.contactAfter": "ご質問がございましたら。",
|
||||
"go.faq.a5.beforeExceptions":
|
||||
"Goのモデルは米国でホストされています。プロバイダーはゼロ保持ポリシーに従い、モデルのトレーニングにデータを使用しません(",
|
||||
"go.faq.a5.exceptionsLink": "以下の例外",
|
||||
"go.faq.q6": "クレジットをチャージできますか?",
|
||||
"go.faq.a6": "利用枠を追加したい場合は、アカウントでクレジットをチャージできます。",
|
||||
"go.faq.q7": "キャンセルできますか?",
|
||||
"go.faq.a7": "はい、いつでもキャンセル可能です。",
|
||||
"go.faq.q8": "他のコーディングエージェントでGoを使えますか?",
|
||||
"go.faq.a8":
|
||||
"はい、Goは任意のエージェントで使用できます。お使いのコーディングエージェントのセットアップ手順に従ってください。",
|
||||
|
||||
"go.faq.q9": "無料モデルとGoの違いは何ですか?",
|
||||
"go.faq.a9":
|
||||
"無料モデルにはBig Pickleと、その時点で利用可能なプロモーションモデルが含まれ、1日200リクエストの制限があります。GoにはGLM-5、Kimi K2.5、MiniMax M2.5が含まれ、ローリングウィンドウ(5時間、週間、月間)全体でより高いリクエスト制限が適用されます。これは概算で5時間あたり$12、週間$30、月間$60相当です(実際のリクエスト数はモデルと使用状況により異なります)。",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "レート制限を超えました。後でもう一度お試しください。",
|
||||
"zen.api.error.modelNotSupported": "モデル {{model}} はサポートされていません",
|
||||
"zen.api.error.modelFormatNotSupported": "フォーマット {{format}} ではモデル {{model}} はサポートされていません",
|
||||
|
||||
@@ -245,6 +245,105 @@ export const dict = {
|
||||
"모든 Zen 모델은 미국에서 호스팅됩니다. 제공자들은 데이터 보존 금지 정책을 따르며 모델 학습에 데이터를 사용하지 않습니다. 단,",
|
||||
"zen.privacy.exceptionsLink": "다음 예외",
|
||||
|
||||
"go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델",
|
||||
"go.meta.description":
|
||||
"Go는 GLM-5, Kimi K2.5, MiniMax M2.5에 대해 넉넉한 5시간 요청 한도를 제공하는 월 $10 구독입니다.",
|
||||
"go.hero.title": "모두를 위한 저비용 코딩 모델",
|
||||
"go.hero.body":
|
||||
"Go는 전 세계 프로그래머들에게 에이전트 코딩을 제공합니다. 가장 유능한 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공하므로, 비용이나 가용성 걱정 없이 강력한 에이전트로 빌드할 수 있습니다.",
|
||||
|
||||
"go.cta.start": "Go 구독하기",
|
||||
"go.cta.template": "{{text}} {{price}}",
|
||||
"go.cta.text": "Go 구독하기",
|
||||
"go.cta.price": "$10/월",
|
||||
"go.pricing.body": "모든 에이전트와 함께 사용하세요. 필요 시 크레딧을 충전하세요. 언제든지 취소 가능.",
|
||||
"go.graph.free": "무료",
|
||||
"go.graph.freePill": "Big Pickle 및 무료 모델",
|
||||
"go.graph.go": "Go",
|
||||
"go.graph.label": "5시간당 요청 수",
|
||||
"go.graph.usageLimits": "사용 한도",
|
||||
"go.graph.tick": "{{n}}배",
|
||||
"go.graph.aria": "5시간당 요청 수: {{free}} 대 {{go}}",
|
||||
|
||||
"go.testimonials.brand.zen": "Zen",
|
||||
"go.testimonials.brand.go": "Go",
|
||||
"go.testimonials.handle": "@OpenCode",
|
||||
"go.testimonials.dax.name": "Dax Raad",
|
||||
"go.testimonials.dax.title": "전 Terminal Products CEO",
|
||||
"go.testimonials.dax.quoteAfter": "(은)는 삶을 변화시켰습니다. 정말 당연한 선택입니다.",
|
||||
"go.testimonials.jay.name": "Jay V",
|
||||
"go.testimonials.jay.title": "전 Founder, SEED, PM, Melt, Pop, Dapt, Cadmus, ViewPoint",
|
||||
"go.testimonials.jay.quoteBefore": "우리 팀 5명 중 4명이",
|
||||
"go.testimonials.jay.quoteAfter": " 사용을 정말 좋아합니다.",
|
||||
"go.testimonials.adam.name": "Adam Elmore",
|
||||
"go.testimonials.adam.title": "전 AWS Hero",
|
||||
"go.testimonials.adam.quoteBefore": "저는",
|
||||
"go.testimonials.adam.quoteAfter": "를(을) 아무리 추천해도 부족합니다. 진심으로 정말 좋습니다.",
|
||||
"go.testimonials.david.name": "David Hill",
|
||||
"go.testimonials.david.title": "전 Laravel 디자인 총괄",
|
||||
"go.testimonials.david.quoteBefore": "",
|
||||
"go.testimonials.david.quoteAfter":
|
||||
"와(과) 함께라면 모든 모델이 테스트를 거쳤고 코딩 에이전트에 완벽하다는 것을 알 수 있습니다.",
|
||||
"go.testimonials.frank.name": "Frank Wang",
|
||||
"go.testimonials.frank.title": "전 Nvidia 인턴 (4회)",
|
||||
"go.testimonials.frank.quote": "아직 Nvidia에 있었으면 좋았을 텐데요.",
|
||||
"go.problem.title": "Go는 어떤 문제를 해결하나요?",
|
||||
"go.problem.body":
|
||||
"우리는 가능한 한 많은 사람들에게 OpenCode 경험을 제공하는 데 집중하고 있습니다. OpenCode Go는 전 세계 프로그래머들에게 에이전트 코딩을 제공하기 위해 설계된 저렴한(월 $10) 구독입니다. 가장 유능한 오픈 소스 모델에 대해 넉넉한 한도와 안정적인 액세스를 제공합니다.",
|
||||
"go.problem.subtitle": " ",
|
||||
"go.problem.item1": "저렴한 구독 가격",
|
||||
"go.problem.item2": "넉넉한 한도와 안정적인 액세스",
|
||||
"go.problem.item3": "가능한 한 많은 프로그래머를 위해 제작됨",
|
||||
"go.problem.item4": "GLM-5, Kimi K2.5, MiniMax M2.5 포함",
|
||||
"go.how.title": "Go 작동 방식",
|
||||
"go.how.body": "Go는 OpenCode 또는 다른 어떤 에이전트와도 사용할 수 있는 월 $10 구독입니다.",
|
||||
"go.how.step1.title": "계정 생성",
|
||||
"go.how.step1.beforeLink": "",
|
||||
"go.how.step1.link": "설정 지침을 따르세요",
|
||||
"go.how.step2.title": "Go 구독",
|
||||
"go.how.step2.link": "$10/월",
|
||||
"go.how.step2.afterLink": "(넉넉한 한도 포함)",
|
||||
"go.how.step3.title": "코딩 시작",
|
||||
"go.how.step3.body": "오픈 소스 모델에 대한 안정적인 액세스와 함께",
|
||||
"go.privacy.title": "귀하의 프라이버시는 우리에게 중요합니다",
|
||||
"go.privacy.body":
|
||||
"이 플랜은 주로 글로벌 사용자를 위해 설계되었으며, 안정적인 글로벌 액세스를 위해 미국, EU, 싱가포르에 모델이 호스팅되어 있습니다.",
|
||||
"go.privacy.contactAfter": "질문이 있으시면 언제든지 문의해 주세요.",
|
||||
"go.privacy.beforeExceptions":
|
||||
"Go 모델은 미국에서 호스팅됩니다. 제공자들은 데이터 보존 금지 정책을 따르며 모델 학습에 데이터를 사용하지 않습니다. 단,",
|
||||
"go.privacy.exceptionsLink": "다음 예외",
|
||||
"go.faq.q1": "OpenCode Go란 무엇인가요?",
|
||||
"go.faq.a1": "Go는 에이전트 코딩을 위한 유능한 오픈 소스 모델에 대해 안정적인 액세스를 제공하는 저비용 구독입니다.",
|
||||
"go.faq.q2": "Go에는 어떤 모델이 포함되나요?",
|
||||
"go.faq.a2": "Go에는 넉넉한 한도와 안정적인 액세스를 제공하는 GLM-5, Kimi K2.5, MiniMax M2.5가 포함됩니다.",
|
||||
"go.faq.q3": "Go는 Zen과 같은가요?",
|
||||
"go.faq.a3":
|
||||
"아니요. Zen은 사용한 만큼 지불(pay-as-you-go)하는 방식인 반면, Go는 월 $10 구독으로 오픈 소스 모델인 GLM-5, Kimi K2.5, MiniMax M2.5에 대해 넉넉한 한도와 안정적인 액세스를 제공합니다.",
|
||||
"go.faq.q4": "Go 비용은 얼마인가요?",
|
||||
"go.faq.a4.p1.beforePricing": "Go 비용은",
|
||||
"go.faq.a4.p1.pricingLink": "$10/월",
|
||||
"go.faq.a4.p1.afterPricing": "이며 넉넉한 한도를 제공합니다.",
|
||||
"go.faq.a4.p2.beforeAccount": "구독 관리는 다음에서 가능합니다:",
|
||||
"go.faq.a4.p2.accountLink": "계정",
|
||||
"go.faq.a4.p3": "언제든지 취소할 수 있습니다.",
|
||||
"go.faq.q5": "데이터와 프라이버시는 어떤가요?",
|
||||
"go.faq.a5.body":
|
||||
"이 플랜은 주로 글로벌 사용자를 위해 설계되었으며, 안정적인 글로벌 액세스를 위해 미국, EU, 싱가포르에 모델이 호스팅되어 있습니다.",
|
||||
"go.faq.a5.contactAfter": "질문이 있으시면 언제든지 문의해 주세요.",
|
||||
"go.faq.a5.beforeExceptions":
|
||||
"Go 모델은 미국에서 호스팅됩니다. 제공자들은 데이터 보존 금지 정책을 따르며 모델 학습에 데이터를 사용하지 않습니다. 단,",
|
||||
"go.faq.a5.exceptionsLink": "다음 예외",
|
||||
"go.faq.q6": "크레딧을 충전할 수 있나요?",
|
||||
"go.faq.a6": "사용량이 더 필요한 경우 계정에서 크레딧을 충전할 수 있습니다.",
|
||||
"go.faq.q7": "취소할 수 있나요?",
|
||||
"go.faq.a7": "네, 언제든지 취소할 수 있습니다.",
|
||||
"go.faq.q8": "다른 코딩 에이전트와 Go를 사용할 수 있나요?",
|
||||
"go.faq.a8": "네, Go는 어떤 에이전트와도 사용할 수 있습니다. 선호하는 코딩 에이전트의 설정 지침을 따르세요.",
|
||||
|
||||
"go.faq.q9": "무료 모델과 Go의 차이점은 무엇인가요?",
|
||||
"go.faq.a9":
|
||||
"무료 모델에는 Big Pickle과 당시 사용 가능한 프로모션 모델이 포함되며, 하루 200회 요청 할당량이 적용됩니다. Go는 GLM-5, Kimi K2.5, MiniMax M2.5를 포함하며, 롤링 윈도우(5시간, 주간, 월간)에 걸쳐 더 높은 요청 할당량을 적용합니다. 이는 대략 5시간당 $12, 주당 $30, 월 $60에 해당합니다(실제 요청 수는 모델 및 사용량에 따라 다름).",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "속도 제한을 초과했습니다. 나중에 다시 시도해 주세요.",
|
||||
"zen.api.error.modelNotSupported": "{{model}} 모델은 지원되지 않습니다",
|
||||
"zen.api.error.modelFormatNotSupported": "{{model}} 모델은 {{format}} 형식에 대해 지원되지 않습니다",
|
||||
|
||||
@@ -249,6 +249,106 @@ export const dict = {
|
||||
"Alle Zen-modeller hostes i USA. Leverandører følger en policy om null oppbevaring og bruker ikke dataene dine til modelltrening, med",
|
||||
"zen.privacy.exceptionsLink": "følgende unntak",
|
||||
|
||||
"go.title": "OpenCode Go | Rimelige kodemodeller for alle",
|
||||
"go.meta.description":
|
||||
"Go er et abonnement til $10/måned med rause grenser på 5 timer for GLM-5, Kimi K2.5 og MiniMax M2.5.",
|
||||
"go.hero.title": "Rimelige kodemodeller for alle",
|
||||
"go.hero.body":
|
||||
"Go bringer agent-koding til programmerere over hele verden. Med rause grenser og pålitelig tilgang til de mest kapable åpen kildekode-modellene, kan du bygge med kraftige agenter uten å bekymre deg for kostnader eller tilgjengelighet.",
|
||||
|
||||
"go.cta.start": "Abonner på Go",
|
||||
"go.cta.template": "{{text}} {{price}}",
|
||||
"go.cta.text": "Abonner på Go",
|
||||
"go.cta.price": "$10/måned",
|
||||
"go.pricing.body": "Bruk med hvilken som helst agent. Fyll på kreditt om nødvendig. Avslutt når som helst.",
|
||||
"go.graph.free": "Gratis",
|
||||
"go.graph.freePill": "Big Pickle og gratis modeller",
|
||||
"go.graph.go": "Go",
|
||||
"go.graph.label": "Forespørsler per 5 timer",
|
||||
"go.graph.usageLimits": "Bruksgrenser",
|
||||
"go.graph.tick": "{{n}}x",
|
||||
"go.graph.aria": "Forespørsler per 5t: {{free}} vs {{go}}",
|
||||
|
||||
"go.testimonials.brand.zen": "Zen",
|
||||
"go.testimonials.brand.go": "Go",
|
||||
"go.testimonials.handle": "@OpenCode",
|
||||
"go.testimonials.dax.name": "Dax Raad",
|
||||
"go.testimonials.dax.title": "tidligere CEO, Terminal Products",
|
||||
"go.testimonials.dax.quoteAfter": "har endret livet mitt, det er virkelig en no-brainer.",
|
||||
"go.testimonials.jay.name": "Jay V",
|
||||
"go.testimonials.jay.title": "tidligere grunnlegger, SEED, PM, Melt, Pop, Dapt, Cadmus og ViewPoint",
|
||||
"go.testimonials.jay.quoteBefore": "4 av 5 personer på teamet vårt elsker å bruke",
|
||||
"go.testimonials.jay.quoteAfter": ".",
|
||||
"go.testimonials.adam.name": "Adam Elmore",
|
||||
"go.testimonials.adam.title": "tidligere Hero, AWS",
|
||||
"go.testimonials.adam.quoteBefore": "Jeg kan ikke anbefale",
|
||||
"go.testimonials.adam.quoteAfter": "nok. Seriøst, det er virkelig bra.",
|
||||
"go.testimonials.david.name": "David Hill",
|
||||
"go.testimonials.david.title": "tidligere Head of Design, Laravel",
|
||||
"go.testimonials.david.quoteBefore": "Med",
|
||||
"go.testimonials.david.quoteAfter": "vet jeg at alle modellene er testet og perfekte for kodeagenter.",
|
||||
"go.testimonials.frank.name": "Frank Wang",
|
||||
"go.testimonials.frank.title": "tidligere intern, Nvidia (4 ganger)",
|
||||
"go.testimonials.frank.quote": "Jeg skulle ønske jeg fortsatt var hos Nvidia.",
|
||||
"go.problem.title": "Hvilket problem løser Go?",
|
||||
"go.problem.body":
|
||||
"Vi fokuserer på å bringe OpenCode-opplevelsen til så mange mennesker som mulig. OpenCode Go er et rimelig ($10/måned) abonnement designet for å bringe agent-koding til programmerere over hele verden. Det gir rause grenser og pålitelig tilgang til de mest kapable åpen kildekode-modellene.",
|
||||
"go.problem.subtitle": " ",
|
||||
"go.problem.item1": "Rimelig abonnementspris",
|
||||
"go.problem.item2": "Rause grenser og pålitelig tilgang",
|
||||
"go.problem.item3": "Bygget for så mange programmerere som mulig",
|
||||
"go.problem.item4": "Inkluderer GLM-5, Kimi K2.5 og MiniMax M2.5",
|
||||
"go.how.title": "Hvordan Go fungerer",
|
||||
"go.how.body": "Go er et abonnement til $10/måned som du kan bruke med OpenCode eller hvilken som helst agent.",
|
||||
"go.how.step1.title": "Opprett en konto",
|
||||
"go.how.step1.beforeLink": "følg",
|
||||
"go.how.step1.link": "oppsettsinstruksjonene",
|
||||
"go.how.step2.title": "Abonner på Go",
|
||||
"go.how.step2.link": "$10/måned",
|
||||
"go.how.step2.afterLink": "med rause grenser",
|
||||
"go.how.step3.title": "Begynn å kode",
|
||||
"go.how.step3.body": "med pålitelig tilgang til åpen kildekode-modeller",
|
||||
"go.privacy.title": "Personvernet ditt er viktig for oss",
|
||||
"go.privacy.body":
|
||||
"Planen er primært designet for internasjonale brukere, med modeller driftet i USA, EU og Singapore for stabil global tilgang.",
|
||||
"go.privacy.contactAfter": "hvis du har spørsmål.",
|
||||
"go.privacy.beforeExceptions":
|
||||
"Go-modeller hostes i USA. Leverandører følger en policy om null oppbevaring og bruker ikke dataene dine til modelltrening, med",
|
||||
"go.privacy.exceptionsLink": "følgende unntak",
|
||||
"go.faq.q1": "Hva er OpenCode Go?",
|
||||
"go.faq.a1":
|
||||
"Go er et rimelig abonnement som gir deg pålitelig tilgang til kapable åpen kildekode-modeller for agent-koding.",
|
||||
"go.faq.q2": "Hvilke modeller inkluderer Go?",
|
||||
"go.faq.a2": "Go inkluderer GLM-5, Kimi K2.5 og MiniMax M2.5, med rause grenser og pålitelig tilgang.",
|
||||
"go.faq.q3": "Er Go det samme som Zen?",
|
||||
"go.faq.a3":
|
||||
"Nei. Zen er pay-as-you-go, mens Go er et abonnement til $10/måned med rause grenser og pålitelig tilgang til åpen kildekode-modellene GLM-5, Kimi K2.5 og MiniMax M2.5.",
|
||||
"go.faq.q4": "Hva koster Go?",
|
||||
"go.faq.a4.p1.beforePricing": "Go koster",
|
||||
"go.faq.a4.p1.pricingLink": "$10/måned",
|
||||
"go.faq.a4.p1.afterPricing": "med rause grenser.",
|
||||
"go.faq.a4.p2.beforeAccount": "Du kan administrere abonnementet ditt i din",
|
||||
"go.faq.a4.p2.accountLink": "konto",
|
||||
"go.faq.a4.p3": "Avslutt når som helst.",
|
||||
"go.faq.q5": "Hva med data og personvern?",
|
||||
"go.faq.a5.body":
|
||||
"Planen er primært designet for internasjonale brukere, med modeller driftet i USA, EU og Singapore for stabil global tilgang.",
|
||||
"go.faq.a5.contactAfter": "hvis du har spørsmål.",
|
||||
"go.faq.a5.beforeExceptions":
|
||||
"Go-modeller hostes i USA. Leverandører følger en policy om null oppbevaring og bruker ikke dataene dine til modelltrening, med",
|
||||
"go.faq.a5.exceptionsLink": "følgende unntak",
|
||||
"go.faq.q6": "Kan jeg fylle på kreditt?",
|
||||
"go.faq.a6": "Hvis du trenger mer bruk, kan du fylle på kreditt i kontoen din.",
|
||||
"go.faq.q7": "Kan jeg avslutte?",
|
||||
"go.faq.a7": "Ja, du kan avslutte når som helst.",
|
||||
"go.faq.q8": "Kan jeg bruke Go med andre kodeagenter?",
|
||||
"go.faq.a8":
|
||||
"Ja, du kan bruke Go med hvilken som helst agent. Følg oppsettinstruksjonene i din foretrukne kodeagent.",
|
||||
|
||||
"go.faq.q9": "Hva er forskjellen mellom gratis modeller og Go?",
|
||||
"go.faq.a9":
|
||||
"Gratis modeller inkluderer Big Pickle pluss kampanjemodeller tilgjengelig på det tidspunktet, med en kvote på 200 forespørsler/dag. Go inkluderer GLM-5, Kimi K2.5 og MiniMax M2.5 med høyere kvoter håndhevet over rullerende vinduer (5 timer, ukentlig og månedlig), omtrent tilsvarende $12 per 5 timer, $30 per uke og $60 per måned (faktiske forespørselsantall varierer etter modell og bruk).",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Rate limit overskredet. Vennligst prøv igjen senere.",
|
||||
"zen.api.error.modelNotSupported": "Modell {{model}} støttes ikke",
|
||||
"zen.api.error.modelFormatNotSupported": "Modell {{model}} støttes ikke for format {{format}}",
|
||||
|
||||
@@ -250,6 +250,106 @@ export const dict = {
|
||||
"Wszystkie modele Zen są hostowane w USA. Dostawcy stosują politykę zerowej retencji i nie wykorzystują Twoich danych do trenowania modeli, z",
|
||||
"zen.privacy.exceptionsLink": "następującymi wyjątkami",
|
||||
|
||||
"go.title": "OpenCode Go | Niskokosztowe modele do kodowania dla każdego",
|
||||
"go.meta.description":
|
||||
"Go to subskrypcja za $10/miesiąc z hojnymi 5-godzinnymi limitami zapytań dla GLM-5, Kimi K2.5 i MiniMax M2.5.",
|
||||
"go.hero.title": "Niskokosztowe modele do kodowania dla każdego",
|
||||
"go.hero.body":
|
||||
"Go udostępnia programowanie z agentami programistom na całym świecie. Oferuje hojne limity i niezawodny dostęp do najzdolniejszych modeli open source, dzięki czemu możesz budować za pomocą potężnych agentów, nie martwiąc się o koszty czy dostępność.",
|
||||
|
||||
"go.cta.start": "Zasubskrybuj Go",
|
||||
"go.cta.template": "{{text}} {{price}}",
|
||||
"go.cta.text": "Zasubskrybuj Go",
|
||||
"go.cta.price": "$10/miesiąc",
|
||||
"go.pricing.body": "Używaj z dowolnym agentem. Doładuj środki w razie potrzeby. Anuluj w dowolnym momencie.",
|
||||
"go.graph.free": "Darmowe",
|
||||
"go.graph.freePill": "Big Pickle i darmowe modele",
|
||||
"go.graph.go": "Go",
|
||||
"go.graph.label": "Żądania na 5 godzin",
|
||||
"go.graph.usageLimits": "Limity użycia",
|
||||
"go.graph.tick": "{{n}}x",
|
||||
"go.graph.aria": "Żądania na 5h: {{free}} vs {{go}}",
|
||||
|
||||
"go.testimonials.brand.zen": "Zen",
|
||||
"go.testimonials.brand.go": "Go",
|
||||
"go.testimonials.handle": "@OpenCode",
|
||||
"go.testimonials.dax.name": "Dax Raad",
|
||||
"go.testimonials.dax.title": "ex-CEO, Terminal Products",
|
||||
"go.testimonials.dax.quoteAfter": "zmieniło moje życie, to naprawdę oczywisty wybór.",
|
||||
"go.testimonials.jay.name": "Jay V",
|
||||
"go.testimonials.jay.title": "ex-Founder, SEED, PM, Melt, Pop, Dapt, Cadmus, and ViewPoint",
|
||||
"go.testimonials.jay.quoteBefore": "4 na 5 osób w naszym zespole uwielbia używać",
|
||||
"go.testimonials.jay.quoteAfter": ".",
|
||||
"go.testimonials.adam.name": "Adam Elmore",
|
||||
"go.testimonials.adam.title": "ex-Hero, AWS",
|
||||
"go.testimonials.adam.quoteBefore": "Nie mogę wystarczająco polecić",
|
||||
"go.testimonials.adam.quoteAfter": ". Poważnie, to jest naprawdę dobre.",
|
||||
"go.testimonials.david.name": "David Hill",
|
||||
"go.testimonials.david.title": "ex-Head of Design, Laravel",
|
||||
"go.testimonials.david.quoteBefore": "Dzięki",
|
||||
"go.testimonials.david.quoteAfter": "wiem, że wszystkie modele są przetestowane i idealne dla agentów kodujących.",
|
||||
"go.testimonials.frank.name": "Frank Wang",
|
||||
"go.testimonials.frank.title": "ex-Intern, Nvidia (4 times)",
|
||||
"go.testimonials.frank.quote": "Chciałbym wciąż być w Nvidia.",
|
||||
"go.problem.title": "Jaki problem rozwiązuje Go?",
|
||||
"go.problem.body":
|
||||
"Skupiamy się na dostarczeniu doświadczenia OpenCode jak największej liczbie osób. OpenCode Go to niskokosztowa ($10/miesiąc) subskrypcja zaprojektowana, aby udostępnić programowanie z agentami programistom na całym świecie. Zapewnia hojne limity i niezawodny dostęp do najzdolniejszych modeli open source.",
|
||||
"go.problem.subtitle": " ",
|
||||
"go.problem.item1": "Niskokosztowa cena subskrypcji",
|
||||
"go.problem.item2": "Hojne limity i niezawodny dostęp",
|
||||
"go.problem.item3": "Stworzony dla jak największej liczby programistów",
|
||||
"go.problem.item4": "Zawiera GLM-5, Kimi K2.5 i MiniMax M2.5",
|
||||
"go.how.title": "Jak działa Go",
|
||||
"go.how.body": "Go to subskrypcja za $10/miesiąc, której możesz używać z OpenCode lub dowolnym agentem.",
|
||||
"go.how.step1.title": "Załóż konto",
|
||||
"go.how.step1.beforeLink": "postępuj zgodnie z",
|
||||
"go.how.step1.link": "instrukcją konfiguracji",
|
||||
"go.how.step2.title": "Zasubskrybuj Go",
|
||||
"go.how.step2.link": "$10/miesiąc",
|
||||
"go.how.step2.afterLink": "z hojnymi limitami",
|
||||
"go.how.step3.title": "Zacznij kodować",
|
||||
"go.how.step3.body": "z niezawodnym dostępem do modeli open source",
|
||||
"go.privacy.title": "Twoja prywatność jest dla nas ważna",
|
||||
"go.privacy.body":
|
||||
"Plan został zaprojektowany głównie dla użytkowników międzynarodowych, z modelami hostowanymi w USA, UE i Singapurze, aby zapewnić stabilny globalny dostęp.",
|
||||
"go.privacy.contactAfter": "jeśli masz jakiekolwiek pytania.",
|
||||
"go.privacy.beforeExceptions":
|
||||
"Modele Go są hostowane w USA. Dostawcy stosują politykę zerowej retencji i nie używają Twoich danych do trenowania modeli, z",
|
||||
"go.privacy.exceptionsLink": "następującymi wyjątkami",
|
||||
"go.faq.q1": "Czym jest OpenCode Go?",
|
||||
"go.faq.a1":
|
||||
"Go to niskokosztowa subskrypcja, która daje niezawodny dostęp do zdolnych modeli open source dla agentów kodujących.",
|
||||
"go.faq.q2": "Jakie modele zawiera Go?",
|
||||
"go.faq.a2": "Go zawiera GLM-5, Kimi K2.5 i MiniMax M2.5, z hojnymi limitami i niezawodnym dostępem.",
|
||||
"go.faq.q3": "Czy Go to to samo co Zen?",
|
||||
"go.faq.a3":
|
||||
"Nie. Zen działa w modelu pay-as-you-go (płacisz za użycie), podczas gdy Go to subskrypcja za $10/miesiąc z hojnymi limitami i niezawodnym dostępem do modeli open source GLM-5, Kimi K2.5 i MiniMax M2.5.",
|
||||
"go.faq.q4": "Ile kosztuje Go?",
|
||||
"go.faq.a4.p1.beforePricing": "Go kosztuje",
|
||||
"go.faq.a4.p1.pricingLink": "$10/miesiąc",
|
||||
"go.faq.a4.p1.afterPricing": "z hojnymi limitami.",
|
||||
"go.faq.a4.p2.beforeAccount": "Możesz zarządzać subskrypcją na swoim",
|
||||
"go.faq.a4.p2.accountLink": "koncie",
|
||||
"go.faq.a4.p3": "Anuluj w dowolnym momencie.",
|
||||
"go.faq.q5": "A co z danymi i prywatnością?",
|
||||
"go.faq.a5.body":
|
||||
"Plan został zaprojektowany głównie dla użytkowników międzynarodowych, z modelami hostowanymi w USA, UE i Singapurze, aby zapewnić stabilny globalny dostęp.",
|
||||
"go.faq.a5.contactAfter": "jeśli masz jakiekolwiek pytania.",
|
||||
"go.faq.a5.beforeExceptions":
|
||||
"Modele Go są hostowane w USA. Dostawcy stosują politykę zerowej retencji i nie używają Twoich danych do trenowania modeli, z",
|
||||
"go.faq.a5.exceptionsLink": "następującymi wyjątkami",
|
||||
"go.faq.q6": "Czy mogę doładować środki?",
|
||||
"go.faq.a6": "Jeśli potrzebujesz większego użycia, możesz doładować środki na swoim koncie.",
|
||||
"go.faq.q7": "Czy mogę anulować?",
|
||||
"go.faq.a7": "Tak, możesz anulować w dowolnym momencie.",
|
||||
"go.faq.q8": "Czy mogę używać Go z innymi agentami kodującymi?",
|
||||
"go.faq.a8":
|
||||
"Tak, możesz używać Go z dowolnym agentem. Postępuj zgodnie z instrukcjami konfiguracji w swoim preferowanym agencie.",
|
||||
|
||||
"go.faq.q9": "Jaka jest różnica między darmowymi modelami a Go?",
|
||||
"go.faq.a9":
|
||||
"Darmowe modele obejmują Big Pickle oraz modele promocyjne dostępne w danym momencie, z limitem 200 zapytań/dzień. Go zawiera GLM-5, Kimi K2.5 i MiniMax M2.5 z wyższymi limitami zapytań egzekwowanymi w oknach kroczących (5-godzinnych, tygodniowych i miesięcznych), w przybliżeniu równoważnymi $12 na 5 godzin, $30 tygodniowo i $60 miesięcznie (rzeczywista liczba zapytań zależy od modelu i użycia).",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Przekroczono limit zapytań. Spróbuj ponownie później.",
|
||||
"zen.api.error.modelNotSupported": "Model {{model}} nie jest obsługiwany",
|
||||
"zen.api.error.modelFormatNotSupported": "Model {{model}} nie jest obsługiwany dla formatu {{format}}",
|
||||
|
||||
@@ -253,6 +253,107 @@ export const dict = {
|
||||
"Все модели Zen размещены в США. Провайдеры следуют политике нулевого хранения и не используют ваши данные для обучения моделей, за",
|
||||
"zen.privacy.exceptionsLink": "следующими исключениями",
|
||||
|
||||
"go.title": "OpenCode Go | Недорогие модели для кодинга для всех",
|
||||
"go.meta.description":
|
||||
"Go — это подписка за $10/месяц с щедрыми 5-часовыми лимитами запросов для GLM-5, Kimi K2.5 и MiniMax M2.5.",
|
||||
"go.hero.title": "Недорогие модели для кодинга для всех",
|
||||
"go.hero.body":
|
||||
"Go открывает доступ к агентам-программистам разработчикам по всему миру. Предлагая щедрые лимиты и надежный доступ к наиболее способным моделям с открытым исходным кодом, вы можете создавать проекты с мощными агентами, не беспокоясь о затратах или доступности.",
|
||||
|
||||
"go.cta.start": "Подписаться на Go",
|
||||
"go.cta.template": "{{text}} {{price}}",
|
||||
"go.cta.text": "Подписаться на Go",
|
||||
"go.cta.price": "$10/месяц",
|
||||
"go.pricing.body": "Используйте с любым агентом. Пополняйте баланс при необходимости. Отменяйте в любое время.",
|
||||
"go.graph.free": "Бесплатно",
|
||||
"go.graph.freePill": "Big Pickle и бесплатные модели",
|
||||
"go.graph.go": "Go",
|
||||
"go.graph.label": "Запросов за 5 часов",
|
||||
"go.graph.usageLimits": "Лимиты использования",
|
||||
"go.graph.tick": "{{n}}x",
|
||||
"go.graph.aria": "Запросов за 5ч: {{free}} против {{go}}",
|
||||
|
||||
"go.testimonials.brand.zen": "Zen",
|
||||
"go.testimonials.brand.go": "Go",
|
||||
"go.testimonials.handle": "@OpenCode",
|
||||
"go.testimonials.dax.name": "Dax Raad",
|
||||
"go.testimonials.dax.title": "ex-CEO, Terminal Products",
|
||||
"go.testimonials.dax.quoteAfter": "изменил мою жизнь, это действительно очевидный выбор.",
|
||||
"go.testimonials.jay.name": "Jay V",
|
||||
"go.testimonials.jay.title": "ex-Founder, SEED, PM, Melt, Pop, Dapt, Cadmus, и ViewPoint",
|
||||
"go.testimonials.jay.quoteBefore": "4 из 5 человек в нашей команде любят использовать",
|
||||
"go.testimonials.jay.quoteAfter": ".",
|
||||
"go.testimonials.adam.name": "Adam Elmore",
|
||||
"go.testimonials.adam.title": "ex-Hero, AWS",
|
||||
"go.testimonials.adam.quoteBefore": "Я не могу не порекомендовать",
|
||||
"go.testimonials.adam.quoteAfter": "достаточно сильно. Серьезно, это очень круто.",
|
||||
"go.testimonials.david.name": "David Hill",
|
||||
"go.testimonials.david.title": "ex-Head of Design, Laravel",
|
||||
"go.testimonials.david.quoteBefore": "С",
|
||||
"go.testimonials.david.quoteAfter":
|
||||
"я знаю, что все модели протестированы и идеально подходят для агентов-программистов.",
|
||||
"go.testimonials.frank.name": "Frank Wang",
|
||||
"go.testimonials.frank.title": "ex-Intern, Nvidia (4 раза)",
|
||||
"go.testimonials.frank.quote": "Жаль, что я больше не в Nvidia.",
|
||||
"go.problem.title": "Какую проблему решает Go?",
|
||||
"go.problem.body":
|
||||
"Мы сосредоточены на том, чтобы сделать OpenCode доступным как можно большему числу людей. OpenCode Go — это недорогая ($10/месяц) подписка, разработанная, чтобы сделать агентов-программистов доступными для разработчиков по всему миру. Она предоставляет щедрые лимиты и надежный доступ к самым способным моделям с открытым исходным кодом.",
|
||||
"go.problem.subtitle": " ",
|
||||
"go.problem.item1": "Недорогая подписка",
|
||||
"go.problem.item2": "Щедрые лимиты и надежный доступ",
|
||||
"go.problem.item3": "Создан для максимального числа программистов",
|
||||
"go.problem.item4": "Включает GLM-5, Kimi K2.5 и MiniMax M2.5",
|
||||
"go.how.title": "Как работает Go",
|
||||
"go.how.body": "Go — это подписка за $10/месяц, которую можно использовать с OpenCode или любым агентом.",
|
||||
"go.how.step1.title": "Создайте аккаунт",
|
||||
"go.how.step1.beforeLink": "следуйте",
|
||||
"go.how.step1.link": "инструкциям по настройке",
|
||||
"go.how.step2.title": "Подпишитесь на Go",
|
||||
"go.how.step2.link": "$10/месяц",
|
||||
"go.how.step2.afterLink": "с щедрыми лимитами",
|
||||
"go.how.step3.title": "Начните кодить",
|
||||
"go.how.step3.body": "с надежным доступом к open-source моделям",
|
||||
"go.privacy.title": "Ваша приватность важна для нас",
|
||||
"go.privacy.body":
|
||||
"План разработан в первую очередь для международных пользователей, с моделями, размещенными в США, ЕС и Сингапуре для стабильного глобального доступа.",
|
||||
"go.privacy.contactAfter": "если у вас есть вопросы.",
|
||||
"go.privacy.beforeExceptions":
|
||||
"Модели Go размещены в США. Провайдеры следуют политике нулевого хранения и не используют ваши данные для обучения моделей, за",
|
||||
"go.privacy.exceptionsLink": "следующими исключениями",
|
||||
"go.faq.q1": "Что такое OpenCode Go?",
|
||||
"go.faq.a1":
|
||||
"Go — это недорогая подписка, дающая надежный доступ к мощным моделям с открытым исходным кодом для агентов-программистов.",
|
||||
"go.faq.q2": "Какие модели включает Go?",
|
||||
"go.faq.a2": "Go включает GLM-5, Kimi K2.5 и MiniMax M2.5, с щедрыми лимитами и надежным доступом.",
|
||||
"go.faq.q3": "Go — это то же самое, что и Zen?",
|
||||
"go.faq.a3":
|
||||
"Нет. Zen работает по системе оплаты за использование (pay-as-you-go), тогда как Go — это подписка за $10/месяц с щедрыми лимитами и надежным доступом к open-source моделям GLM-5, Kimi K2.5 и MiniMax M2.5.",
|
||||
"go.faq.q4": "Сколько стоит Go?",
|
||||
"go.faq.a4.p1.beforePricing": "Go стоит",
|
||||
"go.faq.a4.p1.pricingLink": "$10/месяц",
|
||||
"go.faq.a4.p1.afterPricing": "с щедрыми лимитами.",
|
||||
"go.faq.a4.p2.beforeAccount": "Вы можете управлять подпиской в своем",
|
||||
"go.faq.a4.p2.accountLink": "аккаунте",
|
||||
"go.faq.a4.p3": "Отмена в любое время.",
|
||||
"go.faq.q5": "Как насчет данных и приватности?",
|
||||
"go.faq.a5.body":
|
||||
"План разработан в первую очередь для международных пользователей, с моделями, размещенными в США, ЕС и Сингапуре для стабильного глобального доступа.",
|
||||
"go.faq.a5.contactAfter": "если у вас есть вопросы.",
|
||||
"go.faq.a5.beforeExceptions":
|
||||
"Модели Go размещены в США. Провайдеры следуют политике нулевого хранения и не используют ваши данные для обучения моделей, за",
|
||||
"go.faq.a5.exceptionsLink": "следующими исключениями",
|
||||
"go.faq.q6": "Могу ли я пополнить баланс?",
|
||||
"go.faq.a6": "Если вам нужно больше использования, вы можете пополнить баланс в своем аккаунте.",
|
||||
"go.faq.q7": "Могу ли я отменить подписку?",
|
||||
"go.faq.a7": "Да, вы можете отменить подписку в любое время.",
|
||||
"go.faq.q8": "Могу ли я использовать Go с другими кодинг-агентами?",
|
||||
"go.faq.a8":
|
||||
"Да, вы можете использовать Go с любым агентом. Следуйте инструкциям по настройке в вашем предпочитаемом агенте.",
|
||||
|
||||
"go.faq.q9": "В чем разница между бесплатными моделями и Go?",
|
||||
"go.faq.a9":
|
||||
"Бесплатные модели включают Big Pickle плюс промо-модели, доступные на данный момент, с квотой 200 запросов/день. Go включает GLM-5, Kimi K2.5 и MiniMax M2.5 с более высокими квотами запросов, применяемыми в скользящих окнах (5 часов, неделя и месяц), что примерно эквивалентно $12 за 5 часов, $30 в неделю и $60 в месяц (фактическое количество запросов зависит от модели и использования).",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "Превышен лимит запросов. Пожалуйста, попробуйте позже.",
|
||||
"zen.api.error.modelNotSupported": "Модель {{model}} не поддерживается",
|
||||
"zen.api.error.modelFormatNotSupported": "Модель {{model}} не поддерживается для формата {{format}}",
|
||||
|
||||
@@ -248,6 +248,105 @@ export const dict = {
|
||||
"โมเดล Zen ทั้งหมดโฮสต์ในสหรัฐอเมริกา ผู้ให้บริการปฏิบัติตามนโยบายไม่เก็บรักษาข้อมูล (zero-retention policy) และไม่ใช้ข้อมูลของคุณสำหรับการฝึกโมเดล โดยมี",
|
||||
"zen.privacy.exceptionsLink": "ข้อยกเว้นดังนี้",
|
||||
|
||||
"go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
|
||||
"go.meta.description":
|
||||
"Go คือการสมัครสมาชิกราคา $10/เดือน พร้อมขีดจำกัดการร้องขอที่กว้างขวางถึง 5 ชั่วโมงสำหรับ GLM-5, Kimi K2.5 และ MiniMax M2.5",
|
||||
"go.hero.title": "โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
|
||||
"go.hero.body":
|
||||
"Go นำการเขียนโค้ดแบบเอเจนต์มาสู่นักเขียนโปรแกรมทั่วโลก เสนอขีดจำกัดที่กว้างขวางและการเข้าถึงโมเดลโอเพนซอร์สที่มีความสามารถสูงสุดได้อย่างน่าเชื่อถือ เพื่อให้คุณสามารถสร้างสรรค์ด้วยเอเจนต์ที่ทรงพลังโดยไม่ต้องกังวลเรื่องค่าใช้จ่ายหรือความพร้อมใช้งาน",
|
||||
|
||||
"go.cta.start": "สมัครสมาชิก Go",
|
||||
"go.cta.template": "{{text}} {{price}}",
|
||||
"go.cta.text": "สมัครสมาชิก Go",
|
||||
"go.cta.price": "$10/เดือน",
|
||||
"go.pricing.body": "ใช้กับเอเจนต์ใดก็ได้ เติมเงินเครดิตหากต้องการ ยกเลิกได้ตลอดเวลา",
|
||||
"go.graph.free": "ฟรี",
|
||||
"go.graph.freePill": "Big Pickle และโมเดลฟรี",
|
||||
"go.graph.go": "Go",
|
||||
"go.graph.label": "คำขอต่อ 5 ชั่วโมง",
|
||||
"go.graph.usageLimits": "ขีดจำกัดการใช้งาน",
|
||||
"go.graph.tick": "{{n}}x",
|
||||
"go.graph.aria": "คำขอต่อ 5 ชม.: {{free}} vs {{go}}",
|
||||
|
||||
"go.testimonials.brand.zen": "Zen",
|
||||
"go.testimonials.brand.go": "Go",
|
||||
"go.testimonials.handle": "@OpenCode",
|
||||
"go.testimonials.dax.name": "Dax Raad",
|
||||
"go.testimonials.dax.title": "อดีต CEO, Terminal Products",
|
||||
"go.testimonials.dax.quoteAfter": "เปลี่ยนชีวิตไปเลย มันเป็นสิ่งที่ต้องมีจริงๆ",
|
||||
"go.testimonials.jay.name": "Jay V",
|
||||
"go.testimonials.jay.title": "อดีตผู้ก่อตั้ง SEED, PM, Melt, Pop, Dapt, Cadmus และ ViewPoint",
|
||||
"go.testimonials.jay.quoteBefore": "4 ใน 5 คนในทีมของเราชอบใช้",
|
||||
"go.testimonials.jay.quoteAfter": "",
|
||||
"go.testimonials.adam.name": "Adam Elmore",
|
||||
"go.testimonials.adam.title": "อดีต Hero, AWS",
|
||||
"go.testimonials.adam.quoteBefore": "ผมแนะนำ",
|
||||
"go.testimonials.adam.quoteAfter": "ได้ไม่พอจริงๆ พูดจริงนะ มันดีมากๆ",
|
||||
"go.testimonials.david.name": "David Hill",
|
||||
"go.testimonials.david.title": "อดีตหัวหน้าฝ่ายออกแบบ, Laravel",
|
||||
"go.testimonials.david.quoteBefore": "ด้วย",
|
||||
"go.testimonials.david.quoteAfter": "ผมรู้ว่าโมเดลทั้งหมดผ่านการทดสอบและสมบูรณ์แบบสำหรับเอเจนต์เขียนโค้ด",
|
||||
"go.testimonials.frank.name": "Frank Wang",
|
||||
"go.testimonials.frank.title": "อดีตเด็กฝึกงาน, Nvidia (4 ครั้ง)",
|
||||
"go.testimonials.frank.quote": "ผมหวังว่าผมจะยังอยู่ที่ Nvidia",
|
||||
"go.problem.title": "Go แก้ปัญหาอะไร?",
|
||||
"go.problem.body":
|
||||
"เรามุ่งเน้นที่จะนำประสบการณ์ OpenCode ไปสู่ผู้คนให้ได้มากที่สุด OpenCode Go เป็นการสมัครสมาชิกราคาประหยัด ($10/เดือน) ที่ออกแบบมาเพื่อนำการเขียนโค้ดแบบเอเจนต์มาสู่นักเขียนโปรแกรมทั่วโลก โดยมอบขีดจำกัดที่กว้างขวางและการเข้าถึงโมเดลโอเพนซอร์สที่มีความสามารถสูงสุดได้อย่างน่าเชื่อถือ",
|
||||
"go.problem.subtitle": " ",
|
||||
"go.problem.item1": "ราคาการสมัครสมาชิกที่ต่ำ",
|
||||
"go.problem.item2": "ขีดจำกัดที่กว้างขวางและการเข้าถึงที่เชื่อถือได้",
|
||||
"go.problem.item3": "สร้างขึ้นเพื่อโปรแกรมเมอร์จำนวนมากที่สุดเท่าที่จะเป็นไปได้",
|
||||
"go.problem.item4": "รวมถึง GLM-5, Kimi K2.5 และ MiniMax M2.5",
|
||||
"go.how.title": "Go ทำงานอย่างไร",
|
||||
"go.how.body": "Go คือการสมัครสมาชิกราคา $10/เดือน ที่คุณสามารถใช้กับ OpenCode หรือเอเจนต์ใดก็ได้",
|
||||
"go.how.step1.title": "สร้างบัญชี",
|
||||
"go.how.step1.beforeLink": "ทำตาม",
|
||||
"go.how.step1.link": "คำแนะนำการตั้งค่า",
|
||||
"go.how.step2.title": "สมัครสมาชิก Go",
|
||||
"go.how.step2.link": "$10/เดือน",
|
||||
"go.how.step2.afterLink": "ด้วยขีดจำกัดที่กว้างขวาง",
|
||||
"go.how.step3.title": "เริ่มเขียนโค้ด",
|
||||
"go.how.step3.body": "ด้วยการเข้าถึงโมเดลโอเพนซอร์สที่เชื่อถือได้",
|
||||
"go.privacy.title": "ความเป็นส่วนตัวของคุณสำคัญสำหรับเรา",
|
||||
"go.privacy.body":
|
||||
"แผนนี้ออกแบบมาเพื่อผู้ใช้งานระหว่างประเทศเป็นหลัก โดยมีโมเดลโฮสต์ในสหรัฐอเมริกา สหภาพยุโรป และสิงคโปร์ เพื่อการเข้าถึงทั่วโลกที่เสถียร",
|
||||
"go.privacy.contactAfter": "หากคุณมีคำถามใดๆ",
|
||||
"go.privacy.beforeExceptions":
|
||||
"โมเดล Go โฮสต์ในสหรัฐอเมริกา ผู้ให้บริการปฏิบัติตามนโยบายไม่เก็บรักษาข้อมูล (zero-retention policy) และไม่ใช้ข้อมูลของคุณสำหรับการฝึกโมเดล โดยมี",
|
||||
"go.privacy.exceptionsLink": "ข้อยกเว้นดังนี้",
|
||||
"go.faq.q1": "OpenCode Go คืออะไร?",
|
||||
"go.faq.a1":
|
||||
"Go คือการสมัครสมาชิกราคาประหยัดที่ให้คุณเข้าถึงโมเดลโอเพนซอร์สที่มีความสามารถสำหรับการเขียนโค้ดแบบเอเจนต์ได้อย่างน่าเชื่อถือ",
|
||||
"go.faq.q2": "Go รวมโมเดลอะไรบ้าง?",
|
||||
"go.faq.a2": "Go รวมถึง GLM-5, Kimi K2.5 และ MiniMax M2.5 พร้อมขีดจำกัดที่กว้างขวางและการเข้าถึงที่เชื่อถือได้",
|
||||
"go.faq.q3": "Go เหมือนกับ Zen หรือไม่?",
|
||||
"go.faq.a3":
|
||||
"ไม่ Zen เป็นแบบจ่ายตามการใช้งาน (pay-as-you-go) ในขณะที่ Go เป็นการสมัครสมาชิกราคา $10/เดือน พร้อมขีดจำกัดที่กว้างขวางและการเข้าถึงโมเดลโอเพนซอร์ส GLM-5, Kimi K2.5 และ MiniMax M2.5 ได้อย่างน่าเชื่อถือ",
|
||||
"go.faq.q4": "Go ราคาเท่าไหร่?",
|
||||
"go.faq.a4.p1.beforePricing": "Go ราคา",
|
||||
"go.faq.a4.p1.pricingLink": "$10/เดือน",
|
||||
"go.faq.a4.p1.afterPricing": "พร้อมขีดจำกัดที่กว้างขวาง",
|
||||
"go.faq.a4.p2.beforeAccount": "คุณสามารถจัดการการสมัครสมาชิกของคุณได้ใน",
|
||||
"go.faq.a4.p2.accountLink": "บัญชีของคุณ",
|
||||
"go.faq.a4.p3": "ยกเลิกได้ตลอดเวลา",
|
||||
"go.faq.q5": "แล้วเรื่องข้อมูลและความเป็นส่วนตัวล่ะ?",
|
||||
"go.faq.a5.body":
|
||||
"แผนนี้ออกแบบมาเพื่อผู้ใช้งานระหว่างประเทศเป็นหลัก โดยมีโมเดลโฮสต์ในสหรัฐอเมริกา สหภาพยุโรป และสิงคโปร์ เพื่อการเข้าถึงทั่วโลกที่เสถียร",
|
||||
"go.faq.a5.contactAfter": "หากคุณมีคำถามใดๆ",
|
||||
"go.faq.a5.beforeExceptions":
|
||||
"โมเดล Go โฮสต์ในสหรัฐอเมริกา ผู้ให้บริการปฏิบัติตามนโยบายไม่เก็บรักษาข้อมูล (zero-retention policy) และไม่ใช้ข้อมูลของคุณสำหรับการฝึกโมเดล โดยมี",
|
||||
"go.faq.a5.exceptionsLink": "ข้อยกเว้นดังนี้",
|
||||
"go.faq.q6": "ฉันสามารถเติมเครดิตได้หรือไม่?",
|
||||
"go.faq.a6": "หากคุณต้องการใช้งานเพิ่ม คุณสามารถเติมเครดิตในบัญชีของคุณได้",
|
||||
"go.faq.q7": "ฉันสามารถยกเลิกได้หรือไม่?",
|
||||
"go.faq.a7": "ได้ คุณสามารถยกเลิกได้ตลอดเวลา",
|
||||
"go.faq.q8": "ฉันสามารถใช้ Go กับเอเจนต์เขียนโค้ดอื่นได้หรือไม่?",
|
||||
"go.faq.a8": "ได้ คุณสามารถใช้ Go กับเอเจนต์ใดก็ได้ ทำตามคำแนะนำการตั้งค่าในเอเจนต์เขียนโค้ดที่คุณต้องการ",
|
||||
|
||||
"go.faq.q9": "ความแตกต่างระหว่างโมเดลฟรีและ Go คืออะไร?",
|
||||
"go.faq.a9":
|
||||
"โมเดลฟรีรวมถึง Big Pickle บวกกับโมเดลโปรโมชั่นที่มีให้ในขณะนั้น ด้วยโควต้า 200 คำขอ/วัน Go รวมถึง GLM-5, Kimi K2.5 และ MiniMax M2.5 ที่มีโควต้าคำขอสูงกว่า ซึ่งบังคับใช้ผ่านช่วงเวลาหมุนเวียน (5 ชั่วโมง, รายสัปดาห์ และรายเดือน) เทียบเท่าประมาณ $12 ต่อ 5 ชั่วโมง, $30 ต่อสัปดาห์ และ $60 ต่อเดือน (จำนวนคำขอจริงจะแตกต่างกันไปตามโมเดลและการใช้งาน)",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "เกินขีดจำกัดอัตราการใช้งาน กรุณาลองใหม่ในภายหลัง",
|
||||
"zen.api.error.modelNotSupported": "ไม่รองรับโมเดล {{model}}",
|
||||
"zen.api.error.modelFormatNotSupported": "ไม่รองรับโมเดล {{model}} สำหรับรูปแบบ {{format}}",
|
||||
|
||||
@@ -251,6 +251,107 @@ export const dict = {
|
||||
"Tüm Zen modelleri ABD'de barındırılmaktadır. Sağlayıcılar sıfır saklama politikası izler ve verilerinizi model eğitimi için kullanmaz; şu",
|
||||
"zen.privacy.exceptionsLink": "aşağıdaki istisnalar",
|
||||
|
||||
"go.title": "OpenCode Go | Herkes için düşük maliyetli kodlama modelleri",
|
||||
"go.meta.description":
|
||||
"Go, GLM-5, Kimi K2.5 ve MiniMax M2.5 için cömert 5 saatlik istek limitleri sunan aylık 10$'lık bir aboneliktir.",
|
||||
"go.hero.title": "Herkes için düşük maliyetli kodlama modelleri",
|
||||
"go.hero.body":
|
||||
"Go, dünya çapındaki programcılara ajan tabanlı kodlama getiriyor. En yetenekli açık kaynaklı modellere cömert limitler ve güvenilir erişim sunarak, maliyet veya erişilebilirlik konusunda endişelenmeden güçlü ajanlarla geliştirme yapmanızı sağlar.",
|
||||
|
||||
"go.cta.start": "Go'ya abone ol",
|
||||
"go.cta.template": "{{text}} {{price}}",
|
||||
"go.cta.text": "Go'ya abone ol",
|
||||
"go.cta.price": "Ayda 10$",
|
||||
"go.pricing.body": "Herhangi bir ajanla kullanın. Gerekirse kredi yükleyin. İstediğiniz zaman iptal edin.",
|
||||
"go.graph.free": "Ücretsiz",
|
||||
"go.graph.freePill": "Big Pickle ve ücretsiz modeller",
|
||||
"go.graph.go": "Go",
|
||||
"go.graph.label": "5 saat başına istekler",
|
||||
"go.graph.usageLimits": "Kullanım limitleri",
|
||||
"go.graph.tick": "{{n}}x",
|
||||
"go.graph.aria": "5 saatlik istekler: {{free}} vs {{go}}",
|
||||
|
||||
"go.testimonials.brand.zen": "Zen",
|
||||
"go.testimonials.brand.go": "Go",
|
||||
"go.testimonials.handle": "@OpenCode",
|
||||
"go.testimonials.dax.name": "Dax Raad",
|
||||
"go.testimonials.dax.title": "Eski CEO, Terminal Ürünleri",
|
||||
"go.testimonials.dax.quoteAfter": "hayat değiştirdi, gerçekten düşünmeye bile gerek yok.",
|
||||
"go.testimonials.jay.name": "Jay V",
|
||||
"go.testimonials.jay.title": "Eski Kurucu, SEED, PM, Melt, Pop, Dapt, Cadmus ve ViewPoint",
|
||||
"go.testimonials.jay.quoteBefore": "Ekibimizdeki 5 kişiden 4'ü",
|
||||
"go.testimonials.jay.quoteAfter": "kullanmayı seviyor.",
|
||||
"go.testimonials.adam.name": "Adam Elmore",
|
||||
"go.testimonials.adam.title": "Eski Hero, AWS",
|
||||
"go.testimonials.adam.quoteBefore": "",
|
||||
"go.testimonials.adam.quoteAfter": "için tavsiyem sonsuz. Cidden, gerçekten çok iyi.",
|
||||
"go.testimonials.david.name": "David Hill",
|
||||
"go.testimonials.david.title": "Eski Tasarım Başkanı, Laravel",
|
||||
"go.testimonials.david.quoteBefore": "",
|
||||
"go.testimonials.david.quoteAfter":
|
||||
" ile modellerin test edildiğini ve kodlama ajanları için mükemmel olduğunu biliyorum.",
|
||||
"go.testimonials.frank.name": "Frank Wang",
|
||||
"go.testimonials.frank.title": "Eski Stajyer, Nvidia (4 kez)",
|
||||
"go.testimonials.frank.quote": "Keşke hala Nvidia'da olsaydım.",
|
||||
"go.problem.title": "Go hangi sorunu çözüyor?",
|
||||
"go.problem.body":
|
||||
"OpenCode deneyimini mümkün olduğunca çok kişiye ulaştırmaya odaklanıyoruz. OpenCode Go, ajan tabanlı kodlamayı dünya çapındaki programcılara sunmak için tasarlanmış düşük maliyetli (ayda 10$) bir aboneliktir. En yetenekli açık kaynaklı modellere cömert limitler ve güvenilir erişim sağlar.",
|
||||
"go.problem.subtitle": " ",
|
||||
"go.problem.item1": "Düşük maliyetli abonelik fiyatlandırması",
|
||||
"go.problem.item2": "Cömert limitler ve güvenilir erişim",
|
||||
"go.problem.item3": "Mümkün olduğunca çok programcı için geliştirildi",
|
||||
"go.problem.item4": "GLM-5, Kimi K2.5 ve MiniMax M2.5 içerir",
|
||||
"go.how.title": "Go nasıl çalışır?",
|
||||
"go.how.body": "Go, OpenCode veya herhangi bir ajanla kullanabileceğiniz aylık 10$'lık bir aboneliktir.",
|
||||
"go.how.step1.title": "Bir hesap oluşturun",
|
||||
"go.how.step1.beforeLink": "takip edin",
|
||||
"go.how.step1.link": "kurulum talimatları",
|
||||
"go.how.step2.title": "Go'ya abone olun",
|
||||
"go.how.step2.link": "Ayda 10$",
|
||||
"go.how.step2.afterLink": ", cömert limitlerle",
|
||||
"go.how.step3.title": "Kodlamaya başlayın",
|
||||
"go.how.step3.body": "açık kaynaklı modellere güvenilir erişimle",
|
||||
"go.privacy.title": "Gizliliğiniz bizim için önemlidir",
|
||||
"go.privacy.body":
|
||||
"Bu plan öncelikle uluslararası kullanıcılar için tasarlanmış olup, istikrarlı küresel erişim için modeller ABD, AB ve Singapur'da barındırılmaktadır.",
|
||||
"go.privacy.contactAfter": "herhangi bir sorunuz varsa.",
|
||||
"go.privacy.beforeExceptions":
|
||||
"Go modelleri ABD'de barındırılmaktadır. Sağlayıcılar sıfır saklama politikası izler ve verilerinizi model eğitimi için kullanmaz; şu",
|
||||
"go.privacy.exceptionsLink": "aşağıdaki istisnalar",
|
||||
"go.faq.q1": "OpenCode Go nedir?",
|
||||
"go.faq.a1":
|
||||
"Go, ajan tabanlı kodlama için yetenekli açık kaynaklı modellere güvenilir erişim sağlayan düşük maliyetli bir aboneliktir.",
|
||||
"go.faq.q2": "Go hangi modelleri içerir?",
|
||||
"go.faq.a2": "Go, cömert limitler ve güvenilir erişim ile GLM-5, Kimi K2.5 ve MiniMax M2.5 modellerini içerir.",
|
||||
"go.faq.q3": "Go, Zen ile aynı mı?",
|
||||
"go.faq.a3":
|
||||
"Hayır. Zen kullandıkça öde sistemidir; Go ise GLM-5, Kimi K2.5 ve MiniMax M2.5 açık kaynak modellerine cömert limitler ve güvenilir erişim sağlayan aylık 10$'lık bir aboneliktir.",
|
||||
"go.faq.q4": "Go ne kadar?",
|
||||
"go.faq.a4.p1.beforePricing": "Go'nun maliyeti",
|
||||
"go.faq.a4.p1.pricingLink": "ayda 10$",
|
||||
"go.faq.a4.p1.afterPricing": ", cömert limitlerle.",
|
||||
"go.faq.a4.p2.beforeAccount": "Aboneliğinizi",
|
||||
"go.faq.a4.p2.accountLink": "hesabınızdan",
|
||||
"go.faq.a4.p3": "yönetebilirsiniz. İstediğiniz zaman iptal edin.",
|
||||
"go.faq.q5": "Veri ve gizlilik ne olacak?",
|
||||
"go.faq.a5.body":
|
||||
"Bu plan öncelikle uluslararası kullanıcılar için tasarlanmış olup, istikrarlı küresel erişim için modeller ABD, AB ve Singapur'da barındırılmaktadır.",
|
||||
"go.faq.a5.contactAfter": "herhangi bir sorunuz varsa.",
|
||||
"go.faq.a5.beforeExceptions":
|
||||
"Go modelleri ABD'de barındırılmaktadır. Sağlayıcılar sıfır saklama politikası izler ve verilerinizi model eğitimi için kullanmaz; şu",
|
||||
"go.faq.a5.exceptionsLink": "aşağıdaki istisnalar",
|
||||
"go.faq.q6": "Kredi yükleyebilir miyim?",
|
||||
"go.faq.a6": "Daha fazla kullanıma ihtiyacınız varsa, hesabınıza kredi yükleyebilirsiniz.",
|
||||
"go.faq.q7": "İptal edebilir miyim?",
|
||||
"go.faq.a7": "Evet, istediğiniz zaman iptal edebilirsiniz.",
|
||||
"go.faq.q8": "Go'yu diğer kodlama ajanlarıyla kullanabilir miyim?",
|
||||
"go.faq.a8":
|
||||
"Evet, Go'yu herhangi bir ajanla kullanabilirsiniz. Tercih ettiğiniz kodlama ajanındaki kurulum talimatlarını izleyin.",
|
||||
|
||||
"go.faq.q9": "Ücretsiz modeller ve Go arasındaki fark nedir?",
|
||||
"go.faq.a9":
|
||||
"Ücretsiz modeller, günlük 200 istek kotası ile Big Pickle ve o sırada mevcut olan promosyonel modelleri içerir. Go ise GLM-5, Kimi K2.5 ve MiniMax M2.5 modellerini; yuvarlanan pencereler (5 saatlik, haftalık ve aylık) üzerinden uygulanan daha yüksek istek kotalarıyla içerir. Bu kotalar kabaca her 5 saatte 12$, haftada 30$ ve ayda 60$ değerine eşdeğerdir (gerçek istek sayıları modele ve kullanıma göre değişir).",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "İstek limiti aşıldı. Lütfen daha sonra tekrar deneyin.",
|
||||
"zen.api.error.modelNotSupported": "{{model}} modeli desteklenmiyor",
|
||||
"zen.api.error.modelFormatNotSupported": "{{model}} modeli {{format}} formatı için desteklenmiyor",
|
||||
|
||||
@@ -238,6 +238,99 @@ export const dict = {
|
||||
"zen.privacy.beforeExceptions": "所有 Zen 模型均托管在美国。提供商遵循零留存政策,不使用您的数据进行模型训练,",
|
||||
"zen.privacy.exceptionsLink": "以下例外情况除外",
|
||||
|
||||
"go.title": "OpenCode Go | 人人可用的低成本编程模型",
|
||||
"go.meta.description": "Go 是每月 $10 的订阅服务,提供对 GLM-5, Kimi K2.5, 和 MiniMax M2.5 的 5 小时内充裕请求限额。",
|
||||
"go.hero.title": "人人可用的低成本编程模型",
|
||||
"go.hero.body":
|
||||
"Go 将代理编程带给全世界的程序员。提供充裕的限额和对最强大的开源模型的可靠访问,让您可以利用强大的代理进行构建,而无需担心成本或可用性。",
|
||||
|
||||
"go.cta.start": "订阅 Go",
|
||||
"go.cta.template": "{{text}} {{price}}",
|
||||
"go.cta.text": "订阅 Go",
|
||||
"go.cta.price": "$10/月",
|
||||
"go.pricing.body": "可配合任何代理使用。按需充值。随时取消。",
|
||||
"go.graph.free": "免费",
|
||||
"go.graph.freePill": "Big Pickle 和免费模型",
|
||||
"go.graph.go": "Go",
|
||||
"go.graph.label": "每 5 小时请求数",
|
||||
"go.graph.usageLimits": "使用限制",
|
||||
"go.graph.tick": "{{n}}x",
|
||||
"go.graph.aria": "每 5 小时请求数: {{free}} vs {{go}}",
|
||||
|
||||
"go.testimonials.brand.zen": "Zen",
|
||||
"go.testimonials.brand.go": "Go",
|
||||
"go.testimonials.handle": "@OpenCode",
|
||||
"go.testimonials.dax.name": "Dax Raad",
|
||||
"go.testimonials.dax.title": "前 CEO, Terminal Products",
|
||||
"go.testimonials.dax.quoteAfter": "彻底改变了我的生活,这绝对是不二之选。",
|
||||
"go.testimonials.jay.name": "Jay V",
|
||||
"go.testimonials.jay.title": "前创始人, SEED, PM, Melt, Pop, Dapt, Cadmus, 和 ViewPoint",
|
||||
"go.testimonials.jay.quoteBefore": "我们团队 5 个人里有 4 个都爱用",
|
||||
"go.testimonials.jay.quoteAfter": "。",
|
||||
"go.testimonials.adam.name": "Adam Elmore",
|
||||
"go.testimonials.adam.title": "前 Hero, AWS",
|
||||
"go.testimonials.adam.quoteBefore": "我强烈推荐",
|
||||
"go.testimonials.adam.quoteAfter": "。真的,非常好用。",
|
||||
"go.testimonials.david.name": "David Hill",
|
||||
"go.testimonials.david.title": "前设计主管, Laravel",
|
||||
"go.testimonials.david.quoteBefore": "有了",
|
||||
"go.testimonials.david.quoteAfter": "我知道所有模型都经过测试,非常适合编程代理。",
|
||||
"go.testimonials.frank.name": "Frank Wang",
|
||||
"go.testimonials.frank.title": "前实习生, Nvidia (4 次)",
|
||||
"go.testimonials.frank.quote": "我希望我还在 Nvidia。",
|
||||
"go.problem.title": "Go 解决了什么问题?",
|
||||
"go.problem.body":
|
||||
"我们致力于将 OpenCode 体验带给尽可能多的人。OpenCode Go 是一个低成本 ($10/月) 的订阅服务,旨在将代理编程带给全世界的程序员。它提供充裕的限额和对最强大的开源模型的可靠访问。",
|
||||
"go.problem.subtitle": " ",
|
||||
"go.problem.item1": "低成本订阅定价",
|
||||
"go.problem.item2": "充裕的限额和可靠的访问",
|
||||
"go.problem.item3": "为尽可能多的程序员打造",
|
||||
"go.problem.item4": "包含 GLM-5, Kimi K2.5, 和 MiniMax M2.5",
|
||||
"go.how.title": "Go 如何工作",
|
||||
"go.how.body": "Go 是每月 $10 的订阅服务,您可以配合 OpenCode 或任何代理使用。",
|
||||
"go.how.step1.title": "创建账户",
|
||||
"go.how.step1.beforeLink": "遵循",
|
||||
"go.how.step1.link": "设置说明",
|
||||
"go.how.step2.title": "订阅 Go",
|
||||
"go.how.step2.link": "$10/月",
|
||||
"go.how.step2.afterLink": "享受充裕限额",
|
||||
"go.how.step3.title": "开始编程",
|
||||
"go.how.step3.body": "可靠访问开源模型",
|
||||
"go.privacy.title": "您的隐私对我们很重要",
|
||||
"go.privacy.body": "该计划主要面向国际用户设计,模型部署在美国、欧盟和新加坡,以确保稳定的全球访问。",
|
||||
"go.privacy.contactAfter": "如果您有任何问题。",
|
||||
"go.privacy.beforeExceptions": "Go 模型托管在美国。提供商遵循零留存政策,不使用您的数据进行模型训练,",
|
||||
"go.privacy.exceptionsLink": "以下例外情况除外",
|
||||
"go.faq.q1": "什么是 OpenCode Go?",
|
||||
"go.faq.a1": "Go 是一项低成本订阅服务,为您提供对强大的开源模型的可靠访问,用于代理编程。",
|
||||
"go.faq.q2": "Go 包含哪些模型?",
|
||||
"go.faq.a2": "Go 包含 GLM-5, Kimi K2.5, 和 MiniMax M2.5,并提供充裕的限额和可靠的访问。",
|
||||
"go.faq.q3": "Go 和 Zen 一样吗?",
|
||||
"go.faq.a3":
|
||||
"不一样。Zen 是即用即付,而 Go 是每月 $10 的订阅服务,提供对开源模型 GLM-5, Kimi K2.5, 和 MiniMax M2.5 的充裕限额和可靠访问。",
|
||||
"go.faq.q4": "Go 多少钱?",
|
||||
"go.faq.a4.p1.beforePricing": "Go 费用为",
|
||||
"go.faq.a4.p1.pricingLink": "$10/月",
|
||||
"go.faq.a4.p1.afterPricing": "包含充裕限额。",
|
||||
"go.faq.a4.p2.beforeAccount": "您可以在您的",
|
||||
"go.faq.a4.p2.accountLink": "账户",
|
||||
"go.faq.a4.p3": "中管理订阅。随时取消。",
|
||||
"go.faq.q5": "数据和隐私如何?",
|
||||
"go.faq.a5.body": "该计划主要面向国际用户设计,模型部署在美国、欧盟和新加坡,以确保稳定的全球访问。",
|
||||
"go.faq.a5.contactAfter": "如果您有任何问题。",
|
||||
"go.faq.a5.beforeExceptions": "Go 模型托管在美国。提供商遵循零留存政策,不使用您的数据进行模型训练,",
|
||||
"go.faq.a5.exceptionsLink": "以下例外情况除外",
|
||||
"go.faq.q6": "我可以充值余额吗?",
|
||||
"go.faq.a6": "如果您需要更多用量,可以在账户中充值余额。",
|
||||
"go.faq.q7": "我可以取消吗?",
|
||||
"go.faq.a7": "可以,您可以随时取消。",
|
||||
"go.faq.q8": "我可以在其他编程代理中使用 Go 吗?",
|
||||
"go.faq.a8": "可以,您可以在任何代理中使用 Go。请遵循您首选编程代理中的设置说明。",
|
||||
|
||||
"go.faq.q9": "免费模型和 Go 之间的区别是什么?",
|
||||
"go.faq.a9":
|
||||
"免费模型包含 Big Pickle 加上当时可用的促销模型,每天有 200 次请求的配额。Go 包含 GLM-5, Kimi K2.5, 和 MiniMax M2.5,并在滚动窗口(5 小时、每周和每月)内执行更高的请求配额,大致相当于每 5 小时 $12、每周 $30 和每月 $60(实际请求计数因模型和使用情况而异)。",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "超出速率限制。请稍后重试。",
|
||||
"zen.api.error.modelNotSupported": "不支持模型 {{model}}",
|
||||
"zen.api.error.modelFormatNotSupported": "格式 {{format}} 不支持模型 {{model}}",
|
||||
|
||||
@@ -238,6 +238,100 @@ export const dict = {
|
||||
"zen.privacy.beforeExceptions": "所有 Zen 模型均在美國託管。供應商遵循零留存政策,不會將你的資料用於模型訓練,並且有",
|
||||
"zen.privacy.exceptionsLink": "以下例外情況",
|
||||
|
||||
"go.title": "OpenCode Go | 低成本全民編碼模型",
|
||||
"go.meta.description":
|
||||
"Go 是一個每月 $10 的訂閱方案,提供對 GLM-5、Kimi K2.5 與 MiniMax M2.5 的 5 小時寬裕使用限額。",
|
||||
"go.hero.title": "低成本全民編碼模型",
|
||||
"go.hero.body":
|
||||
"Go 將代理編碼帶給全世界的程式設計師。提供寬裕的限額以及對最強大開源模型的穩定存取,讓你可以使用強大的代理進行構建,而無需擔心成本或可用性。",
|
||||
|
||||
"go.cta.start": "訂閱 Go",
|
||||
"go.cta.template": "{{text}} {{price}}",
|
||||
"go.cta.text": "訂閱 Go",
|
||||
"go.cta.price": "$10/月",
|
||||
"go.pricing.body": "可與任何代理一起使用。需要時可儲值額度。隨時取消。",
|
||||
"go.graph.free": "免費",
|
||||
"go.graph.freePill": "Big Pickle 與免費模型",
|
||||
"go.graph.go": "Go",
|
||||
"go.graph.label": "每 5 小時請求數",
|
||||
"go.graph.usageLimits": "使用限制",
|
||||
"go.graph.tick": "{{n}}x",
|
||||
"go.graph.aria": "每 5 小時請求數:{{free}} vs {{go}}",
|
||||
|
||||
"go.testimonials.brand.zen": "Zen",
|
||||
"go.testimonials.brand.go": "Go",
|
||||
"go.testimonials.handle": "@OpenCode",
|
||||
"go.testimonials.dax.name": "Dax Raad",
|
||||
"go.testimonials.dax.title": "前 Terminal Products CEO",
|
||||
"go.testimonials.dax.quoteAfter": "改變了我的生活,這絕對是不二之選。",
|
||||
"go.testimonials.jay.name": "Jay V",
|
||||
"go.testimonials.jay.title": "前 SEED、Melt、Pop、Dapt、Cadmus 與 ViewPoint 創辦人",
|
||||
"go.testimonials.jay.quoteBefore": "我們團隊中 5 個人有 4 個人喜歡使用",
|
||||
"go.testimonials.jay.quoteAfter": "。",
|
||||
"go.testimonials.adam.name": "Adam Elmore",
|
||||
"go.testimonials.adam.title": "前 AWS Hero",
|
||||
"go.testimonials.adam.quoteBefore": "我強烈推薦",
|
||||
"go.testimonials.adam.quoteAfter": "。認真說,真的很好用。",
|
||||
"go.testimonials.david.name": "David Hill",
|
||||
"go.testimonials.david.title": "前 Laravel 設計總監",
|
||||
"go.testimonials.david.quoteBefore": "有了",
|
||||
"go.testimonials.david.quoteAfter": ",我知道所有模型都經過測試並且完美適用於編碼代理。",
|
||||
"go.testimonials.frank.name": "Frank Wang",
|
||||
"go.testimonials.frank.title": "前 Nvidia 實習生(4 次)",
|
||||
"go.testimonials.frank.quote": "我希望我還在 Nvidia。",
|
||||
"go.problem.title": "Go 正在解決什麼問題?",
|
||||
"go.problem.body":
|
||||
"我們致力於將 OpenCode 體驗帶給盡可能多的人。OpenCode Go 是一個低成本(每月 $10)的訂閱方案,旨在將代理編碼帶給全世界的程式設計師。它提供寬裕的限額以及對最強大開源模型的穩定存取。",
|
||||
"go.problem.subtitle": " ",
|
||||
"go.problem.item1": "低成本訂閱定價",
|
||||
"go.problem.item2": "寬裕的限額與穩定存取",
|
||||
"go.problem.item3": "專為盡可能多的程式設計師打造",
|
||||
"go.problem.item4": "包含 GLM-5、Kimi K2.5 與 MiniMax M2.5",
|
||||
"go.how.title": "Go 如何運作",
|
||||
"go.how.body": "Go 是一個每月 $10 的訂閱方案,你可以將其與 OpenCode 或任何代理一起使用。",
|
||||
"go.how.step1.title": "建立帳號",
|
||||
"go.how.step1.beforeLink": "遵循",
|
||||
"go.how.step1.link": "設定說明",
|
||||
"go.how.step2.title": "訂閱 Go",
|
||||
"go.how.step2.link": "$10/月",
|
||||
"go.how.step2.afterLink": "享寬裕限額",
|
||||
"go.how.step3.title": "開始編碼",
|
||||
"go.how.step3.body": "穩定存取開源模型",
|
||||
"go.privacy.title": "你的隱私對我們很重要",
|
||||
"go.privacy.body": "該方案主要面向國際用戶設計,模型託管在美國、歐盟和新加坡,以確保全球穩定存取。",
|
||||
"go.privacy.contactAfter": "如果你有任何問題。",
|
||||
"go.privacy.beforeExceptions": "Go 模型託管在美國。供應商遵循零留存政策,不會將你的資料用於模型訓練,但有",
|
||||
"go.privacy.exceptionsLink": "以下例外",
|
||||
"go.faq.q1": "什麼是 OpenCode Go?",
|
||||
"go.faq.a1": "Go 是一個低成本訂閱方案,讓你穩定存取強大的開源模型以進行代理編碼。",
|
||||
"go.faq.q2": "Go 包含哪些模型?",
|
||||
"go.faq.a2": "Go 包含 GLM-5、Kimi K2.5 與 MiniMax M2.5,並提供寬裕的限額與穩定存取。",
|
||||
"go.faq.q3": "Go 與 Zen 一樣嗎?",
|
||||
"go.faq.a3":
|
||||
"不一樣。Zen 是按量付費,而 Go 是每月 $10 的訂閱方案,提供對開源模型 GLM-5、Kimi K2.5 與 MiniMax M2.5 的寬裕限額與穩定存取。",
|
||||
"go.faq.q4": "Go 費用是多少?",
|
||||
"go.faq.a4.p1.beforePricing": "Go 費用為",
|
||||
"go.faq.a4.p1.pricingLink": "$10/月",
|
||||
"go.faq.a4.p1.afterPricing": "享寬裕限額。",
|
||||
"go.faq.a4.p2.beforeAccount": "你可以在你的",
|
||||
"go.faq.a4.p2.accountLink": "帳戶",
|
||||
"go.faq.a4.p3": "中管理訂閱。隨時取消。",
|
||||
"go.faq.q5": "資料與隱私怎麼辦?",
|
||||
"go.faq.a5.body": "該方案主要面向國際用戶設計,模型託管在美國、歐盟和新加坡,以確保全球穩定存取。",
|
||||
"go.faq.a5.contactAfter": "如果你有任何問題。",
|
||||
"go.faq.a5.beforeExceptions": "Go 模型託管在美國。供應商遵循零留存政策,不會將你的資料用於模型訓練,但有",
|
||||
"go.faq.a5.exceptionsLink": "以下例外",
|
||||
"go.faq.q6": "我可以儲值額度嗎?",
|
||||
"go.faq.a6": "如果你需要更多使用量,可以在帳戶中儲值額度。",
|
||||
"go.faq.q7": "我可以取消嗎?",
|
||||
"go.faq.a7": "可以,你可以隨時取消。",
|
||||
"go.faq.q8": "我可以在其他編碼代理中使用 Go 嗎?",
|
||||
"go.faq.a8": "可以,你可以將 Go 與任何代理一起使用。請在你偏好的編碼代理中按照設定說明進行配置。",
|
||||
|
||||
"go.faq.q9": "免費模型與 Go 有什麼區別?",
|
||||
"go.faq.a9":
|
||||
"免費模型包括 Big Pickle 以及當時可用的促銷模型,配額為 200 次請求/天。Go 包括 GLM-5、Kimi K2.5 與 MiniMax M2.5,並在滾動視窗(5 小時、每週和每月)內執行更高的請求配額,大約相當於每 5 小時 $12、每週 $30 和每月 $60(實際請求數因模型和使用情況而異)。",
|
||||
|
||||
"zen.api.error.rateLimitExceeded": "超出頻率限制。請稍後再試。",
|
||||
"zen.api.error.modelNotSupported": "不支援模型 {{model}}",
|
||||
"zen.api.error.modelFormatNotSupported": "模型 {{model}} 不支援格式 {{format}}",
|
||||
|
||||
@@ -86,10 +86,10 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 48px;
|
||||
gap: 32px;
|
||||
|
||||
@media (max-width: 55rem) {
|
||||
gap: 32px;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
|
||||
@@ -81,10 +81,10 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 48px;
|
||||
gap: 32px;
|
||||
|
||||
@media (max-width: 55rem) {
|
||||
gap: 32px;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
|
||||
@@ -85,10 +85,10 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 48px;
|
||||
gap: 32px;
|
||||
|
||||
@media (max-width: 55rem) {
|
||||
gap: 32px;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
|
||||
@@ -85,10 +85,10 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 48px;
|
||||
gap: 32px;
|
||||
|
||||
@media (max-width: 55rem) {
|
||||
gap: 32px;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
|
||||
1117
packages/console/app/src/routes/go/index.css
Normal file
1117
packages/console/app/src/routes/go/index.css
Normal file
File diff suppressed because it is too large
Load Diff
453
packages/console/app/src/routes/go/index.tsx
Normal file
453
packages/console/app/src/routes/go/index.tsx
Normal file
@@ -0,0 +1,453 @@
|
||||
import "./index.css"
|
||||
import { createAsync, query, redirect } from "@solidjs/router"
|
||||
import { Title, Meta } from "@solidjs/meta"
|
||||
import { For, createMemo, createSignal, onCleanup, onMount } from "solid-js"
|
||||
//import { HttpHeader } from "@solidjs/start"
|
||||
import goLogoLight from "../../asset/go-ornate-light.svg"
|
||||
import goLogoDark from "../../asset/go-ornate-dark.svg"
|
||||
import { EmailSignup } from "~/component/email-signup"
|
||||
import { Faq } from "~/component/faq"
|
||||
import { Legal } from "~/component/legal"
|
||||
import { Footer } from "~/component/footer"
|
||||
import { Header } from "~/component/header"
|
||||
import { config } from "~/config"
|
||||
import { getLastSeenWorkspaceID } from "../workspace/common"
|
||||
import { IconMiniMax, IconZai } from "~/component/icon"
|
||||
import { useI18n } from "~/context/i18n"
|
||||
import { useLanguage } from "~/context/language"
|
||||
import { LocaleLinks } from "~/component/locale-links"
|
||||
|
||||
const checkLoggedIn = query(async () => {
|
||||
"use server"
|
||||
return await getLastSeenWorkspaceID().catch(() => undefined)
|
||||
}, "checkLoggedIn.get")
|
||||
|
||||
function LimitsGraph(props: { href: string }) {
|
||||
let root!: HTMLElement
|
||||
const [visible, setVisible] = createSignal(false)
|
||||
|
||||
const i18n = useI18n()
|
||||
|
||||
onMount(() => {
|
||||
if (typeof IntersectionObserver === "undefined") return setVisible(true)
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const entry = entries[0]
|
||||
if (!entry?.isIntersecting) return
|
||||
setVisible(true)
|
||||
observer.disconnect()
|
||||
},
|
||||
{ threshold: 0.35 },
|
||||
)
|
||||
observer.observe(root)
|
||||
onCleanup(() => observer.disconnect())
|
||||
})
|
||||
|
||||
const free = 200
|
||||
const models = [
|
||||
{ id: "glm", name: "GLM-5", req: 1150, d: "120ms" },
|
||||
{ id: "kimi", name: "Kimi K2.5", req: 1850, d: "240ms" },
|
||||
{ id: "minimax", name: "MiniMax M2.5", req: 20000, d: "360ms" },
|
||||
]
|
||||
|
||||
const w = 720
|
||||
const h = 220
|
||||
const left = 40
|
||||
const right = 60
|
||||
const top = 18
|
||||
const bottom = 44
|
||||
const plot = w - left - right
|
||||
|
||||
const ratio = (n: number) => n / free
|
||||
const rmax = Math.max(1, ...models.map((m) => ratio(m.req)))
|
||||
const log = (n: number) => Math.log10(Math.max(n, 1))
|
||||
const base = 24
|
||||
const p = 2.2
|
||||
const x = (r: number) => left + base + Math.pow(log(r) / log(rmax), p) * (plot - base)
|
||||
const start = (x(1) / w) * 100
|
||||
|
||||
const ticks = [1, 5, 10, 25, 50, 100].filter((t) => t <= rmax)
|
||||
const labels = (() => {
|
||||
const set = new Set<number>()
|
||||
let last = -Infinity
|
||||
for (const t of ticks) {
|
||||
if (t === 1) {
|
||||
set.add(t)
|
||||
last = x(t)
|
||||
continue
|
||||
}
|
||||
const pos = x(t)
|
||||
if (pos - last < 44) continue
|
||||
set.add(t)
|
||||
last = pos
|
||||
}
|
||||
return set
|
||||
})()
|
||||
const shown = ticks.filter((t) => labels.has(t))
|
||||
const bh = 8
|
||||
const gap = 16
|
||||
const step = bh + gap
|
||||
const sep = bh + 40
|
||||
const fy = top + 22
|
||||
const gy = (i: number) => fy + sep + step * i
|
||||
const my = models.length < 2 ? gy(0) : (gy(0) + gy(models.length - 1)) / 2
|
||||
const px = (n: number) => `${(n / w) * 100}%`
|
||||
const py = (n: number) => `${(n / h) * 100}%`
|
||||
const lx = px(left - 16)
|
||||
const ty = py(h - 18)
|
||||
|
||||
return (
|
||||
<figure
|
||||
data-component="limit-graph"
|
||||
aria-label={i18n.t("go.graph.aria", { free: i18n.t("go.graph.free"), go: i18n.t("go.graph.go") })}
|
||||
data-visible={visible() ? "" : undefined}
|
||||
ref={root}
|
||||
style={{ "--start": `${start}%` } as any}
|
||||
>
|
||||
<div data-slot="plot">
|
||||
<svg
|
||||
viewBox={`0 0 ${w} ${h}`}
|
||||
preserveAspectRatio="none"
|
||||
role="img"
|
||||
aria-hidden="true"
|
||||
style={{ height: `${h}px` }}
|
||||
>
|
||||
<g data-slot="grid">
|
||||
<For each={ticks}>
|
||||
{(t) => (
|
||||
<g>
|
||||
<line x1={x(t)} y1={top} x2={x(t)} y2={h - bottom} data-grid />
|
||||
</g>
|
||||
)}
|
||||
</For>
|
||||
</g>
|
||||
|
||||
<line x1={left} y1={top} x2={left} y2={h - bottom} data-stub />
|
||||
|
||||
<g data-slot="bars">
|
||||
<g style={{ "--d": "0ms" } as any}>
|
||||
<rect x={left} y={fy - bh / 2} width={Math.max(0, x(1) - left)} height={bh} data-bar data-kind="free" />
|
||||
</g>
|
||||
|
||||
<For each={models}>
|
||||
{(m, i) => (
|
||||
<g style={{ "--d": m.d } as any}>
|
||||
<rect
|
||||
x={left}
|
||||
y={gy(i()) - bh / 2}
|
||||
width={Math.max(0, x(ratio(m.req)) - left)}
|
||||
height={bh}
|
||||
data-bar
|
||||
data-kind="go"
|
||||
data-model={m.id}
|
||||
/>
|
||||
</g>
|
||||
)}
|
||||
</For>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<div data-slot="ylabels" aria-hidden="true">
|
||||
<span data-ylabel style={{ "--x": lx, "--y": py(fy) } as any}>
|
||||
{i18n.t("go.graph.free")}
|
||||
</span>
|
||||
<span data-ylabel style={{ "--x": lx, "--y": py(my) } as any}>
|
||||
{i18n.t("go.graph.go")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div data-slot="xlabels" aria-hidden="true">
|
||||
<For each={shown}>
|
||||
{(t) => (
|
||||
<span data-xlabel style={{ "--x": px(x(t)), "--y": ty } as any}>
|
||||
{i18n.t("go.graph.tick", { n: t })}
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<div data-slot="pills" aria-hidden="true">
|
||||
<span data-item data-kind="free" style={{ "--x": px(x(1)), "--y": py(fy), "--d": "0ms" } as any}>
|
||||
<span data-value>{free.toLocaleString()}</span>
|
||||
<span data-name>{i18n.t("go.graph.freePill")}</span>
|
||||
</span>
|
||||
<For each={models}>
|
||||
{(m, i) => (
|
||||
<span
|
||||
data-item
|
||||
data-kind="go"
|
||||
data-model={m.id}
|
||||
style={{ "--x": px(x(ratio(m.req))), "--y": py(gy(i())), "--d": m.d } as any}
|
||||
>
|
||||
<span data-value>{m.req.toLocaleString()}</span>
|
||||
<span data-name>{m.name}</span>
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<figcaption>
|
||||
<div data-slot="caption-row">
|
||||
<div data-slot="caption-left">
|
||||
<div data-slot="caption-meta">
|
||||
<span data-slot="caption-label">{i18n.t("go.graph.label")}</span>
|
||||
<a data-slot="caption-link" href={props.href}>
|
||||
{i18n.t("go.graph.usageLimits")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</figcaption>
|
||||
</figure>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const workspaceID = createAsync(() => checkLoggedIn())
|
||||
const subscribeUrl = createMemo(() => (workspaceID() ? `/workspace/${workspaceID()}/billing` : "/auth"))
|
||||
const i18n = useI18n()
|
||||
const language = useLanguage()
|
||||
return (
|
||||
<main data-page="go">
|
||||
{/*<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />*/}
|
||||
<Title>{i18n.t("go.title")}</Title>
|
||||
<Meta name="description" content={i18n.t("go.meta.description")} />
|
||||
<LocaleLinks path="/go" />
|
||||
<Meta property="og:type" content="website" />
|
||||
<Meta property="og:url" content={`${config.baseUrl}${language.route("/go")}`} />
|
||||
<Meta property="og:title" content={i18n.t("go.title")} />
|
||||
<Meta property="og:description" content={i18n.t("go.meta.description")} />
|
||||
<Meta property="og:image" content="/social-share-black.png" />
|
||||
<Meta name="twitter:card" content="summary_large_image" />
|
||||
<Meta name="twitter:title" content={i18n.t("go.title")} />
|
||||
<Meta name="twitter:description" content={i18n.t("go.meta.description")} />
|
||||
<Meta name="twitter:image" content="/social-share-black.png" />
|
||||
<Meta name="opencode:auth" content={workspaceID() ? "true" : "false"} />
|
||||
|
||||
<div data-component="container">
|
||||
<Header go hideGetStarted />
|
||||
|
||||
<div data-component="content">
|
||||
<section data-component="hero">
|
||||
<div data-slot="hero-copy">
|
||||
<img data-slot="zen logo light" src={goLogoLight} alt="" />
|
||||
<img data-slot="zen logo dark" src={goLogoDark} alt="" />
|
||||
<h1>{i18n.t("go.hero.title")}</h1>
|
||||
<p>{i18n.t("go.hero.body")}</p>
|
||||
<div data-slot="model-logos">
|
||||
{/*
|
||||
<div>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask
|
||||
id="mask0_79_128586"
|
||||
style="mask-type:luminance"
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="1"
|
||||
y="1"
|
||||
width="22"
|
||||
height="22"
|
||||
>
|
||||
<path d="M23 1.5H1V22.2952H23V1.5Z" fill="white" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_79_128586)">
|
||||
<path
|
||||
d="M9.43799 9.06943V7.09387C9.43799 6.92749 9.50347 6.80267 9.65601 6.71959L13.8206 4.43211C14.3875 4.1202 15.0635 3.9747 15.7611 3.9747C18.3775 3.9747 20.0347 5.9087 20.0347 7.96734C20.0347 8.11288 20.0347 8.27926 20.0128 8.44564L15.6956 6.03335C15.434 5.88785 15.1723 5.88785 14.9107 6.03335L9.43799 9.06943ZM19.1624 16.7637V12.0431C19.1624 11.7519 19.0315 11.544 18.7699 11.3984L13.2972 8.36234L15.0851 7.3849C15.2377 7.30182 15.3686 7.30182 15.5212 7.3849L19.6858 9.67238C20.8851 10.3379 21.6917 11.7519 21.6917 13.1243C21.6917 14.7047 20.7106 16.1604 19.1624 16.7636V16.7637ZM8.15158 12.6047L6.36369 11.6066C6.21114 11.5235 6.14566 11.3986 6.14566 11.2323V6.65735C6.14566 4.43233 7.93355 2.7478 10.3538 2.7478C11.2697 2.7478 12.1199 3.039 12.8396 3.55886L8.54424 5.92959C8.28268 6.07508 8.15181 6.28303 8.15181 6.57427V12.6049L8.15158 12.6047ZM12 14.7258L9.43799 13.3533V10.4421L12 9.06965L14.5618 10.4421V13.3533L12 14.7258ZM13.6461 21.0476C12.7303 21.0476 11.8801 20.7564 11.1604 20.2366L15.4557 17.8658C15.7173 17.7203 15.8482 17.5124 15.8482 17.2211V11.1905L17.658 12.1886C17.8105 12.2717 17.876 12.3965 17.876 12.563V17.1379C17.876 19.3629 16.0662 21.0474 13.6461 21.0474V21.0476ZM8.47863 16.4103L4.314 14.1229C3.11471 13.4573 2.30808 12.0433 2.30808 10.6709C2.30808 9.06965 3.31106 7.6348 4.85903 7.03168V11.773C4.85903 12.0642 4.98995 12.2721 5.25151 12.4177L10.7025 15.4328L8.91464 16.4103C8.76209 16.4934 8.63117 16.4934 8.47863 16.4103ZM8.23892 19.8207C5.77508 19.8207 3.96533 18.0531 3.96533 15.8696C3.96533 15.7032 3.98719 15.5368 4.00886 15.3704L8.30418 17.7412C8.56574 17.8867 8.82752 17.8867 9.08909 17.7412L14.5618 14.726V16.7015C14.5618 16.8679 14.4964 16.9927 14.3438 17.0758L10.1792 19.3633C9.61225 19.6752 8.93631 19.8207 8.23869 19.8207H8.23892ZM13.6461 22.2952C16.2844 22.2952 18.4865 20.5069 18.9882 18.1362C21.4301 17.5331 23 15.3495 23 13.1245C23 11.6688 22.346 10.2548 21.1685 9.23581C21.2775 8.79908 21.343 8.36234 21.343 7.92582C21.343 4.95215 18.8137 2.72691 15.892 2.72691C15.3034 2.72691 14.7365 2.80999 14.1695 2.99726C13.1882 2.08223 11.8364 1.5 10.3538 1.5C7.71557 1.5 5.51352 3.28829 5.01185 5.65902C2.56987 6.26214 1 8.44564 1 10.6707C1 12.1264 1.65404 13.5404 2.83147 14.5594C2.72246 14.9961 2.65702 15.4328 2.65702 15.8694C2.65702 18.8431 5.1863 21.0683 8.108 21.0683C8.69661 21.0683 9.26354 20.9852 9.83046 20.7979C10.8115 21.713 12.1634 22.2952 13.6461 22.2952Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.7891 3.93164L20.2223 20.0677H23.7502L17.317 3.93164H13.7891Z" fill="currentColor" />
|
||||
<path
|
||||
d="M6.32538 13.6824L8.52662 8.01177L10.7279 13.6824H6.32538ZM6.68225 3.93164L0.25 20.0677H3.84652L5.16202 16.6791H11.8914L13.2067 20.0677H16.8033L10.371 3.93164H6.68225Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<IconGemini width="24" height="24" />
|
||||
</div>
|
||||
<div>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M9.16861 16.0529L17.2018 9.85156C17.5957 9.54755 18.1586 9.66612 18.3463 10.1384C19.3339 12.6288 18.8926 15.6217 16.9276 17.6766C14.9626 19.7314 12.2285 20.1821 9.72948 19.1557L6.9995 20.4775C10.9151 23.2763 15.6699 22.5841 18.6411 19.4749C20.9979 17.0103 21.7278 13.6508 21.0453 10.6214L21.0515 10.6278C20.0617 6.17736 21.2948 4.39847 23.8207 0.760904C23.8804 0.674655 23.9402 0.588405 24 0.5L20.6762 3.97585V3.96506L9.16658 16.0551"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M7.37742 16.7017C4.67579 14.0395 5.14158 9.91963 7.44676 7.54383C9.15135 5.78544 11.9442 5.06779 14.3821 6.12281L17.0005 4.87559C16.5288 4.52392 15.9242 4.14566 15.2305 3.87986C12.0948 2.54882 8.34069 3.21127 5.79171 5.8386C3.33985 8.36779 2.56881 12.2567 3.89286 15.5751C4.88192 18.0552 3.26056 19.8094 1.62731 21.5801C1.04853 22.2078 0.467774 22.8355 0 23.5L7.3754 16.7037"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
*/}
|
||||
<div>
|
||||
<IconMiniMax width="24" height="24" />
|
||||
</div>
|
||||
<div>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12.6241 11.346L20.3848 3.44816C20.5309 3.29931 20.4487 3 20.2601 3H16.0842C16.0388 3 15.9949 3.01897 15.9594 3.05541L7.59764 11.5629C7.46721 11.6944 7.27446 11.5771 7.27446 11.3666V3.25183C7.27446 3.11242 7.18515 3 7.07594 3H4.19843C4.08932 3 4 3.11242 4 3.25183V20.7482C4 20.8876 4.08932 21 4.19843 21H7.07594C7.18515 21 7.27446 20.8876 7.27446 20.7482V17.1834C7.27446 17.1073 7.30136 17.0344 7.34815 16.987L9.94075 14.3486C10.0031 14.2853 10.0895 14.2757 10.159 14.3232L17.0934 19.5573C18.2289 20.3412 19.4975 20.8226 20.786 20.9652C20.9008 20.9778 21 20.8606 21 20.7133V17.3559C21 17.2276 20.9249 17.1232 20.8243 17.1073C20.0659 16.9853 19.326 16.6845 18.6569 16.222L12.6538 11.764C12.5291 11.6785 12.5135 11.4584 12.6241 11.346Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<IconZai width="24" height="24" />
|
||||
</div>
|
||||
{/*
|
||||
<div>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12.6043 1.34016C12.9973 2.03016 13.3883 2.72215 13.7783 3.41514C13.7941 3.44286 13.8169 3.46589 13.8445 3.48187C13.8721 3.49786 13.9034 3.50624 13.9353 3.50614H19.4873C19.6612 3.50614 19.8092 3.61614 19.9332 3.83314L21.3872 6.40311C21.5772 6.74011 21.6272 6.88111 21.4112 7.24011C21.1512 7.6701 20.8982 8.1041 20.6512 8.54009L20.2842 9.19809C20.1782 9.39409 20.0612 9.47809 20.2442 9.71008L22.8962 14.347C23.0682 14.648 23.0072 14.841 22.8532 15.117C22.4162 15.902 21.9712 16.681 21.5182 17.457C21.3592 17.729 21.1662 17.832 20.8382 17.827C20.0612 17.811 19.2863 17.817 18.5113 17.843C18.4946 17.8439 18.4785 17.8489 18.4644 17.8576C18.4502 17.8664 18.4385 17.8785 18.4303 17.893C17.5361 19.4773 16.6344 21.0573 15.7253 22.633C15.5563 22.926 15.3453 22.996 15.0003 22.997C14.0033 23 12.9983 23.001 11.9833 22.999C11.8889 22.9987 11.7961 22.9735 11.7145 22.9259C11.6328 22.8783 11.5652 22.8101 11.5184 22.728L10.1834 20.405C10.1756 20.3898 10.1637 20.3771 10.149 20.3684C10.1343 20.3598 10.1174 20.3554 10.1004 20.356H4.98244C4.69744 20.386 4.42944 20.355 4.17745 20.264L2.57447 17.494C2.52706 17.412 2.50193 17.319 2.50158 17.2243C2.50123 17.1296 2.52567 17.0364 2.57247 16.954L3.77945 14.834C3.79665 14.8041 3.80569 14.7701 3.80569 14.7355C3.80569 14.701 3.79665 14.667 3.77945 14.637C3.15073 13.5485 2.52573 12.4579 1.90448 11.3651L1.11449 9.97008C0.954488 9.66008 0.941489 9.47409 1.20949 9.00509C1.67448 8.1921 2.13647 7.38011 2.59647 6.56911C2.72847 6.33512 2.90046 6.23512 3.18046 6.23412C4.04344 6.23048 4.90644 6.23015 5.76943 6.23312C5.79123 6.23295 5.81259 6.22704 5.83138 6.21597C5.85016 6.20491 5.8657 6.1891 5.87643 6.17012L8.68239 1.27516C8.72491 1.2007 8.78631 1.13875 8.86039 1.09556C8.93448 1.05238 9.01863 1.02948 9.10439 1.02917C9.62838 1.02817 10.1574 1.02917 10.6874 1.02317L11.7044 1.00017C12.0453 0.997165 12.4283 1.03217 12.6043 1.34016ZM9.17238 1.74316C9.16185 1.74315 9.15149 1.74592 9.14236 1.75119C9.13323 1.75645 9.12565 1.76403 9.12038 1.77316L6.25442 6.78811C6.24066 6.81174 6.22097 6.83137 6.19729 6.84505C6.17361 6.85873 6.14677 6.86599 6.11942 6.86611H3.25346C3.19746 6.86611 3.18346 6.89111 3.21246 6.94011L9.02239 17.096C9.04739 17.138 9.03539 17.158 8.98839 17.159L6.19342 17.174C6.15256 17.1727 6.11214 17.1828 6.07678 17.2033C6.04141 17.2238 6.01253 17.2539 5.99342 17.29L4.67344 19.6C4.62944 19.678 4.65244 19.718 4.74144 19.718L10.4574 19.726C10.5034 19.726 10.5374 19.746 10.5614 19.787L11.9643 22.241C12.0103 22.322 12.0563 22.323 12.1033 22.241L17.1093 13.481L17.8923 12.0991C17.897 12.0905 17.904 12.0834 17.9125 12.0785C17.9209 12.0735 17.9305 12.0709 17.9403 12.0709C17.9501 12.0709 17.9597 12.0735 17.9681 12.0785C17.9765 12.0834 17.9835 12.0905 17.9883 12.0991L19.4123 14.629C19.4229 14.648 19.4385 14.6637 19.4573 14.6746C19.4761 14.6855 19.4975 14.6912 19.5193 14.691L22.2822 14.671C22.2893 14.6711 22.2963 14.6693 22.3024 14.6658C22.3086 14.6623 22.3137 14.6572 22.3172 14.651C22.3206 14.6449 22.3224 14.638 22.3224 14.631C22.3224 14.624 22.3206 14.6172 22.3172 14.611L19.4173 9.52508C19.4068 9.50809 19.4013 9.48853 19.4013 9.46859C19.4013 9.44864 19.4068 9.42908 19.4173 9.41209L19.7102 8.90509L20.8302 6.92811C20.8542 6.88711 20.8422 6.86611 20.7952 6.86611H9.20038C9.14138 6.86611 9.12738 6.84011 9.15738 6.78911L10.5914 4.28413C10.6021 4.26706 10.6078 4.24731 10.6078 4.22714C10.6078 4.20697 10.6021 4.18721 10.5914 4.17014L9.22538 1.77416C9.22016 1.7647 9.21248 1.75682 9.20315 1.75137C9.19382 1.74591 9.18319 1.74307 9.17238 1.74316ZM15.4623 9.76308C15.5083 9.76308 15.5203 9.78308 15.4963 9.82308L14.6643 11.2881L12.0513 15.873C12.0464 15.8819 12.0392 15.8894 12.0304 15.8945C12.0216 15.8996 12.0115 15.9022 12.0013 15.902C11.9912 15.902 11.9813 15.8993 11.9725 15.8942C11.9637 15.8891 11.9564 15.8818 11.9513 15.873L8.49839 9.84108C8.47839 9.80708 8.48839 9.78908 8.52639 9.78708L8.74239 9.77508L15.4643 9.76308H15.4623Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
*/}
|
||||
</div>
|
||||
<a href={subscribeUrl()}>
|
||||
<span>
|
||||
<For
|
||||
each={i18n
|
||||
.t("go.cta.template")
|
||||
.split(/(\{\{text\}\}|\{\{price\}\})/g)
|
||||
.filter(Boolean)}
|
||||
>
|
||||
{(part) => {
|
||||
if (part === "{{text}}") return <span>{i18n.t("go.cta.text")}</span>
|
||||
if (part === "{{price}}") return <span data-slot="cta-price">{i18n.t("go.cta.price")}</span>
|
||||
return part
|
||||
}}
|
||||
</For>
|
||||
</span>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M6.5 12L17 12M13 16.5L17.5 12L13 7.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="square"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div data-slot="pricing-copy">
|
||||
<p>{i18n.t("go.pricing.body")}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section data-component="comparison">
|
||||
<LimitsGraph href={language.route("/docs/go/#usage-limits")} />
|
||||
</section>
|
||||
|
||||
<section data-component="problem">
|
||||
<div data-slot="section-title">
|
||||
<h3>{i18n.t("go.problem.title")}</h3>
|
||||
<p>{i18n.t("go.problem.body")}</p>
|
||||
</div>
|
||||
<p>{i18n.t("go.problem.subtitle")}</p>
|
||||
<ul>
|
||||
<li>
|
||||
<span>[*]</span> {i18n.t("go.problem.item1")}
|
||||
</li>
|
||||
<li>
|
||||
<span>[*]</span> {i18n.t("go.problem.item2")}
|
||||
</li>
|
||||
<li>
|
||||
<span>[*]</span> {i18n.t("go.problem.item3")}
|
||||
</li>
|
||||
<li>
|
||||
<span>[*]</span> {i18n.t("go.problem.item4")}
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section data-component="how">
|
||||
<div data-slot="section-title">
|
||||
<h3>{i18n.t("go.how.title")}</h3>
|
||||
<p>{i18n.t("go.how.body")}</p>
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
<span>[1]</span>
|
||||
<div>
|
||||
<strong>{i18n.t("go.how.step1.title")}</strong> - {i18n.t("go.how.step1.beforeLink")}{" "}
|
||||
<a href={language.route("/docs/go/#how-it-works")} title={i18n.t("go.how.step1.link")}>
|
||||
{i18n.t("go.how.step1.link")}
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<span>[2]</span>
|
||||
<div>
|
||||
<strong>{i18n.t("go.how.step2.title")}</strong> -{" "}
|
||||
<a href={language.route("/docs/go/#pricing")}>{i18n.t("go.how.step2.link")}</a>{" "}
|
||||
{i18n.t("go.how.step2.afterLink")}
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<span>[3]</span>
|
||||
<div>
|
||||
<strong>{i18n.t("go.how.step3.title")}</strong> - {i18n.t("go.how.step3.body")}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section data-component="faq">
|
||||
<div data-slot="section-title">
|
||||
<h3>{i18n.t("common.faq")}</h3>
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
<Faq question={i18n.t("go.faq.q1")}>{i18n.t("go.faq.a1")}</Faq>
|
||||
</li>
|
||||
<li>
|
||||
<Faq question={i18n.t("go.faq.q2")}>{i18n.t("go.faq.a2")}</Faq>
|
||||
</li>
|
||||
<li>
|
||||
<Faq question={i18n.t("go.faq.q9")}>{i18n.t("go.faq.a9")}</Faq>
|
||||
</li>
|
||||
<li>
|
||||
<Faq question={i18n.t("go.faq.q3")}>{i18n.t("go.faq.a3")}</Faq>
|
||||
</li>
|
||||
<li>
|
||||
<Faq question={i18n.t("go.faq.q4")}>
|
||||
{i18n.t("go.faq.a4.p1.beforePricing")}{" "}
|
||||
<a href={language.route("/docs/go/#pricing")}>{i18n.t("go.faq.a4.p1.pricingLink")}</a>{" "}
|
||||
{i18n.t("go.faq.a4.p1.afterPricing")} {i18n.t("go.faq.a4.p2.beforeAccount")}{" "}
|
||||
<a href={subscribeUrl()}>{i18n.t("go.faq.a4.p2.accountLink")}</a>. {i18n.t("go.faq.a4.p3")}
|
||||
</Faq>
|
||||
</li>
|
||||
<li>
|
||||
<Faq question={i18n.t("go.faq.q5")}>
|
||||
{i18n.t("go.faq.a5.body")} <a href="mailto:contact@anoma.ly">{i18n.t("common.contactUs")}</a>{" "}
|
||||
{i18n.t("go.faq.a5.contactAfter")}
|
||||
</Faq>
|
||||
</li>
|
||||
<li>
|
||||
<Faq question={i18n.t("go.faq.q6")}>{i18n.t("go.faq.a6")}</Faq>
|
||||
</li>
|
||||
<li>
|
||||
<Faq question={i18n.t("go.faq.q7")}>{i18n.t("go.faq.a7")}</Faq>
|
||||
</li>
|
||||
<li>
|
||||
<Faq question={i18n.t("go.faq.q8")}>{i18n.t("go.faq.a8")}</Faq>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<EmailSignup />
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Legal />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -212,10 +212,10 @@ body {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 48px;
|
||||
gap: 32px;
|
||||
|
||||
@media (max-width: 55rem) {
|
||||
gap: 32px;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
|
||||
@@ -148,10 +148,10 @@ body {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 48px;
|
||||
gap: 32px;
|
||||
|
||||
@media (max-width: 55rem) {
|
||||
gap: 32px;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
|
||||
@@ -12,19 +12,7 @@
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
<key>com.apple.security.automation.apple-events</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.audio-input</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<true/>
|
||||
<key>com.apple.security.personal-information.addressbook</key>
|
||||
<true/>
|
||||
<key>com.apple.security.personal-information.calendars</key>
|
||||
<true/>
|
||||
<key>com.apple.security.personal-information.location</key>
|
||||
<true/>
|
||||
<key>com.apple.security.personal-information.photos-library</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user