mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-15 17:08:21 -04:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d65db95af | |||
| f5f07310e0 | |||
| c9e9dbeee1 | |||
| b88b323049 | |||
| 6653f868ae | |||
| af29d91dca | |||
| 1a3735b619 | |||
| d4ae13f2a0 | |||
| f4804dac85 | |||
| 843f188aaa | |||
| 05cb3c87ca | |||
| 270cb0b8b4 | |||
| 46ba9c8170 | |||
| 80f91d3fd9 | |||
| a564231caf | |||
| 9457493696 | |||
| ff748b82ca | |||
| 9fafa57562 | |||
| f8475649da | |||
| b94e110a4c | |||
| f0bba10b12 | |||
| d961981e25 | |||
| 5576662200 | |||
| 4a2a046d79 | |||
| 8f8c74cfb8 | |||
| 092f654f63 | |||
| 96b1d8f639 | |||
| dcb17c6a67 | |||
| dd68b85f58 | |||
| 84df96eaef | |||
| d9dd33aeeb | |||
| 0a281c7390 | |||
| 3016efba47 | |||
| 3998df8112 | |||
| 7066e2a25e | |||
| c173988aaa | |||
| 268855dc5a | |||
| bfb736e94a | |||
| df8464f89c | |||
| 3ea387f364 | |||
| 9d3c42c8c4 | |||
| f2cad046e6 | |||
| d722026a8d | |||
| 42a5af6c8f | |||
| f0542fae7a | |||
| 02c75821a8 | |||
| 3ba9ab2c0a | |||
| 184732fc20 | |||
| b66222baf7 |
@@ -8,7 +8,9 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
# Keep every run on dev so cancelled checks do not pollute the default branch
|
||||||
|
# commit history. PRs and other branches still share a group and cancel stale runs.
|
||||||
|
group: ${{ case(github.ref == 'refs/heads/dev', format('{0}-{1}', github.workflow, github.run_id), format('{0}-{1}', github.workflow, github.event.pull_request.number || github.ref)) }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ ts-dist
|
|||||||
/result
|
/result
|
||||||
refs
|
refs
|
||||||
Session.vim
|
Session.vim
|
||||||
opencode.json
|
/opencode.json
|
||||||
a.out
|
a.out
|
||||||
target
|
target
|
||||||
.scripts
|
.scripts
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.2.24",
|
"version": "1.2.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.2.24",
|
"version": "1.2.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.2.24",
|
"version": "1.2.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-sts": "3.782.0",
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
"@jsx-email/render": "1.1.1",
|
"@jsx-email/render": "1.1.1",
|
||||||
@@ -138,7 +138,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.2.24",
|
"version": "1.2.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "2.0.0",
|
"@ai-sdk/anthropic": "2.0.0",
|
||||||
"@ai-sdk/openai": "2.0.2",
|
"@ai-sdk/openai": "2.0.2",
|
||||||
@@ -162,7 +162,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.2.24",
|
"version": "1.2.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
@@ -186,7 +186,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.2.24",
|
"version": "1.2.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/app": "workspace:*",
|
"@opencode-ai/app": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
@@ -219,7 +219,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop-electron": {
|
"packages/desktop-electron": {
|
||||||
"name": "@opencode-ai/desktop-electron",
|
"name": "@opencode-ai/desktop-electron",
|
||||||
"version": "1.2.24",
|
"version": "1.2.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/app": "workspace:*",
|
"@opencode-ai/app": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
@@ -250,7 +250,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.2.24",
|
"version": "1.2.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
@@ -279,7 +279,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.2.24",
|
"version": "1.2.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -295,7 +295,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.2.24",
|
"version": "1.2.25",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -416,7 +416,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.2.24",
|
"version": "1.2.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
@@ -440,7 +440,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.2.24",
|
"version": "1.2.25",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hey-api/openapi-ts": "0.90.10",
|
"@hey-api/openapi-ts": "0.90.10",
|
||||||
"@tsconfig/node22": "catalog:",
|
"@tsconfig/node22": "catalog:",
|
||||||
@@ -451,7 +451,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.2.24",
|
"version": "1.2.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -486,7 +486,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.2.24",
|
"version": "1.2.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -532,7 +532,7 @@
|
|||||||
},
|
},
|
||||||
"packages/util": {
|
"packages/util": {
|
||||||
"name": "@opencode-ai/util",
|
"name": "@opencode-ai/util",
|
||||||
"version": "1.2.24",
|
"version": "1.2.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -543,7 +543,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.2.24",
|
"version": "1.2.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
|
|||||||
@@ -176,6 +176,25 @@ await page.keyboard.press(`${modKey}+Comma`) // Open settings
|
|||||||
- These helpers use the fixture-enabled test-only terminal driver and wait for output after the terminal writer settles.
|
- These helpers use the fixture-enabled test-only terminal driver and wait for output after the terminal writer settles.
|
||||||
- Avoid `waitForTimeout` and custom DOM or `data-*` readiness checks.
|
- Avoid `waitForTimeout` and custom DOM or `data-*` readiness checks.
|
||||||
|
|
||||||
|
### Wait on state
|
||||||
|
|
||||||
|
- Never use wall-clock waits like `page.waitForTimeout(...)` to make a test pass
|
||||||
|
- Avoid race-prone flows that assume work is finished after an action
|
||||||
|
- Wait or poll on observable state with `expect(...)`, `expect.poll(...)`, or existing helpers
|
||||||
|
- Prefer locator assertions like `toBeVisible()`, `toHaveCount(0)`, and `toHaveAttribute(...)` for normal UI state, and reserve `expect.poll(...)` for probe, mock, or backend state
|
||||||
|
|
||||||
|
### Add hooks
|
||||||
|
|
||||||
|
- If required state is not observable from the UI, add a small test-only driver or probe in app code instead of sleeps or fragile DOM checks
|
||||||
|
- Keep these hooks minimal and purpose-built, following the style of `packages/app/src/testing/terminal.ts`
|
||||||
|
- Test-only hooks must be inert unless explicitly enabled; do not add normal-runtime listeners, reactive subscriptions, or per-update allocations for e2e ceremony
|
||||||
|
- When mocking routes or APIs, expose explicit mock state and wait on that before asserting post-action UI
|
||||||
|
|
||||||
|
### Prefer helpers
|
||||||
|
|
||||||
|
- Prefer fluent helpers and drivers when they make intent obvious and reduce locator-heavy noise
|
||||||
|
- Use direct locators when the interaction is simple and a helper would not add clarity
|
||||||
|
|
||||||
## Writing New Tests
|
## Writing New Tests
|
||||||
|
|
||||||
1. Choose appropriate folder or create new one
|
1. Choose appropriate folder or create new one
|
||||||
|
|||||||
@@ -36,6 +36,22 @@ async function terminalID(term: Locator) {
|
|||||||
throw new Error(`Active terminal missing ${terminalAttr}`)
|
throw new Error(`Active terminal missing ${terminalAttr}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function terminalConnects(page: Page, input?: { term?: Locator }) {
|
||||||
|
const term = input?.term ?? page.locator(terminalSelector).first()
|
||||||
|
const id = await terminalID(term)
|
||||||
|
return page.evaluate((id) => {
|
||||||
|
return (window as E2EWindow).__opencode_e2e?.terminal?.terminals?.[id]?.connects ?? 0
|
||||||
|
}, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function disconnectTerminal(page: Page, input?: { term?: Locator }) {
|
||||||
|
const term = input?.term ?? page.locator(terminalSelector).first()
|
||||||
|
const id = await terminalID(term)
|
||||||
|
await page.evaluate((id) => {
|
||||||
|
;(window as E2EWindow).__opencode_e2e?.terminal?.controls?.[id]?.disconnect?.()
|
||||||
|
}, id)
|
||||||
|
}
|
||||||
|
|
||||||
async function terminalReady(page: Page, term?: Locator) {
|
async function terminalReady(page: Page, term?: Locator) {
|
||||||
const next = term ?? page.locator(terminalSelector).first()
|
const next = term ?? page.locator(terminalSelector).first()
|
||||||
const id = await terminalID(next)
|
const id = await terminalID(next)
|
||||||
@@ -588,12 +604,19 @@ export async function seedSessionTask(
|
|||||||
.flatMap((message) => message.parts)
|
.flatMap((message) => message.parts)
|
||||||
.find((part) => {
|
.find((part) => {
|
||||||
if (part.type !== "tool" || part.tool !== "task") return false
|
if (part.type !== "tool" || part.tool !== "task") return false
|
||||||
if (part.state.input?.description !== input.description) return false
|
if (!("state" in part) || !part.state || typeof part.state !== "object") return false
|
||||||
return typeof part.state.metadata?.sessionId === "string" && part.state.metadata.sessionId.length > 0
|
if (!("input" in part.state) || !part.state.input || typeof part.state.input !== "object") return false
|
||||||
|
if (!("description" in part.state.input) || part.state.input.description !== input.description) return false
|
||||||
|
if (!("metadata" in part.state) || !part.state.metadata || typeof part.state.metadata !== "object")
|
||||||
|
return false
|
||||||
|
if (!("sessionId" in part.state.metadata)) return false
|
||||||
|
return typeof part.state.metadata.sessionId === "string" && part.state.metadata.sessionId.length > 0
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!part) return
|
if (!part || !("state" in part) || !part.state || typeof part.state !== "object") return
|
||||||
const id = part.state.metadata?.sessionId
|
if (!("metadata" in part.state) || !part.state.metadata || typeof part.state.metadata !== "object") return
|
||||||
|
if (!("sessionId" in part.state.metadata)) return
|
||||||
|
const id = part.state.metadata.sessionId
|
||||||
if (typeof id !== "string" || !id) return
|
if (typeof id !== "string" || !id) return
|
||||||
const child = await sdk.session
|
const child = await sdk.session
|
||||||
.get({ sessionID: id })
|
.get({ sessionID: id })
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { test, expect } from "../fixtures"
|
import { test, expect } from "../fixtures"
|
||||||
import { cleanupSession, clearSessionDockSeed, seedSessionQuestion, seedSessionTodos } from "../actions"
|
import {
|
||||||
|
composerEvent,
|
||||||
|
type ComposerDriverState,
|
||||||
|
type ComposerProbeState,
|
||||||
|
type ComposerWindow,
|
||||||
|
} from "../../src/testing/session-composer"
|
||||||
|
import { cleanupSession, clearSessionDockSeed, seedSessionQuestion } from "../actions"
|
||||||
import {
|
import {
|
||||||
permissionDockSelector,
|
permissionDockSelector,
|
||||||
promptSelector,
|
promptSelector,
|
||||||
questionDockSelector,
|
questionDockSelector,
|
||||||
sessionComposerDockSelector,
|
sessionComposerDockSelector,
|
||||||
sessionTodoDockSelector,
|
|
||||||
sessionTodoListSelector,
|
|
||||||
sessionTodoToggleButtonSelector,
|
sessionTodoToggleButtonSelector,
|
||||||
} from "../selectors"
|
} from "../selectors"
|
||||||
|
|
||||||
@@ -42,12 +46,8 @@ async function withDockSeed<T>(sdk: Sdk, sessionID: string, fn: () => Promise<T>
|
|||||||
|
|
||||||
async function clearPermissionDock(page: any, label: RegExp) {
|
async function clearPermissionDock(page: any, label: RegExp) {
|
||||||
const dock = page.locator(permissionDockSelector)
|
const dock = page.locator(permissionDockSelector)
|
||||||
for (let i = 0; i < 3; i++) {
|
await expect(dock).toBeVisible()
|
||||||
const count = await dock.count()
|
await dock.getByRole("button", { name: label }).click()
|
||||||
if (count === 0) return
|
|
||||||
await dock.getByRole("button", { name: label }).click()
|
|
||||||
await page.waitForTimeout(150)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setAutoAccept(page: any, enabled: boolean) {
|
async function setAutoAccept(page: any, enabled: boolean) {
|
||||||
@@ -59,6 +59,120 @@ async function setAutoAccept(page: any, enabled: boolean) {
|
|||||||
await expect(button).toHaveAttribute("aria-pressed", enabled ? "true" : "false")
|
await expect(button).toHaveAttribute("aria-pressed", enabled ? "true" : "false")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function expectQuestionBlocked(page: any) {
|
||||||
|
await expect(page.locator(questionDockSelector)).toBeVisible()
|
||||||
|
await expect(page.locator(promptSelector)).toHaveCount(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectQuestionOpen(page: any) {
|
||||||
|
await expect(page.locator(questionDockSelector)).toHaveCount(0)
|
||||||
|
await expect(page.locator(promptSelector)).toBeVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectPermissionBlocked(page: any) {
|
||||||
|
await expect(page.locator(permissionDockSelector)).toBeVisible()
|
||||||
|
await expect(page.locator(promptSelector)).toHaveCount(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectPermissionOpen(page: any) {
|
||||||
|
await expect(page.locator(permissionDockSelector)).toHaveCount(0)
|
||||||
|
await expect(page.locator(promptSelector)).toBeVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function todoDock(page: any, sessionID: string) {
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
const win = window as ComposerWindow
|
||||||
|
win.__opencode_e2e = {
|
||||||
|
...win.__opencode_e2e,
|
||||||
|
composer: {
|
||||||
|
enabled: true,
|
||||||
|
sessions: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const write = async (driver: ComposerDriverState | undefined) => {
|
||||||
|
await page.evaluate(
|
||||||
|
(input) => {
|
||||||
|
const win = window as ComposerWindow
|
||||||
|
const composer = win.__opencode_e2e?.composer
|
||||||
|
if (!composer?.enabled) throw new Error("Composer e2e driver is not enabled")
|
||||||
|
composer.sessions ??= {}
|
||||||
|
const prev = composer.sessions[input.sessionID] ?? {}
|
||||||
|
if (!input.driver) {
|
||||||
|
if (!prev.probe) {
|
||||||
|
delete composer.sessions[input.sessionID]
|
||||||
|
} else {
|
||||||
|
composer.sessions[input.sessionID] = { probe: prev.probe }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
composer.sessions[input.sessionID] = {
|
||||||
|
...prev,
|
||||||
|
driver: input.driver,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.dispatchEvent(new CustomEvent(input.event, { detail: { sessionID: input.sessionID } }))
|
||||||
|
},
|
||||||
|
{ event: composerEvent, sessionID, driver },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const read = () =>
|
||||||
|
page.evaluate((sessionID) => {
|
||||||
|
const win = window as ComposerWindow
|
||||||
|
return win.__opencode_e2e?.composer?.sessions?.[sessionID]?.probe ?? null
|
||||||
|
}, sessionID) as Promise<ComposerProbeState | null>
|
||||||
|
|
||||||
|
const api = {
|
||||||
|
async clear() {
|
||||||
|
await write(undefined)
|
||||||
|
return api
|
||||||
|
},
|
||||||
|
async open(todos: NonNullable<ComposerDriverState["todos"]>) {
|
||||||
|
await write({ live: true, todos })
|
||||||
|
return api
|
||||||
|
},
|
||||||
|
async finish(todos: NonNullable<ComposerDriverState["todos"]>) {
|
||||||
|
await write({ live: false, todos })
|
||||||
|
return api
|
||||||
|
},
|
||||||
|
async expectOpen(states: ComposerProbeState["states"]) {
|
||||||
|
await expect.poll(read, { timeout: 10_000 }).toMatchObject({
|
||||||
|
mounted: true,
|
||||||
|
collapsed: false,
|
||||||
|
hidden: false,
|
||||||
|
count: states.length,
|
||||||
|
states,
|
||||||
|
})
|
||||||
|
return api
|
||||||
|
},
|
||||||
|
async expectCollapsed(states: ComposerProbeState["states"]) {
|
||||||
|
await expect.poll(read, { timeout: 10_000 }).toMatchObject({
|
||||||
|
mounted: true,
|
||||||
|
collapsed: true,
|
||||||
|
hidden: true,
|
||||||
|
count: states.length,
|
||||||
|
states,
|
||||||
|
})
|
||||||
|
return api
|
||||||
|
},
|
||||||
|
async expectClosed() {
|
||||||
|
await expect.poll(read, { timeout: 10_000 }).toMatchObject({ mounted: false })
|
||||||
|
return api
|
||||||
|
},
|
||||||
|
async collapse() {
|
||||||
|
await page.locator(sessionTodoToggleButtonSelector).click()
|
||||||
|
return api
|
||||||
|
},
|
||||||
|
async expand() {
|
||||||
|
await page.locator(sessionTodoToggleButtonSelector).click()
|
||||||
|
return api
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return api
|
||||||
|
}
|
||||||
|
|
||||||
async function withMockPermission<T>(
|
async function withMockPermission<T>(
|
||||||
page: any,
|
page: any,
|
||||||
request: {
|
request: {
|
||||||
@@ -70,7 +184,7 @@ async function withMockPermission<T>(
|
|||||||
always?: string[]
|
always?: string[]
|
||||||
},
|
},
|
||||||
opts: { child?: any } | undefined,
|
opts: { child?: any } | undefined,
|
||||||
fn: () => Promise<T>,
|
fn: (state: { resolved: () => Promise<void> }) => Promise<T>,
|
||||||
) {
|
) {
|
||||||
let pending = [
|
let pending = [
|
||||||
{
|
{
|
||||||
@@ -119,8 +233,14 @@ async function withMockPermission<T>(
|
|||||||
|
|
||||||
if (sessionList) await page.route("**/session?*", sessionList)
|
if (sessionList) await page.route("**/session?*", sessionList)
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
async resolved() {
|
||||||
|
await expect.poll(() => pending.length, { timeout: 10_000 }).toBe(0)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await fn()
|
return await fn(state)
|
||||||
} finally {
|
} finally {
|
||||||
await page.unroute("**/permission", list)
|
await page.unroute("**/permission", list)
|
||||||
await page.unroute("**/session/*/permissions/*", reply)
|
await page.unroute("**/session/*/permissions/*", reply)
|
||||||
@@ -173,14 +293,12 @@ test("blocked question flow unblocks after submit", async ({ page, sdk, gotoSess
|
|||||||
})
|
})
|
||||||
|
|
||||||
const dock = page.locator(questionDockSelector)
|
const dock = page.locator(questionDockSelector)
|
||||||
await expect.poll(() => dock.count(), { timeout: 10_000 }).toBe(1)
|
await expectQuestionBlocked(page)
|
||||||
await expect(page.locator(promptSelector)).toHaveCount(0)
|
|
||||||
|
|
||||||
await dock.locator('[data-slot="question-option"]').first().click()
|
await dock.locator('[data-slot="question-option"]').first().click()
|
||||||
await dock.getByRole("button", { name: /submit/i }).click()
|
await dock.getByRole("button", { name: /submit/i }).click()
|
||||||
|
|
||||||
await expect.poll(() => page.locator(questionDockSelector).count(), { timeout: 10_000 }).toBe(0)
|
await expectQuestionOpen(page)
|
||||||
await expect(page.locator(promptSelector)).toBeVisible()
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -199,15 +317,14 @@ test("blocked permission flow supports allow once", async ({ page, sdk, gotoSess
|
|||||||
metadata: { description: "Need permission for command" },
|
metadata: { description: "Need permission for command" },
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
async () => {
|
async (state) => {
|
||||||
await page.goto(page.url())
|
await page.goto(page.url())
|
||||||
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1)
|
await expectPermissionBlocked(page)
|
||||||
await expect(page.locator(promptSelector)).toHaveCount(0)
|
|
||||||
|
|
||||||
await clearPermissionDock(page, /allow once/i)
|
await clearPermissionDock(page, /allow once/i)
|
||||||
|
await state.resolved()
|
||||||
await page.goto(page.url())
|
await page.goto(page.url())
|
||||||
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0)
|
await expectPermissionOpen(page)
|
||||||
await expect(page.locator(promptSelector)).toBeVisible()
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -226,15 +343,14 @@ test("blocked permission flow supports reject", async ({ page, sdk, gotoSession
|
|||||||
patterns: ["/tmp/opencode-e2e-perm-reject"],
|
patterns: ["/tmp/opencode-e2e-perm-reject"],
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
async () => {
|
async (state) => {
|
||||||
await page.goto(page.url())
|
await page.goto(page.url())
|
||||||
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1)
|
await expectPermissionBlocked(page)
|
||||||
await expect(page.locator(promptSelector)).toHaveCount(0)
|
|
||||||
|
|
||||||
await clearPermissionDock(page, /deny/i)
|
await clearPermissionDock(page, /deny/i)
|
||||||
|
await state.resolved()
|
||||||
await page.goto(page.url())
|
await page.goto(page.url())
|
||||||
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0)
|
await expectPermissionOpen(page)
|
||||||
await expect(page.locator(promptSelector)).toBeVisible()
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -254,15 +370,14 @@ test("blocked permission flow supports allow always", async ({ page, sdk, gotoSe
|
|||||||
metadata: { description: "Need permission for command" },
|
metadata: { description: "Need permission for command" },
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
async () => {
|
async (state) => {
|
||||||
await page.goto(page.url())
|
await page.goto(page.url())
|
||||||
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(1)
|
await expectPermissionBlocked(page)
|
||||||
await expect(page.locator(promptSelector)).toHaveCount(0)
|
|
||||||
|
|
||||||
await clearPermissionDock(page, /allow always/i)
|
await clearPermissionDock(page, /allow always/i)
|
||||||
|
await state.resolved()
|
||||||
await page.goto(page.url())
|
await page.goto(page.url())
|
||||||
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0)
|
await expectPermissionOpen(page)
|
||||||
await expect(page.locator(promptSelector)).toBeVisible()
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -301,14 +416,12 @@ test("child session question request blocks parent dock and unblocks after submi
|
|||||||
})
|
})
|
||||||
|
|
||||||
const dock = page.locator(questionDockSelector)
|
const dock = page.locator(questionDockSelector)
|
||||||
await expect.poll(() => dock.count(), { timeout: 10_000 }).toBe(1)
|
await expectQuestionBlocked(page)
|
||||||
await expect(page.locator(promptSelector)).toHaveCount(0)
|
|
||||||
|
|
||||||
await dock.locator('[data-slot="question-option"]').first().click()
|
await dock.locator('[data-slot="question-option"]').first().click()
|
||||||
await dock.getByRole("button", { name: /submit/i }).click()
|
await dock.getByRole("button", { name: /submit/i }).click()
|
||||||
|
|
||||||
await expect.poll(() => page.locator(questionDockSelector).count(), { timeout: 10_000 }).toBe(0)
|
await expectQuestionOpen(page)
|
||||||
await expect(page.locator(promptSelector)).toBeVisible()
|
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
await cleanupSession({ sdk, sessionID: child.id })
|
await cleanupSession({ sdk, sessionID: child.id })
|
||||||
@@ -344,17 +457,15 @@ test("child session permission request blocks parent dock and supports allow onc
|
|||||||
metadata: { description: "Need child permission" },
|
metadata: { description: "Need child permission" },
|
||||||
},
|
},
|
||||||
{ child },
|
{ child },
|
||||||
async () => {
|
async (state) => {
|
||||||
await page.goto(page.url())
|
await page.goto(page.url())
|
||||||
const dock = page.locator(permissionDockSelector)
|
await expectPermissionBlocked(page)
|
||||||
await expect.poll(() => dock.count(), { timeout: 10_000 }).toBe(1)
|
|
||||||
await expect(page.locator(promptSelector)).toHaveCount(0)
|
|
||||||
|
|
||||||
await clearPermissionDock(page, /allow once/i)
|
await clearPermissionDock(page, /allow once/i)
|
||||||
|
await state.resolved()
|
||||||
await page.goto(page.url())
|
await page.goto(page.url())
|
||||||
|
|
||||||
await expect.poll(() => page.locator(permissionDockSelector).count(), { timeout: 10_000 }).toBe(0)
|
await expectPermissionOpen(page)
|
||||||
await expect(page.locator(promptSelector)).toBeVisible()
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -365,36 +476,31 @@ test("child session permission request blocks parent dock and supports allow onc
|
|||||||
|
|
||||||
test("todo dock transitions and collapse behavior", async ({ page, sdk, gotoSession }) => {
|
test("todo dock transitions and collapse behavior", async ({ page, sdk, gotoSession }) => {
|
||||||
await withDockSession(sdk, "e2e composer dock todo", async (session) => {
|
await withDockSession(sdk, "e2e composer dock todo", async (session) => {
|
||||||
await withDockSeed(sdk, session.id, async () => {
|
const dock = await todoDock(page, session.id)
|
||||||
await gotoSession(session.id)
|
await gotoSession(session.id)
|
||||||
|
await expect(page.locator(sessionComposerDockSelector)).toBeVisible()
|
||||||
|
|
||||||
await seedSessionTodos(sdk, {
|
try {
|
||||||
sessionID: session.id,
|
await dock.open([
|
||||||
todos: [
|
{ content: "first task", status: "pending", priority: "high" },
|
||||||
{ content: "first task", status: "pending", priority: "high" },
|
{ content: "second task", status: "in_progress", priority: "medium" },
|
||||||
{ content: "second task", status: "in_progress", priority: "medium" },
|
])
|
||||||
],
|
await dock.expectOpen(["pending", "in_progress"])
|
||||||
})
|
|
||||||
|
|
||||||
await expect.poll(() => page.locator(sessionTodoDockSelector).count(), { timeout: 10_000 }).toBe(1)
|
await dock.collapse()
|
||||||
await expect(page.locator(sessionTodoListSelector)).toBeVisible()
|
await dock.expectCollapsed(["pending", "in_progress"])
|
||||||
|
|
||||||
await page.locator(sessionTodoToggleButtonSelector).click()
|
await dock.expand()
|
||||||
await expect(page.locator(sessionTodoListSelector)).toBeHidden()
|
await dock.expectOpen(["pending", "in_progress"])
|
||||||
|
|
||||||
await page.locator(sessionTodoToggleButtonSelector).click()
|
await dock.finish([
|
||||||
await expect(page.locator(sessionTodoListSelector)).toBeVisible()
|
{ content: "first task", status: "completed", priority: "high" },
|
||||||
|
{ content: "second task", status: "cancelled", priority: "medium" },
|
||||||
await seedSessionTodos(sdk, {
|
])
|
||||||
sessionID: session.id,
|
await dock.expectClosed()
|
||||||
todos: [
|
} finally {
|
||||||
{ content: "first task", status: "completed", priority: "high" },
|
await dock.clear()
|
||||||
{ content: "second task", status: "cancelled", priority: "medium" },
|
}
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
await expect.poll(() => page.locator(sessionTodoDockSelector).count(), { timeout: 10_000 }).toBe(0)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -414,8 +520,7 @@ test("keyboard focus stays off prompt while blocked", async ({ page, sdk, gotoSe
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect.poll(() => page.locator(questionDockSelector).count(), { timeout: 10_000 }).toBe(1)
|
await expectQuestionBlocked(page)
|
||||||
await expect(page.locator(promptSelector)).toHaveCount(0)
|
|
||||||
|
|
||||||
await page.locator("main").click({ position: { x: 5, y: 5 } })
|
await page.locator("main").click({ position: { x: 5, y: 5 } })
|
||||||
await page.keyboard.type("abc")
|
await page.keyboard.type("abc")
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import type { Page } from "@playwright/test"
|
||||||
|
import { disconnectTerminal, runTerminal, terminalConnects, waitTerminalReady } from "../actions"
|
||||||
|
import { test, expect } from "../fixtures"
|
||||||
|
import { terminalSelector } from "../selectors"
|
||||||
|
import { terminalToggleKey } from "../utils"
|
||||||
|
|
||||||
|
async function open(page: Page) {
|
||||||
|
const term = page.locator(terminalSelector).first()
|
||||||
|
const visible = await term.isVisible().catch(() => false)
|
||||||
|
if (!visible) await page.keyboard.press(terminalToggleKey)
|
||||||
|
await waitTerminalReady(page, { term })
|
||||||
|
return term
|
||||||
|
}
|
||||||
|
|
||||||
|
test("terminal reconnects without replacing the pty", async ({ page, withProject }) => {
|
||||||
|
await withProject(async ({ gotoSession }) => {
|
||||||
|
const name = `OPENCODE_E2E_RECONNECT_${Date.now()}`
|
||||||
|
const token = `E2E_RECONNECT_${Date.now()}`
|
||||||
|
|
||||||
|
await gotoSession()
|
||||||
|
|
||||||
|
const term = await open(page)
|
||||||
|
const id = await term.getAttribute("data-pty-id")
|
||||||
|
if (!id) throw new Error("Active terminal missing data-pty-id")
|
||||||
|
|
||||||
|
const prev = await terminalConnects(page, { term })
|
||||||
|
|
||||||
|
await runTerminal(page, {
|
||||||
|
term,
|
||||||
|
cmd: `export ${name}=${token}; echo ${token}`,
|
||||||
|
token,
|
||||||
|
})
|
||||||
|
|
||||||
|
await disconnectTerminal(page, { term })
|
||||||
|
|
||||||
|
await expect.poll(() => terminalConnects(page, { term }), { timeout: 15_000 }).toBeGreaterThan(prev)
|
||||||
|
await expect.poll(() => term.getAttribute("data-pty-id"), { timeout: 5_000 }).toBe(id)
|
||||||
|
|
||||||
|
await runTerminal(page, {
|
||||||
|
term,
|
||||||
|
cmd: `echo $${name}`,
|
||||||
|
token,
|
||||||
|
timeout: 15_000,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -2,7 +2,8 @@
|
|||||||
"extends": "../tsconfig.json",
|
"extends": "../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
"rootDir": "..",
|
||||||
"types": ["node", "bun"]
|
"types": ["node", "bun"]
|
||||||
},
|
},
|
||||||
"include": ["./**/*.ts"]
|
"include": ["./**/*.ts", "../src/testing/terminal.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.2.24",
|
"version": "1.2.25",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const serverHost = process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"
|
|||||||
const serverPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"
|
const serverPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"
|
||||||
const command = `bun run dev -- --host 0.0.0.0 --port ${port}`
|
const command = `bun run dev -- --host 0.0.0.0 --port ${port}`
|
||||||
const reuse = !process.env.CI
|
const reuse = !process.env.CI
|
||||||
|
const workers = Number(process.env.PLAYWRIGHT_WORKERS ?? (process.env.CI ? 5 : 0)) || undefined
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: "./e2e",
|
testDir: "./e2e",
|
||||||
@@ -17,6 +18,7 @@ export default defineConfig({
|
|||||||
fullyParallel: process.env.PLAYWRIGHT_FULLY_PARALLEL === "1",
|
fullyParallel: process.env.PLAYWRIGHT_FULLY_PARALLEL === "1",
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
retries: process.env.CI ? 2 : 0,
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers,
|
||||||
reporter: [["html", { outputFolder: "e2e/playwright-report", open: "never" }], ["line"]],
|
reporter: [["html", { outputFolder: "e2e/playwright-report", open: "never" }], ["line"]],
|
||||||
webServer: {
|
webServer: {
|
||||||
command,
|
command,
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ const serverEnv = {
|
|||||||
OPENCODE_E2E_MESSAGE: "Seeded for UI e2e",
|
OPENCODE_E2E_MESSAGE: "Seeded for UI e2e",
|
||||||
OPENCODE_E2E_MODEL: "opencode/gpt-5-nano",
|
OPENCODE_E2E_MODEL: "opencode/gpt-5-nano",
|
||||||
OPENCODE_CLIENT: "app",
|
OPENCODE_CLIENT: "app",
|
||||||
|
OPENCODE_STRICT_CONFIG_DEPS: "true",
|
||||||
} satisfies Record<string, string>
|
} satisfies Record<string, string>
|
||||||
|
|
||||||
const runnerEnv = {
|
const runnerEnv = {
|
||||||
|
|||||||
+30
-20
@@ -12,6 +12,7 @@ import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router"
|
|||||||
import { type Duration, Effect } from "effect"
|
import { type Duration, Effect } from "effect"
|
||||||
import {
|
import {
|
||||||
type Component,
|
type Component,
|
||||||
|
createMemo,
|
||||||
createResource,
|
createResource,
|
||||||
createSignal,
|
createSignal,
|
||||||
ErrorBoundary,
|
ErrorBoundary,
|
||||||
@@ -67,7 +68,7 @@ const SessionIndexRoute = () => <Navigate href="session" />
|
|||||||
|
|
||||||
function UiI18nBridge(props: ParentProps) {
|
function UiI18nBridge(props: ParentProps) {
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
return <I18nProvider value={{ locale: language.locale, t: language.t }}>{props.children}</I18nProvider>
|
return <I18nProvider value={{ locale: language.intl, t: language.t }}>{props.children}</I18nProvider>
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -159,7 +160,7 @@ const effectMinDuration =
|
|||||||
<A, E, R>(e: Effect.Effect<A, E, R>) =>
|
<A, E, R>(e: Effect.Effect<A, E, R>) =>
|
||||||
Effect.all([e, Effect.sleep(duration)], { concurrency: "unbounded" }).pipe(Effect.map((v) => v[0]))
|
Effect.all([e, Effect.sleep(duration)], { concurrency: "unbounded" }).pipe(Effect.map((v) => v[0]))
|
||||||
|
|
||||||
function ConnectionGate(props: ParentProps) {
|
function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
const checkServerHealth = useCheckServerHealth()
|
const checkServerHealth = useCheckServerHealth()
|
||||||
|
|
||||||
@@ -168,21 +169,23 @@ function ConnectionGate(props: ParentProps) {
|
|||||||
// performs repeated health check with a grace period for
|
// performs repeated health check with a grace period for
|
||||||
// non-http connections, otherwise fails instantly
|
// non-http connections, otherwise fails instantly
|
||||||
const [startupHealthCheck, healthCheckActions] = createResource(() =>
|
const [startupHealthCheck, healthCheckActions] = createResource(() =>
|
||||||
Effect.gen(function* () {
|
props.disableHealthCheck
|
||||||
if (!server.current) return true
|
? true
|
||||||
const { http, type } = server.current
|
: Effect.gen(function* () {
|
||||||
|
if (!server.current) return true
|
||||||
|
const { http, type } = server.current
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const res = yield* Effect.promise(() => checkServerHealth(http))
|
const res = yield* Effect.promise(() => checkServerHealth(http))
|
||||||
if (res.healthy) return true
|
if (res.healthy) return true
|
||||||
if (checkMode() === "background" || type === "http") return false
|
if (checkMode() === "background" || type === "http") return false
|
||||||
}
|
}
|
||||||
}).pipe(
|
}).pipe(
|
||||||
effectMinDuration(checkMode() === "blocking" ? "1.2 seconds" : 0),
|
effectMinDuration(checkMode() === "blocking" ? "1.2 seconds" : 0),
|
||||||
Effect.timeoutOrElse({ duration: "10 seconds", onTimeout: () => Effect.succeed(false) }),
|
Effect.timeoutOrElse({ duration: "10 seconds", onTimeout: () => Effect.succeed(false) }),
|
||||||
Effect.ensuring(Effect.sync(() => setCheckMode("background"))),
|
Effect.ensuring(Effect.sync(() => setCheckMode("background"))),
|
||||||
Effect.runPromise,
|
Effect.runPromise,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -216,8 +219,12 @@ function ConnectionGate(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ConnectionError(props: { onRetry?: () => void; onServerSelected?: (key: ServerConnection.Key) => void }) {
|
function ConnectionError(props: { onRetry?: () => void; onServerSelected?: (key: ServerConnection.Key) => void }) {
|
||||||
|
const language = useLanguage()
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
const others = () => server.list.filter((s) => ServerConnection.key(s) !== server.key)
|
const others = () => server.list.filter((s) => ServerConnection.key(s) !== server.key)
|
||||||
|
const name = createMemo(() => server.name || server.key)
|
||||||
|
const serverToken = "\u0000server\u0000"
|
||||||
|
const unreachable = createMemo(() => language.t("app.server.unreachable", { server: serverToken }).split(serverToken))
|
||||||
|
|
||||||
const timer = setInterval(() => props.onRetry?.(), 1000)
|
const timer = setInterval(() => props.onRetry?.(), 1000)
|
||||||
onCleanup(() => clearInterval(timer))
|
onCleanup(() => clearInterval(timer))
|
||||||
@@ -227,13 +234,15 @@ function ConnectionError(props: { onRetry?: () => void; onServerSelected?: (key:
|
|||||||
<div class="flex flex-col items-center max-w-md text-center">
|
<div class="flex flex-col items-center max-w-md text-center">
|
||||||
<Splash class="w-12 h-15 mb-4" />
|
<Splash class="w-12 h-15 mb-4" />
|
||||||
<p class="text-14-regular text-text-base">
|
<p class="text-14-regular text-text-base">
|
||||||
Could not reach <span class="text-text-strong font-medium">{server.name || server.key}</span>
|
{unreachable()[0]}
|
||||||
|
<span class="text-text-strong font-medium">{name()}</span>
|
||||||
|
{unreachable()[1]}
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-1 text-12-regular text-text-weak">Retrying automatically...</p>
|
<p class="mt-1 text-12-regular text-text-weak">{language.t("app.server.retrying")}</p>
|
||||||
</div>
|
</div>
|
||||||
<Show when={others().length > 0}>
|
<Show when={others().length > 0}>
|
||||||
<div class="flex flex-col gap-2 w-full max-w-sm">
|
<div class="flex flex-col gap-2 w-full max-w-sm">
|
||||||
<span class="text-12-regular text-text-base text-center">Other servers</span>
|
<span class="text-12-regular text-text-base text-center">{language.t("app.server.otherServers")}</span>
|
||||||
<div class="flex flex-col gap-1 bg-surface-base rounded-lg p-2">
|
<div class="flex flex-col gap-1 bg-surface-base rounded-lg p-2">
|
||||||
<For each={others()}>
|
<For each={others()}>
|
||||||
{(conn) => {
|
{(conn) => {
|
||||||
@@ -261,10 +270,11 @@ export function AppInterface(props: {
|
|||||||
defaultServer: ServerConnection.Key
|
defaultServer: ServerConnection.Key
|
||||||
servers?: Array<ServerConnection.Any>
|
servers?: Array<ServerConnection.Any>
|
||||||
router?: Component<BaseRouterProps>
|
router?: Component<BaseRouterProps>
|
||||||
|
disableHealthCheck?: boolean
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ServerProvider defaultServer={props.defaultServer} servers={props.servers}>
|
<ServerProvider defaultServer={props.defaultServer} servers={props.servers}>
|
||||||
<ConnectionGate>
|
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
|
||||||
<GlobalSDKProvider>
|
<GlobalSDKProvider>
|
||||||
<GlobalSyncProvider>
|
<GlobalSyncProvider>
|
||||||
<Dynamic
|
<Dynamic
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useIsRouting, useLocation } from "@solidjs/router"
|
|||||||
import { batch, createEffect, onCleanup, onMount } from "solid-js"
|
import { batch, createEffect, onCleanup, onMount } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
|
||||||
type Mem = Performance & {
|
type Mem = Performance & {
|
||||||
memory?: {
|
memory?: {
|
||||||
@@ -27,17 +28,17 @@ type Obs = PerformanceObserverInit & {
|
|||||||
const span = 5000
|
const span = 5000
|
||||||
|
|
||||||
const ms = (n?: number, d = 0) => {
|
const ms = (n?: number, d = 0) => {
|
||||||
if (n === undefined || Number.isNaN(n)) return "n/a"
|
if (n === undefined || Number.isNaN(n)) return
|
||||||
return `${n.toFixed(d)}ms`
|
return `${n.toFixed(d)}ms`
|
||||||
}
|
}
|
||||||
|
|
||||||
const time = (n?: number) => {
|
const time = (n?: number) => {
|
||||||
if (n === undefined || Number.isNaN(n)) return "n/a"
|
if (n === undefined || Number.isNaN(n)) return
|
||||||
return `${Math.round(n)}`
|
return `${Math.round(n)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const mb = (n?: number) => {
|
const mb = (n?: number) => {
|
||||||
if (n === undefined || Number.isNaN(n)) return "n/a"
|
if (n === undefined || Number.isNaN(n)) return
|
||||||
const v = n / 1024 / 1024
|
const v = n / 1024 / 1024
|
||||||
return `${v >= 1024 ? v.toFixed(0) : v.toFixed(1)}MB`
|
return `${v >= 1024 ? v.toFixed(0) : v.toFixed(1)}MB`
|
||||||
}
|
}
|
||||||
@@ -74,6 +75,7 @@ function Cell(props: { bad?: boolean; dim?: boolean; label: string; tip: string;
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function DebugBar() {
|
export function DebugBar() {
|
||||||
|
const language = useLanguage()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const routing = useIsRouting()
|
const routing = useIsRouting()
|
||||||
const [state, setState] = createStore({
|
const [state, setState] = createStore({
|
||||||
@@ -98,14 +100,15 @@ export function DebugBar() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const na = () => language.t("debugBar.na")
|
||||||
const heap = () => (state.heap.limit ? (state.heap.used ?? 0) / state.heap.limit : undefined)
|
const heap = () => (state.heap.limit ? (state.heap.used ?? 0) / state.heap.limit : undefined)
|
||||||
const heapv = () => {
|
const heapv = () => {
|
||||||
const value = heap()
|
const value = heap()
|
||||||
if (value === undefined) return "n/a"
|
if (value === undefined) return na()
|
||||||
return `${Math.round(value * 100)}%`
|
return `${Math.round(value * 100)}%`
|
||||||
}
|
}
|
||||||
const longv = () => (state.long.count === undefined ? "n/a" : `${time(state.long.block)}/${state.long.count}`)
|
const longv = () => (state.long.count === undefined ? na() : `${time(state.long.block) ?? na()}/${state.long.count}`)
|
||||||
const navv = () => (state.nav.pending ? "..." : time(state.nav.dur))
|
const navv = () => (state.nav.pending ? "..." : (time(state.nav.dur) ?? na()))
|
||||||
|
|
||||||
let prev = ""
|
let prev = ""
|
||||||
let start = 0
|
let start = 0
|
||||||
@@ -359,7 +362,7 @@ export function DebugBar() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
aria-label="Development performance diagnostics"
|
aria-label={language.t("debugBar.ariaLabel")}
|
||||||
class="pointer-events-auto fixed bottom-3 right-3 z-50 w-[308px] max-w-[calc(100vw-1.5rem)] overflow-hidden rounded-xl border p-0.5 text-text-on-interactive-base shadow-[var(--shadow-lg-border-base)] sm:bottom-4 sm:right-4 sm:w-[324px]"
|
class="pointer-events-auto fixed bottom-3 right-3 z-50 w-[308px] max-w-[calc(100vw-1.5rem)] overflow-hidden rounded-xl border p-0.5 text-text-on-interactive-base shadow-[var(--shadow-lg-border-base)] sm:bottom-4 sm:right-4 sm:w-[324px]"
|
||||||
style={{
|
style={{
|
||||||
"background-color": "color-mix(in srgb, var(--icon-interactive-base) 42%, black)",
|
"background-color": "color-mix(in srgb, var(--icon-interactive-base) 42%, black)",
|
||||||
@@ -368,67 +371,70 @@ export function DebugBar() {
|
|||||||
>
|
>
|
||||||
<div class="grid grid-cols-5 gap-px font-mono">
|
<div class="grid grid-cols-5 gap-px font-mono">
|
||||||
<Cell
|
<Cell
|
||||||
label="NAV"
|
label={language.t("debugBar.nav.label")}
|
||||||
tip="Last completed route transition touching a session page, measured from router start until the first paint after it settles."
|
tip={language.t("debugBar.nav.tip")}
|
||||||
value={navv()}
|
value={navv()}
|
||||||
bad={bad(state.nav.dur, 400)}
|
bad={bad(state.nav.dur, 400)}
|
||||||
dim={state.nav.dur === undefined && !state.nav.pending}
|
dim={state.nav.dur === undefined && !state.nav.pending}
|
||||||
/>
|
/>
|
||||||
<Cell
|
<Cell
|
||||||
label="FPS"
|
label={language.t("debugBar.fps.label")}
|
||||||
tip="Rolling frames per second over the last 5 seconds."
|
tip={language.t("debugBar.fps.tip")}
|
||||||
value={state.fps === undefined ? "n/a" : `${Math.round(state.fps)}`}
|
value={state.fps === undefined ? na() : `${Math.round(state.fps)}`}
|
||||||
bad={bad(state.fps, 50, true)}
|
bad={bad(state.fps, 50, true)}
|
||||||
dim={state.fps === undefined}
|
dim={state.fps === undefined}
|
||||||
/>
|
/>
|
||||||
<Cell
|
<Cell
|
||||||
label="FRAME"
|
label={language.t("debugBar.frame.label")}
|
||||||
tip="Worst frame time over the last 5 seconds."
|
tip={language.t("debugBar.frame.tip")}
|
||||||
value={time(state.gap)}
|
value={time(state.gap) ?? na()}
|
||||||
bad={bad(state.gap, 50)}
|
bad={bad(state.gap, 50)}
|
||||||
dim={state.gap === undefined}
|
dim={state.gap === undefined}
|
||||||
/>
|
/>
|
||||||
<Cell
|
<Cell
|
||||||
label="JANK"
|
label={language.t("debugBar.jank.label")}
|
||||||
tip="Frames over 32ms in the last 5 seconds."
|
tip={language.t("debugBar.jank.tip")}
|
||||||
value={state.jank === undefined ? "n/a" : `${state.jank}`}
|
value={state.jank === undefined ? na() : `${state.jank}`}
|
||||||
bad={bad(state.jank, 8)}
|
bad={bad(state.jank, 8)}
|
||||||
dim={state.jank === undefined}
|
dim={state.jank === undefined}
|
||||||
/>
|
/>
|
||||||
<Cell
|
<Cell
|
||||||
label="LONG"
|
label={language.t("debugBar.long.label")}
|
||||||
tip={`Blocked time and long-task count in the last 5 seconds. Max task: ${ms(state.long.max)}.`}
|
tip={language.t("debugBar.long.tip", { max: ms(state.long.max) ?? na() })}
|
||||||
value={longv()}
|
value={longv()}
|
||||||
bad={bad(state.long.block, 200)}
|
bad={bad(state.long.block, 200)}
|
||||||
dim={state.long.count === undefined}
|
dim={state.long.count === undefined}
|
||||||
/>
|
/>
|
||||||
<Cell
|
<Cell
|
||||||
label="DELAY"
|
label={language.t("debugBar.delay.label")}
|
||||||
tip="Worst observed input delay in the last 5 seconds."
|
tip={language.t("debugBar.delay.tip")}
|
||||||
value={time(state.delay)}
|
value={time(state.delay) ?? na()}
|
||||||
bad={bad(state.delay, 100)}
|
bad={bad(state.delay, 100)}
|
||||||
dim={state.delay === undefined}
|
dim={state.delay === undefined}
|
||||||
/>
|
/>
|
||||||
<Cell
|
<Cell
|
||||||
label="INP"
|
label={language.t("debugBar.inp.label")}
|
||||||
tip="Approximate interaction duration over the last 5 seconds. This is INP-like, not the official Web Vitals INP."
|
tip={language.t("debugBar.inp.tip")}
|
||||||
value={time(state.inp)}
|
value={time(state.inp) ?? na()}
|
||||||
bad={bad(state.inp, 200)}
|
bad={bad(state.inp, 200)}
|
||||||
dim={state.inp === undefined}
|
dim={state.inp === undefined}
|
||||||
/>
|
/>
|
||||||
<Cell
|
<Cell
|
||||||
label="CLS"
|
label={language.t("debugBar.cls.label")}
|
||||||
tip="Cumulative layout shift for the current app lifetime."
|
tip={language.t("debugBar.cls.tip")}
|
||||||
value={state.cls === undefined ? "n/a" : state.cls.toFixed(2)}
|
value={state.cls === undefined ? na() : state.cls.toFixed(2)}
|
||||||
bad={bad(state.cls, 0.1)}
|
bad={bad(state.cls, 0.1)}
|
||||||
dim={state.cls === undefined}
|
dim={state.cls === undefined}
|
||||||
/>
|
/>
|
||||||
<Cell
|
<Cell
|
||||||
label="MEM"
|
label={language.t("debugBar.mem.label")}
|
||||||
tip={
|
tip={
|
||||||
state.heap.used === undefined
|
state.heap.used === undefined
|
||||||
? "Used JS heap vs heap limit. Chromium only."
|
? language.t("debugBar.mem.tipUnavailable")
|
||||||
: `Used JS heap vs heap limit. ${mb(state.heap.used)} of ${mb(state.heap.limit)}.`
|
: language.t("debugBar.mem.tip", {
|
||||||
|
used: mb(state.heap.used) ?? na(),
|
||||||
|
limit: mb(state.heap.limit) ?? na(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
value={heapv()}
|
value={heapv()}
|
||||||
bad={bad(heap(), 0.8)}
|
bad={bad(heap(), 0.8)}
|
||||||
|
|||||||
@@ -426,7 +426,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Show when={item.keybind}>
|
<Show when={item.keybind}>
|
||||||
<Keybind class="rounded-[4px]">{formatKeybind(item.keybind ?? "")}</Keybind>
|
<Keybind class="rounded-[4px]">{formatKeybind(item.keybind ?? "", language.t)}</Keybind>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ function ServerForm(props: ServerFormProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="px-5">
|
<div class="px-5">
|
||||||
<div class="bg-surface-raised-base rounded-md p-5 flex flex-col gap-3">
|
<div class="bg-surface-base rounded-md p-5 flex flex-col gap-3">
|
||||||
<div class="flex-1 min-w-0 [&_[data-slot=input-wrapper]]:relative">
|
<div class="flex-1 min-w-0 [&_[data-slot=input-wrapper]]:relative">
|
||||||
<TextField
|
<TextField
|
||||||
type="text"
|
type="text"
|
||||||
@@ -149,7 +149,7 @@ function ServerForm(props: ServerFormProps) {
|
|||||||
<TextField
|
<TextField
|
||||||
type="text"
|
type="text"
|
||||||
label={language.t("dialog.server.add.username")}
|
label={language.t("dialog.server.add.username")}
|
||||||
placeholder="username"
|
placeholder={language.t("dialog.server.add.usernamePlaceholder")}
|
||||||
value={props.username}
|
value={props.username}
|
||||||
disabled={props.busy}
|
disabled={props.busy}
|
||||||
onChange={props.onUsernameChange}
|
onChange={props.onUsernameChange}
|
||||||
@@ -158,7 +158,7 @@ function ServerForm(props: ServerFormProps) {
|
|||||||
<TextField
|
<TextField
|
||||||
type="password"
|
type="password"
|
||||||
label={language.t("dialog.server.add.password")}
|
label={language.t("dialog.server.add.password")}
|
||||||
placeholder="password"
|
placeholder={language.t("dialog.server.add.passwordPlaceholder")}
|
||||||
value={props.password}
|
value={props.password}
|
||||||
disabled={props.busy}
|
disabled={props.busy}
|
||||||
onChange={props.onPasswordChange}
|
onChange={props.onPasswordChange}
|
||||||
@@ -542,7 +542,7 @@ export function DialogSelectServer() {
|
|||||||
if (x) select(x)
|
if (x) select(x)
|
||||||
}}
|
}}
|
||||||
divider={true}
|
divider={true}
|
||||||
class="px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-raised-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:min-h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent"
|
class="px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:min-h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent"
|
||||||
>
|
>
|
||||||
{(i) => {
|
{(i) => {
|
||||||
const key = ServerConnection.key(i)
|
const key = ServerConnection.key(i)
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
|||||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Select } from "@opencode-ai/ui/select"
|
import { Select } from "@opencode-ai/ui/select"
|
||||||
import { RadioGroup } from "@opencode-ai/ui/radio-group"
|
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { ModelSelectorPopover } from "@/components/dialog-select-model"
|
import { ModelSelectorPopover } from "@/components/dialog-select-model"
|
||||||
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
|
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
|
||||||
@@ -39,7 +38,8 @@ import { usePlatform } from "@/context/platform"
|
|||||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||||
import { createSessionTabs } from "@/pages/session/helpers"
|
import { createSessionTabs } from "@/pages/session/helpers"
|
||||||
import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom"
|
import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom"
|
||||||
import { createPromptAttachments, ACCEPTED_FILE_TYPES } from "./prompt-input/attachments"
|
import { createPromptAttachments } from "./prompt-input/attachments"
|
||||||
|
import { ACCEPTED_FILE_TYPES } from "./prompt-input/files"
|
||||||
import {
|
import {
|
||||||
canNavigateHistoryAtCursor,
|
canNavigateHistoryAtCursor,
|
||||||
navigatePromptHistory,
|
navigatePromptHistory,
|
||||||
@@ -49,7 +49,7 @@ import {
|
|||||||
type PromptHistoryStoredEntry,
|
type PromptHistoryStoredEntry,
|
||||||
promptLength,
|
promptLength,
|
||||||
} from "./prompt-input/history"
|
} from "./prompt-input/history"
|
||||||
import { createPromptSubmit } from "./prompt-input/submit"
|
import { createPromptSubmit, type FollowupDraft } from "./prompt-input/submit"
|
||||||
import { PromptPopover, type AtOption, type SlashCommand } from "./prompt-input/slash-popover"
|
import { PromptPopover, type AtOption, type SlashCommand } from "./prompt-input/slash-popover"
|
||||||
import { PromptContextItems } from "./prompt-input/context-items"
|
import { PromptContextItems } from "./prompt-input/context-items"
|
||||||
import { PromptImageAttachments } from "./prompt-input/image-attachments"
|
import { PromptImageAttachments } from "./prompt-input/image-attachments"
|
||||||
@@ -62,6 +62,11 @@ interface PromptInputProps {
|
|||||||
ref?: (el: HTMLDivElement) => void
|
ref?: (el: HTMLDivElement) => void
|
||||||
newSessionWorktree?: string
|
newSessionWorktree?: string
|
||||||
onNewSessionWorktreeReset?: () => void
|
onNewSessionWorktreeReset?: () => void
|
||||||
|
edit?: { id: string; prompt: Prompt; context: FollowupDraft["context"] }
|
||||||
|
onEditLoaded?: () => void
|
||||||
|
shouldQueue?: () => boolean
|
||||||
|
onQueue?: (draft: FollowupDraft) => void
|
||||||
|
onAbort?: () => void
|
||||||
onSubmit?: () => void
|
onSubmit?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +121,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
let slashPopoverRef!: HTMLDivElement
|
let slashPopoverRef!: HTMLDivElement
|
||||||
|
|
||||||
const mirror = { input: false }
|
const mirror = { input: false }
|
||||||
const inset = 44
|
const inset = 52
|
||||||
|
const space = `${inset}px`
|
||||||
|
|
||||||
const scrollCursorIntoView = () => {
|
const scrollCursorIntoView = () => {
|
||||||
const container = scrollRef
|
const container = scrollRef
|
||||||
@@ -151,8 +157,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const queueScroll = () => {
|
const queueScroll = (count = 2) => {
|
||||||
requestAnimationFrame(scrollCursorIntoView)
|
requestAnimationFrame(() => {
|
||||||
|
scrollCursorIntoView()
|
||||||
|
if (count > 1) queueScroll(count - 1)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeFileTab = createSessionTabs({
|
const activeFileTab = createSessionTabs({
|
||||||
@@ -948,6 +957,45 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
setCurrentHistory("entries", next)
|
setCurrentHistory("entries", next)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
() => props.edit?.id,
|
||||||
|
(id) => {
|
||||||
|
const edit = props.edit
|
||||||
|
if (!id || !edit) return
|
||||||
|
|
||||||
|
for (const item of prompt.context.items()) {
|
||||||
|
prompt.context.remove(item.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of edit.context) {
|
||||||
|
prompt.context.add({
|
||||||
|
type: item.type,
|
||||||
|
path: item.path,
|
||||||
|
selection: item.selection,
|
||||||
|
comment: item.comment,
|
||||||
|
commentID: item.commentID,
|
||||||
|
commentOrigin: item.commentOrigin,
|
||||||
|
preview: item.preview,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setStore("mode", "normal")
|
||||||
|
setStore("popover", null)
|
||||||
|
setStore("historyIndex", -1)
|
||||||
|
setStore("savedPrompt", null)
|
||||||
|
prompt.set(edit.prompt, promptLength(edit.prompt))
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
editorRef.focus()
|
||||||
|
setCursorPosition(editorRef, promptLength(edit.prompt))
|
||||||
|
queueScroll()
|
||||||
|
})
|
||||||
|
props.onEditLoaded?.()
|
||||||
|
},
|
||||||
|
{ defer: true },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
const navigateHistory = (direction: "up" | "down") => {
|
const navigateHistory = (direction: "up" | "down") => {
|
||||||
const result = navigatePromptHistory({
|
const result = navigatePromptHistory({
|
||||||
direction,
|
direction,
|
||||||
@@ -964,7 +1012,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const { addImageAttachment, removeImageAttachment, handlePaste } = createPromptAttachments({
|
const { addAttachment, removeAttachment, handlePaste } = createPromptAttachments({
|
||||||
editor: () => editorRef,
|
editor: () => editorRef,
|
||||||
isFocused,
|
isFocused,
|
||||||
isDialogActive: () => !!dialog.active,
|
isDialogActive: () => !!dialog.active,
|
||||||
@@ -1002,6 +1050,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
setPopover: (popover) => setStore("popover", popover),
|
setPopover: (popover) => setStore("popover", popover),
|
||||||
newSessionWorktree: () => props.newSessionWorktree,
|
newSessionWorktree: () => props.newSessionWorktree,
|
||||||
onNewSessionWorktreeReset: props.onNewSessionWorktreeReset,
|
onNewSessionWorktreeReset: props.onNewSessionWorktreeReset,
|
||||||
|
shouldQueue: props.shouldQueue,
|
||||||
|
onQueue: props.onQueue,
|
||||||
|
onAbort: props.onAbort,
|
||||||
onSubmit: props.onSubmit,
|
onSubmit: props.onSubmit,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1201,7 +1252,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
onOpen={(attachment) =>
|
onOpen={(attachment) =>
|
||||||
dialog.show(() => <ImagePreview src={attachment.dataUrl} alt={attachment.filename} />)
|
dialog.show(() => <ImagePreview src={attachment.dataUrl} alt={attachment.filename} />)
|
||||||
}
|
}
|
||||||
onRemove={removeImageAttachment}
|
onRemove={removeAttachment}
|
||||||
removeLabel={language.t("prompt.attachment.remove")}
|
removeLabel={language.t("prompt.attachment.remove")}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
@@ -1219,7 +1270,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
editorRef?.focus()
|
editorRef?.focus()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="relative max-h-[240px] overflow-y-auto no-scrollbar" ref={(el) => (scrollRef = el)}>
|
<div
|
||||||
|
class="relative max-h-[240px] overflow-y-auto no-scrollbar"
|
||||||
|
ref={(el) => (scrollRef = el)}
|
||||||
|
style={{ "scroll-padding-bottom": space }}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
data-component="prompt-input"
|
data-component="prompt-input"
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
@@ -1241,22 +1296,34 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
classList={{
|
classList={{
|
||||||
"select-text": true,
|
"select-text": true,
|
||||||
"w-full pl-3 pr-2 pt-2 pb-11 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true,
|
"w-full pl-3 pr-2 pt-2 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true,
|
||||||
"[&_[data-type=file]]:text-syntax-property": true,
|
"[&_[data-type=file]]:text-syntax-property": true,
|
||||||
"[&_[data-type=agent]]:text-syntax-type": true,
|
"[&_[data-type=agent]]:text-syntax-type": true,
|
||||||
"font-mono!": store.mode === "shell",
|
"font-mono!": store.mode === "shell",
|
||||||
}}
|
}}
|
||||||
|
style={{ "padding-bottom": space }}
|
||||||
/>
|
/>
|
||||||
<Show when={!prompt.dirty()}>
|
<Show when={!prompt.dirty()}>
|
||||||
<div
|
<div
|
||||||
class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 pb-11 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate"
|
class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate"
|
||||||
classList={{ "font-mono!": store.mode === "shell" }}
|
classList={{ "font-mono!": store.mode === "shell" }}
|
||||||
|
style={{ "padding-bottom": space }}
|
||||||
>
|
>
|
||||||
{placeholder()}
|
{placeholder()}
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="pointer-events-none absolute inset-x-0 bottom-0"
|
||||||
|
style={{
|
||||||
|
height: space,
|
||||||
|
background:
|
||||||
|
"linear-gradient(to top, var(--surface-raised-stronger-non-alpha) calc(100% - 20px), transparent)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="pointer-events-none absolute bottom-2 right-2 flex items-center gap-2">
|
<div class="pointer-events-none absolute bottom-2 right-2 flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
@@ -1265,7 +1332,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
class="hidden"
|
class="hidden"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const file = e.currentTarget.files?.[0]
|
const file = e.currentTarget.files?.[0]
|
||||||
if (file) addImageAttachment(file)
|
if (file) void addAttachment(file)
|
||||||
e.currentTarget.value = ""
|
e.currentTarget.value = ""
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -1488,36 +1555,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="shrink-0">
|
|
||||||
<RadioGroup
|
|
||||||
options={["shell", "normal"] as const}
|
|
||||||
current={store.mode}
|
|
||||||
value={(mode) => mode}
|
|
||||||
label={(mode) => (
|
|
||||||
<TooltipKeybind
|
|
||||||
placement="top"
|
|
||||||
gutter={4}
|
|
||||||
openDelay={2000}
|
|
||||||
title={language.t(mode === "shell" ? "prompt.mode.shell" : "prompt.mode.normal")}
|
|
||||||
keybind={command.keybind(mode === "shell" ? "prompt.mode.shell" : "prompt.mode.normal")}
|
|
||||||
class="size-full flex items-center justify-center"
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
name={mode === "shell" ? "console" : "prompt"}
|
|
||||||
class="size-[18px]"
|
|
||||||
classList={{
|
|
||||||
"text-icon-strong-base": store.mode === mode,
|
|
||||||
"text-icon-weak": store.mode !== mode,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TooltipKeybind>
|
|
||||||
)}
|
|
||||||
onSelect={(mode) => mode && setMode(mode)}
|
|
||||||
fill
|
|
||||||
pad="none"
|
|
||||||
class="w-[68px]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</DockTray>
|
</DockTray>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { attachmentMime } from "./files"
|
||||||
|
|
||||||
|
describe("attachmentMime", () => {
|
||||||
|
test("keeps PDFs when the browser reports the mime", async () => {
|
||||||
|
const file = new File(["%PDF-1.7"], "guide.pdf", { type: "application/pdf" })
|
||||||
|
expect(await attachmentMime(file)).toBe("application/pdf")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("normalizes structured text types to text/plain", async () => {
|
||||||
|
const file = new File(['{"ok":true}\n'], "data.json", { type: "application/json" })
|
||||||
|
expect(await attachmentMime(file)).toBe("text/plain")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("accepts text files even with a misleading browser mime", async () => {
|
||||||
|
const file = new File(["export const x = 1\n"], "main.ts", { type: "video/mp2t" })
|
||||||
|
expect(await attachmentMime(file)).toBe("text/plain")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects binary files", async () => {
|
||||||
|
const file = new File([Uint8Array.of(0, 255, 1, 2)], "blob.bin", { type: "application/octet-stream" })
|
||||||
|
expect(await attachmentMime(file)).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -4,12 +4,27 @@ import { usePrompt, type ContentPart, type ImageAttachmentPart } from "@/context
|
|||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { uuid } from "@/utils/uuid"
|
import { uuid } from "@/utils/uuid"
|
||||||
import { getCursorPosition } from "./editor-dom"
|
import { getCursorPosition } from "./editor-dom"
|
||||||
|
import { attachmentMime } from "./files"
|
||||||
export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
|
|
||||||
export const ACCEPTED_FILE_TYPES = [...ACCEPTED_IMAGE_TYPES, "application/pdf"]
|
|
||||||
const LARGE_PASTE_CHARS = 8000
|
const LARGE_PASTE_CHARS = 8000
|
||||||
const LARGE_PASTE_BREAKS = 120
|
const LARGE_PASTE_BREAKS = 120
|
||||||
|
|
||||||
|
function dataUrl(file: File, mime: string) {
|
||||||
|
return new Promise<string>((resolve) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.addEventListener("error", () => resolve(""))
|
||||||
|
reader.addEventListener("load", () => {
|
||||||
|
const value = typeof reader.result === "string" ? reader.result : ""
|
||||||
|
const idx = value.indexOf(",")
|
||||||
|
if (idx === -1) {
|
||||||
|
resolve(value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resolve(`data:${mime};base64,${value.slice(idx + 1)}`)
|
||||||
|
})
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function largePaste(text: string) {
|
function largePaste(text: string) {
|
||||||
if (text.length >= LARGE_PASTE_CHARS) return true
|
if (text.length >= LARGE_PASTE_CHARS) return true
|
||||||
let breaks = 0
|
let breaks = 0
|
||||||
@@ -35,28 +50,41 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
|||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
|
||||||
const addImageAttachment = async (file: File) => {
|
const warn = () => {
|
||||||
if (!ACCEPTED_FILE_TYPES.includes(file.type)) return
|
showToast({
|
||||||
|
title: language.t("prompt.toast.pasteUnsupported.title"),
|
||||||
const reader = new FileReader()
|
description: language.t("prompt.toast.pasteUnsupported.description"),
|
||||||
reader.onload = () => {
|
})
|
||||||
const editor = input.editor()
|
|
||||||
if (!editor) return
|
|
||||||
const dataUrl = reader.result as string
|
|
||||||
const attachment: ImageAttachmentPart = {
|
|
||||||
type: "image",
|
|
||||||
id: uuid(),
|
|
||||||
filename: file.name,
|
|
||||||
mime: file.type,
|
|
||||||
dataUrl,
|
|
||||||
}
|
|
||||||
const cursorPosition = prompt.cursor() ?? getCursorPosition(editor)
|
|
||||||
prompt.set([...prompt.current(), attachment], cursorPosition)
|
|
||||||
}
|
|
||||||
reader.readAsDataURL(file)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeImageAttachment = (id: string) => {
|
const add = async (file: File, toast = true) => {
|
||||||
|
const mime = await attachmentMime(file)
|
||||||
|
if (!mime) {
|
||||||
|
if (toast) warn()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const editor = input.editor()
|
||||||
|
if (!editor) return false
|
||||||
|
|
||||||
|
const url = await dataUrl(file, mime)
|
||||||
|
if (!url) return false
|
||||||
|
|
||||||
|
const attachment: ImageAttachmentPart = {
|
||||||
|
type: "image",
|
||||||
|
id: uuid(),
|
||||||
|
filename: file.name,
|
||||||
|
mime,
|
||||||
|
dataUrl: url,
|
||||||
|
}
|
||||||
|
const cursor = prompt.cursor() ?? getCursorPosition(editor)
|
||||||
|
prompt.set([...prompt.current(), attachment], cursor)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const addAttachment = (file: File) => add(file)
|
||||||
|
|
||||||
|
const removeAttachment = (id: string) => {
|
||||||
const current = prompt.current()
|
const current = prompt.current()
|
||||||
const next = current.filter((part) => part.type !== "image" || part.id !== id)
|
const next = current.filter((part) => part.type !== "image" || part.id !== id)
|
||||||
prompt.set(next, prompt.cursor())
|
prompt.set(next, prompt.cursor())
|
||||||
@@ -72,21 +100,16 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
|||||||
|
|
||||||
const items = Array.from(clipboardData.items)
|
const items = Array.from(clipboardData.items)
|
||||||
const fileItems = items.filter((item) => item.kind === "file")
|
const fileItems = items.filter((item) => item.kind === "file")
|
||||||
const imageItems = fileItems.filter((item) => ACCEPTED_FILE_TYPES.includes(item.type))
|
|
||||||
|
|
||||||
if (imageItems.length > 0) {
|
|
||||||
for (const item of imageItems) {
|
|
||||||
const file = item.getAsFile()
|
|
||||||
if (file) await addImageAttachment(file)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileItems.length > 0) {
|
if (fileItems.length > 0) {
|
||||||
showToast({
|
let found = false
|
||||||
title: language.t("prompt.toast.pasteUnsupported.title"),
|
for (const item of fileItems) {
|
||||||
description: language.t("prompt.toast.pasteUnsupported.description"),
|
const file = item.getAsFile()
|
||||||
})
|
if (!file) continue
|
||||||
|
const ok = await add(file, false)
|
||||||
|
if (ok) found = true
|
||||||
|
}
|
||||||
|
if (!found) warn()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +119,7 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
|||||||
if (input.readClipboardImage && !plainText) {
|
if (input.readClipboardImage && !plainText) {
|
||||||
const file = await input.readClipboardImage()
|
const file = await input.readClipboardImage()
|
||||||
if (file) {
|
if (file) {
|
||||||
await addImageAttachment(file)
|
await addAttachment(file)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,11 +176,12 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
|||||||
const dropped = event.dataTransfer?.files
|
const dropped = event.dataTransfer?.files
|
||||||
if (!dropped) return
|
if (!dropped) return
|
||||||
|
|
||||||
|
let found = false
|
||||||
for (const file of Array.from(dropped)) {
|
for (const file of Array.from(dropped)) {
|
||||||
if (ACCEPTED_FILE_TYPES.includes(file.type)) {
|
const ok = await add(file, false)
|
||||||
await addImageAttachment(file)
|
if (ok) found = true
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (!found && dropped.length > 0) warn()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@@ -173,8 +197,8 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
addImageAttachment,
|
addAttachment,
|
||||||
removeImageAttachment,
|
removeAttachment,
|
||||||
handlePaste,
|
handlePaste,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
|
||||||
|
|
||||||
|
const IMAGE_MIMES = new Set(ACCEPTED_IMAGE_TYPES)
|
||||||
|
const IMAGE_EXTS = new Map([
|
||||||
|
["gif", "image/gif"],
|
||||||
|
["jpeg", "image/jpeg"],
|
||||||
|
["jpg", "image/jpeg"],
|
||||||
|
["png", "image/png"],
|
||||||
|
["webp", "image/webp"],
|
||||||
|
])
|
||||||
|
const TEXT_MIMES = new Set([
|
||||||
|
"application/json",
|
||||||
|
"application/ld+json",
|
||||||
|
"application/toml",
|
||||||
|
"application/x-toml",
|
||||||
|
"application/x-yaml",
|
||||||
|
"application/xml",
|
||||||
|
"application/yaml",
|
||||||
|
])
|
||||||
|
|
||||||
|
export const ACCEPTED_FILE_TYPES = [
|
||||||
|
...ACCEPTED_IMAGE_TYPES,
|
||||||
|
"application/pdf",
|
||||||
|
"text/*",
|
||||||
|
"application/json",
|
||||||
|
"application/ld+json",
|
||||||
|
"application/toml",
|
||||||
|
"application/x-toml",
|
||||||
|
"application/x-yaml",
|
||||||
|
"application/xml",
|
||||||
|
"application/yaml",
|
||||||
|
".c",
|
||||||
|
".cc",
|
||||||
|
".cjs",
|
||||||
|
".conf",
|
||||||
|
".cpp",
|
||||||
|
".css",
|
||||||
|
".csv",
|
||||||
|
".cts",
|
||||||
|
".env",
|
||||||
|
".go",
|
||||||
|
".gql",
|
||||||
|
".graphql",
|
||||||
|
".h",
|
||||||
|
".hh",
|
||||||
|
".hpp",
|
||||||
|
".htm",
|
||||||
|
".html",
|
||||||
|
".ini",
|
||||||
|
".java",
|
||||||
|
".js",
|
||||||
|
".json",
|
||||||
|
".jsx",
|
||||||
|
".log",
|
||||||
|
".md",
|
||||||
|
".mdx",
|
||||||
|
".mjs",
|
||||||
|
".mts",
|
||||||
|
".py",
|
||||||
|
".rb",
|
||||||
|
".rs",
|
||||||
|
".sass",
|
||||||
|
".scss",
|
||||||
|
".sh",
|
||||||
|
".sql",
|
||||||
|
".toml",
|
||||||
|
".ts",
|
||||||
|
".tsx",
|
||||||
|
".txt",
|
||||||
|
".xml",
|
||||||
|
".yaml",
|
||||||
|
".yml",
|
||||||
|
".zsh",
|
||||||
|
]
|
||||||
|
|
||||||
|
const SAMPLE = 4096
|
||||||
|
|
||||||
|
function kind(type: string) {
|
||||||
|
return type.split(";", 1)[0]?.trim().toLowerCase() ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function ext(name: string) {
|
||||||
|
const idx = name.lastIndexOf(".")
|
||||||
|
if (idx === -1) return ""
|
||||||
|
return name.slice(idx + 1).toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
function textMime(type: string) {
|
||||||
|
if (!type) return false
|
||||||
|
if (type.startsWith("text/")) return true
|
||||||
|
if (TEXT_MIMES.has(type)) return true
|
||||||
|
if (type.endsWith("+json")) return true
|
||||||
|
return type.endsWith("+xml")
|
||||||
|
}
|
||||||
|
|
||||||
|
function textBytes(bytes: Uint8Array) {
|
||||||
|
if (bytes.length === 0) return true
|
||||||
|
let count = 0
|
||||||
|
for (const byte of bytes) {
|
||||||
|
if (byte === 0) return false
|
||||||
|
if (byte < 9 || (byte > 13 && byte < 32)) count += 1
|
||||||
|
}
|
||||||
|
return count / bytes.length <= 0.3
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function attachmentMime(file: File) {
|
||||||
|
const type = kind(file.type)
|
||||||
|
if (IMAGE_MIMES.has(type)) return type
|
||||||
|
if (type === "application/pdf") return type
|
||||||
|
|
||||||
|
const suffix = ext(file.name)
|
||||||
|
const fallback = IMAGE_EXTS.get(suffix) ?? (suffix === "pdf" ? "application/pdf" : undefined)
|
||||||
|
if ((!type || type === "application/octet-stream") && fallback) return fallback
|
||||||
|
|
||||||
|
if (textMime(type)) return "text/plain"
|
||||||
|
const bytes = new Uint8Array(await file.slice(0, SAMPLE).arrayBuffer())
|
||||||
|
if (!textBytes(bytes)) return
|
||||||
|
return "text/plain"
|
||||||
|
}
|
||||||
@@ -7,12 +7,16 @@ const createdClients: string[] = []
|
|||||||
const createdSessions: string[] = []
|
const createdSessions: string[] = []
|
||||||
const enabledAutoAccept: Array<{ sessionID: string; directory: string }> = []
|
const enabledAutoAccept: Array<{ sessionID: string; directory: string }> = []
|
||||||
const optimistic: Array<{
|
const optimistic: Array<{
|
||||||
|
directory?: string
|
||||||
|
sessionID?: string
|
||||||
message: {
|
message: {
|
||||||
agent: string
|
agent: string
|
||||||
model: { providerID: string; modelID: string }
|
model: { providerID: string; modelID: string }
|
||||||
variant?: string
|
variant?: string
|
||||||
}
|
}
|
||||||
}> = []
|
}> = []
|
||||||
|
const optimisticSeeded: boolean[] = []
|
||||||
|
const storedSessions: Record<string, Array<{ id: string; title?: string }>> = {}
|
||||||
const sentShell: string[] = []
|
const sentShell: string[] = []
|
||||||
const syncedDirectories: string[] = []
|
const syncedDirectories: string[] = []
|
||||||
|
|
||||||
@@ -28,7 +32,12 @@ const clientFor = (directory: string) => {
|
|||||||
session: {
|
session: {
|
||||||
create: async () => {
|
create: async () => {
|
||||||
createdSessions.push(directory)
|
createdSessions.push(directory)
|
||||||
return { data: { id: `session-${createdSessions.length}` } }
|
return {
|
||||||
|
data: {
|
||||||
|
id: `session-${createdSessions.length}`,
|
||||||
|
title: `New session ${createdSessions.length}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
},
|
},
|
||||||
shell: async () => {
|
shell: async () => {
|
||||||
sentShell.push(directory)
|
sentShell.push(directory)
|
||||||
@@ -129,9 +138,16 @@ beforeAll(async () => {
|
|||||||
session: {
|
session: {
|
||||||
optimistic: {
|
optimistic: {
|
||||||
add: (value: {
|
add: (value: {
|
||||||
|
directory?: string
|
||||||
|
sessionID?: string
|
||||||
message: { agent: string; model: { providerID: string; modelID: string }; variant?: string }
|
message: { agent: string; model: { providerID: string; modelID: string }; variant?: string }
|
||||||
}) => {
|
}) => {
|
||||||
optimistic.push(value)
|
optimistic.push(value)
|
||||||
|
optimisticSeeded.push(
|
||||||
|
!!value.directory &&
|
||||||
|
!!value.sessionID &&
|
||||||
|
!!storedSessions[value.directory]?.find((item) => item.id === value.sessionID)?.title,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
remove: () => undefined,
|
remove: () => undefined,
|
||||||
},
|
},
|
||||||
@@ -144,7 +160,21 @@ beforeAll(async () => {
|
|||||||
useGlobalSync: () => ({
|
useGlobalSync: () => ({
|
||||||
child: (directory: string) => {
|
child: (directory: string) => {
|
||||||
syncedDirectories.push(directory)
|
syncedDirectories.push(directory)
|
||||||
return [{}, () => undefined]
|
storedSessions[directory] ??= []
|
||||||
|
return [
|
||||||
|
{ session: storedSessions[directory] },
|
||||||
|
(...args: unknown[]) => {
|
||||||
|
if (args[0] !== "session") return
|
||||||
|
const next = args[1]
|
||||||
|
if (typeof next === "function") {
|
||||||
|
storedSessions[directory] = next(storedSessions[directory]) as Array<{ id: string; title?: string }>
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (Array.isArray(next)) {
|
||||||
|
storedSessions[directory] = next as Array<{ id: string; title?: string }>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
@@ -170,11 +200,13 @@ beforeEach(() => {
|
|||||||
createdSessions.length = 0
|
createdSessions.length = 0
|
||||||
enabledAutoAccept.length = 0
|
enabledAutoAccept.length = 0
|
||||||
optimistic.length = 0
|
optimistic.length = 0
|
||||||
|
optimisticSeeded.length = 0
|
||||||
params = {}
|
params = {}
|
||||||
sentShell.length = 0
|
sentShell.length = 0
|
||||||
syncedDirectories.length = 0
|
syncedDirectories.length = 0
|
||||||
selected = "/repo/worktree-a"
|
selected = "/repo/worktree-a"
|
||||||
variant = undefined
|
variant = undefined
|
||||||
|
for (const key of Object.keys(storedSessions)) delete storedSessions[key]
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("prompt submit worktree selection", () => {
|
describe("prompt submit worktree selection", () => {
|
||||||
@@ -207,7 +239,7 @@ describe("prompt submit worktree selection", () => {
|
|||||||
expect(createdClients).toEqual(["/repo/worktree-a", "/repo/worktree-b"])
|
expect(createdClients).toEqual(["/repo/worktree-a", "/repo/worktree-b"])
|
||||||
expect(createdSessions).toEqual(["/repo/worktree-a", "/repo/worktree-b"])
|
expect(createdSessions).toEqual(["/repo/worktree-a", "/repo/worktree-b"])
|
||||||
expect(sentShell).toEqual(["/repo/worktree-a", "/repo/worktree-b"])
|
expect(sentShell).toEqual(["/repo/worktree-a", "/repo/worktree-b"])
|
||||||
expect(syncedDirectories).toEqual(["/repo/worktree-a", "/repo/worktree-b"])
|
expect(syncedDirectories).toEqual(["/repo/worktree-a", "/repo/worktree-a", "/repo/worktree-b", "/repo/worktree-b"])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("applies auto-accept to newly created sessions", async () => {
|
test("applies auto-accept to newly created sessions", async () => {
|
||||||
@@ -271,4 +303,32 @@ describe("prompt submit worktree selection", () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("seeds new sessions before optimistic prompts are added", async () => {
|
||||||
|
const submit = createPromptSubmit({
|
||||||
|
info: () => undefined,
|
||||||
|
imageAttachments: () => [],
|
||||||
|
commentCount: () => 0,
|
||||||
|
autoAccept: () => false,
|
||||||
|
mode: () => "normal",
|
||||||
|
working: () => false,
|
||||||
|
editor: () => undefined,
|
||||||
|
queueScroll: () => undefined,
|
||||||
|
promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0),
|
||||||
|
addToHistory: () => undefined,
|
||||||
|
resetHistoryNavigation: () => undefined,
|
||||||
|
setMode: () => undefined,
|
||||||
|
setPopover: () => undefined,
|
||||||
|
newSessionWorktree: () => selected,
|
||||||
|
onNewSessionWorktreeReset: () => undefined,
|
||||||
|
onSubmit: () => undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
const event = { preventDefault: () => undefined } as unknown as Event
|
||||||
|
|
||||||
|
await submit.handleSubmit(event)
|
||||||
|
|
||||||
|
expect(storedSessions["/repo/worktree-a"]).toEqual([{ id: "session-1", title: "New session 1" }])
|
||||||
|
expect(optimisticSeeded).toEqual([true])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Message } from "@opencode-ai/sdk/v2/client"
|
import type { Message, Session } from "@opencode-ai/sdk/v2/client"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { base64Encode } from "@opencode-ai/util/encode"
|
import { base64Encode } from "@opencode-ai/util/encode"
|
||||||
|
import { Binary } from "@opencode-ai/util/binary"
|
||||||
import { useNavigate, useParams } from "@solidjs/router"
|
import { useNavigate, useParams } from "@solidjs/router"
|
||||||
import type { Accessor } from "solid-js"
|
import type { Accessor } from "solid-js"
|
||||||
import type { FileSelection } from "@/context/file"
|
import type { FileSelection } from "@/context/file"
|
||||||
@@ -9,7 +10,7 @@ import { useLanguage } from "@/context/language"
|
|||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { useLocal } from "@/context/local"
|
import { useLocal } from "@/context/local"
|
||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
import { type ImageAttachmentPart, type Prompt, usePrompt } from "@/context/prompt"
|
import { type ContextItem, type ImageAttachmentPart, type Prompt, usePrompt } from "@/context/prompt"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { Identifier } from "@/utils/id"
|
import { Identifier } from "@/utils/id"
|
||||||
@@ -25,6 +26,145 @@ type PendingPrompt = {
|
|||||||
|
|
||||||
const pending = new Map<string, PendingPrompt>()
|
const pending = new Map<string, PendingPrompt>()
|
||||||
|
|
||||||
|
export type FollowupDraft = {
|
||||||
|
sessionID: string
|
||||||
|
sessionDirectory: string
|
||||||
|
prompt: Prompt
|
||||||
|
context: (ContextItem & { key: string })[]
|
||||||
|
agent: string
|
||||||
|
model: { providerID: string; modelID: string }
|
||||||
|
variant?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type FollowupSendInput = {
|
||||||
|
client: ReturnType<typeof useSDK>["client"]
|
||||||
|
globalSync: ReturnType<typeof useGlobalSync>
|
||||||
|
sync: ReturnType<typeof useSync>
|
||||||
|
draft: FollowupDraft
|
||||||
|
messageID?: string
|
||||||
|
optimisticBusy?: boolean
|
||||||
|
before?: () => Promise<boolean> | boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const draftText = (prompt: Prompt) => prompt.map((part) => ("content" in part ? part.content : "")).join("")
|
||||||
|
|
||||||
|
const draftImages = (prompt: Prompt) => prompt.filter((part): part is ImageAttachmentPart => part.type === "image")
|
||||||
|
|
||||||
|
export async function sendFollowupDraft(input: FollowupSendInput) {
|
||||||
|
const text = draftText(input.draft.prompt)
|
||||||
|
const images = draftImages(input.draft.prompt)
|
||||||
|
const [, setStore] = input.globalSync.child(input.draft.sessionDirectory)
|
||||||
|
|
||||||
|
const setBusy = () => {
|
||||||
|
if (!input.optimisticBusy) return
|
||||||
|
setStore("session_status", input.draft.sessionID, { type: "busy" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const setIdle = () => {
|
||||||
|
if (!input.optimisticBusy) return
|
||||||
|
setStore("session_status", input.draft.sessionID, { type: "idle" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const wait = async () => {
|
||||||
|
const ok = await input.before?.()
|
||||||
|
if (ok === false) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const [head, ...tail] = text.split(" ")
|
||||||
|
const cmd = head?.startsWith("/") ? head.slice(1) : undefined
|
||||||
|
if (cmd && input.sync.data.command.find((item) => item.name === cmd)) {
|
||||||
|
setBusy()
|
||||||
|
try {
|
||||||
|
if (!(await wait())) {
|
||||||
|
setIdle()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
await input.client.session.command({
|
||||||
|
sessionID: input.draft.sessionID,
|
||||||
|
command: cmd,
|
||||||
|
arguments: tail.join(" "),
|
||||||
|
agent: input.draft.agent,
|
||||||
|
model: `${input.draft.model.providerID}/${input.draft.model.modelID}`,
|
||||||
|
variant: input.draft.variant,
|
||||||
|
parts: images.map((attachment) => ({
|
||||||
|
id: Identifier.ascending("part"),
|
||||||
|
type: "file" as const,
|
||||||
|
mime: attachment.mime,
|
||||||
|
url: attachment.dataUrl,
|
||||||
|
filename: attachment.filename,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
setIdle()
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageID = input.messageID ?? Identifier.ascending("message")
|
||||||
|
const { requestParts, optimisticParts } = buildRequestParts({
|
||||||
|
prompt: input.draft.prompt,
|
||||||
|
context: input.draft.context,
|
||||||
|
images,
|
||||||
|
text,
|
||||||
|
sessionID: input.draft.sessionID,
|
||||||
|
messageID,
|
||||||
|
sessionDirectory: input.draft.sessionDirectory,
|
||||||
|
})
|
||||||
|
|
||||||
|
const message: Message = {
|
||||||
|
id: messageID,
|
||||||
|
sessionID: input.draft.sessionID,
|
||||||
|
role: "user",
|
||||||
|
time: { created: Date.now() },
|
||||||
|
agent: input.draft.agent,
|
||||||
|
model: input.draft.model,
|
||||||
|
variant: input.draft.variant,
|
||||||
|
}
|
||||||
|
|
||||||
|
const add = () =>
|
||||||
|
input.sync.session.optimistic.add({
|
||||||
|
directory: input.draft.sessionDirectory,
|
||||||
|
sessionID: input.draft.sessionID,
|
||||||
|
message,
|
||||||
|
parts: optimisticParts,
|
||||||
|
})
|
||||||
|
|
||||||
|
const remove = () =>
|
||||||
|
input.sync.session.optimistic.remove({
|
||||||
|
directory: input.draft.sessionDirectory,
|
||||||
|
sessionID: input.draft.sessionID,
|
||||||
|
messageID,
|
||||||
|
})
|
||||||
|
|
||||||
|
setBusy()
|
||||||
|
add()
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!(await wait())) {
|
||||||
|
setIdle()
|
||||||
|
remove()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
await input.client.session.promptAsync({
|
||||||
|
sessionID: input.draft.sessionID,
|
||||||
|
agent: input.draft.agent,
|
||||||
|
model: input.draft.model,
|
||||||
|
messageID,
|
||||||
|
parts: requestParts,
|
||||||
|
variant: input.draft.variant,
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
setIdle()
|
||||||
|
remove()
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type PromptSubmitInput = {
|
type PromptSubmitInput = {
|
||||||
info: Accessor<{ id: string } | undefined>
|
info: Accessor<{ id: string } | undefined>
|
||||||
imageAttachments: Accessor<ImageAttachmentPart[]>
|
imageAttachments: Accessor<ImageAttachmentPart[]>
|
||||||
@@ -41,6 +181,9 @@ type PromptSubmitInput = {
|
|||||||
setPopover: (popover: "at" | "slash" | null) => void
|
setPopover: (popover: "at" | "slash" | null) => void
|
||||||
newSessionWorktree?: Accessor<string | undefined>
|
newSessionWorktree?: Accessor<string | undefined>
|
||||||
onNewSessionWorktreeReset?: () => void
|
onNewSessionWorktreeReset?: () => void
|
||||||
|
shouldQueue?: Accessor<boolean>
|
||||||
|
onQueue?: (draft: FollowupDraft) => void
|
||||||
|
onAbort?: () => void
|
||||||
onSubmit?: () => void
|
onSubmit?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +225,8 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
const [, setStore] = globalSync.child(sdk.directory)
|
const [, setStore] = globalSync.child(sdk.directory)
|
||||||
setStore("todo", sessionID, [])
|
setStore("todo", sessionID, [])
|
||||||
|
|
||||||
|
input.onAbort?.()
|
||||||
|
|
||||||
const queued = pending.get(sessionID)
|
const queued = pending.get(sessionID)
|
||||||
if (queued) {
|
if (queued) {
|
||||||
queued.abort.abort()
|
queued.abort.abort()
|
||||||
@@ -116,6 +261,26 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clearContext = () => {
|
||||||
|
for (const item of prompt.context.items()) {
|
||||||
|
prompt.context.remove(item.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const seed = (dir: string, info: Session) => {
|
||||||
|
const [, setStore] = globalSync.child(dir)
|
||||||
|
setStore("session", (list: Session[]) => {
|
||||||
|
const result = Binary.search(list, info.id, (item) => item.id)
|
||||||
|
const next = [...list]
|
||||||
|
if (result.found) {
|
||||||
|
next[result.index] = info
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
next.splice(result.index, 0, info)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const handleSubmit = async (event: Event) => {
|
const handleSubmit = async (event: Event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
@@ -191,7 +356,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
|
|
||||||
let session = input.info()
|
let session = input.info()
|
||||||
if (!session && isNewSession) {
|
if (!session && isNewSession) {
|
||||||
session = await client.session
|
const created = await client.session
|
||||||
.create()
|
.create()
|
||||||
.then((x) => x.data ?? undefined)
|
.then((x) => x.data ?? undefined)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@@ -201,7 +366,9 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
})
|
})
|
||||||
return undefined
|
return undefined
|
||||||
})
|
})
|
||||||
if (session) {
|
if (created) {
|
||||||
|
seed(sessionDirectory, created)
|
||||||
|
session = created
|
||||||
if (shouldAutoAccept) permission.enableAutoAccept(session.id, sessionDirectory)
|
if (shouldAutoAccept) permission.enableAutoAccept(session.id, sessionDirectory)
|
||||||
layout.handoff.setTabs(base64Encode(sessionDirectory), session.id)
|
layout.handoff.setTabs(base64Encode(sessionDirectory), session.id)
|
||||||
navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`)
|
navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`)
|
||||||
@@ -215,14 +382,22 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
input.onSubmit?.()
|
|
||||||
|
|
||||||
const model = {
|
const model = {
|
||||||
modelID: currentModel.id,
|
modelID: currentModel.id,
|
||||||
providerID: currentModel.provider.id,
|
providerID: currentModel.provider.id,
|
||||||
}
|
}
|
||||||
const agent = currentAgent.name
|
const agent = currentAgent.name
|
||||||
const variant = local.model.variant.current()
|
const variant = local.model.variant.current()
|
||||||
|
const context = prompt.context.items().slice()
|
||||||
|
const draft: FollowupDraft = {
|
||||||
|
sessionID: session.id,
|
||||||
|
sessionDirectory,
|
||||||
|
prompt: currentPrompt,
|
||||||
|
context,
|
||||||
|
agent,
|
||||||
|
model,
|
||||||
|
variant,
|
||||||
|
}
|
||||||
|
|
||||||
const clearInput = () => {
|
const clearInput = () => {
|
||||||
prompt.reset()
|
prompt.reset()
|
||||||
@@ -243,6 +418,15 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isNewSession && mode === "normal" && input.shouldQueue?.()) {
|
||||||
|
input.onQueue?.(draft)
|
||||||
|
clearContext()
|
||||||
|
clearInput()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
input.onSubmit?.()
|
||||||
|
|
||||||
if (mode === "shell") {
|
if (mode === "shell") {
|
||||||
clearInput()
|
clearInput()
|
||||||
client.session
|
client.session
|
||||||
@@ -295,48 +479,19 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const context = prompt.context.items().slice()
|
|
||||||
const commentItems = context.filter((item) => item.type === "file" && !!item.comment?.trim())
|
const commentItems = context.filter((item) => item.type === "file" && !!item.comment?.trim())
|
||||||
|
|
||||||
const messageID = Identifier.ascending("message")
|
const messageID = Identifier.ascending("message")
|
||||||
const { requestParts, optimisticParts } = buildRequestParts({
|
|
||||||
prompt: currentPrompt,
|
|
||||||
context,
|
|
||||||
images,
|
|
||||||
text,
|
|
||||||
sessionID: session.id,
|
|
||||||
messageID,
|
|
||||||
sessionDirectory,
|
|
||||||
})
|
|
||||||
|
|
||||||
const optimisticMessage: Message = {
|
const removeOptimisticMessage = () => {
|
||||||
id: messageID,
|
|
||||||
sessionID: session.id,
|
|
||||||
role: "user",
|
|
||||||
time: { created: Date.now() },
|
|
||||||
agent,
|
|
||||||
model,
|
|
||||||
variant,
|
|
||||||
}
|
|
||||||
|
|
||||||
const addOptimisticMessage = () =>
|
|
||||||
sync.session.optimistic.add({
|
|
||||||
directory: sessionDirectory,
|
|
||||||
sessionID: session.id,
|
|
||||||
message: optimisticMessage,
|
|
||||||
parts: optimisticParts,
|
|
||||||
})
|
|
||||||
|
|
||||||
const removeOptimisticMessage = () =>
|
|
||||||
sync.session.optimistic.remove({
|
sync.session.optimistic.remove({
|
||||||
directory: sessionDirectory,
|
directory: sessionDirectory,
|
||||||
sessionID: session.id,
|
sessionID: session.id,
|
||||||
messageID,
|
messageID,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
removeCommentItems(commentItems)
|
removeCommentItems(commentItems)
|
||||||
clearInput()
|
clearInput()
|
||||||
addOptimisticMessage()
|
|
||||||
|
|
||||||
const waitForWorktree = async () => {
|
const waitForWorktree = async () => {
|
||||||
const worktree = WorktreeState.get(sessionDirectory)
|
const worktree = WorktreeState.get(sessionDirectory)
|
||||||
@@ -393,20 +548,15 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const send = async () => {
|
void sendFollowupDraft({
|
||||||
const ok = await waitForWorktree()
|
client,
|
||||||
if (!ok) return
|
sync,
|
||||||
await client.session.promptAsync({
|
globalSync,
|
||||||
sessionID: session.id,
|
draft,
|
||||||
agent,
|
messageID,
|
||||||
model,
|
optimisticBusy: sessionDirectory === projectDirectory,
|
||||||
messageID,
|
before: waitForWorktree,
|
||||||
parts: requestParts,
|
}).catch((err) => {
|
||||||
variant,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
void send().catch((err) => {
|
|
||||||
pending.delete(session.id)
|
pending.delete(session.id)
|
||||||
if (sessionDirectory === projectDirectory) {
|
if (sessionDirectory === projectDirectory) {
|
||||||
sync.set("session_status", session.id, { type: "idle" })
|
sync.set("session_status", session.id, { type: "idle" })
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
type ParentProps,
|
type ParentProps,
|
||||||
Show,
|
Show,
|
||||||
} from "solid-js"
|
} from "solid-js"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
import { type ServerConnection, serverName } from "@/context/server"
|
import { type ServerConnection, serverName } from "@/context/server"
|
||||||
import type { ServerHealth } from "@/utils/server-health"
|
import type { ServerHealth } from "@/utils/server-health"
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ interface ServerRowProps extends ParentProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ServerRow(props: ServerRowProps) {
|
export function ServerRow(props: ServerRowProps) {
|
||||||
|
const language = useLanguage()
|
||||||
const [truncated, setTruncated] = createSignal(false)
|
const [truncated, setTruncated] = createSignal(false)
|
||||||
let nameRef: HTMLSpanElement | undefined
|
let nameRef: HTMLSpanElement | undefined
|
||||||
let versionRef: HTMLSpanElement | undefined
|
let versionRef: HTMLSpanElement | undefined
|
||||||
@@ -65,22 +67,26 @@ export function ServerRow(props: ServerRowProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
class="flex-1"
|
class="flex-1 min-w-0"
|
||||||
value={tooltipValue()}
|
value={tooltipValue()}
|
||||||
|
contentStyle={{ "max-width": "none", "white-space": "nowrap" }}
|
||||||
placement="top-start"
|
placement="top-start"
|
||||||
inactive={!truncated() && !props.conn.displayName}
|
inactive={!truncated() && !props.conn.displayName}
|
||||||
>
|
>
|
||||||
<div class={props.class} classList={{ "opacity-50": props.dimmed }}>
|
<div class={props.class} classList={{ "opacity-50": props.dimmed }}>
|
||||||
<div class="flex flex-col items-start">
|
<div class="flex flex-col items-start min-w-0 w-full">
|
||||||
<div class="flex flex-row items-center gap-2">
|
<div class="flex flex-row items-center gap-2 min-w-0 w-full">
|
||||||
<span ref={nameRef} class={props.nameClass ?? "truncate"}>
|
<span ref={nameRef} class={`${props.nameClass ?? "truncate"} min-w-0`}>
|
||||||
{name()}
|
{name()}
|
||||||
</span>
|
</span>
|
||||||
<Show
|
<Show
|
||||||
when={badge()}
|
when={badge()}
|
||||||
fallback={
|
fallback={
|
||||||
<Show when={props.status?.version}>
|
<Show when={props.status?.version}>
|
||||||
<span ref={versionRef} class={props.versionClass ?? "text-text-weak text-14-regular truncate"}>
|
<span
|
||||||
|
ref={versionRef}
|
||||||
|
class={`${props.versionClass ?? "text-text-weak text-14-regular truncate"} min-w-0`}
|
||||||
|
>
|
||||||
v{props.status?.version}
|
v{props.status?.version}
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -96,7 +102,7 @@ export function ServerRow(props: ServerRowProps) {
|
|||||||
{conn().http.username ? (
|
{conn().http.username ? (
|
||||||
<span class="text-text-weak">{conn().http.username}</span>
|
<span class="text-text-weak">{conn().http.username}</span>
|
||||||
) : (
|
) : (
|
||||||
<span class="text-text-weaker">no username</span>
|
<span class="text-text-weaker">{language.t("server.row.noUsername")}</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
{conn().http.password && <span class="text-text-weak">••••••••</span>}
|
{conn().http.password && <span class="text-text-weak">••••••••</span>}
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
|||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
import { Keybind } from "@opencode-ai/ui/keybind"
|
||||||
import { Popover } from "@opencode-ai/ui/popover"
|
|
||||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||||
import { TextField } from "@opencode-ai/ui/text-field"
|
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||||
import { getFilename } from "@opencode-ai/util/path"
|
import { getFilename } from "@opencode-ai/util/path"
|
||||||
@@ -14,12 +12,10 @@ import { createEffect, createMemo, For, onCleanup, Show } from "solid-js"
|
|||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { Portal } from "solid-js/web"
|
import { Portal } from "solid-js/web"
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useServer } from "@/context/server"
|
import { useServer } from "@/context/server"
|
||||||
import { useSync } from "@/context/sync"
|
|
||||||
import { useTerminal } from "@/context/terminal"
|
import { useTerminal } from "@/context/terminal"
|
||||||
import { focusTerminalById } from "@/pages/session/helpers"
|
import { focusTerminalById } from "@/pages/session/helpers"
|
||||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||||
@@ -50,74 +46,68 @@ type OS = "macos" | "windows" | "linux" | "unknown"
|
|||||||
const MAC_APPS = [
|
const MAC_APPS = [
|
||||||
{
|
{
|
||||||
id: "vscode",
|
id: "vscode",
|
||||||
label: "VS Code",
|
label: "session.header.open.app.vscode",
|
||||||
icon: "vscode",
|
icon: "vscode",
|
||||||
openWith: "Visual Studio Code",
|
openWith: "Visual Studio Code",
|
||||||
},
|
},
|
||||||
{ id: "cursor", label: "Cursor", icon: "cursor", openWith: "Cursor" },
|
{ id: "cursor", label: "session.header.open.app.cursor", icon: "cursor", openWith: "Cursor" },
|
||||||
{ id: "zed", label: "Zed", icon: "zed", openWith: "Zed" },
|
{ id: "zed", label: "session.header.open.app.zed", icon: "zed", openWith: "Zed" },
|
||||||
{ id: "textmate", label: "TextMate", icon: "textmate", openWith: "TextMate" },
|
{ id: "textmate", label: "session.header.open.app.textmate", icon: "textmate", openWith: "TextMate" },
|
||||||
{
|
{
|
||||||
id: "antigravity",
|
id: "antigravity",
|
||||||
label: "Antigravity",
|
label: "session.header.open.app.antigravity",
|
||||||
icon: "antigravity",
|
icon: "antigravity",
|
||||||
openWith: "Antigravity",
|
openWith: "Antigravity",
|
||||||
},
|
},
|
||||||
{ id: "terminal", label: "Terminal", icon: "terminal", openWith: "Terminal" },
|
{ id: "terminal", label: "session.header.open.app.terminal", icon: "terminal", openWith: "Terminal" },
|
||||||
{ id: "iterm2", label: "iTerm2", icon: "iterm2", openWith: "iTerm" },
|
{ id: "iterm2", label: "session.header.open.app.iterm2", icon: "iterm2", openWith: "iTerm" },
|
||||||
{ id: "ghostty", label: "Ghostty", icon: "ghostty", openWith: "Ghostty" },
|
{ id: "ghostty", label: "session.header.open.app.ghostty", icon: "ghostty", openWith: "Ghostty" },
|
||||||
{ id: "warp", label: "Warp", icon: "warp", openWith: "Warp" },
|
{ id: "warp", label: "session.header.open.app.warp", icon: "warp", openWith: "Warp" },
|
||||||
{ id: "xcode", label: "Xcode", icon: "xcode", openWith: "Xcode" },
|
{ id: "xcode", label: "session.header.open.app.xcode", icon: "xcode", openWith: "Xcode" },
|
||||||
{
|
{
|
||||||
id: "android-studio",
|
id: "android-studio",
|
||||||
label: "Android Studio",
|
label: "session.header.open.app.androidStudio",
|
||||||
icon: "android-studio",
|
icon: "android-studio",
|
||||||
openWith: "Android Studio",
|
openWith: "Android Studio",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "sublime-text",
|
id: "sublime-text",
|
||||||
label: "Sublime Text",
|
label: "session.header.open.app.sublimeText",
|
||||||
icon: "sublime-text",
|
icon: "sublime-text",
|
||||||
openWith: "Sublime Text",
|
openWith: "Sublime Text",
|
||||||
},
|
},
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
const WINDOWS_APPS = [
|
const WINDOWS_APPS = [
|
||||||
{ id: "vscode", label: "VS Code", icon: "vscode", openWith: "code" },
|
{ id: "vscode", label: "session.header.open.app.vscode", icon: "vscode", openWith: "code" },
|
||||||
{ id: "cursor", label: "Cursor", icon: "cursor", openWith: "cursor" },
|
{ id: "cursor", label: "session.header.open.app.cursor", icon: "cursor", openWith: "cursor" },
|
||||||
{ id: "zed", label: "Zed", icon: "zed", openWith: "zed" },
|
{ id: "zed", label: "session.header.open.app.zed", icon: "zed", openWith: "zed" },
|
||||||
{
|
{
|
||||||
id: "powershell",
|
id: "powershell",
|
||||||
label: "PowerShell",
|
label: "session.header.open.app.powershell",
|
||||||
icon: "powershell",
|
icon: "powershell",
|
||||||
openWith: "powershell",
|
openWith: "powershell",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "sublime-text",
|
id: "sublime-text",
|
||||||
label: "Sublime Text",
|
label: "session.header.open.app.sublimeText",
|
||||||
icon: "sublime-text",
|
icon: "sublime-text",
|
||||||
openWith: "Sublime Text",
|
openWith: "Sublime Text",
|
||||||
},
|
},
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
const LINUX_APPS = [
|
const LINUX_APPS = [
|
||||||
{ id: "vscode", label: "VS Code", icon: "vscode", openWith: "code" },
|
{ id: "vscode", label: "session.header.open.app.vscode", icon: "vscode", openWith: "code" },
|
||||||
{ id: "cursor", label: "Cursor", icon: "cursor", openWith: "cursor" },
|
{ id: "cursor", label: "session.header.open.app.cursor", icon: "cursor", openWith: "cursor" },
|
||||||
{ id: "zed", label: "Zed", icon: "zed", openWith: "zed" },
|
{ id: "zed", label: "session.header.open.app.zed", icon: "zed", openWith: "zed" },
|
||||||
{
|
{
|
||||||
id: "sublime-text",
|
id: "sublime-text",
|
||||||
label: "Sublime Text",
|
label: "session.header.open.app.sublimeText",
|
||||||
icon: "sublime-text",
|
icon: "sublime-text",
|
||||||
openWith: "Sublime Text",
|
openWith: "Sublime Text",
|
||||||
},
|
},
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
type OpenOption = (typeof MAC_APPS)[number] | (typeof WINDOWS_APPS)[number] | (typeof LINUX_APPS)[number]
|
|
||||||
type OpenIcon = OpenApp | "file-explorer"
|
|
||||||
const OPEN_ICON_BASE = new Set<OpenIcon>(["finder", "vscode", "cursor", "zed"])
|
|
||||||
|
|
||||||
const openIconSize = (id: OpenIcon) => (OPEN_ICON_BASE.has(id) ? "size-4" : "size-[19px]")
|
|
||||||
|
|
||||||
const detectOS = (platform: ReturnType<typeof usePlatform>): OS => {
|
const detectOS = (platform: ReturnType<typeof usePlatform>): OS => {
|
||||||
if (platform.platform === "desktop" && platform.os) return platform.os
|
if (platform.platform === "desktop" && platform.os) return platform.os
|
||||||
if (typeof navigator !== "object") return "unknown"
|
if (typeof navigator !== "object") return "unknown"
|
||||||
@@ -136,98 +126,10 @@ const showRequestError = (language: ReturnType<typeof useLanguage>, err: unknown
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function useSessionShare(args: {
|
|
||||||
globalSDK: ReturnType<typeof useGlobalSDK>
|
|
||||||
currentSession: () =>
|
|
||||||
| {
|
|
||||||
share?: {
|
|
||||||
url?: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
| undefined
|
|
||||||
sessionID: () => string | undefined
|
|
||||||
projectDirectory: () => string
|
|
||||||
platform: ReturnType<typeof usePlatform>
|
|
||||||
}) {
|
|
||||||
const [state, setState] = createStore({
|
|
||||||
share: false,
|
|
||||||
unshare: false,
|
|
||||||
copied: false,
|
|
||||||
timer: undefined as number | undefined,
|
|
||||||
})
|
|
||||||
const shareUrl = createMemo(() => args.currentSession()?.share?.url)
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const url = shareUrl()
|
|
||||||
if (url) return
|
|
||||||
if (state.timer) window.clearTimeout(state.timer)
|
|
||||||
setState({ copied: false, timer: undefined })
|
|
||||||
})
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
if (state.timer) window.clearTimeout(state.timer)
|
|
||||||
})
|
|
||||||
|
|
||||||
const shareSession = () => {
|
|
||||||
const sessionID = args.sessionID()
|
|
||||||
if (!sessionID || state.share) return
|
|
||||||
setState("share", true)
|
|
||||||
args.globalSDK.client.session
|
|
||||||
.share({ sessionID, directory: args.projectDirectory() })
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Failed to share session", error)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setState("share", false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const unshareSession = () => {
|
|
||||||
const sessionID = args.sessionID()
|
|
||||||
if (!sessionID || state.unshare) return
|
|
||||||
setState("unshare", true)
|
|
||||||
args.globalSDK.client.session
|
|
||||||
.unshare({ sessionID, directory: args.projectDirectory() })
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Failed to unshare session", error)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setState("unshare", false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const copyLink = (onError: (error: unknown) => void) => {
|
|
||||||
const url = shareUrl()
|
|
||||||
if (!url) return
|
|
||||||
navigator.clipboard
|
|
||||||
.writeText(url)
|
|
||||||
.then(() => {
|
|
||||||
if (state.timer) window.clearTimeout(state.timer)
|
|
||||||
setState("copied", true)
|
|
||||||
const timer = window.setTimeout(() => {
|
|
||||||
setState("copied", false)
|
|
||||||
setState("timer", undefined)
|
|
||||||
}, 3000)
|
|
||||||
setState("timer", timer)
|
|
||||||
})
|
|
||||||
.catch(onError)
|
|
||||||
}
|
|
||||||
|
|
||||||
const viewShare = () => {
|
|
||||||
const url = shareUrl()
|
|
||||||
if (!url) return
|
|
||||||
args.platform.openLink(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
return { state, shareUrl, shareSession, unshareSession, copyLink, viewShare }
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SessionHeader() {
|
export function SessionHeader() {
|
||||||
const globalSDK = useGlobalSDK()
|
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
const sync = useSync()
|
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const terminal = useTerminal()
|
const terminal = useTerminal()
|
||||||
@@ -245,10 +147,6 @@ export function SessionHeader() {
|
|||||||
return getFilename(projectDirectory())
|
return getFilename(projectDirectory())
|
||||||
})
|
})
|
||||||
const hotkey = createMemo(() => command.keybind("file.open"))
|
const hotkey = createMemo(() => command.keybind("file.open"))
|
||||||
|
|
||||||
const currentSession = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
|
||||||
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
|
|
||||||
const showShare = createMemo(() => shareEnabled() && !!params.id)
|
|
||||||
const os = createMemo(() => detectOS(platform))
|
const os = createMemo(() => detectOS(platform))
|
||||||
|
|
||||||
const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({
|
const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({
|
||||||
@@ -262,9 +160,9 @@ export function SessionHeader() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const fileManager = createMemo(() => {
|
const fileManager = createMemo(() => {
|
||||||
if (os() === "macos") return { label: "Finder", icon: "finder" as const }
|
if (os() === "macos") return { label: "session.header.open.finder", icon: "finder" as const }
|
||||||
if (os() === "windows") return { label: "File Explorer", icon: "file-explorer" as const }
|
if (os() === "windows") return { label: "session.header.open.fileExplorer", icon: "file-explorer" as const }
|
||||||
return { label: "File Manager", icon: "finder" as const }
|
return { label: "session.header.open.fileManager", icon: "finder" as const }
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
@@ -289,8 +187,10 @@ export function SessionHeader() {
|
|||||||
|
|
||||||
const options = createMemo(() => {
|
const options = createMemo(() => {
|
||||||
return [
|
return [
|
||||||
{ id: "finder", label: fileManager().label, icon: fileManager().icon },
|
{ id: "finder", label: language.t(fileManager().label), icon: fileManager().icon },
|
||||||
...apps().filter((app) => exists[app.id]),
|
...apps()
|
||||||
|
.filter((app) => exists[app.id])
|
||||||
|
.map((app) => ({ ...app, label: language.t(app.label) })),
|
||||||
] as const
|
] as const
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -356,14 +256,6 @@ export function SessionHeader() {
|
|||||||
.catch((err: unknown) => showRequestError(language, err))
|
.catch((err: unknown) => showRequestError(language, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
const share = useSessionShare({
|
|
||||||
globalSDK,
|
|
||||||
currentSession,
|
|
||||||
sessionID: () => params.id,
|
|
||||||
projectDirectory,
|
|
||||||
platform,
|
|
||||||
})
|
|
||||||
|
|
||||||
const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center"))
|
const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center"))
|
||||||
const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right"))
|
const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right"))
|
||||||
|
|
||||||
@@ -391,7 +283,9 @@ export function SessionHeader() {
|
|||||||
|
|
||||||
<Show when={hotkey()}>
|
<Show when={hotkey()}>
|
||||||
{(keybind) => (
|
{(keybind) => (
|
||||||
<Keybind class="shrink-0 !border-0 !bg-transparent !shadow-none px-0">{keybind()}</Keybind>
|
<Keybind class="shrink-0 !border-0 !bg-transparent !shadow-none px-0 text-text-weaker">
|
||||||
|
{keybind()}
|
||||||
|
</Keybind>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -402,7 +296,6 @@ export function SessionHeader() {
|
|||||||
{(mount) => (
|
{(mount) => (
|
||||||
<Portal mount={mount()}>
|
<Portal mount={mount()}>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<StatusPopover />
|
|
||||||
<Show when={projectDirectory()}>
|
<Show when={projectDirectory()}>
|
||||||
<div class="hidden xl:flex items-center">
|
<div class="hidden xl:flex items-center">
|
||||||
<Show
|
<Show
|
||||||
@@ -427,7 +320,7 @@ export function SessionHeader() {
|
|||||||
<div class="flex h-[24px] box-border items-center rounded-md border border-border-weak-base bg-surface-panel overflow-hidden">
|
<div class="flex h-[24px] box-border items-center rounded-md border border-border-weak-base bg-surface-panel overflow-hidden">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="rounded-none h-full py-0 pr-3 pl-0.5 gap-1.5 border-none shadow-none disabled:!cursor-default"
|
class="rounded-none h-full py-0 pr-1.5 pl-px gap-1.5 border-none shadow-none disabled:!cursor-default"
|
||||||
classList={{
|
classList={{
|
||||||
"bg-surface-raised-base-active": opening(),
|
"bg-surface-raised-base-active": opening(),
|
||||||
}}
|
}}
|
||||||
@@ -435,17 +328,13 @@ export function SessionHeader() {
|
|||||||
disabled={opening()}
|
disabled={opening()}
|
||||||
aria-label={language.t("session.header.open.ariaLabel", { app: current().label })}
|
aria-label={language.t("session.header.open.ariaLabel", { app: current().label })}
|
||||||
>
|
>
|
||||||
<div class="flex size-5 shrink-0 items-center justify-center">
|
<div class="flex size-5 shrink-0 items-center justify-center [&_[data-component=app-icon]]:size-5">
|
||||||
<Show
|
<Show when={opening()} fallback={<AppIcon id={current().icon} />}>
|
||||||
when={opening()}
|
|
||||||
fallback={<AppIcon id={current().icon} class={openIconSize(current().icon)} />}
|
|
||||||
>
|
|
||||||
<Spinner class="size-3.5 text-icon-base" />
|
<Spinner class="size-3.5 text-icon-base" />
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-12-regular text-text-strong">{language.t("common.open")}</span>
|
<span class="text-12-regular text-text-strong">{language.t("common.open")}</span>
|
||||||
</Button>
|
</Button>
|
||||||
<div class="self-stretch w-px bg-border-weak-base" />
|
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
gutter={4}
|
gutter={4}
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
@@ -457,17 +346,20 @@ export function SessionHeader() {
|
|||||||
icon="chevron-down"
|
icon="chevron-down"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
disabled={opening()}
|
disabled={opening()}
|
||||||
class="rounded-none h-full w-[24px] p-0 border-none shadow-none data-[expanded]:bg-surface-raised-base-active disabled:!cursor-default"
|
class="rounded-none h-full w-[20px] p-0 border-none shadow-none data-[expanded]:bg-surface-raised-base-active disabled:!cursor-default"
|
||||||
classList={{
|
classList={{
|
||||||
"bg-surface-raised-base-active": opening(),
|
"bg-surface-raised-base-active": opening(),
|
||||||
}}
|
}}
|
||||||
aria-label={language.t("session.header.open.menu")}
|
aria-label={language.t("session.header.open.menu")}
|
||||||
/>
|
/>
|
||||||
<DropdownMenu.Portal>
|
<DropdownMenu.Portal>
|
||||||
<DropdownMenu.Content>
|
<DropdownMenu.Content class="[&_[data-slot=dropdown-menu-item]]:pl-1 [&_[data-slot=dropdown-menu-radio-item]]:pl-1 [&_[data-slot=dropdown-menu-radio-item]+[data-slot=dropdown-menu-radio-item]]:mt-1">
|
||||||
<DropdownMenu.Group>
|
<DropdownMenu.Group>
|
||||||
<DropdownMenu.GroupLabel>{language.t("session.header.openIn")}</DropdownMenu.GroupLabel>
|
<DropdownMenu.GroupLabel class="!px-1 !py-1">
|
||||||
|
{language.t("session.header.openIn")}
|
||||||
|
</DropdownMenu.GroupLabel>
|
||||||
<DropdownMenu.RadioGroup
|
<DropdownMenu.RadioGroup
|
||||||
|
class="mt-1"
|
||||||
value={current().id}
|
value={current().id}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
if (!OPEN_APPS.includes(value as OpenApp)) return
|
if (!OPEN_APPS.includes(value as OpenApp)) return
|
||||||
@@ -484,8 +376,8 @@ export function SessionHeader() {
|
|||||||
openDir(o.id)
|
openDir(o.id)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="flex size-5 shrink-0 items-center justify-center">
|
<div class="flex size-5 shrink-0 items-center justify-center [&_[data-component=app-icon]]:size-5">
|
||||||
<AppIcon id={o.icon} class={openIconSize(o.icon)} />
|
<AppIcon id={o.icon} />
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenu.ItemLabel>{o.label}</DropdownMenu.ItemLabel>
|
<DropdownMenu.ItemLabel>{o.label}</DropdownMenu.ItemLabel>
|
||||||
<DropdownMenu.ItemIndicator>
|
<DropdownMenu.ItemIndicator>
|
||||||
@@ -518,113 +410,10 @@ export function SessionHeader() {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={showShare()}>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<Popover
|
|
||||||
title={language.t("session.share.popover.title")}
|
|
||||||
description={
|
|
||||||
share.shareUrl()
|
|
||||||
? language.t("session.share.popover.description.shared")
|
|
||||||
: language.t("session.share.popover.description.unshared")
|
|
||||||
}
|
|
||||||
gutter={4}
|
|
||||||
placement="bottom-end"
|
|
||||||
shift={-64}
|
|
||||||
class="rounded-xl [&_[data-slot=popover-close-button]]:hidden"
|
|
||||||
triggerAs={Button}
|
|
||||||
triggerProps={{
|
|
||||||
variant: "ghost",
|
|
||||||
class:
|
|
||||||
"rounded-md h-[24px] px-3 border border-border-weak-base bg-surface-panel shadow-none data-[expanded]:bg-surface-base-active",
|
|
||||||
classList: {
|
|
||||||
"rounded-r-none": share.shareUrl() !== undefined,
|
|
||||||
"border-r-0": share.shareUrl() !== undefined,
|
|
||||||
},
|
|
||||||
style: { scale: 1 },
|
|
||||||
}}
|
|
||||||
trigger={<span class="text-12-regular">{language.t("session.share.action.share")}</span>}
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<Show
|
|
||||||
when={share.shareUrl()}
|
|
||||||
fallback={
|
|
||||||
<div class="flex">
|
|
||||||
<Button
|
|
||||||
size="large"
|
|
||||||
variant="primary"
|
|
||||||
class="w-1/2"
|
|
||||||
onClick={share.shareSession}
|
|
||||||
disabled={share.state.share}
|
|
||||||
>
|
|
||||||
{share.state.share
|
|
||||||
? language.t("session.share.action.publishing")
|
|
||||||
: language.t("session.share.action.publish")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<TextField
|
|
||||||
value={share.shareUrl() ?? ""}
|
|
||||||
readOnly
|
|
||||||
copyable
|
|
||||||
copyKind="link"
|
|
||||||
tabIndex={-1}
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
<div class="grid grid-cols-2 gap-2">
|
|
||||||
<Button
|
|
||||||
size="large"
|
|
||||||
variant="secondary"
|
|
||||||
class="w-full shadow-none border border-border-weak-base"
|
|
||||||
onClick={share.unshareSession}
|
|
||||||
disabled={share.state.unshare}
|
|
||||||
>
|
|
||||||
{share.state.unshare
|
|
||||||
? language.t("session.share.action.unpublishing")
|
|
||||||
: language.t("session.share.action.unpublish")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="large"
|
|
||||||
variant="primary"
|
|
||||||
class="w-full"
|
|
||||||
onClick={share.viewShare}
|
|
||||||
disabled={share.state.unshare}
|
|
||||||
>
|
|
||||||
{language.t("session.share.action.view")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
<Show when={share.shareUrl()} fallback={<div aria-hidden="true" />}>
|
|
||||||
<Tooltip
|
|
||||||
value={
|
|
||||||
share.state.copied
|
|
||||||
? language.t("session.share.copy.copied")
|
|
||||||
: language.t("session.share.copy.copyLink")
|
|
||||||
}
|
|
||||||
placement="top"
|
|
||||||
gutter={8}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
icon={share.state.copied ? "check" : "link"}
|
|
||||||
variant="ghost"
|
|
||||||
class="rounded-l-none h-[24px] border border-border-weak-base bg-surface-panel shadow-none"
|
|
||||||
onClick={() => share.copyLink((error) => showRequestError(language, error))}
|
|
||||||
disabled={share.state.unshare}
|
|
||||||
aria-label={
|
|
||||||
share.state.copied
|
|
||||||
? language.t("session.share.copy.copied")
|
|
||||||
: language.t("session.share.copy.copyLink")
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
|
<Tooltip placement="bottom" value={language.t("status.popover.trigger")}>
|
||||||
|
<StatusPopover />
|
||||||
|
</Tooltip>
|
||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
title={language.t("command.terminal.toggle")}
|
title={language.t("command.terminal.toggle")}
|
||||||
keybind={command.keybind("terminal.toggle")}
|
keybind={command.keybind("terminal.toggle")}
|
||||||
@@ -637,23 +426,7 @@ export function SessionHeader() {
|
|||||||
aria-expanded={view().terminal.opened()}
|
aria-expanded={view().terminal.opened()}
|
||||||
aria-controls="terminal-panel"
|
aria-controls="terminal-panel"
|
||||||
>
|
>
|
||||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
<Icon size="small" name={view().terminal.opened() ? "terminal-active" : "terminal"} />
|
||||||
<Icon
|
|
||||||
size="small"
|
|
||||||
name={view().terminal.opened() ? "layout-bottom-partial" : "layout-bottom"}
|
|
||||||
class="group-hover/terminal-toggle:hidden"
|
|
||||||
/>
|
|
||||||
<Icon
|
|
||||||
size="small"
|
|
||||||
name="layout-bottom-partial"
|
|
||||||
class="hidden group-hover/terminal-toggle:inline-block"
|
|
||||||
/>
|
|
||||||
<Icon
|
|
||||||
size="small"
|
|
||||||
name={view().terminal.opened() ? "layout-bottom" : "layout-bottom-partial"}
|
|
||||||
class="hidden group-active/terminal-toggle:inline-block"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
|
|
||||||
@@ -670,23 +443,7 @@ export function SessionHeader() {
|
|||||||
aria-expanded={view().reviewPanel.opened()}
|
aria-expanded={view().reviewPanel.opened()}
|
||||||
aria-controls="review-panel"
|
aria-controls="review-panel"
|
||||||
>
|
>
|
||||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
<Icon size="small" name={view().reviewPanel.opened() ? "review-active" : "review"} />
|
||||||
<Icon
|
|
||||||
size="small"
|
|
||||||
name={view().reviewPanel.opened() ? "layout-right-partial" : "layout-right"}
|
|
||||||
class="group-hover/review-toggle:hidden"
|
|
||||||
/>
|
|
||||||
<Icon
|
|
||||||
size="small"
|
|
||||||
name="layout-right-partial"
|
|
||||||
class="hidden group-hover/review-toggle:inline-block"
|
|
||||||
/>
|
|
||||||
<Icon
|
|
||||||
size="small"
|
|
||||||
name={view().reviewPanel.opened() ? "layout-right" : "layout-right-partial"}
|
|
||||||
class="hidden group-active/review-toggle:inline-block"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
|
|||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import { isDefaultTitle as isDefaultTerminalTitle } from "@/context/terminal-title"
|
||||||
import { useTerminal, type LocalPTY } from "@/context/terminal"
|
import { useTerminal, type LocalPTY } from "@/context/terminal"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { focusTerminalById } from "@/pages/session/helpers"
|
import { focusTerminalById } from "@/pages/session/helpers"
|
||||||
@@ -27,11 +28,7 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
|
|||||||
const isDefaultTitle = () => {
|
const isDefaultTitle = () => {
|
||||||
const number = props.terminal.titleNumber
|
const number = props.terminal.titleNumber
|
||||||
if (!Number.isFinite(number) || number <= 0) return false
|
if (!Number.isFinite(number) || number <= 0) return false
|
||||||
const match = props.terminal.title.match(/^Terminal (\d+)$/)
|
return isDefaultTerminalTitle(props.terminal.title, number)
|
||||||
if (!match) return false
|
|
||||||
const parsed = Number(match[1])
|
|
||||||
if (!Number.isFinite(parsed) || parsed <= 0) return false
|
|
||||||
return parsed === number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const label = () => {
|
const label = () => {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { usePlatform } from "@/context/platform"
|
|||||||
import { useSettings, monoFontFamily } from "@/context/settings"
|
import { useSettings, monoFontFamily } from "@/context/settings"
|
||||||
import { playSound, SOUND_OPTIONS } from "@/utils/sound"
|
import { playSound, SOUND_OPTIONS } from "@/utils/sound"
|
||||||
import { Link } from "./link"
|
import { Link } from "./link"
|
||||||
|
import { SettingsList } from "./settings-list"
|
||||||
|
|
||||||
let demoSoundState = {
|
let demoSoundState = {
|
||||||
cleanup: undefined as (() => void) | undefined,
|
cleanup: undefined as (() => void) | undefined,
|
||||||
@@ -113,6 +114,11 @@ export const SettingsGeneral: Component = () => {
|
|||||||
{ value: "dark", label: language.t("theme.scheme.dark") },
|
{ value: "dark", label: language.t("theme.scheme.dark") },
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const followupOptions = createMemo((): { value: "queue" | "steer"; label: string }[] => [
|
||||||
|
{ value: "queue", label: language.t("settings.general.row.followup.option.queue") },
|
||||||
|
{ value: "steer", label: language.t("settings.general.row.followup.option.steer") },
|
||||||
|
])
|
||||||
|
|
||||||
const languageOptions = createMemo(() =>
|
const languageOptions = createMemo(() =>
|
||||||
language.locales.map((locale) => ({
|
language.locales.map((locale) => ({
|
||||||
value: locale,
|
value: locale,
|
||||||
@@ -170,11 +176,9 @@ export const SettingsGeneral: Component = () => {
|
|||||||
triggerVariant: "settings" as const,
|
triggerVariant: "settings" as const,
|
||||||
})
|
})
|
||||||
|
|
||||||
const AppearanceSection = () => (
|
const GeneralSection = () => (
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.appearance")}</h3>
|
<SettingsList>
|
||||||
|
|
||||||
<div class="bg-surface-raised-base px-4 rounded-lg">
|
|
||||||
<SettingsRow
|
<SettingsRow
|
||||||
title={language.t("settings.general.row.language.title")}
|
title={language.t("settings.general.row.language.title")}
|
||||||
description={language.t("settings.general.row.language.description")}
|
description={language.t("settings.general.row.language.description")}
|
||||||
@@ -193,8 +197,70 @@ export const SettingsGeneral: Component = () => {
|
|||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
|
||||||
<SettingsRow
|
<SettingsRow
|
||||||
title={language.t("settings.general.row.appearance.title")}
|
title={language.t("settings.general.row.reasoningSummaries.title")}
|
||||||
description={language.t("settings.general.row.appearance.description")}
|
description={language.t("settings.general.row.reasoningSummaries.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-feed-reasoning-summaries">
|
||||||
|
<Switch
|
||||||
|
checked={settings.general.showReasoningSummaries()}
|
||||||
|
onChange={(checked) => settings.general.setShowReasoningSummaries(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRow>
|
||||||
|
|
||||||
|
<SettingsRow
|
||||||
|
title={language.t("settings.general.row.shellToolPartsExpanded.title")}
|
||||||
|
description={language.t("settings.general.row.shellToolPartsExpanded.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-feed-shell-tool-parts-expanded">
|
||||||
|
<Switch
|
||||||
|
checked={settings.general.shellToolPartsExpanded()}
|
||||||
|
onChange={(checked) => settings.general.setShellToolPartsExpanded(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRow>
|
||||||
|
|
||||||
|
<SettingsRow
|
||||||
|
title={language.t("settings.general.row.editToolPartsExpanded.title")}
|
||||||
|
description={language.t("settings.general.row.editToolPartsExpanded.description")}
|
||||||
|
>
|
||||||
|
<div data-action="settings-feed-edit-tool-parts-expanded">
|
||||||
|
<Switch
|
||||||
|
checked={settings.general.editToolPartsExpanded()}
|
||||||
|
onChange={(checked) => settings.general.setEditToolPartsExpanded(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsRow>
|
||||||
|
|
||||||
|
<SettingsRow
|
||||||
|
title={language.t("settings.general.row.followup.title")}
|
||||||
|
description={language.t("settings.general.row.followup.description")}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
data-action="settings-followup"
|
||||||
|
options={followupOptions()}
|
||||||
|
current={followupOptions().find((o) => o.value === settings.general.followup())}
|
||||||
|
value={(o) => o.value}
|
||||||
|
label={(o) => o.label}
|
||||||
|
onSelect={(option) => option && settings.general.setFollowup(option.value)}
|
||||||
|
variant="secondary"
|
||||||
|
size="small"
|
||||||
|
triggerVariant="settings"
|
||||||
|
triggerStyle={{ "min-width": "180px" }}
|
||||||
|
/>
|
||||||
|
</SettingsRow>
|
||||||
|
</SettingsList>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const AppearanceSection = () => (
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.appearance")}</h3>
|
||||||
|
|
||||||
|
<SettingsList>
|
||||||
|
<SettingsRow
|
||||||
|
title={language.t("settings.general.row.colorScheme.title")}
|
||||||
|
description={language.t("settings.general.row.colorScheme.description")}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
data-action="settings-color-scheme"
|
data-action="settings-color-scheme"
|
||||||
@@ -211,6 +277,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
triggerVariant="settings"
|
triggerVariant="settings"
|
||||||
|
triggerStyle={{ "min-width": "220px" }}
|
||||||
/>
|
/>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
|
||||||
@@ -267,51 +334,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
)}
|
)}
|
||||||
</Select>
|
</Select>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
</div>
|
</SettingsList>
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
const FeedSection = () => (
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.feed")}</h3>
|
|
||||||
|
|
||||||
<div class="bg-surface-raised-base px-4 rounded-lg">
|
|
||||||
<SettingsRow
|
|
||||||
title={language.t("settings.general.row.reasoningSummaries.title")}
|
|
||||||
description={language.t("settings.general.row.reasoningSummaries.description")}
|
|
||||||
>
|
|
||||||
<div data-action="settings-feed-reasoning-summaries">
|
|
||||||
<Switch
|
|
||||||
checked={settings.general.showReasoningSummaries()}
|
|
||||||
onChange={(checked) => settings.general.setShowReasoningSummaries(checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SettingsRow>
|
|
||||||
|
|
||||||
<SettingsRow
|
|
||||||
title={language.t("settings.general.row.shellToolPartsExpanded.title")}
|
|
||||||
description={language.t("settings.general.row.shellToolPartsExpanded.description")}
|
|
||||||
>
|
|
||||||
<div data-action="settings-feed-shell-tool-parts-expanded">
|
|
||||||
<Switch
|
|
||||||
checked={settings.general.shellToolPartsExpanded()}
|
|
||||||
onChange={(checked) => settings.general.setShellToolPartsExpanded(checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SettingsRow>
|
|
||||||
|
|
||||||
<SettingsRow
|
|
||||||
title={language.t("settings.general.row.editToolPartsExpanded.title")}
|
|
||||||
description={language.t("settings.general.row.editToolPartsExpanded.description")}
|
|
||||||
>
|
|
||||||
<div data-action="settings-feed-edit-tool-parts-expanded">
|
|
||||||
<Switch
|
|
||||||
checked={settings.general.editToolPartsExpanded()}
|
|
||||||
onChange={(checked) => settings.general.setEditToolPartsExpanded(checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SettingsRow>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -319,7 +342,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.notifications")}</h3>
|
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.notifications")}</h3>
|
||||||
|
|
||||||
<div class="bg-surface-raised-base px-4 rounded-lg">
|
<SettingsList>
|
||||||
<SettingsRow
|
<SettingsRow
|
||||||
title={language.t("settings.general.notifications.agent.title")}
|
title={language.t("settings.general.notifications.agent.title")}
|
||||||
description={language.t("settings.general.notifications.agent.description")}
|
description={language.t("settings.general.notifications.agent.description")}
|
||||||
@@ -355,7 +378,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
</div>
|
</SettingsList>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -363,7 +386,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.sounds")}</h3>
|
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.sounds")}</h3>
|
||||||
|
|
||||||
<div class="bg-surface-raised-base px-4 rounded-lg">
|
<SettingsList>
|
||||||
<SettingsRow
|
<SettingsRow
|
||||||
title={language.t("settings.general.sounds.agent.title")}
|
title={language.t("settings.general.sounds.agent.title")}
|
||||||
description={language.t("settings.general.sounds.agent.description")}
|
description={language.t("settings.general.sounds.agent.description")}
|
||||||
@@ -408,7 +431,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
</div>
|
</SettingsList>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -416,7 +439,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.updates")}</h3>
|
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.updates")}</h3>
|
||||||
|
|
||||||
<div class="bg-surface-raised-base px-4 rounded-lg">
|
<SettingsList>
|
||||||
<SettingsRow
|
<SettingsRow
|
||||||
title={language.t("settings.updates.row.startup.title")}
|
title={language.t("settings.updates.row.startup.title")}
|
||||||
description={language.t("settings.updates.row.startup.description")}
|
description={language.t("settings.updates.row.startup.description")}
|
||||||
@@ -452,7 +475,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
: language.t("settings.updates.action.checkNow")}
|
: language.t("settings.updates.action.checkNow")}
|
||||||
</Button>
|
</Button>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
</div>
|
</SettingsList>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -465,9 +488,9 @@ export const SettingsGeneral: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-8 w-full">
|
<div class="flex flex-col gap-8 w-full">
|
||||||
<AppearanceSection />
|
<GeneralSection />
|
||||||
|
|
||||||
<FeedSection />
|
<AppearanceSection />
|
||||||
|
|
||||||
<NotificationsSection />
|
<NotificationsSection />
|
||||||
|
|
||||||
@@ -482,7 +505,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.desktop.section.wsl")}</h3>
|
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.desktop.section.wsl")}</h3>
|
||||||
|
|
||||||
<div class="bg-surface-raised-base px-4 rounded-lg">
|
<SettingsList>
|
||||||
<SettingsRow
|
<SettingsRow
|
||||||
title={language.t("settings.desktop.wsl.title")}
|
title={language.t("settings.desktop.wsl.title")}
|
||||||
description={language.t("settings.desktop.wsl.description")}
|
description={language.t("settings.desktop.wsl.description")}
|
||||||
@@ -495,7 +518,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
</div>
|
</SettingsList>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
@@ -515,7 +538,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.display")}</h3>
|
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.display")}</h3>
|
||||||
|
|
||||||
<div class="bg-surface-raised-base px-4 rounded-lg">
|
<SettingsList>
|
||||||
<SettingsRow
|
<SettingsRow
|
||||||
title={
|
title={
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -533,7 +556,7 @@ export const SettingsGeneral: Component = () => {
|
|||||||
<Switch checked={value() === "wayland"} onChange={onChange} />
|
<Switch checked={value() === "wayland"} onChange={onChange} />
|
||||||
</div>
|
</div>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
</div>
|
</SettingsList>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
@@ -551,12 +574,12 @@ interface SettingsRowProps {
|
|||||||
|
|
||||||
const SettingsRow: Component<SettingsRowProps> = (props) => {
|
const SettingsRow: Component<SettingsRowProps> = (props) => {
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-wrap items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
|
<div class="flex flex-wrap items-center gap-4 py-3 border-b border-border-weak-base last:border-none sm:flex-nowrap">
|
||||||
<div class="flex flex-col gap-0.5 min-w-0">
|
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||||
<span class="text-14-medium text-text-strong">{props.title}</span>
|
<span class="text-14-medium text-text-strong">{props.title}</span>
|
||||||
<span class="text-12-regular text-text-weak">{props.description}</span>
|
<span class="text-12-regular text-text-weak">{props.description}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-shrink-0">{props.children}</div>
|
<div class="flex w-full justify-end sm:w-auto sm:shrink-0">{props.children}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import fuzzysort from "fuzzysort"
|
|||||||
import { formatKeybind, parseKeybind, useCommand } from "@/context/command"
|
import { formatKeybind, parseKeybind, useCommand } from "@/context/command"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
|
import { SettingsList } from "./settings-list"
|
||||||
|
|
||||||
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
||||||
const PALETTE_ID = "command.palette"
|
const PALETTE_ID = "command.palette"
|
||||||
@@ -238,7 +239,7 @@ function useKeyCapture(input: {
|
|||||||
showToast({
|
showToast({
|
||||||
title: input.language.t("settings.shortcuts.conflict.title"),
|
title: input.language.t("settings.shortcuts.conflict.title"),
|
||||||
description: input.language.t("settings.shortcuts.conflict.description", {
|
description: input.language.t("settings.shortcuts.conflict.description", {
|
||||||
keybind: formatKeybind(next),
|
keybind: formatKeybind(next, input.language.t),
|
||||||
titles: [...conflicts.values()].join(", "),
|
titles: [...conflicts.values()].join(", "),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@@ -406,7 +407,7 @@ export const SettingsKeybinds: Component = () => {
|
|||||||
<Show when={(filtered().get(group) ?? []).length > 0}>
|
<Show when={(filtered().get(group) ?? []).length > 0}>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t(groupKey[group])}</h3>
|
<h3 class="text-14-medium text-text-strong pb-2">{language.t(groupKey[group])}</h3>
|
||||||
<div class="bg-surface-raised-base px-4 rounded-lg">
|
<SettingsList>
|
||||||
<For each={filtered().get(group) ?? []}>
|
<For each={filtered().get(group) ?? []}>
|
||||||
{(id) => (
|
{(id) => (
|
||||||
<div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
|
<div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
|
||||||
@@ -432,7 +433,7 @@ export const SettingsKeybinds: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</SettingsList>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { type Component, type JSX } from "solid-js"
|
||||||
|
|
||||||
|
export const SettingsList: Component<{ children: JSX.Element }> = (props) => {
|
||||||
|
return <div class="bg-surface-base px-4 rounded-lg">{props.children}</div>
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import { type Component, For, Show } from "solid-js"
|
|||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useModels } from "@/context/models"
|
import { useModels } from "@/context/models"
|
||||||
import { popularProviders } from "@/hooks/use-providers"
|
import { popularProviders } from "@/hooks/use-providers"
|
||||||
|
import { SettingsList } from "./settings-list"
|
||||||
|
|
||||||
type ModelItem = ReturnType<ReturnType<typeof useModels>["list"]>[number]
|
type ModelItem = ReturnType<ReturnType<typeof useModels>["list"]>[number]
|
||||||
|
|
||||||
@@ -100,7 +101,7 @@ export const SettingsModels: Component = () => {
|
|||||||
<ProviderIcon id={group.category} class="size-5 shrink-0 icon-strong-base" />
|
<ProviderIcon id={group.category} class="size-5 shrink-0 icon-strong-base" />
|
||||||
<span class="text-14-medium text-text-strong">{group.items[0].provider.name}</span>
|
<span class="text-14-medium text-text-strong">{group.items[0].provider.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-surface-raised-base px-4 rounded-lg">
|
<SettingsList>
|
||||||
<For each={group.items}>
|
<For each={group.items}>
|
||||||
{(item) => {
|
{(item) => {
|
||||||
const key = { providerID: item.provider.id, modelID: item.id }
|
const key = { providerID: item.provider.id, modelID: item.id }
|
||||||
@@ -124,7 +125,7 @@ export const SettingsModels: Component = () => {
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</SettingsList>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { useGlobalSync } from "@/context/global-sync"
|
|||||||
import { DialogConnectProvider } from "./dialog-connect-provider"
|
import { DialogConnectProvider } from "./dialog-connect-provider"
|
||||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||||
import { DialogCustomProvider } from "./dialog-custom-provider"
|
import { DialogCustomProvider } from "./dialog-custom-provider"
|
||||||
|
import { SettingsList } from "./settings-list"
|
||||||
|
|
||||||
type ProviderSource = "env" | "api" | "config" | "custom"
|
type ProviderSource = "env" | "api" | "config" | "custom"
|
||||||
type ProviderItem = ReturnType<ReturnType<typeof useProviders>["connected"]>[number]
|
type ProviderItem = ReturnType<ReturnType<typeof useProviders>["connected"]>[number]
|
||||||
@@ -136,7 +137,7 @@ export const SettingsProviders: Component = () => {
|
|||||||
<div class="flex flex-col gap-8 max-w-[720px]">
|
<div class="flex flex-col gap-8 max-w-[720px]">
|
||||||
<div class="flex flex-col gap-1" data-component="connected-providers-section">
|
<div class="flex flex-col gap-1" data-component="connected-providers-section">
|
||||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.providers.section.connected")}</h3>
|
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.providers.section.connected")}</h3>
|
||||||
<div class="bg-surface-raised-base px-4 rounded-lg">
|
<SettingsList>
|
||||||
<Show
|
<Show
|
||||||
when={connected().length > 0}
|
when={connected().length > 0}
|
||||||
fallback={
|
fallback={
|
||||||
@@ -169,12 +170,12 @@ export const SettingsProviders: Component = () => {
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</SettingsList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.providers.section.popular")}</h3>
|
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.providers.section.popular")}</h3>
|
||||||
<div class="bg-surface-raised-base px-4 rounded-lg">
|
<SettingsList>
|
||||||
<For each={popular()}>
|
<For each={popular()}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<div class="flex flex-wrap items-center justify-between gap-4 min-h-16 py-3 border-b border-border-weak-base last:border-none">
|
<div class="flex flex-wrap items-center justify-between gap-4 min-h-16 py-3 border-b border-border-weak-base last:border-none">
|
||||||
@@ -232,7 +233,7 @@ export const SettingsProviders: Component = () => {
|
|||||||
{language.t("common.connect")}
|
{language.t("common.connect")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SettingsList>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@@ -86,15 +86,17 @@ const useServerHealth = (servers: Accessor<ServerConnection.Any[]>) => {
|
|||||||
const useDefaultServerKey = (
|
const useDefaultServerKey = (
|
||||||
get: (() => string | Promise<string | null | undefined> | null | undefined) | undefined,
|
get: (() => string | Promise<string | null | undefined> | null | undefined) | undefined,
|
||||||
) => {
|
) => {
|
||||||
const [url, setUrl] = createSignal<string | undefined>()
|
const [state, setState] = createStore({
|
||||||
const [tick, setTick] = createSignal(0)
|
url: undefined as string | undefined,
|
||||||
|
tick: 0,
|
||||||
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
tick()
|
state.tick
|
||||||
let dead = false
|
let dead = false
|
||||||
const result = get?.()
|
const result = get?.()
|
||||||
if (!result) {
|
if (!result) {
|
||||||
setUrl(undefined)
|
setState("url", undefined)
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
dead = true
|
dead = true
|
||||||
})
|
})
|
||||||
@@ -104,7 +106,7 @@ const useDefaultServerKey = (
|
|||||||
if (result instanceof Promise) {
|
if (result instanceof Promise) {
|
||||||
void result.then((next) => {
|
void result.then((next) => {
|
||||||
if (dead) return
|
if (dead) return
|
||||||
setUrl(next ? normalizeServerUrl(next) : undefined)
|
setState("url", next ? normalizeServerUrl(next) : undefined)
|
||||||
})
|
})
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
dead = true
|
dead = true
|
||||||
@@ -112,7 +114,7 @@ const useDefaultServerKey = (
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setUrl(normalizeServerUrl(result))
|
setState("url", normalizeServerUrl(result))
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
dead = true
|
dead = true
|
||||||
})
|
})
|
||||||
@@ -120,11 +122,11 @@ const useDefaultServerKey = (
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
key: () => {
|
key: () => {
|
||||||
const u = url()
|
const u = state.url
|
||||||
if (!u) return
|
if (!u) return
|
||||||
return ServerConnection.key({ type: "http", http: { url: u } })
|
return ServerConnection.key({ type: "http", http: { url: u } })
|
||||||
},
|
},
|
||||||
refresh: () => setTick((value) => value + 1),
|
refresh: () => setState("tick", (value) => value + 1),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,6 +171,7 @@ export function StatusPopover() {
|
|||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const [shown, setShown] = createSignal(false)
|
||||||
const servers = createMemo(() => {
|
const servers = createMemo(() => {
|
||||||
const current = server.current
|
const current = server.current
|
||||||
const list = server.list
|
const list = server.list
|
||||||
@@ -199,18 +202,23 @@ export function StatusPopover() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
|
open={shown()}
|
||||||
|
onOpenChange={setShown}
|
||||||
triggerAs={Button}
|
triggerAs={Button}
|
||||||
triggerProps={{
|
triggerProps={{
|
||||||
variant: "ghost",
|
variant: "ghost",
|
||||||
class: "titlebar-icon w-6 h-6 p-0 box-border",
|
class: "titlebar-icon w-8 h-6 p-0 box-border",
|
||||||
"aria-label": language.t("status.popover.trigger"),
|
"aria-label": language.t("status.popover.trigger"),
|
||||||
style: { scale: 1 },
|
style: { scale: 1 },
|
||||||
}}
|
}}
|
||||||
trigger={
|
trigger={
|
||||||
<div class="flex size-4 items-center justify-center">
|
<div class="relative size-4">
|
||||||
|
<div class="badge-mask-tight size-4 flex items-center justify-center">
|
||||||
|
<Icon name={shown() ? "status-active" : "status"} size="small" />
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
"size-1.5 rounded-full": true,
|
"absolute -top-px -right-px size-1.5 rounded-full": true,
|
||||||
"bg-icon-success-base": overallHealthy(),
|
"bg-icon-success-base": overallHealthy(),
|
||||||
"bg-icon-critical-base": !overallHealthy() && server.healthy() !== undefined,
|
"bg-icon-critical-base": !overallHealthy() && server.healthy() !== undefined,
|
||||||
"bg-border-weak-base": server.healthy() === undefined,
|
"bg-border-weak-base": server.healthy() === undefined,
|
||||||
|
|||||||
@@ -65,6 +65,16 @@ const debugTerminal = (...values: unknown[]) => {
|
|||||||
console.debug("[terminal]", ...values)
|
console.debug("[terminal]", ...values)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const errorStatus = (err: unknown) => {
|
||||||
|
if (!err || typeof err !== "object") return
|
||||||
|
if (!("data" in err)) return
|
||||||
|
const data = err.data
|
||||||
|
if (!data || typeof data !== "object") return
|
||||||
|
if (!("statusCode" in data)) return
|
||||||
|
const status = data.statusCode
|
||||||
|
return typeof status === "number" ? status : undefined
|
||||||
|
}
|
||||||
|
|
||||||
const useTerminalUiBindings = (input: {
|
const useTerminalUiBindings = (input: {
|
||||||
container: HTMLDivElement
|
container: HTMLDivElement
|
||||||
term: Term
|
term: Term
|
||||||
@@ -189,7 +199,11 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
const start =
|
const start =
|
||||||
typeof local.pty.cursor === "number" && Number.isSafeInteger(local.pty.cursor) ? local.pty.cursor : undefined
|
typeof local.pty.cursor === "number" && Number.isSafeInteger(local.pty.cursor) ? local.pty.cursor : undefined
|
||||||
let cursor = start ?? 0
|
let cursor = start ?? 0
|
||||||
|
let seek = start !== undefined ? start : restore ? -1 : 0
|
||||||
let output: ReturnType<typeof terminalWriter> | undefined
|
let output: ReturnType<typeof terminalWriter> | undefined
|
||||||
|
let drop: VoidFunction | undefined
|
||||||
|
let reconn: ReturnType<typeof setTimeout> | undefined
|
||||||
|
let tries = 0
|
||||||
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
if (!cleanups.length) return
|
if (!cleanups.length) return
|
||||||
@@ -453,85 +467,135 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const once = { value: false }
|
const once = { value: false }
|
||||||
let closing = false
|
|
||||||
|
|
||||||
const url = new URL(sdk.url + `/pty/${id}/connect`)
|
|
||||||
url.searchParams.set("directory", sdk.directory)
|
|
||||||
url.searchParams.set("cursor", String(start !== undefined ? start : restore ? -1 : 0))
|
|
||||||
url.protocol = url.protocol === "https:" ? "wss:" : "ws:"
|
|
||||||
url.username = server.current?.http.username ?? "opencode"
|
|
||||||
url.password = server.current?.http.password ?? ""
|
|
||||||
|
|
||||||
const socket = new WebSocket(url)
|
|
||||||
socket.binaryType = "arraybuffer"
|
|
||||||
ws = socket
|
|
||||||
|
|
||||||
const handleOpen = () => {
|
|
||||||
probe.connect()
|
|
||||||
local.onConnect?.()
|
|
||||||
scheduleSize(t.cols, t.rows)
|
|
||||||
}
|
|
||||||
socket.addEventListener("open", handleOpen)
|
|
||||||
if (socket.readyState === WebSocket.OPEN) handleOpen()
|
|
||||||
|
|
||||||
const decoder = new TextDecoder()
|
const decoder = new TextDecoder()
|
||||||
const handleMessage = (event: MessageEvent) => {
|
|
||||||
if (disposed) return
|
|
||||||
if (closing) return
|
|
||||||
if (event.data instanceof ArrayBuffer) {
|
|
||||||
const bytes = new Uint8Array(event.data)
|
|
||||||
if (bytes[0] !== 0) return
|
|
||||||
const json = decoder.decode(bytes.subarray(1))
|
|
||||||
try {
|
|
||||||
const meta = JSON.parse(json) as { cursor?: unknown }
|
|
||||||
const next = meta?.cursor
|
|
||||||
if (typeof next === "number" && Number.isSafeInteger(next) && next >= 0) {
|
|
||||||
cursor = next
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
debugTerminal("invalid websocket control frame", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = typeof event.data === "string" ? event.data : ""
|
const fail = (err: unknown) => {
|
||||||
if (!data) return
|
|
||||||
output?.push(data)
|
|
||||||
cursor += data.length
|
|
||||||
}
|
|
||||||
socket.addEventListener("message", handleMessage)
|
|
||||||
|
|
||||||
const handleError = (error: Event) => {
|
|
||||||
if (disposed) return
|
if (disposed) return
|
||||||
if (closing) return
|
|
||||||
if (once.value) return
|
if (once.value) return
|
||||||
once.value = true
|
once.value = true
|
||||||
console.error("WebSocket error:", error)
|
local.onConnectError?.(err)
|
||||||
local.onConnectError?.(error)
|
|
||||||
}
|
}
|
||||||
socket.addEventListener("error", handleError)
|
|
||||||
|
|
||||||
const handleClose = (event: CloseEvent) => {
|
const gone = () =>
|
||||||
|
sdk.client.pty
|
||||||
|
.get({ ptyID: id })
|
||||||
|
.then(() => false)
|
||||||
|
.catch((err) => {
|
||||||
|
if (errorStatus(err) === 404) return true
|
||||||
|
debugTerminal("failed to inspect terminal session", err)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
const retry = (err: unknown) => {
|
||||||
if (disposed) return
|
if (disposed) return
|
||||||
if (closing) return
|
if (reconn !== undefined) return
|
||||||
// Normal closure (code 1000) means PTY process exited - server event handles cleanup
|
|
||||||
// For other codes (network issues, server restart), trigger error handler
|
|
||||||
if (event.code !== 1000) {
|
|
||||||
if (once.value) return
|
|
||||||
once.value = true
|
|
||||||
local.onConnectError?.(new Error(`WebSocket closed abnormally: ${event.code}`))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
socket.addEventListener("close", handleClose)
|
|
||||||
|
|
||||||
cleanups.push(() => {
|
const ms = Math.min(250 * 2 ** Math.min(tries, 4), 4_000)
|
||||||
closing = true
|
reconn = setTimeout(async () => {
|
||||||
socket.removeEventListener("open", handleOpen)
|
reconn = undefined
|
||||||
socket.removeEventListener("message", handleMessage)
|
if (disposed) return
|
||||||
socket.removeEventListener("error", handleError)
|
if (await gone()) {
|
||||||
socket.removeEventListener("close", handleClose)
|
if (disposed) return
|
||||||
if (socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) socket.close(1000)
|
fail(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (disposed) return
|
||||||
|
tries += 1
|
||||||
|
open()
|
||||||
|
}, ms)
|
||||||
|
}
|
||||||
|
|
||||||
|
const open = () => {
|
||||||
|
if (disposed) return
|
||||||
|
drop?.()
|
||||||
|
|
||||||
|
const url = new URL(sdk.url + `/pty/${id}/connect`)
|
||||||
|
url.searchParams.set("directory", sdk.directory)
|
||||||
|
url.searchParams.set("cursor", String(seek))
|
||||||
|
url.protocol = url.protocol === "https:" ? "wss:" : "ws:"
|
||||||
|
url.username = server.current?.http.username ?? "opencode"
|
||||||
|
url.password = server.current?.http.password ?? ""
|
||||||
|
|
||||||
|
const socket = new WebSocket(url)
|
||||||
|
socket.binaryType = "arraybuffer"
|
||||||
|
ws = socket
|
||||||
|
|
||||||
|
const handleOpen = () => {
|
||||||
|
if (disposed) return
|
||||||
|
tries = 0
|
||||||
|
probe.connect()
|
||||||
|
local.onConnect?.()
|
||||||
|
scheduleSize(t.cols, t.rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMessage = (event: MessageEvent) => {
|
||||||
|
if (disposed) return
|
||||||
|
if (event.data instanceof ArrayBuffer) {
|
||||||
|
const bytes = new Uint8Array(event.data)
|
||||||
|
if (bytes[0] !== 0) return
|
||||||
|
const json = decoder.decode(bytes.subarray(1))
|
||||||
|
try {
|
||||||
|
const meta = JSON.parse(json) as { cursor?: unknown }
|
||||||
|
const next = meta?.cursor
|
||||||
|
if (typeof next === "number" && Number.isSafeInteger(next) && next >= 0) {
|
||||||
|
cursor = next
|
||||||
|
seek = next
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
debugTerminal("invalid websocket control frame", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = typeof event.data === "string" ? event.data : ""
|
||||||
|
if (!data) return
|
||||||
|
output?.push(data)
|
||||||
|
cursor += data.length
|
||||||
|
seek = cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleError = (error: Event) => {
|
||||||
|
if (disposed) return
|
||||||
|
debugTerminal("websocket error", error)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stop = () => {
|
||||||
|
socket.removeEventListener("open", handleOpen)
|
||||||
|
socket.removeEventListener("message", handleMessage)
|
||||||
|
socket.removeEventListener("error", handleError)
|
||||||
|
socket.removeEventListener("close", handleClose)
|
||||||
|
if (ws === socket) ws = undefined
|
||||||
|
if (drop === stop) drop = undefined
|
||||||
|
if (socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) socket.close(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = (event: CloseEvent) => {
|
||||||
|
if (ws === socket) ws = undefined
|
||||||
|
if (drop === stop) drop = undefined
|
||||||
|
socket.removeEventListener("open", handleOpen)
|
||||||
|
socket.removeEventListener("message", handleMessage)
|
||||||
|
socket.removeEventListener("error", handleError)
|
||||||
|
socket.removeEventListener("close", handleClose)
|
||||||
|
if (disposed) return
|
||||||
|
if (event.code === 1000) return
|
||||||
|
retry(new Error(language.t("terminal.connectionLost.abnormalClose", { code: event.code })))
|
||||||
|
}
|
||||||
|
|
||||||
|
drop = stop
|
||||||
|
socket.addEventListener("open", handleOpen)
|
||||||
|
socket.addEventListener("message", handleMessage)
|
||||||
|
socket.addEventListener("error", handleError)
|
||||||
|
socket.addEventListener("close", handleClose)
|
||||||
|
}
|
||||||
|
|
||||||
|
probe.control({
|
||||||
|
disconnect: () => {
|
||||||
|
if (!ws) return
|
||||||
|
ws.close(4_000, "e2e")
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
open()
|
||||||
}
|
}
|
||||||
|
|
||||||
void run().catch((err) => {
|
void run().catch((err) => {
|
||||||
@@ -549,6 +613,8 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
disposed = true
|
disposed = true
|
||||||
if (fitFrame !== undefined) cancelAnimationFrame(fitFrame)
|
if (fitFrame !== undefined) cancelAnimationFrame(fitFrame)
|
||||||
if (sizeTimer !== undefined) clearTimeout(sizeTimer)
|
if (sizeTimer !== undefined) clearTimeout(sizeTimer)
|
||||||
|
if (reconn !== undefined) clearTimeout(reconn)
|
||||||
|
drop?.()
|
||||||
if (ws && ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) ws.close(1000)
|
if (ws && ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) ws.close(1000)
|
||||||
|
|
||||||
const finalize = () => {
|
const finalize = () => {
|
||||||
|
|||||||
@@ -58,6 +58,12 @@ export function Titlebar() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const path = () => `${location.pathname}${location.search}${location.hash}`
|
const path = () => `${location.pathname}${location.search}${location.hash}`
|
||||||
|
const creating = createMemo(() => {
|
||||||
|
if (!params.dir) return false
|
||||||
|
if (params.id) return false
|
||||||
|
const parts = location.pathname.replace(/\/+$/, "").split("/")
|
||||||
|
return parts.at(-1) === "session"
|
||||||
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const current = path()
|
const current = path()
|
||||||
@@ -206,19 +212,7 @@ export function Titlebar() {
|
|||||||
aria-label={language.t("command.sidebar.toggle")}
|
aria-label={language.t("command.sidebar.toggle")}
|
||||||
aria-expanded={layout.sidebar.opened()}
|
aria-expanded={layout.sidebar.opened()}
|
||||||
>
|
>
|
||||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
<Icon size="small" name={layout.sidebar.opened() ? "sidebar-active" : "sidebar"} />
|
||||||
<Icon
|
|
||||||
size="small"
|
|
||||||
name={layout.sidebar.opened() ? "layout-left-partial" : "layout-left"}
|
|
||||||
class="group-hover/sidebar-toggle:hidden"
|
|
||||||
/>
|
|
||||||
<Icon size="small" name="layout-left-partial" class="hidden group-hover/sidebar-toggle:inline-block" />
|
|
||||||
<Icon
|
|
||||||
size="small"
|
|
||||||
name={layout.sidebar.opened() ? "layout-left" : "layout-left-partial"}
|
|
||||||
class="hidden group-active/sidebar-toggle:inline-block"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
<div class="hidden xl:flex items-center shrink-0">
|
<div class="hidden xl:flex items-center shrink-0">
|
||||||
@@ -231,13 +225,14 @@ export function Titlebar() {
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
icon="new-session"
|
icon={creating() ? "new-session-active" : "new-session"}
|
||||||
class="titlebar-icon w-8 h-6 p-0 box-border"
|
class="titlebar-icon w-8 h-6 p-0 box-border"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!params.dir) return
|
if (!params.dir) return
|
||||||
navigate(`/${params.dir}/session`)
|
navigate(`/${params.dir}/session`)
|
||||||
}}
|
}}
|
||||||
aria-label={language.t("command.session.new")}
|
aria-label={language.t("command.session.new")}
|
||||||
|
aria-current={creating() ? "page" : undefined}
|
||||||
/>
|
/>
|
||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { createEffect, createMemo, onCleanup, onMount, type Accessor } from "sol
|
|||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
|
import { dict as en } from "@/i18n/en"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
@@ -13,6 +14,27 @@ const DEFAULT_PALETTE_KEYBIND = "mod+shift+p"
|
|||||||
const SUGGESTED_PREFIX = "suggested."
|
const SUGGESTED_PREFIX = "suggested."
|
||||||
const EDITABLE_KEYBIND_IDS = new Set(["terminal.toggle", "terminal.new", "file.attach"])
|
const EDITABLE_KEYBIND_IDS = new Set(["terminal.toggle", "terminal.new", "file.attach"])
|
||||||
|
|
||||||
|
type KeyLabel =
|
||||||
|
| "common.key.ctrl"
|
||||||
|
| "common.key.alt"
|
||||||
|
| "common.key.shift"
|
||||||
|
| "common.key.meta"
|
||||||
|
| "common.key.space"
|
||||||
|
| "common.key.backspace"
|
||||||
|
| "common.key.enter"
|
||||||
|
| "common.key.tab"
|
||||||
|
| "common.key.delete"
|
||||||
|
| "common.key.home"
|
||||||
|
| "common.key.end"
|
||||||
|
| "common.key.pageUp"
|
||||||
|
| "common.key.pageDown"
|
||||||
|
| "common.key.insert"
|
||||||
|
| "common.key.esc"
|
||||||
|
|
||||||
|
function keyText(key: KeyLabel, t?: (key: KeyLabel) => string) {
|
||||||
|
return t ? t(key) : en[key]
|
||||||
|
}
|
||||||
|
|
||||||
function actionId(id: string) {
|
function actionId(id: string) {
|
||||||
if (!id.startsWith(SUGGESTED_PREFIX)) return id
|
if (!id.startsWith(SUGGESTED_PREFIX)) return id
|
||||||
return id.slice(SUGGESTED_PREFIX.length)
|
return id.slice(SUGGESTED_PREFIX.length)
|
||||||
@@ -145,7 +167,7 @@ export function matchKeybind(keybinds: Keybind[], event: KeyboardEvent): boolean
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatKeybind(config: string): string {
|
export function formatKeybind(config: string, t?: (key: KeyLabel) => string): string {
|
||||||
if (!config || config === "none") return ""
|
if (!config || config === "none") return ""
|
||||||
|
|
||||||
const keybinds = parseKeybind(config)
|
const keybinds = parseKeybind(config)
|
||||||
@@ -154,10 +176,10 @@ export function formatKeybind(config: string): string {
|
|||||||
const kb = keybinds[0]
|
const kb = keybinds[0]
|
||||||
const parts: string[] = []
|
const parts: string[] = []
|
||||||
|
|
||||||
if (kb.ctrl) parts.push(IS_MAC ? "⌃" : "Ctrl")
|
if (kb.ctrl) parts.push(IS_MAC ? "⌃" : keyText("common.key.ctrl", t))
|
||||||
if (kb.alt) parts.push(IS_MAC ? "⌥" : "Alt")
|
if (kb.alt) parts.push(IS_MAC ? "⌥" : keyText("common.key.alt", t))
|
||||||
if (kb.shift) parts.push(IS_MAC ? "⇧" : "Shift")
|
if (kb.shift) parts.push(IS_MAC ? "⇧" : keyText("common.key.shift", t))
|
||||||
if (kb.meta) parts.push(IS_MAC ? "⌘" : "Meta")
|
if (kb.meta) parts.push(IS_MAC ? "⌘" : keyText("common.key.meta", t))
|
||||||
|
|
||||||
if (kb.key) {
|
if (kb.key) {
|
||||||
const keys: Record<string, string> = {
|
const keys: Record<string, string> = {
|
||||||
@@ -167,10 +189,29 @@ export function formatKeybind(config: string): string {
|
|||||||
arrowright: "→",
|
arrowright: "→",
|
||||||
comma: ",",
|
comma: ",",
|
||||||
plus: "+",
|
plus: "+",
|
||||||
space: "Space",
|
}
|
||||||
|
const named: Record<string, KeyLabel> = {
|
||||||
|
backspace: "common.key.backspace",
|
||||||
|
delete: "common.key.delete",
|
||||||
|
end: "common.key.end",
|
||||||
|
enter: "common.key.enter",
|
||||||
|
esc: "common.key.esc",
|
||||||
|
escape: "common.key.esc",
|
||||||
|
home: "common.key.home",
|
||||||
|
insert: "common.key.insert",
|
||||||
|
pagedown: "common.key.pageDown",
|
||||||
|
pageup: "common.key.pageUp",
|
||||||
|
space: "common.key.space",
|
||||||
|
tab: "common.key.tab",
|
||||||
}
|
}
|
||||||
const key = kb.key.toLowerCase()
|
const key = kb.key.toLowerCase()
|
||||||
const displayKey = keys[key] ?? (key.length === 1 ? key.toUpperCase() : key.charAt(0).toUpperCase() + key.slice(1))
|
const displayKey =
|
||||||
|
keys[key] ??
|
||||||
|
(named[key]
|
||||||
|
? keyText(named[key], t)
|
||||||
|
: key.length === 1
|
||||||
|
? key.toUpperCase()
|
||||||
|
: key.charAt(0).toUpperCase() + key.slice(1))
|
||||||
parts.push(displayKey)
|
parts.push(displayKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,17 +405,17 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
|
|||||||
},
|
},
|
||||||
keybind(id: string) {
|
keybind(id: string) {
|
||||||
if (id === PALETTE_ID) {
|
if (id === PALETTE_ID) {
|
||||||
return formatKeybind(settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND)
|
return formatKeybind(settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND, language.t)
|
||||||
}
|
}
|
||||||
|
|
||||||
const base = actionId(id)
|
const base = actionId(id)
|
||||||
const option = options().find((x) => actionId(x.id) === base)
|
const option = options().find((x) => actionId(x.id) === base)
|
||||||
if (option?.keybind) return formatKeybind(option.keybind)
|
if (option?.keybind) return formatKeybind(option.keybind, language.t)
|
||||||
|
|
||||||
const meta = catalog[base]
|
const meta = catalog[base]
|
||||||
const config = bind(base, meta?.keybind)
|
const config = bind(base, meta?.keybind)
|
||||||
if (!config) return ""
|
if (!config) return ""
|
||||||
return formatKeybind(config)
|
return formatKeybind(config, language.t)
|
||||||
},
|
},
|
||||||
show: showPalette,
|
show: showPalette,
|
||||||
keybinds(enabled: boolean) {
|
keybinds(enabled: boolean) {
|
||||||
|
|||||||
@@ -43,10 +43,10 @@ export {
|
|||||||
touchFileContent,
|
touchFileContent,
|
||||||
}
|
}
|
||||||
|
|
||||||
function errorMessage(error: unknown) {
|
function errorMessage(error: unknown, fallback: string) {
|
||||||
if (error instanceof Error && error.message) return error.message
|
if (error instanceof Error && error.message) return error.message
|
||||||
if (typeof error === "string" && error) return error
|
if (typeof error === "string" && error) return error
|
||||||
return "Unknown error"
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
||||||
@@ -184,7 +184,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
|||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
if (scope() !== directory) return
|
if (scope() !== directory) return
|
||||||
setLoadError(file, errorMessage(e))
|
setLoadError(file, errorMessage(e, language.t("error.chain.unknown")))
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
inflight.delete(key)
|
inflight.delete(key)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
|||||||
import { batch, onCleanup } from "solid-js"
|
import { batch, onCleanup } from "solid-js"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { createSdkForServer } from "@/utils/server"
|
import { createSdkForServer } from "@/utils/server"
|
||||||
|
import { useLanguage } from "./language"
|
||||||
import { usePlatform } from "./platform"
|
import { usePlatform } from "./platform"
|
||||||
import { useServer } from "./server"
|
import { useServer } from "./server"
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ const abortError = z.object({
|
|||||||
export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleContext({
|
export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleContext({
|
||||||
name: "GlobalSDK",
|
name: "GlobalSDK",
|
||||||
init: () => {
|
init: () => {
|
||||||
|
const language = useLanguage()
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const abort = new AbortController()
|
const abort = new AbortController()
|
||||||
@@ -30,7 +32,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
|||||||
})()
|
})()
|
||||||
|
|
||||||
const currentServer = server.current
|
const currentServer = server.current
|
||||||
if (!currentServer) throw new Error("No server available")
|
if (!currentServer) throw new Error(language.t("error.globalSDK.noServerAvailable"))
|
||||||
|
|
||||||
const eventSdk = createSdkForServer({
|
const eventSdk = createSdkForServer({
|
||||||
signal: abort.signal,
|
signal: abort.signal,
|
||||||
@@ -218,7 +220,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
|||||||
event: emitter,
|
event: emitter,
|
||||||
createClient(opts: Omit<Parameters<typeof createSdkForServer>[0], "server" | "fetch">) {
|
createClient(opts: Omit<Parameters<typeof createSdkForServer>[0], "server" | "fetch">) {
|
||||||
const s = server.current
|
const s = server.current
|
||||||
if (!s) throw new Error("Server not available")
|
if (!s) throw new Error(language.t("error.globalSDK.serverNotAvailable"))
|
||||||
return createSdkForServer({
|
return createSdkForServer({
|
||||||
server: s.http,
|
server: s.http,
|
||||||
fetch: platform.fetch,
|
fetch: platform.fetch,
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ function createGlobalSync() {
|
|||||||
sdkCache.delete(directory)
|
sdkCache.delete(directory)
|
||||||
clearSessionPrefetchDirectory(directory)
|
clearSessionPrefetchDirectory(directory)
|
||||||
},
|
},
|
||||||
|
translate: language.t,
|
||||||
})
|
})
|
||||||
|
|
||||||
const sdkFor = (directory: string) => {
|
const sdkFor = (directory: string) => {
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export async function bootstrapDirectory(input: {
|
|||||||
const project = getFilename(input.directory)
|
const project = getFilename(input.directory)
|
||||||
showToast({
|
showToast({
|
||||||
variant: "error",
|
variant: "error",
|
||||||
title: `Failed to reload ${project}`,
|
title: input.translate("toast.project.reloadFailed.title", { project }),
|
||||||
description: formatServerError(err, input.translate),
|
description: formatServerError(err, input.translate),
|
||||||
})
|
})
|
||||||
input.setStore("status", "partial")
|
input.setStore("status", "partial")
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ describe("createChildStoreManager", () => {
|
|||||||
isLoadingSessions: () => false,
|
isLoadingSessions: () => false,
|
||||||
onBootstrap() {},
|
onBootstrap() {},
|
||||||
onDispose() {},
|
onDispose() {},
|
||||||
|
translate: (key) => key,
|
||||||
})
|
})
|
||||||
|
|
||||||
Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => {
|
Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export function createChildStoreManager(input: {
|
|||||||
isLoadingSessions: (directory: string) => boolean
|
isLoadingSessions: (directory: string) => boolean
|
||||||
onBootstrap: (directory: string) => void
|
onBootstrap: (directory: string) => void
|
||||||
onDispose: (directory: string) => void
|
onDispose: (directory: string) => void
|
||||||
|
translate: (key: string, vars?: Record<string, string | number>) => string
|
||||||
}) {
|
}) {
|
||||||
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
|
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
|
||||||
const vcsCache = new Map<string, VcsCache>()
|
const vcsCache = new Map<string, VcsCache>()
|
||||||
@@ -129,7 +130,7 @@ export function createChildStoreManager(input: {
|
|||||||
createStore({ value: undefined as VcsInfo | undefined }),
|
createStore({ value: undefined as VcsInfo | undefined }),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if (!vcs) throw new Error("Failed to create persisted cache")
|
if (!vcs) throw new Error(input.translate("error.childStore.persistedCacheCreateFailed"))
|
||||||
const vcsStore = vcs[0]
|
const vcsStore = vcs[0]
|
||||||
vcsCache.set(directory, { store: vcsStore, setStore: vcs[1], ready: vcs[3] })
|
vcsCache.set(directory, { store: vcsStore, setStore: vcs[1], ready: vcs[3] })
|
||||||
|
|
||||||
@@ -139,7 +140,7 @@ export function createChildStoreManager(input: {
|
|||||||
createStore({ value: undefined as ProjectMeta | undefined }),
|
createStore({ value: undefined as ProjectMeta | undefined }),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if (!meta) throw new Error("Failed to create persisted project metadata")
|
if (!meta) throw new Error(input.translate("error.childStore.persistedProjectMetadataCreateFailed"))
|
||||||
metaCache.set(directory, { store: meta[0], setStore: meta[1], ready: meta[3] })
|
metaCache.set(directory, { store: meta[0], setStore: meta[1], ready: meta[3] })
|
||||||
|
|
||||||
const icon = runWithOwner(input.owner, () =>
|
const icon = runWithOwner(input.owner, () =>
|
||||||
@@ -148,7 +149,7 @@ export function createChildStoreManager(input: {
|
|||||||
createStore({ value: undefined as string | undefined }),
|
createStore({ value: undefined as string | undefined }),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if (!icon) throw new Error("Failed to create persisted project icon")
|
if (!icon) throw new Error(input.translate("error.childStore.persistedProjectIconCreateFailed"))
|
||||||
iconCache.set(directory, { store: icon[0], setStore: icon[1], ready: icon[3] })
|
iconCache.set(directory, { store: icon[0], setStore: icon[1], ready: icon[3] })
|
||||||
|
|
||||||
const init = () =>
|
const init = () =>
|
||||||
@@ -211,7 +212,7 @@ export function createChildStoreManager(input: {
|
|||||||
}
|
}
|
||||||
mark(directory)
|
mark(directory)
|
||||||
const childStore = children[directory]
|
const childStore = children[directory]
|
||||||
if (!childStore) throw new Error("Failed to create store")
|
if (!childStore) throw new Error(input.translate("error.childStore.storeCreateFailed"))
|
||||||
return childStore
|
return childStore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
getSessionPrefetch,
|
getSessionPrefetch,
|
||||||
runSessionPrefetch,
|
runSessionPrefetch,
|
||||||
setSessionPrefetch,
|
setSessionPrefetch,
|
||||||
|
shouldSkipSessionPrefetch,
|
||||||
} from "./session-prefetch"
|
} from "./session-prefetch"
|
||||||
|
|
||||||
describe("session prefetch", () => {
|
describe("session prefetch", () => {
|
||||||
@@ -16,11 +17,12 @@ describe("session prefetch", () => {
|
|||||||
directory: "/tmp/a",
|
directory: "/tmp/a",
|
||||||
sessionID: "ses_1",
|
sessionID: "ses_1",
|
||||||
limit: 200,
|
limit: 200,
|
||||||
|
cursor: "abc",
|
||||||
complete: false,
|
complete: false,
|
||||||
at: 123,
|
at: 123,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(getSessionPrefetch("/tmp/a", "ses_1")).toEqual({ limit: 200, complete: false, at: 123 })
|
expect(getSessionPrefetch("/tmp/a", "ses_1")).toEqual({ limit: 200, cursor: "abc", complete: false, at: 123 })
|
||||||
expect(getSessionPrefetch("/tmp/b", "ses_1")).toBeUndefined()
|
expect(getSessionPrefetch("/tmp/b", "ses_1")).toBeUndefined()
|
||||||
|
|
||||||
clearSessionPrefetch("/tmp/a", ["ses_1"])
|
clearSessionPrefetch("/tmp/a", ["ses_1"])
|
||||||
@@ -38,26 +40,57 @@ describe("session prefetch", () => {
|
|||||||
sessionID: "ses_2",
|
sessionID: "ses_2",
|
||||||
task: async () => {
|
task: async () => {
|
||||||
calls += 1
|
calls += 1
|
||||||
return { limit: 100, complete: true, at: 456 }
|
return { limit: 100, cursor: "next", complete: true, at: 456 }
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const [a, b] = await Promise.all([run(), run()])
|
const [a, b] = await Promise.all([run(), run()])
|
||||||
|
|
||||||
expect(calls).toBe(1)
|
expect(calls).toBe(1)
|
||||||
expect(a).toEqual({ limit: 100, complete: true, at: 456 })
|
expect(a).toEqual({ limit: 100, cursor: "next", complete: true, at: 456 })
|
||||||
expect(b).toEqual({ limit: 100, complete: true, at: 456 })
|
expect(b).toEqual({ limit: 100, cursor: "next", complete: true, at: 456 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("clears a whole directory", () => {
|
test("clears a whole directory", () => {
|
||||||
setSessionPrefetch({ directory: "/tmp/d", sessionID: "ses_1", limit: 10, complete: true, at: 1 })
|
setSessionPrefetch({ directory: "/tmp/d", sessionID: "ses_1", limit: 10, cursor: "a", complete: true, at: 1 })
|
||||||
setSessionPrefetch({ directory: "/tmp/d", sessionID: "ses_2", limit: 20, complete: false, at: 2 })
|
setSessionPrefetch({ directory: "/tmp/d", sessionID: "ses_2", limit: 20, cursor: "b", complete: false, at: 2 })
|
||||||
setSessionPrefetch({ directory: "/tmp/e", sessionID: "ses_1", limit: 30, complete: true, at: 3 })
|
setSessionPrefetch({ directory: "/tmp/e", sessionID: "ses_1", limit: 30, cursor: "c", complete: true, at: 3 })
|
||||||
|
|
||||||
clearSessionPrefetchDirectory("/tmp/d")
|
clearSessionPrefetchDirectory("/tmp/d")
|
||||||
|
|
||||||
expect(getSessionPrefetch("/tmp/d", "ses_1")).toBeUndefined()
|
expect(getSessionPrefetch("/tmp/d", "ses_1")).toBeUndefined()
|
||||||
expect(getSessionPrefetch("/tmp/d", "ses_2")).toBeUndefined()
|
expect(getSessionPrefetch("/tmp/d", "ses_2")).toBeUndefined()
|
||||||
expect(getSessionPrefetch("/tmp/e", "ses_1")).toEqual({ limit: 30, complete: true, at: 3 })
|
expect(getSessionPrefetch("/tmp/e", "ses_1")).toEqual({ limit: 30, cursor: "c", complete: true, at: 3 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("refreshes stale first-page prefetched history", () => {
|
||||||
|
expect(
|
||||||
|
shouldSkipSessionPrefetch({
|
||||||
|
message: true,
|
||||||
|
info: { limit: 200, cursor: "x", complete: false, at: 1 },
|
||||||
|
chunk: 200,
|
||||||
|
now: 1 + 15_001,
|
||||||
|
}),
|
||||||
|
).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("keeps deeper or complete history cached", () => {
|
||||||
|
expect(
|
||||||
|
shouldSkipSessionPrefetch({
|
||||||
|
message: true,
|
||||||
|
info: { limit: 400, cursor: "x", complete: false, at: 1 },
|
||||||
|
chunk: 200,
|
||||||
|
now: 1 + 15_001,
|
||||||
|
}),
|
||||||
|
).toBe(true)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
shouldSkipSessionPrefetch({
|
||||||
|
message: true,
|
||||||
|
info: { limit: 120, complete: true, at: 1 },
|
||||||
|
chunk: 200,
|
||||||
|
now: 1 + 15_001,
|
||||||
|
}),
|
||||||
|
).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,10 +4,23 @@ export const SESSION_PREFETCH_TTL = 15_000
|
|||||||
|
|
||||||
type Meta = {
|
type Meta = {
|
||||||
limit: number
|
limit: number
|
||||||
|
cursor?: string
|
||||||
complete: boolean
|
complete: boolean
|
||||||
at: number
|
at: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function shouldSkipSessionPrefetch(input: { message: boolean; info?: Meta; chunk: number; now?: number }) {
|
||||||
|
if (input.message) {
|
||||||
|
if (!input.info) return true
|
||||||
|
if (input.info.complete) return true
|
||||||
|
if (input.info.limit > input.chunk) return true
|
||||||
|
} else {
|
||||||
|
if (!input.info) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return (input.now ?? Date.now()) - input.info.at < SESSION_PREFETCH_TTL
|
||||||
|
}
|
||||||
|
|
||||||
const cache = new Map<string, Meta>()
|
const cache = new Map<string, Meta>()
|
||||||
const inflight = new Map<string, Promise<Meta | undefined>>()
|
const inflight = new Map<string, Promise<Meta | undefined>>()
|
||||||
const rev = new Map<string, number>()
|
const rev = new Map<string, number>()
|
||||||
@@ -53,11 +66,13 @@ export function setSessionPrefetch(input: {
|
|||||||
directory: string
|
directory: string
|
||||||
sessionID: string
|
sessionID: string
|
||||||
limit: number
|
limit: number
|
||||||
|
cursor?: string
|
||||||
complete: boolean
|
complete: boolean
|
||||||
at?: number
|
at?: number
|
||||||
}) {
|
}) {
|
||||||
cache.set(key(input.directory, input.sessionID), {
|
cache.set(key(input.directory, input.sessionID), {
|
||||||
limit: input.limit,
|
limit: input.limit,
|
||||||
|
cursor: input.cursor,
|
||||||
complete: input.complete,
|
complete: input.complete,
|
||||||
at: input.at ?? Date.now(),
|
at: input.at ?? Date.now(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export interface Settings {
|
|||||||
general: {
|
general: {
|
||||||
autoSave: boolean
|
autoSave: boolean
|
||||||
releaseNotes: boolean
|
releaseNotes: boolean
|
||||||
|
followup: "queue" | "steer"
|
||||||
showReasoningSummaries: boolean
|
showReasoningSummaries: boolean
|
||||||
shellToolPartsExpanded: boolean
|
shellToolPartsExpanded: boolean
|
||||||
editToolPartsExpanded: boolean
|
editToolPartsExpanded: boolean
|
||||||
@@ -45,6 +46,7 @@ const defaultSettings: Settings = {
|
|||||||
general: {
|
general: {
|
||||||
autoSave: true,
|
autoSave: true,
|
||||||
releaseNotes: true,
|
releaseNotes: true,
|
||||||
|
followup: "steer",
|
||||||
showReasoningSummaries: false,
|
showReasoningSummaries: false,
|
||||||
shellToolPartsExpanded: true,
|
shellToolPartsExpanded: true,
|
||||||
editToolPartsExpanded: false,
|
editToolPartsExpanded: false,
|
||||||
@@ -126,6 +128,10 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
|||||||
setReleaseNotes(value: boolean) {
|
setReleaseNotes(value: boolean) {
|
||||||
setStore("general", "releaseNotes", value)
|
setStore("general", "releaseNotes", value)
|
||||||
},
|
},
|
||||||
|
followup: withFallback(() => store.general?.followup, defaultSettings.general.followup),
|
||||||
|
setFollowup(value: "queue" | "steer") {
|
||||||
|
setStore("general", "followup", value)
|
||||||
|
},
|
||||||
showReasoningSummaries: withFallback(
|
showReasoningSummaries: withFallback(
|
||||||
() => store.general?.showReasoningSummaries,
|
() => store.general?.showReasoningSummaries,
|
||||||
defaultSettings.general.showReasoningSummaries,
|
defaultSettings.general.showReasoningSummaries,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
|
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
|
||||||
import { applyOptimisticAdd, applyOptimisticRemove } from "./sync"
|
import { applyOptimisticAdd, applyOptimisticRemove, mergeOptimisticPage } from "./sync"
|
||||||
|
|
||||||
|
type Text = Extract<Part, { type: "text" }>
|
||||||
|
|
||||||
const userMessage = (id: string, sessionID: string): Message => ({
|
const userMessage = (id: string, sessionID: string): Message => ({
|
||||||
id,
|
id,
|
||||||
@@ -11,7 +13,7 @@ const userMessage = (id: string, sessionID: string): Message => ({
|
|||||||
model: { providerID: "openai", modelID: "gpt" },
|
model: { providerID: "openai", modelID: "gpt" },
|
||||||
})
|
})
|
||||||
|
|
||||||
const textPart = (id: string, sessionID: string, messageID: string): Part => ({
|
const textPart = (id: string, sessionID: string, messageID: string): Text => ({
|
||||||
id,
|
id,
|
||||||
sessionID,
|
sessionID,
|
||||||
messageID,
|
messageID,
|
||||||
@@ -53,4 +55,69 @@ describe("sync optimistic reducers", () => {
|
|||||||
expect(draft.part.msg_1).toBeUndefined()
|
expect(draft.part.msg_1).toBeUndefined()
|
||||||
expect(draft.part.msg_2).toHaveLength(1)
|
expect(draft.part.msg_2).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("mergeOptimisticPage keeps pending messages in fetched timelines", () => {
|
||||||
|
const sessionID = "ses_1"
|
||||||
|
const page = mergeOptimisticPage(
|
||||||
|
{
|
||||||
|
session: [userMessage("msg_1", sessionID)],
|
||||||
|
part: [{ id: "msg_1", part: [textPart("prt_1", sessionID, "msg_1")] }],
|
||||||
|
complete: true,
|
||||||
|
},
|
||||||
|
[{ message: userMessage("msg_2", sessionID), parts: [textPart("prt_2", sessionID, "msg_2")] }],
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(page.session.map((x) => x.id)).toEqual(["msg_1", "msg_2"])
|
||||||
|
expect(page.part.find((x) => x.id === "msg_2")?.part.map((x) => x.id)).toEqual(["prt_2"])
|
||||||
|
expect(page.confirmed).toEqual([])
|
||||||
|
expect(page.complete).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("mergeOptimisticPage keeps missing optimistic parts until the server has them", () => {
|
||||||
|
const sessionID = "ses_1"
|
||||||
|
const page = mergeOptimisticPage(
|
||||||
|
{
|
||||||
|
session: [userMessage("msg_2", sessionID)],
|
||||||
|
part: [{ id: "msg_2", part: [textPart("prt_2", sessionID, "msg_2")] }],
|
||||||
|
complete: true,
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
message: userMessage("msg_2", sessionID),
|
||||||
|
parts: [textPart("prt_1", sessionID, "msg_2"), textPart("prt_2", sessionID, "msg_2")],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(page.part.find((x) => x.id === "msg_2")?.part.map((x) => x.id)).toEqual(["prt_1", "prt_2"])
|
||||||
|
expect(page.confirmed).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("mergeOptimisticPage confirms echoed messages once all parts arrive", () => {
|
||||||
|
const sessionID = "ses_1"
|
||||||
|
const page = mergeOptimisticPage(
|
||||||
|
{
|
||||||
|
session: [userMessage("msg_2", sessionID)],
|
||||||
|
part: [
|
||||||
|
{
|
||||||
|
id: "msg_2",
|
||||||
|
part: [{ ...textPart("prt_1", sessionID, "msg_2"), text: "server" }, textPart("prt_2", sessionID, "msg_2")],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
complete: true,
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
message: userMessage("msg_2", sessionID),
|
||||||
|
parts: [textPart("prt_1", sessionID, "msg_2"), textPart("prt_2", sessionID, "msg_2")],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(page.confirmed).toEqual(["msg_2"])
|
||||||
|
expect(page.part.find((x) => x.id === "msg_2")?.part).toMatchObject([
|
||||||
|
{ id: "prt_1", type: "text", text: "server" },
|
||||||
|
{ id: "prt_2", type: "text", text: "prt_2" },
|
||||||
|
])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ const keyFor = (directory: string, id: string) => `${directory}\n${id}`
|
|||||||
|
|
||||||
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
||||||
|
|
||||||
|
function merge<T extends { id: string }>(a: readonly T[], b: readonly T[]) {
|
||||||
|
const map = new Map(a.map((item) => [item.id, item] as const))
|
||||||
|
for (const item of b) map.set(item.id, item)
|
||||||
|
return [...map.values()].sort((x, y) => cmp(x.id, y.id))
|
||||||
|
}
|
||||||
|
|
||||||
type OptimisticStore = {
|
type OptimisticStore = {
|
||||||
message: Record<string, Message[] | undefined>
|
message: Record<string, Message[] | undefined>
|
||||||
part: Record<string, Part[] | undefined>
|
part: Record<string, Part[] | undefined>
|
||||||
@@ -48,6 +54,67 @@ type OptimisticRemoveInput = {
|
|||||||
messageID: string
|
messageID: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OptimisticItem = {
|
||||||
|
message: Message
|
||||||
|
parts: Part[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessagePage = {
|
||||||
|
session: Message[]
|
||||||
|
part: { id: string; part: Part[] }[]
|
||||||
|
cursor?: string
|
||||||
|
complete: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasParts = (parts: Part[] | undefined, want: Part[]) => {
|
||||||
|
if (!parts) return want.length === 0
|
||||||
|
return want.every((part) => Binary.search(parts, part.id, (item) => item.id).found)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergeParts = (parts: Part[] | undefined, want: Part[]) => {
|
||||||
|
if (!parts) return sortParts(want)
|
||||||
|
const next = [...parts]
|
||||||
|
let changed = false
|
||||||
|
for (const part of want) {
|
||||||
|
const result = Binary.search(next, part.id, (item) => item.id)
|
||||||
|
if (result.found) continue
|
||||||
|
next.splice(result.index, 0, part)
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if (!changed) return parts
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergeOptimisticPage(page: MessagePage, items: OptimisticItem[]) {
|
||||||
|
if (items.length === 0) return { ...page, confirmed: [] as string[] }
|
||||||
|
|
||||||
|
const session = [...page.session]
|
||||||
|
const part = new Map(page.part.map((item) => [item.id, sortParts(item.part)]))
|
||||||
|
const confirmed: string[] = []
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const result = Binary.search(session, item.message.id, (message) => message.id)
|
||||||
|
const found = result.found
|
||||||
|
if (!found) session.splice(result.index, 0, item.message)
|
||||||
|
|
||||||
|
const current = part.get(item.message.id)
|
||||||
|
if (found && hasParts(current, item.parts)) {
|
||||||
|
confirmed.push(item.message.id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
part.set(item.message.id, mergeParts(current, item.parts))
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
cursor: page.cursor,
|
||||||
|
complete: page.complete,
|
||||||
|
session,
|
||||||
|
part: [...part.entries()].sort((a, b) => cmp(a[0], b[0])).map(([id, part]) => ({ id, part })),
|
||||||
|
confirmed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function applyOptimisticAdd(draft: OptimisticStore, input: OptimisticAddInput) {
|
export function applyOptimisticAdd(draft: OptimisticStore, input: OptimisticAddInput) {
|
||||||
const messages = draft.message[input.sessionID]
|
const messages = draft.message[input.sessionID]
|
||||||
if (messages) {
|
if (messages) {
|
||||||
@@ -115,10 +182,12 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
const inflight = new Map<string, Promise<void>>()
|
const inflight = new Map<string, Promise<void>>()
|
||||||
const inflightDiff = new Map<string, Promise<void>>()
|
const inflightDiff = new Map<string, Promise<void>>()
|
||||||
const inflightTodo = new Map<string, Promise<void>>()
|
const inflightTodo = new Map<string, Promise<void>>()
|
||||||
|
const optimistic = new Map<string, Map<string, OptimisticItem>>()
|
||||||
const maxDirs = 30
|
const maxDirs = 30
|
||||||
const seen = new Map<string, Set<string>>()
|
const seen = new Map<string, Set<string>>()
|
||||||
const [meta, setMeta] = createStore({
|
const [meta, setMeta] = createStore({
|
||||||
limit: {} as Record<string, number>,
|
limit: {} as Record<string, number>,
|
||||||
|
cursor: {} as Record<string, string | undefined>,
|
||||||
complete: {} as Record<string, boolean>,
|
complete: {} as Record<string, boolean>,
|
||||||
loading: {} as Record<string, boolean>,
|
loading: {} as Record<string, boolean>,
|
||||||
})
|
})
|
||||||
@@ -130,6 +199,33 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setOptimistic = (directory: string, sessionID: string, item: OptimisticItem) => {
|
||||||
|
const key = keyFor(directory, sessionID)
|
||||||
|
const list = optimistic.get(key)
|
||||||
|
if (list) {
|
||||||
|
list.set(item.message.id, { message: item.message, parts: sortParts(item.parts) })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
optimistic.set(key, new Map([[item.message.id, { message: item.message, parts: sortParts(item.parts) }]]))
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearOptimistic = (directory: string, sessionID: string, messageID?: string) => {
|
||||||
|
const key = keyFor(directory, sessionID)
|
||||||
|
if (!messageID) {
|
||||||
|
optimistic.delete(key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = optimistic.get(key)
|
||||||
|
if (!list) return
|
||||||
|
list.delete(messageID)
|
||||||
|
if (list.size === 0) optimistic.delete(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOptimistic = (directory: string, sessionID: string) => [
|
||||||
|
...(optimistic.get(keyFor(directory, sessionID))?.values() ?? []),
|
||||||
|
]
|
||||||
|
|
||||||
const seenFor = (directory: string) => {
|
const seenFor = (directory: string) => {
|
||||||
const existing = seen.get(directory)
|
const existing = seen.get(directory)
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -152,11 +248,15 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
|
|
||||||
const clearMeta = (directory: string, sessionIDs: string[]) => {
|
const clearMeta = (directory: string, sessionIDs: string[]) => {
|
||||||
if (sessionIDs.length === 0) return
|
if (sessionIDs.length === 0) return
|
||||||
|
for (const sessionID of sessionIDs) {
|
||||||
|
clearOptimistic(directory, sessionID)
|
||||||
|
}
|
||||||
setMeta(
|
setMeta(
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
for (const sessionID of sessionIDs) {
|
for (const sessionID of sessionIDs) {
|
||||||
const key = keyFor(directory, sessionID)
|
const key = keyFor(directory, sessionID)
|
||||||
delete draft.limit[key]
|
delete draft.limit[key]
|
||||||
|
delete draft.cursor[key]
|
||||||
delete draft.complete[key]
|
delete draft.complete[key]
|
||||||
delete draft.loading[key]
|
delete draft.loading[key]
|
||||||
}
|
}
|
||||||
@@ -187,17 +287,24 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
evict(directory, setStore, stale)
|
evict(directory, setStore, stale)
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchMessages = async (input: { client: typeof sdk.client; sessionID: string; limit: number }) => {
|
const fetchMessages = async (input: {
|
||||||
|
client: typeof sdk.client
|
||||||
|
sessionID: string
|
||||||
|
limit: number
|
||||||
|
before?: string
|
||||||
|
}) => {
|
||||||
const messages = await retry(() =>
|
const messages = await retry(() =>
|
||||||
input.client.session.messages({ sessionID: input.sessionID, limit: input.limit }),
|
input.client.session.messages({ sessionID: input.sessionID, limit: input.limit, before: input.before }),
|
||||||
)
|
)
|
||||||
const items = (messages.data ?? []).filter((x) => !!x?.info?.id)
|
const items = (messages.data ?? []).filter((x) => !!x?.info?.id)
|
||||||
const session = items.map((x) => x.info).sort((a, b) => cmp(a.id, b.id))
|
const session = items.map((x) => x.info).sort((a, b) => cmp(a.id, b.id))
|
||||||
const part = items.map((message) => ({ id: message.info.id, part: sortParts(message.parts) }))
|
const part = items.map((message) => ({ id: message.info.id, part: sortParts(message.parts) }))
|
||||||
|
const cursor = messages.response.headers.get("x-next-cursor") ?? undefined
|
||||||
return {
|
return {
|
||||||
session,
|
session,
|
||||||
part,
|
part,
|
||||||
complete: session.length < input.limit,
|
cursor,
|
||||||
|
complete: !cursor,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,32 +316,50 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
setStore: Setter
|
setStore: Setter
|
||||||
sessionID: string
|
sessionID: string
|
||||||
limit: number
|
limit: number
|
||||||
|
before?: string
|
||||||
|
mode?: "replace" | "prepend"
|
||||||
}) => {
|
}) => {
|
||||||
const key = keyFor(input.directory, input.sessionID)
|
const key = keyFor(input.directory, input.sessionID)
|
||||||
if (meta.loading[key]) return
|
if (meta.loading[key]) return
|
||||||
|
|
||||||
setMeta("loading", key, true)
|
setMeta("loading", key, true)
|
||||||
await fetchMessages(input)
|
await fetchMessages(input)
|
||||||
.then((next) => {
|
.then((page) => {
|
||||||
if (!tracked(input.directory, input.sessionID)) return
|
if (!tracked(input.directory, input.sessionID)) return
|
||||||
|
const next = mergeOptimisticPage(page, getOptimistic(input.directory, input.sessionID))
|
||||||
|
for (const messageID of next.confirmed) {
|
||||||
|
clearOptimistic(input.directory, input.sessionID, messageID)
|
||||||
|
}
|
||||||
|
const [store] = globalSync.child(input.directory, { bootstrap: false })
|
||||||
|
const cached = input.mode === "prepend" ? (store.message[input.sessionID] ?? []) : []
|
||||||
|
const message = input.mode === "prepend" ? merge(cached, next.session) : next.session
|
||||||
batch(() => {
|
batch(() => {
|
||||||
input.setStore("message", input.sessionID, reconcile(next.session, { key: "id" }))
|
input.setStore("message", input.sessionID, reconcile(message, { key: "id" }))
|
||||||
for (const p of next.part) {
|
for (const p of next.part) {
|
||||||
input.setStore("part", p.id, p.part)
|
input.setStore("part", p.id, p.part)
|
||||||
}
|
}
|
||||||
setMeta("limit", key, input.limit)
|
setMeta("limit", key, message.length)
|
||||||
|
setMeta("cursor", key, next.cursor)
|
||||||
setMeta("complete", key, next.complete)
|
setMeta("complete", key, next.complete)
|
||||||
setSessionPrefetch({
|
setSessionPrefetch({
|
||||||
directory: input.directory,
|
directory: input.directory,
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
limit: input.limit,
|
limit: message.length,
|
||||||
|
cursor: next.cursor,
|
||||||
complete: next.complete,
|
complete: next.complete,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
if (!tracked(input.directory, input.sessionID)) return
|
setMeta(
|
||||||
setMeta("loading", key, false)
|
produce((draft) => {
|
||||||
|
if (!tracked(input.directory, input.sessionID)) {
|
||||||
|
delete draft.loading[key]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
draft.loading[key] = false
|
||||||
|
}),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,11 +386,15 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
get: getSession,
|
get: getSession,
|
||||||
optimistic: {
|
optimistic: {
|
||||||
add(input: { directory?: string; sessionID: string; message: Message; parts: Part[] }) {
|
add(input: { directory?: string; sessionID: string; message: Message; parts: Part[] }) {
|
||||||
|
const directory = input.directory ?? sdk.directory
|
||||||
const [, setStore] = target(input.directory)
|
const [, setStore] = target(input.directory)
|
||||||
|
setOptimistic(directory, input.sessionID, { message: input.message, parts: input.parts })
|
||||||
setOptimisticAdd(setStore as (...args: unknown[]) => void, input)
|
setOptimisticAdd(setStore as (...args: unknown[]) => void, input)
|
||||||
},
|
},
|
||||||
remove(input: { directory?: string; sessionID: string; messageID: string }) {
|
remove(input: { directory?: string; sessionID: string; messageID: string }) {
|
||||||
|
const directory = input.directory ?? sdk.directory
|
||||||
const [, setStore] = target(input.directory)
|
const [, setStore] = target(input.directory)
|
||||||
|
clearOptimistic(directory, input.sessionID, input.messageID)
|
||||||
setOptimisticRemove(setStore as (...args: unknown[]) => void, input)
|
setOptimisticRemove(setStore as (...args: unknown[]) => void, input)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -287,6 +416,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
variant: input.variant,
|
variant: input.variant,
|
||||||
}
|
}
|
||||||
const [, setStore] = target()
|
const [, setStore] = target()
|
||||||
|
setOptimistic(sdk.directory, input.sessionID, { message, parts: input.parts })
|
||||||
setOptimisticAdd(setStore as (...args: unknown[]) => void, {
|
setOptimisticAdd(setStore as (...args: unknown[]) => void, {
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
message,
|
message,
|
||||||
@@ -305,6 +435,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
if (seeded && store.message[sessionID] !== undefined && meta.limit[key] === undefined) {
|
if (seeded && store.message[sessionID] !== undefined && meta.limit[key] === undefined) {
|
||||||
batch(() => {
|
batch(() => {
|
||||||
setMeta("limit", key, seeded.limit)
|
setMeta("limit", key, seeded.limit)
|
||||||
|
setMeta("cursor", key, seeded.cursor)
|
||||||
setMeta("complete", key, seeded.complete)
|
setMeta("complete", key, seeded.complete)
|
||||||
setMeta("loading", key, false)
|
setMeta("loading", key, false)
|
||||||
})
|
})
|
||||||
@@ -318,6 +449,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
if (seeded && store.message[sessionID] !== undefined && meta.limit[key] === undefined) {
|
if (seeded && store.message[sessionID] !== undefined && meta.limit[key] === undefined) {
|
||||||
batch(() => {
|
batch(() => {
|
||||||
setMeta("limit", key, seeded.limit)
|
setMeta("limit", key, seeded.limit)
|
||||||
|
setMeta("cursor", key, seeded.cursor)
|
||||||
setMeta("complete", key, seeded.complete)
|
setMeta("complete", key, seeded.complete)
|
||||||
setMeta("loading", key, false)
|
setMeta("loading", key, false)
|
||||||
})
|
})
|
||||||
@@ -413,7 +545,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
if (store.message[sessionID] === undefined) return false
|
if (store.message[sessionID] === undefined) return false
|
||||||
if (meta.limit[key] === undefined) return false
|
if (meta.limit[key] === undefined) return false
|
||||||
if (meta.complete[key]) return false
|
if (meta.complete[key]) return false
|
||||||
return true
|
return !!meta.cursor[key]
|
||||||
},
|
},
|
||||||
loading(sessionID: string) {
|
loading(sessionID: string) {
|
||||||
const key = keyFor(sdk.directory, sessionID)
|
const key = keyFor(sdk.directory, sessionID)
|
||||||
@@ -428,14 +560,17 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
const step = count ?? messagePageSize
|
const step = count ?? messagePageSize
|
||||||
if (meta.loading[key]) return
|
if (meta.loading[key]) return
|
||||||
if (meta.complete[key]) return
|
if (meta.complete[key]) return
|
||||||
|
const before = meta.cursor[key]
|
||||||
|
if (!before) return
|
||||||
|
|
||||||
const currentLimit = meta.limit[key] ?? messagePageSize
|
|
||||||
await loadMessages({
|
await loadMessages({
|
||||||
directory,
|
directory,
|
||||||
client,
|
client,
|
||||||
setStore,
|
setStore,
|
||||||
sessionID,
|
sessionID,
|
||||||
limit: currentLimit + step,
|
limit: step,
|
||||||
|
before,
|
||||||
|
mode: "prepend",
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { dict as ar } from "@/i18n/ar"
|
||||||
|
import { dict as br } from "@/i18n/br"
|
||||||
|
import { dict as bs } from "@/i18n/bs"
|
||||||
|
import { dict as da } from "@/i18n/da"
|
||||||
|
import { dict as de } from "@/i18n/de"
|
||||||
|
import { dict as en } from "@/i18n/en"
|
||||||
|
import { dict as es } from "@/i18n/es"
|
||||||
|
import { dict as fr } from "@/i18n/fr"
|
||||||
|
import { dict as ja } from "@/i18n/ja"
|
||||||
|
import { dict as ko } from "@/i18n/ko"
|
||||||
|
import { dict as no } from "@/i18n/no"
|
||||||
|
import { dict as pl } from "@/i18n/pl"
|
||||||
|
import { dict as ru } from "@/i18n/ru"
|
||||||
|
import { dict as th } from "@/i18n/th"
|
||||||
|
import { dict as tr } from "@/i18n/tr"
|
||||||
|
import { dict as zh } from "@/i18n/zh"
|
||||||
|
import { dict as zht } from "@/i18n/zht"
|
||||||
|
|
||||||
|
const numbered = Array.from(
|
||||||
|
new Set([
|
||||||
|
en["terminal.title.numbered"],
|
||||||
|
ar["terminal.title.numbered"],
|
||||||
|
br["terminal.title.numbered"],
|
||||||
|
bs["terminal.title.numbered"],
|
||||||
|
da["terminal.title.numbered"],
|
||||||
|
de["terminal.title.numbered"],
|
||||||
|
es["terminal.title.numbered"],
|
||||||
|
fr["terminal.title.numbered"],
|
||||||
|
ja["terminal.title.numbered"],
|
||||||
|
ko["terminal.title.numbered"],
|
||||||
|
no["terminal.title.numbered"],
|
||||||
|
pl["terminal.title.numbered"],
|
||||||
|
ru["terminal.title.numbered"],
|
||||||
|
th["terminal.title.numbered"],
|
||||||
|
tr["terminal.title.numbered"],
|
||||||
|
zh["terminal.title.numbered"],
|
||||||
|
zht["terminal.title.numbered"],
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
|
||||||
|
export function defaultTitle(number: number) {
|
||||||
|
return en["terminal.title.numbered"].replace("{{number}}", String(number))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDefaultTitle(title: string, number: number) {
|
||||||
|
return numbered.some((text) => title === text.replace("{{number}}", String(number)))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function titleNumber(title: string, max: number) {
|
||||||
|
return Array.from({ length: max }, (_, idx) => idx + 1).find((number) => isDefaultTitle(title, number))
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { batch, createEffect, createMemo, createRoot, on, onCleanup } from "soli
|
|||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { useSDK } from "./sdk"
|
import { useSDK } from "./sdk"
|
||||||
import type { Platform } from "./platform"
|
import type { Platform } from "./platform"
|
||||||
|
import { defaultTitle, titleNumber } from "./terminal-title"
|
||||||
import { Persist, persisted, removePersisted } from "@/utils/persist"
|
import { Persist, persisted, removePersisted } from "@/utils/persist"
|
||||||
|
|
||||||
export type LocalPTY = {
|
export type LocalPTY = {
|
||||||
@@ -33,11 +34,7 @@ function num(value: unknown) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function numberFromTitle(title: string) {
|
function numberFromTitle(title: string) {
|
||||||
const match = title.match(/^Terminal (\d+)$/)
|
return titleNumber(title, MAX_TERMINAL_SESSIONS)
|
||||||
if (!match) return
|
|
||||||
const value = Number(match[1])
|
|
||||||
if (!Number.isFinite(value) || value <= 0) return
|
|
||||||
return value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function pty(value: unknown): LocalPTY | undefined {
|
function pty(value: unknown): LocalPTY | undefined {
|
||||||
@@ -202,13 +199,13 @@ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: str
|
|||||||
const nextNumber = pickNextTerminalNumber()
|
const nextNumber = pickNextTerminalNumber()
|
||||||
|
|
||||||
sdk.client.pty
|
sdk.client.pty
|
||||||
.create({ title: `Terminal ${nextNumber}` })
|
.create({ title: defaultTitle(nextNumber) })
|
||||||
.then((pty: { data?: { id?: string; title?: string } }) => {
|
.then((pty: { data?: { id?: string; title?: string } }) => {
|
||||||
const id = pty.data?.id
|
const id = pty.data?.id
|
||||||
if (!id) return
|
if (!id) return
|
||||||
const newTerminal = {
|
const newTerminal = {
|
||||||
id,
|
id,
|
||||||
title: pty.data?.title ?? "Terminal",
|
title: pty.data?.title ?? defaultTitle(nextNumber),
|
||||||
titleNumber: nextNumber,
|
titleNumber: nextNumber,
|
||||||
}
|
}
|
||||||
setStore("all", store.all.length, newTerminal)
|
setStore("all", store.all.length, newTerminal)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// @refresh reload
|
// @refresh reload
|
||||||
|
|
||||||
import { iife } from "@opencode-ai/util/iife"
|
|
||||||
import { render } from "solid-js/web"
|
import { render } from "solid-js/web"
|
||||||
import { AppBaseProviders, AppInterface } from "@/app"
|
import { AppBaseProviders, AppInterface } from "@/app"
|
||||||
import { type Platform, PlatformProvider } from "@/context/platform"
|
import { type Platform, PlatformProvider } from "@/context/platform"
|
||||||
@@ -132,7 +131,11 @@ if (root instanceof HTMLElement) {
|
|||||||
() => (
|
() => (
|
||||||
<PlatformProvider value={platform}>
|
<PlatformProvider value={platform}>
|
||||||
<AppBaseProviders>
|
<AppBaseProviders>
|
||||||
<AppInterface defaultServer={ServerConnection.Key.make(getDefaultUrl())} servers={[server]} />
|
<AppInterface
|
||||||
|
defaultServer={ServerConnection.Key.make(getDefaultUrl())}
|
||||||
|
servers={[server]}
|
||||||
|
disableHealthCheck
|
||||||
|
/>
|
||||||
</AppBaseProviders>
|
</AppBaseProviders>
|
||||||
</PlatformProvider>
|
</PlatformProvider>
|
||||||
),
|
),
|
||||||
|
|||||||
+105
-3
@@ -104,6 +104,7 @@ export const dict = {
|
|||||||
"dialog.model.empty": "لا توجد نتائج للنماذج",
|
"dialog.model.empty": "لا توجد نتائج للنماذج",
|
||||||
"dialog.model.manage": "إدارة النماذج",
|
"dialog.model.manage": "إدارة النماذج",
|
||||||
"dialog.model.manage.description": "تخصيص النماذج التي تظهر في محدد النماذج.",
|
"dialog.model.manage.description": "تخصيص النماذج التي تظهر في محدد النماذج.",
|
||||||
|
"dialog.model.manage.provider.toggle": "تبديل جميع نماذج {{provider}}",
|
||||||
"dialog.model.unpaid.freeModels.title": "نماذج مجانية مقدمة من OpenCode",
|
"dialog.model.unpaid.freeModels.title": "نماذج مجانية مقدمة من OpenCode",
|
||||||
"dialog.model.unpaid.addMore.title": "إضافة المزيد من النماذج من موفرين مشهورين",
|
"dialog.model.unpaid.addMore.title": "إضافة المزيد من النماذج من موفرين مشهورين",
|
||||||
"dialog.provider.viewAll": "عرض المزيد من الموفرين",
|
"dialog.provider.viewAll": "عرض المزيد من الموفرين",
|
||||||
@@ -243,7 +244,7 @@ export const dict = {
|
|||||||
"prompt.example.25": "كيف تعمل متغيرات البيئة هنا؟",
|
"prompt.example.25": "كيف تعمل متغيرات البيئة هنا؟",
|
||||||
"prompt.popover.emptyResults": "لا توجد نتائج مطابقة",
|
"prompt.popover.emptyResults": "لا توجد نتائج مطابقة",
|
||||||
"prompt.popover.emptyCommands": "لا توجد أوامر مطابقة",
|
"prompt.popover.emptyCommands": "لا توجد أوامر مطابقة",
|
||||||
"prompt.dropzone.label": "أفلت الصور أو ملفات PDF هنا",
|
"prompt.dropzone.label": "أفلت الصور أو ملفات PDF أو الملفات النصية هنا",
|
||||||
"prompt.dropzone.file.label": "أفلت لإشارة @ للملف",
|
"prompt.dropzone.file.label": "أفلت لإشارة @ للملف",
|
||||||
"prompt.slash.badge.custom": "مخصص",
|
"prompt.slash.badge.custom": "مخصص",
|
||||||
"prompt.slash.badge.skill": "مهارة",
|
"prompt.slash.badge.skill": "مهارة",
|
||||||
@@ -256,8 +257,8 @@ export const dict = {
|
|||||||
"prompt.attachment.remove": "إزالة المرفق",
|
"prompt.attachment.remove": "إزالة المرفق",
|
||||||
"prompt.action.send": "إرسال",
|
"prompt.action.send": "إرسال",
|
||||||
"prompt.action.stop": "توقف",
|
"prompt.action.stop": "توقف",
|
||||||
"prompt.toast.pasteUnsupported.title": "لصق غير مدعوم",
|
"prompt.toast.pasteUnsupported.title": "مرفق غير مدعوم",
|
||||||
"prompt.toast.pasteUnsupported.description": "يمكن لصق الصور أو ملفات PDF فقط هنا.",
|
"prompt.toast.pasteUnsupported.description": "يمكن إرفاق الصور أو ملفات PDF أو الملفات النصية فقط هنا.",
|
||||||
"prompt.toast.modelAgentRequired.title": "حدد وكيلاً ونموذجاً",
|
"prompt.toast.modelAgentRequired.title": "حدد وكيلاً ونموذجاً",
|
||||||
"prompt.toast.modelAgentRequired.description": "اختر وكيلاً ونموذجاً قبل إرسال الموجه.",
|
"prompt.toast.modelAgentRequired.description": "اختر وكيلاً ونموذجاً قبل إرسال الموجه.",
|
||||||
"prompt.toast.worktreeCreateFailed.title": "فشل إنشاء شجرة العمل",
|
"prompt.toast.worktreeCreateFailed.title": "فشل إنشاء شجرة العمل",
|
||||||
@@ -288,6 +289,11 @@ export const dict = {
|
|||||||
"dialog.server.add.error": "تعذر الاتصال بالخادم",
|
"dialog.server.add.error": "تعذر الاتصال بالخادم",
|
||||||
"dialog.server.add.checking": "جارٍ التحقق...",
|
"dialog.server.add.checking": "جارٍ التحقق...",
|
||||||
"dialog.server.add.button": "إضافة خادم",
|
"dialog.server.add.button": "إضافة خادم",
|
||||||
|
"dialog.server.add.name": "اسم الخادم (اختياري)",
|
||||||
|
"dialog.server.add.namePlaceholder": "Localhost",
|
||||||
|
"dialog.server.add.username": "اسم المستخدم (اختياري)",
|
||||||
|
"dialog.server.add.password": "كلمة المرور (اختياري)",
|
||||||
|
"dialog.server.edit.title": "تحرير الخادم",
|
||||||
"dialog.server.default.title": "الخادم الافتراضي",
|
"dialog.server.default.title": "الخادم الافتراضي",
|
||||||
"dialog.server.default.description":
|
"dialog.server.default.description":
|
||||||
"الاتصال بهذا الخادم عند بدء تشغيل التطبيق بدلاً من بدء خادم محلي. يتطلب إعادة التشغيل.",
|
"الاتصال بهذا الخادم عند بدء تشغيل التطبيق بدلاً من بدء خادم محلي. يتطلب إعادة التشغيل.",
|
||||||
@@ -358,6 +364,7 @@ export const dict = {
|
|||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.bs": "Bosanski",
|
"language.bs": "Bosanski",
|
||||||
"language.th": "ไทย",
|
"language.th": "ไทย",
|
||||||
|
"language.tr": "Türkçe",
|
||||||
"toast.language.title": "لغة",
|
"toast.language.title": "لغة",
|
||||||
"toast.language.description": "تم التبديل إلى {{language}}",
|
"toast.language.description": "تم التبديل إلى {{language}}",
|
||||||
"toast.theme.title": "تم تبديل السمة",
|
"toast.theme.title": "تم تبديل السمة",
|
||||||
@@ -444,8 +451,11 @@ export const dict = {
|
|||||||
"session.review.loadingChanges": "جارٍ تحميل التغييرات...",
|
"session.review.loadingChanges": "جارٍ تحميل التغييرات...",
|
||||||
"session.review.empty": "لا توجد تغييرات في هذه الجلسة بعد",
|
"session.review.empty": "لا توجد تغييرات في هذه الجلسة بعد",
|
||||||
"session.review.noChanges": "لا توجد تغييرات",
|
"session.review.noChanges": "لا توجد تغييرات",
|
||||||
|
"session.review.noVcs": "لم يتم اكتشاف نظام التحكم في الإصدار Git، لن يتم عرض التغييرات",
|
||||||
|
"session.review.noSnapshot": "تم تعطيل تتبع اللقطات في التكوين، لذا فإن تغييرات الجلسة غير متوفرة",
|
||||||
"session.files.selectToOpen": "اختر ملفًا لفتحه",
|
"session.files.selectToOpen": "اختر ملفًا لفتحه",
|
||||||
"session.files.all": "كل الملفات",
|
"session.files.all": "كل الملفات",
|
||||||
|
"session.files.empty": "لا توجد ملفات",
|
||||||
"session.files.binaryContent": "ملف ثنائي (لا يمكن عرض المحتوى)",
|
"session.files.binaryContent": "ملف ثنائي (لا يمكن عرض المحتوى)",
|
||||||
"session.messages.renderEarlier": "عرض الرسائل السابقة",
|
"session.messages.renderEarlier": "عرض الرسائل السابقة",
|
||||||
"session.messages.loadingEarlier": "جارٍ تحميل الرسائل السابقة...",
|
"session.messages.loadingEarlier": "جارٍ تحميل الرسائل السابقة...",
|
||||||
@@ -456,6 +466,17 @@ export const dict = {
|
|||||||
"session.todo.title": "المهام",
|
"session.todo.title": "المهام",
|
||||||
"session.todo.collapse": "طي",
|
"session.todo.collapse": "طي",
|
||||||
"session.todo.expand": "توسيع",
|
"session.todo.expand": "توسيع",
|
||||||
|
"session.followupDock.summary.one": "{{count}} رسالة في الانتظار",
|
||||||
|
"session.followupDock.summary.other": "{{count}} رسائل في الانتظار",
|
||||||
|
"session.followupDock.sendNow": "إرسال الآن",
|
||||||
|
"session.followupDock.edit": "تحرير",
|
||||||
|
"session.followupDock.collapse": "طي الرسائل المنتظرة",
|
||||||
|
"session.followupDock.expand": "توسيع الرسائل المنتظرة",
|
||||||
|
"session.revertDock.summary.one": "{{count}} رسالة تم التراجع عنها",
|
||||||
|
"session.revertDock.summary.other": "{{count}} رسائل تم التراجع عنها",
|
||||||
|
"session.revertDock.collapse": "طي الرسائل التي تم التراجع عنها",
|
||||||
|
"session.revertDock.expand": "توسيع الرسائل التي تم التراجع عنها",
|
||||||
|
"session.revertDock.restore": "استعادة الرسالة",
|
||||||
"session.new.title": "ابنِ أي شيء",
|
"session.new.title": "ابنِ أي شيء",
|
||||||
"session.new.worktree.main": "الفرع الرئيسي",
|
"session.new.worktree.main": "الفرع الرئيسي",
|
||||||
"session.new.worktree.mainWithBranch": "الفرع الرئيسي ({{branch}})",
|
"session.new.worktree.mainWithBranch": "الفرع الرئيسي ({{branch}})",
|
||||||
@@ -538,10 +559,18 @@ export const dict = {
|
|||||||
"settings.general.row.language.description": "تغيير لغة العرض لـ OpenCode",
|
"settings.general.row.language.description": "تغيير لغة العرض لـ OpenCode",
|
||||||
"settings.general.row.appearance.title": "المظهر",
|
"settings.general.row.appearance.title": "المظهر",
|
||||||
"settings.general.row.appearance.description": "تخصيص كيفية ظهور OpenCode على جهازك",
|
"settings.general.row.appearance.description": "تخصيص كيفية ظهور OpenCode على جهازك",
|
||||||
|
"settings.general.row.colorScheme.title": "مخطط الألوان",
|
||||||
|
"settings.general.row.colorScheme.description": "اختر ما إذا كان OpenCode يتبع سمة النظام أو الفاتح أو الداكن",
|
||||||
"settings.general.row.theme.title": "السمة",
|
"settings.general.row.theme.title": "السمة",
|
||||||
"settings.general.row.theme.description": "تخصيص سمة OpenCode.",
|
"settings.general.row.theme.description": "تخصيص سمة OpenCode.",
|
||||||
"settings.general.row.font.title": "الخط",
|
"settings.general.row.font.title": "الخط",
|
||||||
"settings.general.row.font.description": "تخصيص الخط الأحادي المستخدم في كتل التعليمات البرمجية",
|
"settings.general.row.font.description": "تخصيص الخط الأحادي المستخدم في كتل التعليمات البرمجية",
|
||||||
|
"settings.general.row.followup.title": "سلوك المتابعة",
|
||||||
|
"settings.general.row.followup.description": "اختر ما إذا كانت طلبات المتابعة توجه فورًا أو تنتظر في قائمة انتظار",
|
||||||
|
"settings.general.row.followup.option.queue": "قائمة انتظار",
|
||||||
|
"settings.general.row.followup.option.steer": "توجيه",
|
||||||
|
"settings.general.row.reasoningSummaries.title": "إظهار ملخصات الاستنتاج",
|
||||||
|
"settings.general.row.reasoningSummaries.description": "عرض ملخصات استنتاج النموذج في الشريط الزمني",
|
||||||
"settings.general.row.shellToolPartsExpanded.title": "توسيع أجزاء أداة shell",
|
"settings.general.row.shellToolPartsExpanded.title": "توسيع أجزاء أداة shell",
|
||||||
"settings.general.row.shellToolPartsExpanded.description":
|
"settings.general.row.shellToolPartsExpanded.description":
|
||||||
"إظهار أجزاء أداة shell موسعة بشكل افتراضي في الشريط الزمني",
|
"إظهار أجزاء أداة shell موسعة بشكل افتراضي في الشريط الزمني",
|
||||||
@@ -749,4 +778,77 @@ export const dict = {
|
|||||||
"common.time.daysAgo.short": "قبل {{count}} ي",
|
"common.time.daysAgo.short": "قبل {{count}} ي",
|
||||||
"settings.providers.connected.environmentDescription": "متصل من متغيرات البيئة الخاصة بك",
|
"settings.providers.connected.environmentDescription": "متصل من متغيرات البيئة الخاصة بك",
|
||||||
"settings.providers.custom.description": "أضف مزود متوافق مع OpenAI بواسطة عنوان URL الأساسي.",
|
"settings.providers.custom.description": "أضف مزود متوافق مع OpenAI بواسطة عنوان URL الأساسي.",
|
||||||
|
|
||||||
|
"app.server.unreachable": "تعذر الوصول إلى {{server}}",
|
||||||
|
"app.server.retrying": "جاري إعادة المحاولة تلقائيًا...",
|
||||||
|
"app.server.otherServers": "خوادم أخرى",
|
||||||
|
"dialog.server.add.usernamePlaceholder": "اسم المستخدم",
|
||||||
|
"dialog.server.add.passwordPlaceholder": "كلمة المرور",
|
||||||
|
"server.row.noUsername": "لا يوجد اسم مستخدم",
|
||||||
|
"session.review.noVcs.createGit.title": "إنشاء مستودع Git",
|
||||||
|
"session.review.noVcs.createGit.description": "تتبع ومراجعة والتراجع عن التغييرات في هذا المشروع",
|
||||||
|
"session.review.noVcs.createGit.actionLoading": "جاري إنشاء مستودع Git...",
|
||||||
|
"session.review.noVcs.createGit.action": "إنشاء مستودع Git",
|
||||||
|
"session.todo.progress": "تم إكمال {{done}} من {{total}} مهام",
|
||||||
|
"session.question.progress": "{{current}} من {{total}} أسئلة",
|
||||||
|
"session.header.open.finder": "Finder",
|
||||||
|
"session.header.open.fileExplorer": "مستكشف الملفات",
|
||||||
|
"session.header.open.fileManager": "مدير الملفات",
|
||||||
|
"session.header.open.app.vscode": "VS Code",
|
||||||
|
"session.header.open.app.cursor": "Cursor",
|
||||||
|
"session.header.open.app.zed": "Zed",
|
||||||
|
"session.header.open.app.textmate": "TextMate",
|
||||||
|
"session.header.open.app.antigravity": "Antigravity",
|
||||||
|
"session.header.open.app.terminal": "المحطة الطرفية",
|
||||||
|
"session.header.open.app.iterm2": "iTerm2",
|
||||||
|
"session.header.open.app.ghostty": "Ghostty",
|
||||||
|
"session.header.open.app.warp": "Warp",
|
||||||
|
"session.header.open.app.xcode": "Xcode",
|
||||||
|
"session.header.open.app.androidStudio": "Android Studio",
|
||||||
|
"session.header.open.app.powershell": "PowerShell",
|
||||||
|
"session.header.open.app.sublimeText": "Sublime Text",
|
||||||
|
"debugBar.ariaLabel": "تشخيص أداء التطوير",
|
||||||
|
"debugBar.na": "غير متاح",
|
||||||
|
"debugBar.nav.label": "NAV",
|
||||||
|
"debugBar.nav.tip": "آخر انتقال مكتمل للمسار يمس صفحة جلسة، مُقاسًا من بدء التوجيه حتى أول رسم بعد استقراره.",
|
||||||
|
"debugBar.fps.label": "FPS",
|
||||||
|
"debugBar.fps.tip": "الإطارات المتجددة في الثانية خلال آخر 5 ثوانٍ.",
|
||||||
|
"debugBar.frame.label": "FRAME",
|
||||||
|
"debugBar.frame.tip": "أسوأ وقت للإطار خلال آخر 5 ثوانٍ.",
|
||||||
|
"debugBar.jank.label": "JANK",
|
||||||
|
"debugBar.jank.tip": "الإطارات التي تزيد عن 32 مللي ثانية في آخر 5 ثوانٍ.",
|
||||||
|
"debugBar.long.label": "LONG",
|
||||||
|
"debugBar.long.tip": "الوقت المحظور وعدد المهام الطويلة في آخر 5 ثوانٍ. أقصى مهمة: {{max}}.",
|
||||||
|
"debugBar.delay.label": "DELAY",
|
||||||
|
"debugBar.delay.tip": "أسوأ تأخير إدخال تمت ملاحظته في آخر 5 ثوانٍ.",
|
||||||
|
"debugBar.inp.label": "INP",
|
||||||
|
"debugBar.inp.tip": "مدة التفاعل التقريبية خلال آخر 5 ثوانٍ. هذا يشبه INP، وليس Web Vitals INP الرسمي.",
|
||||||
|
"debugBar.cls.label": "CLS",
|
||||||
|
"debugBar.cls.tip": "التحول التخطيطي التراكمي لعمر التطبيق الحالي.",
|
||||||
|
"debugBar.mem.label": "MEM",
|
||||||
|
"debugBar.mem.tipUnavailable": "كومة JS المستخدمة مقابل حد الكومة. Chromium فقط.",
|
||||||
|
"debugBar.mem.tip": "كومة JS المستخدمة مقابل حد الكومة. {{used}} من {{limit}}.",
|
||||||
|
"common.key.ctrl": "Ctrl",
|
||||||
|
"common.key.alt": "Alt",
|
||||||
|
"common.key.shift": "Shift",
|
||||||
|
"common.key.meta": "Meta",
|
||||||
|
"common.key.space": "Space",
|
||||||
|
"common.key.backspace": "Backspace",
|
||||||
|
"common.key.enter": "Enter",
|
||||||
|
"common.key.tab": "Tab",
|
||||||
|
"common.key.delete": "Delete",
|
||||||
|
"common.key.home": "Home",
|
||||||
|
"common.key.end": "End",
|
||||||
|
"common.key.pageUp": "Page Up",
|
||||||
|
"common.key.pageDown": "Page Down",
|
||||||
|
"common.key.insert": "Insert",
|
||||||
|
"common.unknown": "غير معروف",
|
||||||
|
"error.page.circular": "[دائري]",
|
||||||
|
"error.globalSDK.noServerAvailable": "لا يوجد خادم متاح",
|
||||||
|
"error.globalSDK.serverNotAvailable": "الخادم غير متاح",
|
||||||
|
"error.childStore.persistedCacheCreateFailed": "فشل إنشاء ذاكرة التخزين المؤقت الدائمة",
|
||||||
|
"error.childStore.persistedProjectMetadataCreateFailed": "فشل إنشاء بيانات تعريف المشروع الدائمة",
|
||||||
|
"error.childStore.persistedProjectIconCreateFailed": "فشل إنشاء أيقونة المشروع الدائمة",
|
||||||
|
"error.childStore.storeCreateFailed": "فشل إنشاء المخزن",
|
||||||
|
"terminal.connectionLost.abnormalClose": "تم إغلاق WebSocket بشكل غير طبيعي: {{code}}",
|
||||||
}
|
}
|
||||||
|
|||||||
+109
-3
@@ -104,6 +104,7 @@ export const dict = {
|
|||||||
"dialog.model.empty": "Nenhum resultado de modelo",
|
"dialog.model.empty": "Nenhum resultado de modelo",
|
||||||
"dialog.model.manage": "Gerenciar modelos",
|
"dialog.model.manage": "Gerenciar modelos",
|
||||||
"dialog.model.manage.description": "Personalizar quais modelos aparecem no seletor de modelos.",
|
"dialog.model.manage.description": "Personalizar quais modelos aparecem no seletor de modelos.",
|
||||||
|
"dialog.model.manage.provider.toggle": "Alternar todos os modelos {{provider}}",
|
||||||
"dialog.model.unpaid.freeModels.title": "Modelos gratuitos fornecidos pelo OpenCode",
|
"dialog.model.unpaid.freeModels.title": "Modelos gratuitos fornecidos pelo OpenCode",
|
||||||
"dialog.model.unpaid.addMore.title": "Adicionar mais modelos de provedores populares",
|
"dialog.model.unpaid.addMore.title": "Adicionar mais modelos de provedores populares",
|
||||||
"dialog.provider.viewAll": "Ver mais provedores",
|
"dialog.provider.viewAll": "Ver mais provedores",
|
||||||
@@ -243,7 +244,7 @@ export const dict = {
|
|||||||
"prompt.example.25": "Como funcionam as variáveis de ambiente aqui?",
|
"prompt.example.25": "Como funcionam as variáveis de ambiente aqui?",
|
||||||
"prompt.popover.emptyResults": "Nenhum resultado correspondente",
|
"prompt.popover.emptyResults": "Nenhum resultado correspondente",
|
||||||
"prompt.popover.emptyCommands": "Nenhum comando correspondente",
|
"prompt.popover.emptyCommands": "Nenhum comando correspondente",
|
||||||
"prompt.dropzone.label": "Solte imagens ou PDFs aqui",
|
"prompt.dropzone.label": "Arraste imagens, PDFs ou arquivos de texto aqui",
|
||||||
"prompt.dropzone.file.label": "Solte para @mencionar arquivo",
|
"prompt.dropzone.file.label": "Solte para @mencionar arquivo",
|
||||||
"prompt.slash.badge.custom": "personalizado",
|
"prompt.slash.badge.custom": "personalizado",
|
||||||
"prompt.slash.badge.skill": "skill",
|
"prompt.slash.badge.skill": "skill",
|
||||||
@@ -256,8 +257,8 @@ export const dict = {
|
|||||||
"prompt.attachment.remove": "Remover anexo",
|
"prompt.attachment.remove": "Remover anexo",
|
||||||
"prompt.action.send": "Enviar",
|
"prompt.action.send": "Enviar",
|
||||||
"prompt.action.stop": "Parar",
|
"prompt.action.stop": "Parar",
|
||||||
"prompt.toast.pasteUnsupported.title": "Colagem não suportada",
|
"prompt.toast.pasteUnsupported.title": "Anexo não suportado",
|
||||||
"prompt.toast.pasteUnsupported.description": "Somente imagens ou PDFs podem ser colados aqui.",
|
"prompt.toast.pasteUnsupported.description": "Apenas imagens, PDFs ou arquivos de texto podem ser anexados aqui.",
|
||||||
"prompt.toast.modelAgentRequired.title": "Selecione um agente e modelo",
|
"prompt.toast.modelAgentRequired.title": "Selecione um agente e modelo",
|
||||||
"prompt.toast.modelAgentRequired.description": "Escolha um agente e modelo antes de enviar um prompt.",
|
"prompt.toast.modelAgentRequired.description": "Escolha um agente e modelo antes de enviar um prompt.",
|
||||||
"prompt.toast.worktreeCreateFailed.title": "Falha ao criar worktree",
|
"prompt.toast.worktreeCreateFailed.title": "Falha ao criar worktree",
|
||||||
@@ -288,6 +289,11 @@ export const dict = {
|
|||||||
"dialog.server.add.error": "Não foi possível conectar ao servidor",
|
"dialog.server.add.error": "Não foi possível conectar ao servidor",
|
||||||
"dialog.server.add.checking": "Verificando...",
|
"dialog.server.add.checking": "Verificando...",
|
||||||
"dialog.server.add.button": "Adicionar",
|
"dialog.server.add.button": "Adicionar",
|
||||||
|
"dialog.server.add.name": "Nome do servidor (opcional)",
|
||||||
|
"dialog.server.add.namePlaceholder": "Localhost",
|
||||||
|
"dialog.server.add.username": "Nome de usuário (opcional)",
|
||||||
|
"dialog.server.add.password": "Senha (opcional)",
|
||||||
|
"dialog.server.edit.title": "Editar servidor",
|
||||||
"dialog.server.default.title": "Servidor padrão",
|
"dialog.server.default.title": "Servidor padrão",
|
||||||
"dialog.server.default.description":
|
"dialog.server.default.description":
|
||||||
"Conectar a este servidor na inicialização do aplicativo ao invés de iniciar um servidor local. Requer reinicialização.",
|
"Conectar a este servidor na inicialização do aplicativo ao invés de iniciar um servidor local. Requer reinicialização.",
|
||||||
@@ -359,6 +365,7 @@ export const dict = {
|
|||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.bs": "Bosanski",
|
"language.bs": "Bosanski",
|
||||||
"language.th": "ไทย",
|
"language.th": "ไทย",
|
||||||
|
"language.tr": "Türkçe",
|
||||||
"toast.language.title": "Idioma",
|
"toast.language.title": "Idioma",
|
||||||
"toast.language.description": "Alterado para {{language}}",
|
"toast.language.description": "Alterado para {{language}}",
|
||||||
"toast.theme.title": "Tema alterado",
|
"toast.theme.title": "Tema alterado",
|
||||||
@@ -446,9 +453,13 @@ export const dict = {
|
|||||||
"session.review.change.other": "Alterações",
|
"session.review.change.other": "Alterações",
|
||||||
"session.review.loadingChanges": "Carregando alterações...",
|
"session.review.loadingChanges": "Carregando alterações...",
|
||||||
"session.review.empty": "Nenhuma alteração nesta sessão ainda",
|
"session.review.empty": "Nenhuma alteração nesta sessão ainda",
|
||||||
|
"session.review.noVcs": "Nenhum Sistema de Controle de Versão Git detectado, alterações não exibidas",
|
||||||
|
"session.review.noSnapshot":
|
||||||
|
"O rastreamento de snapshot está desabilitado na configuração, então as alterações da sessão estão indisponíveis",
|
||||||
"session.review.noChanges": "Sem alterações",
|
"session.review.noChanges": "Sem alterações",
|
||||||
"session.files.selectToOpen": "Selecione um arquivo para abrir",
|
"session.files.selectToOpen": "Selecione um arquivo para abrir",
|
||||||
"session.files.all": "Todos os arquivos",
|
"session.files.all": "Todos os arquivos",
|
||||||
|
"session.files.empty": "Nenhum arquivo",
|
||||||
"session.files.binaryContent": "Arquivo binário (conteúdo não pode ser exibido)",
|
"session.files.binaryContent": "Arquivo binário (conteúdo não pode ser exibido)",
|
||||||
"session.messages.renderEarlier": "Renderizar mensagens anteriores",
|
"session.messages.renderEarlier": "Renderizar mensagens anteriores",
|
||||||
"session.messages.loadingEarlier": "Carregando mensagens anteriores...",
|
"session.messages.loadingEarlier": "Carregando mensagens anteriores...",
|
||||||
@@ -459,6 +470,17 @@ export const dict = {
|
|||||||
"session.todo.title": "Tarefas",
|
"session.todo.title": "Tarefas",
|
||||||
"session.todo.collapse": "Recolher",
|
"session.todo.collapse": "Recolher",
|
||||||
"session.todo.expand": "Expandir",
|
"session.todo.expand": "Expandir",
|
||||||
|
"session.followupDock.summary.one": "{{count}} mensagem na fila",
|
||||||
|
"session.followupDock.summary.other": "{{count}} mensagens na fila",
|
||||||
|
"session.followupDock.sendNow": "Enviar agora",
|
||||||
|
"session.followupDock.edit": "Editar",
|
||||||
|
"session.followupDock.collapse": "Recolher mensagens na fila",
|
||||||
|
"session.followupDock.expand": "Expandir mensagens na fila",
|
||||||
|
"session.revertDock.summary.one": "{{count}} mensagem revertida",
|
||||||
|
"session.revertDock.summary.other": "{{count}} mensagens revertidas",
|
||||||
|
"session.revertDock.collapse": "Recolher mensagens revertidas",
|
||||||
|
"session.revertDock.expand": "Expandir mensagens revertidas",
|
||||||
|
"session.revertDock.restore": "Restaurar mensagem",
|
||||||
"session.new.title": "Crie qualquer coisa",
|
"session.new.title": "Crie qualquer coisa",
|
||||||
"session.new.worktree.main": "Branch principal",
|
"session.new.worktree.main": "Branch principal",
|
||||||
"session.new.worktree.mainWithBranch": "Branch principal ({{branch}})",
|
"session.new.worktree.mainWithBranch": "Branch principal ({{branch}})",
|
||||||
@@ -544,10 +566,19 @@ export const dict = {
|
|||||||
"settings.general.row.language.description": "Alterar o idioma de exibição do OpenCode",
|
"settings.general.row.language.description": "Alterar o idioma de exibição do OpenCode",
|
||||||
"settings.general.row.appearance.title": "Aparência",
|
"settings.general.row.appearance.title": "Aparência",
|
||||||
"settings.general.row.appearance.description": "Personalize como o OpenCode aparece no seu dispositivo",
|
"settings.general.row.appearance.description": "Personalize como o OpenCode aparece no seu dispositivo",
|
||||||
|
"settings.general.row.colorScheme.title": "Esquema de cores",
|
||||||
|
"settings.general.row.colorScheme.description": "Escolha se o OpenCode segue o tema do sistema, claro ou escuro",
|
||||||
"settings.general.row.theme.title": "Tema",
|
"settings.general.row.theme.title": "Tema",
|
||||||
"settings.general.row.theme.description": "Personalize como o OpenCode é tematizado.",
|
"settings.general.row.theme.description": "Personalize como o OpenCode é tematizado.",
|
||||||
"settings.general.row.font.title": "Fonte",
|
"settings.general.row.font.title": "Fonte",
|
||||||
"settings.general.row.font.description": "Personalize a fonte monoespaçada usada em blocos de código",
|
"settings.general.row.font.description": "Personalize a fonte monoespaçada usada em blocos de código",
|
||||||
|
"settings.general.row.followup.title": "Comportamento de acompanhamento",
|
||||||
|
"settings.general.row.followup.description":
|
||||||
|
"Escolha se os prompts de acompanhamento orientam imediatamente ou esperam na fila",
|
||||||
|
"settings.general.row.followup.option.queue": "Fila",
|
||||||
|
"settings.general.row.followup.option.steer": "Orientar",
|
||||||
|
"settings.general.row.reasoningSummaries.title": "Mostrar resumos de raciocínio",
|
||||||
|
"settings.general.row.reasoningSummaries.description": "Exibir resumos de raciocínio do modelo na linha do tempo",
|
||||||
"settings.general.row.shellToolPartsExpanded.title": "Expandir partes da ferramenta shell",
|
"settings.general.row.shellToolPartsExpanded.title": "Expandir partes da ferramenta shell",
|
||||||
"settings.general.row.shellToolPartsExpanded.description":
|
"settings.general.row.shellToolPartsExpanded.description":
|
||||||
"Mostrar partes da ferramenta shell expandidas por padrão na linha do tempo",
|
"Mostrar partes da ferramenta shell expandidas por padrão na linha do tempo",
|
||||||
@@ -757,4 +788,79 @@ export const dict = {
|
|||||||
"common.time.daysAgo.short": "{{count}}d atrás",
|
"common.time.daysAgo.short": "{{count}}d atrás",
|
||||||
"settings.providers.connected.environmentDescription": "Conectado a partir de suas variáveis de ambiente",
|
"settings.providers.connected.environmentDescription": "Conectado a partir de suas variáveis de ambiente",
|
||||||
"settings.providers.custom.description": "Adicionar um provedor compatível com a OpenAI através do URL base.",
|
"settings.providers.custom.description": "Adicionar um provedor compatível com a OpenAI através do URL base.",
|
||||||
|
|
||||||
|
"app.server.unreachable": "Não foi possível conectar a {{server}}",
|
||||||
|
"app.server.retrying": "Tentando novamente automaticamente...",
|
||||||
|
"app.server.otherServers": "Outros servidores",
|
||||||
|
"dialog.server.add.usernamePlaceholder": "nome de usuário",
|
||||||
|
"dialog.server.add.passwordPlaceholder": "senha",
|
||||||
|
"server.row.noUsername": "sem nome de usuário",
|
||||||
|
"session.review.noVcs.createGit.title": "Criar um repositório Git",
|
||||||
|
"session.review.noVcs.createGit.description": "Rastreie, revise e desfaça alterações neste projeto",
|
||||||
|
"session.review.noVcs.createGit.actionLoading": "Criando repositório Git...",
|
||||||
|
"session.review.noVcs.createGit.action": "Criar repositório Git",
|
||||||
|
"session.todo.progress": "{{done}} de {{total}} tarefas concluídas",
|
||||||
|
"session.question.progress": "{{current}} de {{total}} perguntas",
|
||||||
|
"session.header.open.finder": "Finder",
|
||||||
|
"session.header.open.fileExplorer": "Explorador de Arquivos",
|
||||||
|
"session.header.open.fileManager": "Gerenciador de Arquivos",
|
||||||
|
"session.header.open.app.vscode": "VS Code",
|
||||||
|
"session.header.open.app.cursor": "Cursor",
|
||||||
|
"session.header.open.app.zed": "Zed",
|
||||||
|
"session.header.open.app.textmate": "TextMate",
|
||||||
|
"session.header.open.app.antigravity": "Antigravity",
|
||||||
|
"session.header.open.app.terminal": "Terminal",
|
||||||
|
"session.header.open.app.iterm2": "iTerm2",
|
||||||
|
"session.header.open.app.ghostty": "Ghostty",
|
||||||
|
"session.header.open.app.warp": "Warp",
|
||||||
|
"session.header.open.app.xcode": "Xcode",
|
||||||
|
"session.header.open.app.androidStudio": "Android Studio",
|
||||||
|
"session.header.open.app.powershell": "PowerShell",
|
||||||
|
"session.header.open.app.sublimeText": "Sublime Text",
|
||||||
|
"debugBar.ariaLabel": "Diagnóstico de desempenho de desenvolvimento",
|
||||||
|
"debugBar.na": "n/a",
|
||||||
|
"debugBar.nav.label": "NAV",
|
||||||
|
"debugBar.nav.tip":
|
||||||
|
"Última transição de rota concluída tocando em uma página de sessão, medida desde o início do roteador até a primeira pintura após o estabelecimento.",
|
||||||
|
"debugBar.fps.label": "FPS",
|
||||||
|
"debugBar.fps.tip": "Quadros por segundo nos últimos 5 segundos.",
|
||||||
|
"debugBar.frame.label": "FRAME",
|
||||||
|
"debugBar.frame.tip": "Pior tempo de quadro nos últimos 5 segundos.",
|
||||||
|
"debugBar.jank.label": "JANK",
|
||||||
|
"debugBar.jank.tip": "Quadros acima de 32ms nos últimos 5 segundos.",
|
||||||
|
"debugBar.long.label": "LONG",
|
||||||
|
"debugBar.long.tip": "Tempo bloqueado e contagem de tarefas longas nos últimos 5 segundos. Tarefa máx: {{max}}.",
|
||||||
|
"debugBar.delay.label": "DELAY",
|
||||||
|
"debugBar.delay.tip": "Pior atraso de entrada observado nos últimos 5 segundos.",
|
||||||
|
"debugBar.inp.label": "INP",
|
||||||
|
"debugBar.inp.tip":
|
||||||
|
"Duração aproximada da interação nos últimos 5 segundos. Isso é semelhante ao INP, não o INP oficial do Web Vitals.",
|
||||||
|
"debugBar.cls.label": "CLS",
|
||||||
|
"debugBar.cls.tip": "Mudança cumulativa de layout para o tempo de vida atual do aplicativo.",
|
||||||
|
"debugBar.mem.label": "MEM",
|
||||||
|
"debugBar.mem.tipUnavailable": "Heap JS usado vs limite de heap. Apenas Chromium.",
|
||||||
|
"debugBar.mem.tip": "Heap JS usado vs limite de heap. {{used}} de {{limit}}.",
|
||||||
|
"common.key.ctrl": "Ctrl",
|
||||||
|
"common.key.alt": "Alt",
|
||||||
|
"common.key.shift": "Shift",
|
||||||
|
"common.key.meta": "Meta",
|
||||||
|
"common.key.space": "Espaço",
|
||||||
|
"common.key.backspace": "Backspace",
|
||||||
|
"common.key.enter": "Enter",
|
||||||
|
"common.key.tab": "Tab",
|
||||||
|
"common.key.delete": "Delete",
|
||||||
|
"common.key.home": "Home",
|
||||||
|
"common.key.end": "End",
|
||||||
|
"common.key.pageUp": "Page Up",
|
||||||
|
"common.key.pageDown": "Page Down",
|
||||||
|
"common.key.insert": "Insert",
|
||||||
|
"common.unknown": "desconhecido",
|
||||||
|
"error.page.circular": "[Circular]",
|
||||||
|
"error.globalSDK.noServerAvailable": "Nenhum servidor disponível",
|
||||||
|
"error.globalSDK.serverNotAvailable": "Servidor indisponível",
|
||||||
|
"error.childStore.persistedCacheCreateFailed": "Falha ao criar cache persistente",
|
||||||
|
"error.childStore.persistedProjectMetadataCreateFailed": "Falha ao criar metadados de projeto persistentes",
|
||||||
|
"error.childStore.persistedProjectIconCreateFailed": "Falha ao criar ícone de projeto persistente",
|
||||||
|
"error.childStore.storeCreateFailed": "Falha ao criar armazenamento",
|
||||||
|
"terminal.connectionLost.abnormalClose": "WebSocket fechado anormalmente: {{code}}",
|
||||||
}
|
}
|
||||||
|
|||||||
+108
-3
@@ -113,6 +113,7 @@ export const dict = {
|
|||||||
"dialog.model.empty": "Nema rezultata za modele",
|
"dialog.model.empty": "Nema rezultata za modele",
|
||||||
"dialog.model.manage": "Upravljaj modelima",
|
"dialog.model.manage": "Upravljaj modelima",
|
||||||
"dialog.model.manage.description": "Prilagodi koji se modeli prikazuju u izborniku modela.",
|
"dialog.model.manage.description": "Prilagodi koji se modeli prikazuju u izborniku modela.",
|
||||||
|
"dialog.model.manage.provider.toggle": "Uključi/isključi sve {{provider}} modele",
|
||||||
|
|
||||||
"dialog.model.unpaid.freeModels.title": "Besplatni modeli koje obezbjeđuje OpenCode",
|
"dialog.model.unpaid.freeModels.title": "Besplatni modeli koje obezbjeđuje OpenCode",
|
||||||
"dialog.model.unpaid.addMore.title": "Dodaj još modela od popularnih provajdera",
|
"dialog.model.unpaid.addMore.title": "Dodaj još modela od popularnih provajdera",
|
||||||
@@ -263,7 +264,7 @@ export const dict = {
|
|||||||
|
|
||||||
"prompt.popover.emptyResults": "Nema rezultata",
|
"prompt.popover.emptyResults": "Nema rezultata",
|
||||||
"prompt.popover.emptyCommands": "Nema komandi",
|
"prompt.popover.emptyCommands": "Nema komandi",
|
||||||
"prompt.dropzone.label": "Spusti slike ili PDF-ove ovdje",
|
"prompt.dropzone.label": "Ovdje prevucite slike, PDF-ove ili tekstualne datoteke",
|
||||||
"prompt.dropzone.file.label": "Spusti za @spominjanje datoteke",
|
"prompt.dropzone.file.label": "Spusti za @spominjanje datoteke",
|
||||||
"prompt.slash.badge.custom": "prilagođeno",
|
"prompt.slash.badge.custom": "prilagođeno",
|
||||||
"prompt.slash.badge.skill": "skill",
|
"prompt.slash.badge.skill": "skill",
|
||||||
@@ -277,8 +278,8 @@ export const dict = {
|
|||||||
"prompt.action.send": "Pošalji",
|
"prompt.action.send": "Pošalji",
|
||||||
"prompt.action.stop": "Zaustavi",
|
"prompt.action.stop": "Zaustavi",
|
||||||
|
|
||||||
"prompt.toast.pasteUnsupported.title": "Nepodržano lijepljenje",
|
"prompt.toast.pasteUnsupported.title": "Nepodržan prilog",
|
||||||
"prompt.toast.pasteUnsupported.description": "Ovdje se mogu zalijepiti samo slike ili PDF-ovi.",
|
"prompt.toast.pasteUnsupported.description": "Ovdje se mogu priložiti samo slike, PDF-ovi ili tekstualne datoteke.",
|
||||||
"prompt.toast.modelAgentRequired.title": "Odaberi agenta i model",
|
"prompt.toast.modelAgentRequired.title": "Odaberi agenta i model",
|
||||||
"prompt.toast.modelAgentRequired.description": "Odaberi agenta i model prije slanja upita.",
|
"prompt.toast.modelAgentRequired.description": "Odaberi agenta i model prije slanja upita.",
|
||||||
"prompt.toast.worktreeCreateFailed.title": "Neuspješno kreiranje worktree-a",
|
"prompt.toast.worktreeCreateFailed.title": "Neuspješno kreiranje worktree-a",
|
||||||
@@ -315,6 +316,11 @@ export const dict = {
|
|||||||
"dialog.server.add.error": "Nije moguće povezati se na server",
|
"dialog.server.add.error": "Nije moguće povezati se na server",
|
||||||
"dialog.server.add.checking": "Provjera...",
|
"dialog.server.add.checking": "Provjera...",
|
||||||
"dialog.server.add.button": "Dodaj server",
|
"dialog.server.add.button": "Dodaj server",
|
||||||
|
"dialog.server.add.name": "Ime servera (opcionalno)",
|
||||||
|
"dialog.server.add.namePlaceholder": "Localhost",
|
||||||
|
"dialog.server.add.username": "Korisničko ime (opcionalno)",
|
||||||
|
"dialog.server.add.password": "Lozinka (opcionalno)",
|
||||||
|
"dialog.server.edit.title": "Uredi server",
|
||||||
"dialog.server.default.title": "Podrazumijevani server",
|
"dialog.server.default.title": "Podrazumijevani server",
|
||||||
"dialog.server.default.description":
|
"dialog.server.default.description":
|
||||||
"Poveži se na ovaj server pri pokretanju aplikacije umjesto pokretanja lokalnog servera. Potreban je restart.",
|
"Poveži se na ovaj server pri pokretanju aplikacije umjesto pokretanja lokalnog servera. Potreban je restart.",
|
||||||
@@ -393,6 +399,7 @@ export const dict = {
|
|||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.bs": "Bosanski",
|
"language.bs": "Bosanski",
|
||||||
"language.th": "ไทย",
|
"language.th": "ไทย",
|
||||||
|
"language.tr": "Türkçe",
|
||||||
|
|
||||||
"toast.language.title": "Jezik",
|
"toast.language.title": "Jezik",
|
||||||
"toast.language.description": "Prebačeno na {{language}}",
|
"toast.language.description": "Prebačeno na {{language}}",
|
||||||
@@ -498,10 +505,14 @@ export const dict = {
|
|||||||
"session.review.change.other": "Izmjene",
|
"session.review.change.other": "Izmjene",
|
||||||
"session.review.loadingChanges": "Učitavanje izmjena...",
|
"session.review.loadingChanges": "Učitavanje izmjena...",
|
||||||
"session.review.empty": "Još nema izmjena u ovoj sesiji",
|
"session.review.empty": "Još nema izmjena u ovoj sesiji",
|
||||||
|
"session.review.noVcs": "Nije detektovan Git sistem kontrole verzija, promjene se ne prikazuju",
|
||||||
|
"session.review.noSnapshot":
|
||||||
|
"Praćenje snimaka (snapshot) je onemogućeno u konfiguraciji, pa promjene sesije nisu dostupne",
|
||||||
"session.review.noChanges": "Nema izmjena",
|
"session.review.noChanges": "Nema izmjena",
|
||||||
|
|
||||||
"session.files.selectToOpen": "Odaberi datoteku za otvaranje",
|
"session.files.selectToOpen": "Odaberi datoteku za otvaranje",
|
||||||
"session.files.all": "Sve datoteke",
|
"session.files.all": "Sve datoteke",
|
||||||
|
"session.files.empty": "Nema datoteka",
|
||||||
"session.files.binaryContent": "Binarna datoteka (sadržaj se ne može prikazati)",
|
"session.files.binaryContent": "Binarna datoteka (sadržaj se ne može prikazati)",
|
||||||
|
|
||||||
"session.messages.renderEarlier": "Prikaži ranije poruke",
|
"session.messages.renderEarlier": "Prikaži ranije poruke",
|
||||||
@@ -514,6 +525,17 @@ export const dict = {
|
|||||||
"session.todo.title": "Zadaci",
|
"session.todo.title": "Zadaci",
|
||||||
"session.todo.collapse": "Sažmi",
|
"session.todo.collapse": "Sažmi",
|
||||||
"session.todo.expand": "Proširi",
|
"session.todo.expand": "Proširi",
|
||||||
|
"session.followupDock.summary.one": "{{count}} poruka na čekanju",
|
||||||
|
"session.followupDock.summary.other": "{{count}} poruka na čekanju",
|
||||||
|
"session.followupDock.sendNow": "Pošalji sada",
|
||||||
|
"session.followupDock.edit": "Uredi",
|
||||||
|
"session.followupDock.collapse": "Sažmi poruke na čekanju",
|
||||||
|
"session.followupDock.expand": "Proširi poruke na čekanju",
|
||||||
|
"session.revertDock.summary.one": "{{count}} vraćena poruka",
|
||||||
|
"session.revertDock.summary.other": "{{count}} vraćenih poruka",
|
||||||
|
"session.revertDock.collapse": "Sažmi vraćene poruke",
|
||||||
|
"session.revertDock.expand": "Proširi vraćene poruke",
|
||||||
|
"session.revertDock.restore": "Vrati poruku",
|
||||||
|
|
||||||
"session.new.title": "Napravi bilo šta",
|
"session.new.title": "Napravi bilo šta",
|
||||||
"session.new.worktree.main": "Glavna grana",
|
"session.new.worktree.main": "Glavna grana",
|
||||||
@@ -609,10 +631,18 @@ export const dict = {
|
|||||||
"settings.general.row.language.description": "Promijeni jezik prikaza u OpenCode-u",
|
"settings.general.row.language.description": "Promijeni jezik prikaza u OpenCode-u",
|
||||||
"settings.general.row.appearance.title": "Izgled",
|
"settings.general.row.appearance.title": "Izgled",
|
||||||
"settings.general.row.appearance.description": "Prilagodi kako OpenCode izgleda na tvom uređaju",
|
"settings.general.row.appearance.description": "Prilagodi kako OpenCode izgleda na tvom uređaju",
|
||||||
|
"settings.general.row.colorScheme.title": "Šema boja",
|
||||||
|
"settings.general.row.colorScheme.description": "Odaberi da li OpenCode prati sistemsku, svijetlu ili tamnu temu",
|
||||||
"settings.general.row.theme.title": "Tema",
|
"settings.general.row.theme.title": "Tema",
|
||||||
"settings.general.row.theme.description": "Prilagodi temu OpenCode-a.",
|
"settings.general.row.theme.description": "Prilagodi temu OpenCode-a.",
|
||||||
"settings.general.row.font.title": "Font",
|
"settings.general.row.font.title": "Font",
|
||||||
"settings.general.row.font.description": "Prilagodi monospace font koji se koristi u blokovima koda",
|
"settings.general.row.font.description": "Prilagodi monospace font koji se koristi u blokovima koda",
|
||||||
|
"settings.general.row.followup.title": "Ponašanje nadovezivanja",
|
||||||
|
"settings.general.row.followup.description": "Odaberi da li upiti nadovezivanja usmjeravaju odmah ili čekaju u redu",
|
||||||
|
"settings.general.row.followup.option.queue": "Red čekanja",
|
||||||
|
"settings.general.row.followup.option.steer": "Usmjeri",
|
||||||
|
"settings.general.row.reasoningSummaries.title": "Prikaži sažetke rasuđivanja",
|
||||||
|
"settings.general.row.reasoningSummaries.description": "Prikaži sažetke rasuđivanja modela na vremenskoj traci",
|
||||||
|
|
||||||
"settings.general.row.shellToolPartsExpanded.title": "Proširi dijelove shell alata",
|
"settings.general.row.shellToolPartsExpanded.title": "Proširi dijelove shell alata",
|
||||||
"settings.general.row.shellToolPartsExpanded.description":
|
"settings.general.row.shellToolPartsExpanded.description":
|
||||||
@@ -834,4 +864,79 @@ export const dict = {
|
|||||||
"common.time.daysAgo.short": "prije {{count}} d",
|
"common.time.daysAgo.short": "prije {{count}} d",
|
||||||
"settings.providers.connected.environmentDescription": "Povezano sa vašim varijablama okruženja",
|
"settings.providers.connected.environmentDescription": "Povezano sa vašim varijablama okruženja",
|
||||||
"settings.providers.custom.description": "Dodajte provajdera kompatibilnog s OpenAI putem osnovnog URL-a.",
|
"settings.providers.custom.description": "Dodajte provajdera kompatibilnog s OpenAI putem osnovnog URL-a.",
|
||||||
|
|
||||||
|
"app.server.unreachable": "Nije moguće pristupiti {{server}}",
|
||||||
|
"app.server.retrying": "Automatski ponovni pokušaj...",
|
||||||
|
"app.server.otherServers": "Drugi serveri",
|
||||||
|
"dialog.server.add.usernamePlaceholder": "korisničko ime",
|
||||||
|
"dialog.server.add.passwordPlaceholder": "lozinka",
|
||||||
|
"server.row.noUsername": "nema korisničkog imena",
|
||||||
|
"session.review.noVcs.createGit.title": "Kreiraj Git repozitorij",
|
||||||
|
"session.review.noVcs.createGit.description": "Pratite, pregledajte i poništite promjene u ovom projektu",
|
||||||
|
"session.review.noVcs.createGit.actionLoading": "Kreiranje Git repozitorija...",
|
||||||
|
"session.review.noVcs.createGit.action": "Kreiraj Git repozitorij",
|
||||||
|
"session.todo.progress": "{{done}} od {{total}} zadataka završeno",
|
||||||
|
"session.question.progress": "{{current}} od {{total}} pitanja",
|
||||||
|
"session.header.open.finder": "Finder",
|
||||||
|
"session.header.open.fileExplorer": "File Explorer",
|
||||||
|
"session.header.open.fileManager": "File Manager",
|
||||||
|
"session.header.open.app.vscode": "VS Code",
|
||||||
|
"session.header.open.app.cursor": "Cursor",
|
||||||
|
"session.header.open.app.zed": "Zed",
|
||||||
|
"session.header.open.app.textmate": "TextMate",
|
||||||
|
"session.header.open.app.antigravity": "Antigravity",
|
||||||
|
"session.header.open.app.terminal": "Terminal",
|
||||||
|
"session.header.open.app.iterm2": "iTerm2",
|
||||||
|
"session.header.open.app.ghostty": "Ghostty",
|
||||||
|
"session.header.open.app.warp": "Warp",
|
||||||
|
"session.header.open.app.xcode": "Xcode",
|
||||||
|
"session.header.open.app.androidStudio": "Android Studio",
|
||||||
|
"session.header.open.app.powershell": "PowerShell",
|
||||||
|
"session.header.open.app.sublimeText": "Sublime Text",
|
||||||
|
"debugBar.ariaLabel": "Dijagnostika performansi razvoja",
|
||||||
|
"debugBar.na": "n/a",
|
||||||
|
"debugBar.nav.label": "NAV",
|
||||||
|
"debugBar.nav.tip":
|
||||||
|
"Posljednji završeni prelazak rute koji dotiče stranicu sesije, mjeren od početka rutera do prvog iscrtavanja nakon smirivanja.",
|
||||||
|
"debugBar.fps.label": "FPS",
|
||||||
|
"debugBar.fps.tip": "Kadrovi u sekundi tokom posljednjih 5 sekundi.",
|
||||||
|
"debugBar.frame.label": "FRAME",
|
||||||
|
"debugBar.frame.tip": "Najgore vrijeme kadra u posljednjih 5 sekundi.",
|
||||||
|
"debugBar.jank.label": "JANK",
|
||||||
|
"debugBar.jank.tip": "Kadrovi duži od 32ms u posljednjih 5 sekundi.",
|
||||||
|
"debugBar.long.label": "LONG",
|
||||||
|
"debugBar.long.tip": "Blokirano vrijeme i broj dugih zadataka u posljednjih 5 sekundi. Maks zadatak: {{max}}.",
|
||||||
|
"debugBar.delay.label": "DELAY",
|
||||||
|
"debugBar.delay.tip": "Najgore zabilježeno kašnjenje unosa u posljednjih 5 sekundi.",
|
||||||
|
"debugBar.inp.label": "INP",
|
||||||
|
"debugBar.inp.tip":
|
||||||
|
"Približno trajanje interakcije tokom posljednjih 5 sekundi. Ovo je slično INP-u, nije službeni Web Vitals INP.",
|
||||||
|
"debugBar.cls.label": "CLS",
|
||||||
|
"debugBar.cls.tip": "Kumulativni pomak rasporeda za trenutni životni vijek aplikacije.",
|
||||||
|
"debugBar.mem.label": "MEM",
|
||||||
|
"debugBar.mem.tipUnavailable": "Korišteni JS heap naspram limita heapa. Samo Chromium.",
|
||||||
|
"debugBar.mem.tip": "Korišteni JS heap naspram limita heapa. {{used}} od {{limit}}.",
|
||||||
|
"common.key.ctrl": "Ctrl",
|
||||||
|
"common.key.alt": "Alt",
|
||||||
|
"common.key.shift": "Shift",
|
||||||
|
"common.key.meta": "Meta",
|
||||||
|
"common.key.space": "Space",
|
||||||
|
"common.key.backspace": "Backspace",
|
||||||
|
"common.key.enter": "Enter",
|
||||||
|
"common.key.tab": "Tab",
|
||||||
|
"common.key.delete": "Delete",
|
||||||
|
"common.key.home": "Home",
|
||||||
|
"common.key.end": "End",
|
||||||
|
"common.key.pageUp": "Page Up",
|
||||||
|
"common.key.pageDown": "Page Down",
|
||||||
|
"common.key.insert": "Insert",
|
||||||
|
"common.unknown": "nepoznato",
|
||||||
|
"error.page.circular": "[Kružno]",
|
||||||
|
"error.globalSDK.noServerAvailable": "Nema dostupnog servera",
|
||||||
|
"error.globalSDK.serverNotAvailable": "Server nije dostupan",
|
||||||
|
"error.childStore.persistedCacheCreateFailed": "Nije uspjelo kreiranje trajnog keša",
|
||||||
|
"error.childStore.persistedProjectMetadataCreateFailed": "Nije uspjelo kreiranje trajnih metapodataka projekta",
|
||||||
|
"error.childStore.persistedProjectIconCreateFailed": "Nije uspjelo kreiranje trajne ikone projekta",
|
||||||
|
"error.childStore.storeCreateFailed": "Nije uspjelo kreiranje skladišta",
|
||||||
|
"terminal.connectionLost.abnormalClose": "WebSocket zatvoren nenormalno: {{code}}",
|
||||||
}
|
}
|
||||||
|
|||||||
+108
-3
@@ -113,6 +113,7 @@ export const dict = {
|
|||||||
"dialog.model.empty": "Ingen modeller fundet",
|
"dialog.model.empty": "Ingen modeller fundet",
|
||||||
"dialog.model.manage": "Administrer modeller",
|
"dialog.model.manage": "Administrer modeller",
|
||||||
"dialog.model.manage.description": "Tilpas hvilke modeller der vises i modelvælgeren.",
|
"dialog.model.manage.description": "Tilpas hvilke modeller der vises i modelvælgeren.",
|
||||||
|
"dialog.model.manage.provider.toggle": "Skift alle {{provider}}-modeller",
|
||||||
|
|
||||||
"dialog.model.unpaid.freeModels.title": "Gratis modeller leveret af OpenCode",
|
"dialog.model.unpaid.freeModels.title": "Gratis modeller leveret af OpenCode",
|
||||||
"dialog.model.unpaid.addMore.title": "Tilføj flere modeller fra populære udbydere",
|
"dialog.model.unpaid.addMore.title": "Tilføj flere modeller fra populære udbydere",
|
||||||
@@ -261,7 +262,7 @@ export const dict = {
|
|||||||
|
|
||||||
"prompt.popover.emptyResults": "Ingen matchende resultater",
|
"prompt.popover.emptyResults": "Ingen matchende resultater",
|
||||||
"prompt.popover.emptyCommands": "Ingen matchende kommandoer",
|
"prompt.popover.emptyCommands": "Ingen matchende kommandoer",
|
||||||
"prompt.dropzone.label": "Slip billeder eller PDF'er her",
|
"prompt.dropzone.label": "Slip billeder, PDF'er eller tekstfiler her",
|
||||||
"prompt.dropzone.file.label": "Slip for at @nævne fil",
|
"prompt.dropzone.file.label": "Slip for at @nævne fil",
|
||||||
"prompt.slash.badge.custom": "brugerdefineret",
|
"prompt.slash.badge.custom": "brugerdefineret",
|
||||||
"prompt.slash.badge.skill": "skill",
|
"prompt.slash.badge.skill": "skill",
|
||||||
@@ -275,8 +276,8 @@ export const dict = {
|
|||||||
"prompt.action.send": "Send",
|
"prompt.action.send": "Send",
|
||||||
"prompt.action.stop": "Stop",
|
"prompt.action.stop": "Stop",
|
||||||
|
|
||||||
"prompt.toast.pasteUnsupported.title": "Ikke understøttet indsæt",
|
"prompt.toast.pasteUnsupported.title": "Ikke understøttet vedhæftning",
|
||||||
"prompt.toast.pasteUnsupported.description": "Kun billeder eller PDF'er kan indsættes her.",
|
"prompt.toast.pasteUnsupported.description": "Kun billeder, PDF'er eller tekstfiler kan vedhæftes her.",
|
||||||
"prompt.toast.modelAgentRequired.title": "Vælg en agent og model",
|
"prompt.toast.modelAgentRequired.title": "Vælg en agent og model",
|
||||||
"prompt.toast.modelAgentRequired.description": "Vælg en agent og model før du sender en forespørgsel.",
|
"prompt.toast.modelAgentRequired.description": "Vælg en agent og model før du sender en forespørgsel.",
|
||||||
"prompt.toast.worktreeCreateFailed.title": "Kunne ikke oprette worktree",
|
"prompt.toast.worktreeCreateFailed.title": "Kunne ikke oprette worktree",
|
||||||
@@ -313,6 +314,11 @@ export const dict = {
|
|||||||
"dialog.server.add.error": "Kunne ikke forbinde til server",
|
"dialog.server.add.error": "Kunne ikke forbinde til server",
|
||||||
"dialog.server.add.checking": "Tjekker...",
|
"dialog.server.add.checking": "Tjekker...",
|
||||||
"dialog.server.add.button": "Tilføj server",
|
"dialog.server.add.button": "Tilføj server",
|
||||||
|
"dialog.server.add.name": "Servernavn (valgfrit)",
|
||||||
|
"dialog.server.add.namePlaceholder": "Localhost",
|
||||||
|
"dialog.server.add.username": "Brugernavn (valgfrit)",
|
||||||
|
"dialog.server.add.password": "Adgangskode (valgfrit)",
|
||||||
|
"dialog.server.edit.title": "Rediger server",
|
||||||
"dialog.server.default.title": "Standardserver",
|
"dialog.server.default.title": "Standardserver",
|
||||||
"dialog.server.default.description":
|
"dialog.server.default.description":
|
||||||
"Forbind til denne server ved start af app i stedet for at starte en lokal server. Kræver genstart.",
|
"Forbind til denne server ved start af app i stedet for at starte en lokal server. Kræver genstart.",
|
||||||
@@ -391,6 +397,7 @@ export const dict = {
|
|||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.bs": "Bosanski",
|
"language.bs": "Bosanski",
|
||||||
"language.th": "ไทย",
|
"language.th": "ไทย",
|
||||||
|
"language.tr": "Türkçe",
|
||||||
|
|
||||||
"toast.language.title": "Sprog",
|
"toast.language.title": "Sprog",
|
||||||
"toast.language.description": "Skiftede til {{language}}",
|
"toast.language.description": "Skiftede til {{language}}",
|
||||||
@@ -495,9 +502,13 @@ export const dict = {
|
|||||||
"session.review.change.other": "Ændringer",
|
"session.review.change.other": "Ændringer",
|
||||||
"session.review.loadingChanges": "Indlæser ændringer...",
|
"session.review.loadingChanges": "Indlæser ændringer...",
|
||||||
"session.review.empty": "Ingen ændringer i denne session endnu",
|
"session.review.empty": "Ingen ændringer i denne session endnu",
|
||||||
|
"session.review.noVcs": "Intet Git versionsstyringssystem fundet, ændringer vises ikke",
|
||||||
|
"session.review.noSnapshot":
|
||||||
|
"Snapshot-sporing er deaktiveret i konfigurationen, så sessionsændringer er ikke tilgængelige",
|
||||||
"session.review.noChanges": "Ingen ændringer",
|
"session.review.noChanges": "Ingen ændringer",
|
||||||
"session.files.selectToOpen": "Vælg en fil at åbne",
|
"session.files.selectToOpen": "Vælg en fil at åbne",
|
||||||
"session.files.all": "Alle filer",
|
"session.files.all": "Alle filer",
|
||||||
|
"session.files.empty": "Ingen filer",
|
||||||
"session.files.binaryContent": "Binær fil (indhold kan ikke vises)",
|
"session.files.binaryContent": "Binær fil (indhold kan ikke vises)",
|
||||||
"session.messages.renderEarlier": "Vis tidligere beskeder",
|
"session.messages.renderEarlier": "Vis tidligere beskeder",
|
||||||
"session.messages.loadingEarlier": "Indlæser tidligere beskeder...",
|
"session.messages.loadingEarlier": "Indlæser tidligere beskeder...",
|
||||||
@@ -509,6 +520,17 @@ export const dict = {
|
|||||||
"session.todo.title": "Opgaver",
|
"session.todo.title": "Opgaver",
|
||||||
"session.todo.collapse": "Skjul",
|
"session.todo.collapse": "Skjul",
|
||||||
"session.todo.expand": "Udvid",
|
"session.todo.expand": "Udvid",
|
||||||
|
"session.followupDock.summary.one": "{{count}} besked i kø",
|
||||||
|
"session.followupDock.summary.other": "{{count}} beskeder i kø",
|
||||||
|
"session.followupDock.sendNow": "Send nu",
|
||||||
|
"session.followupDock.edit": "Rediger",
|
||||||
|
"session.followupDock.collapse": "Skjul beskeder i kø",
|
||||||
|
"session.followupDock.expand": "Udvid beskeder i kø",
|
||||||
|
"session.revertDock.summary.one": "{{count}} tilbagerullet besked",
|
||||||
|
"session.revertDock.summary.other": "{{count}} tilbagerullede beskeder",
|
||||||
|
"session.revertDock.collapse": "Skjul tilbagerullede beskeder",
|
||||||
|
"session.revertDock.expand": "Udvid tilbagerullede beskeder",
|
||||||
|
"session.revertDock.restore": "Gendan besked",
|
||||||
|
|
||||||
"session.new.title": "Byg hvad som helst",
|
"session.new.title": "Byg hvad som helst",
|
||||||
"session.new.worktree.main": "Hovedgren",
|
"session.new.worktree.main": "Hovedgren",
|
||||||
@@ -604,10 +626,18 @@ export const dict = {
|
|||||||
"settings.general.row.language.description": "Ændr visningssproget for OpenCode",
|
"settings.general.row.language.description": "Ændr visningssproget for OpenCode",
|
||||||
"settings.general.row.appearance.title": "Udseende",
|
"settings.general.row.appearance.title": "Udseende",
|
||||||
"settings.general.row.appearance.description": "Tilpas hvordan OpenCode ser ud på din enhed",
|
"settings.general.row.appearance.description": "Tilpas hvordan OpenCode ser ud på din enhed",
|
||||||
|
"settings.general.row.colorScheme.title": "Farveskema",
|
||||||
|
"settings.general.row.colorScheme.description": "Vælg om OpenCode følger systemets, lyst eller mørkt tema",
|
||||||
"settings.general.row.theme.title": "Tema",
|
"settings.general.row.theme.title": "Tema",
|
||||||
"settings.general.row.theme.description": "Tilpas hvordan OpenCode er temabestemt.",
|
"settings.general.row.theme.description": "Tilpas hvordan OpenCode er temabestemt.",
|
||||||
"settings.general.row.font.title": "Skrifttype",
|
"settings.general.row.font.title": "Skrifttype",
|
||||||
"settings.general.row.font.description": "Tilpas mono-skrifttypen brugt i kodeblokke",
|
"settings.general.row.font.description": "Tilpas mono-skrifttypen brugt i kodeblokke",
|
||||||
|
"settings.general.row.followup.title": "Opfølgningsadfærd",
|
||||||
|
"settings.general.row.followup.description": "Vælg om opfølgende forespørgsler skal styre straks eller vente i kø",
|
||||||
|
"settings.general.row.followup.option.queue": "Kø",
|
||||||
|
"settings.general.row.followup.option.steer": "Styr",
|
||||||
|
"settings.general.row.reasoningSummaries.title": "Vis tænkeoversigter",
|
||||||
|
"settings.general.row.reasoningSummaries.description": "Vis model tænkeoversigter i tidslinjen",
|
||||||
|
|
||||||
"settings.general.row.shellToolPartsExpanded.title": "Udvid shell-værktøjsdele",
|
"settings.general.row.shellToolPartsExpanded.title": "Udvid shell-værktøjsdele",
|
||||||
"settings.general.row.shellToolPartsExpanded.description": "Vis shell-værktøjsdele udvidet som standard i tidslinjen",
|
"settings.general.row.shellToolPartsExpanded.description": "Vis shell-værktøjsdele udvidet som standard i tidslinjen",
|
||||||
@@ -828,4 +858,79 @@ export const dict = {
|
|||||||
"common.time.daysAgo.short": "{{count}}d siden",
|
"common.time.daysAgo.short": "{{count}}d siden",
|
||||||
"settings.providers.connected.environmentDescription": "Tilsluttet fra dine miljøvariabler",
|
"settings.providers.connected.environmentDescription": "Tilsluttet fra dine miljøvariabler",
|
||||||
"settings.providers.custom.description": "Tilføj en OpenAI-kompatibel udbyder via basis-URL.",
|
"settings.providers.custom.description": "Tilføj en OpenAI-kompatibel udbyder via basis-URL.",
|
||||||
|
|
||||||
|
"app.server.unreachable": "Kunne ikke nå {{server}}",
|
||||||
|
"app.server.retrying": "Prøver igen automatisk...",
|
||||||
|
"app.server.otherServers": "Andre servere",
|
||||||
|
"dialog.server.add.usernamePlaceholder": "brugernavn",
|
||||||
|
"dialog.server.add.passwordPlaceholder": "adgangskode",
|
||||||
|
"server.row.noUsername": "intet brugernavn",
|
||||||
|
"session.review.noVcs.createGit.title": "Opret et Git-repository",
|
||||||
|
"session.review.noVcs.createGit.description": "Spor, gennemgå og fortryd ændringer i dette projekt",
|
||||||
|
"session.review.noVcs.createGit.actionLoading": "Opretter Git-repository...",
|
||||||
|
"session.review.noVcs.createGit.action": "Opret Git-repository",
|
||||||
|
"session.todo.progress": "{{done}} af {{total}} opgaver fuldført",
|
||||||
|
"session.question.progress": "{{current}} af {{total}} spørgsmål",
|
||||||
|
"session.header.open.finder": "Finder",
|
||||||
|
"session.header.open.fileExplorer": "Stifinder",
|
||||||
|
"session.header.open.fileManager": "Filhåndtering",
|
||||||
|
"session.header.open.app.vscode": "VS Code",
|
||||||
|
"session.header.open.app.cursor": "Cursor",
|
||||||
|
"session.header.open.app.zed": "Zed",
|
||||||
|
"session.header.open.app.textmate": "TextMate",
|
||||||
|
"session.header.open.app.antigravity": "Antigravity",
|
||||||
|
"session.header.open.app.terminal": "Terminal",
|
||||||
|
"session.header.open.app.iterm2": "iTerm2",
|
||||||
|
"session.header.open.app.ghostty": "Ghostty",
|
||||||
|
"session.header.open.app.warp": "Warp",
|
||||||
|
"session.header.open.app.xcode": "Xcode",
|
||||||
|
"session.header.open.app.androidStudio": "Android Studio",
|
||||||
|
"session.header.open.app.powershell": "PowerShell",
|
||||||
|
"session.header.open.app.sublimeText": "Sublime Text",
|
||||||
|
"debugBar.ariaLabel": "Udviklingsydelsesdiagnostik",
|
||||||
|
"debugBar.na": "n/a",
|
||||||
|
"debugBar.nav.label": "NAV",
|
||||||
|
"debugBar.nav.tip":
|
||||||
|
"Sidste gennemførte ruteovergang, der berører en sessionsside, målt fra routerstart til den første optegning efter den falder til ro.",
|
||||||
|
"debugBar.fps.label": "FPS",
|
||||||
|
"debugBar.fps.tip": "Rullende billeder pr. sekund over de sidste 5 sekunder.",
|
||||||
|
"debugBar.frame.label": "FRAME",
|
||||||
|
"debugBar.frame.tip": "Værste billedtid over de sidste 5 sekunder.",
|
||||||
|
"debugBar.jank.label": "JANK",
|
||||||
|
"debugBar.jank.tip": "Billeder over 32ms i de sidste 5 sekunder.",
|
||||||
|
"debugBar.long.label": "LONG",
|
||||||
|
"debugBar.long.tip": "Blokeret tid og antal lange opgaver i de sidste 5 sekunder. Maks opgave: {{max}}.",
|
||||||
|
"debugBar.delay.label": "DELAY",
|
||||||
|
"debugBar.delay.tip": "Værste observerede inputforsinkelse i de sidste 5 sekunder.",
|
||||||
|
"debugBar.inp.label": "INP",
|
||||||
|
"debugBar.inp.tip":
|
||||||
|
"Omtrentlig interaktionsvarighed over de sidste 5 sekunder. Dette er INP-lignende, ikke den officielle Web Vitals INP.",
|
||||||
|
"debugBar.cls.label": "CLS",
|
||||||
|
"debugBar.cls.tip": "Kumulativt layoutskift for den nuværende app-levetid.",
|
||||||
|
"debugBar.mem.label": "MEM",
|
||||||
|
"debugBar.mem.tipUnavailable": "Brugt JS-heap vs heap-grænse. Kun Chromium.",
|
||||||
|
"debugBar.mem.tip": "Brugt JS-heap vs heap-grænse. {{used}} af {{limit}}.",
|
||||||
|
"common.key.ctrl": "Ctrl",
|
||||||
|
"common.key.alt": "Alt",
|
||||||
|
"common.key.shift": "Shift",
|
||||||
|
"common.key.meta": "Meta",
|
||||||
|
"common.key.space": "Mellemrum",
|
||||||
|
"common.key.backspace": "Backspace",
|
||||||
|
"common.key.enter": "Enter",
|
||||||
|
"common.key.tab": "Tab",
|
||||||
|
"common.key.delete": "Delete",
|
||||||
|
"common.key.home": "Home",
|
||||||
|
"common.key.end": "End",
|
||||||
|
"common.key.pageUp": "Page Up",
|
||||||
|
"common.key.pageDown": "Page Down",
|
||||||
|
"common.key.insert": "Insert",
|
||||||
|
"common.unknown": "ukendt",
|
||||||
|
"error.page.circular": "[Cirkulær]",
|
||||||
|
"error.globalSDK.noServerAvailable": "Ingen server tilgængelig",
|
||||||
|
"error.globalSDK.serverNotAvailable": "Server ikke tilgængelig",
|
||||||
|
"error.childStore.persistedCacheCreateFailed": "Kunne ikke oprette vedvarende cache",
|
||||||
|
"error.childStore.persistedProjectMetadataCreateFailed": "Kunne ikke oprette vedvarende projektmetadata",
|
||||||
|
"error.childStore.persistedProjectIconCreateFailed": "Kunne ikke oprette vedvarende projektikon",
|
||||||
|
"error.childStore.storeCreateFailed": "Kunne ikke oprette lager",
|
||||||
|
"terminal.connectionLost.abnormalClose": "WebSocket lukkede unormalt: {{code}}",
|
||||||
}
|
}
|
||||||
|
|||||||
+112
-3
@@ -108,6 +108,7 @@ export const dict = {
|
|||||||
"dialog.model.empty": "Keine Modellergebnisse",
|
"dialog.model.empty": "Keine Modellergebnisse",
|
||||||
"dialog.model.manage": "Modelle verwalten",
|
"dialog.model.manage": "Modelle verwalten",
|
||||||
"dialog.model.manage.description": "Anpassen, welche Modelle in der Modellauswahl erscheinen.",
|
"dialog.model.manage.description": "Anpassen, welche Modelle in der Modellauswahl erscheinen.",
|
||||||
|
"dialog.model.manage.provider.toggle": "Alle {{provider}}-Modelle umschalten",
|
||||||
"dialog.model.unpaid.freeModels.title": "Kostenlose Modelle von OpenCode",
|
"dialog.model.unpaid.freeModels.title": "Kostenlose Modelle von OpenCode",
|
||||||
"dialog.model.unpaid.addMore.title": "Weitere Modelle von beliebten Anbietern hinzufügen",
|
"dialog.model.unpaid.addMore.title": "Weitere Modelle von beliebten Anbietern hinzufügen",
|
||||||
"dialog.provider.viewAll": "Mehr Anbieter anzeigen",
|
"dialog.provider.viewAll": "Mehr Anbieter anzeigen",
|
||||||
@@ -248,7 +249,7 @@ export const dict = {
|
|||||||
"prompt.example.25": "Wie funktionieren Umgebungsvariablen hier?",
|
"prompt.example.25": "Wie funktionieren Umgebungsvariablen hier?",
|
||||||
"prompt.popover.emptyResults": "Keine passenden Ergebnisse",
|
"prompt.popover.emptyResults": "Keine passenden Ergebnisse",
|
||||||
"prompt.popover.emptyCommands": "Keine passenden Befehle",
|
"prompt.popover.emptyCommands": "Keine passenden Befehle",
|
||||||
"prompt.dropzone.label": "Bilder oder PDFs hier ablegen",
|
"prompt.dropzone.label": "Bilder, PDFs oder Textdateien hier ablegen",
|
||||||
"prompt.dropzone.file.label": "Ablegen zum @Erwähnen der Datei",
|
"prompt.dropzone.file.label": "Ablegen zum @Erwähnen der Datei",
|
||||||
"prompt.slash.badge.custom": "benutzerdefiniert",
|
"prompt.slash.badge.custom": "benutzerdefiniert",
|
||||||
"prompt.slash.badge.skill": "Skill",
|
"prompt.slash.badge.skill": "Skill",
|
||||||
@@ -261,8 +262,8 @@ export const dict = {
|
|||||||
"prompt.attachment.remove": "Anhang entfernen",
|
"prompt.attachment.remove": "Anhang entfernen",
|
||||||
"prompt.action.send": "Senden",
|
"prompt.action.send": "Senden",
|
||||||
"prompt.action.stop": "Stopp",
|
"prompt.action.stop": "Stopp",
|
||||||
"prompt.toast.pasteUnsupported.title": "Nicht unterstütztes Einfügen",
|
"prompt.toast.pasteUnsupported.title": "Nicht unterstützter Anhang",
|
||||||
"prompt.toast.pasteUnsupported.description": "Hier können nur Bilder oder PDFs eingefügt werden.",
|
"prompt.toast.pasteUnsupported.description": "Hier können nur Bilder, PDFs oder Textdateien angehängt werden.",
|
||||||
"prompt.toast.modelAgentRequired.title": "Wählen Sie einen Agenten und ein Modell",
|
"prompt.toast.modelAgentRequired.title": "Wählen Sie einen Agenten und ein Modell",
|
||||||
"prompt.toast.modelAgentRequired.description":
|
"prompt.toast.modelAgentRequired.description":
|
||||||
"Wählen Sie einen Agenten und ein Modell, bevor Sie eine Eingabe senden.",
|
"Wählen Sie einen Agenten und ein Modell, bevor Sie eine Eingabe senden.",
|
||||||
@@ -294,6 +295,11 @@ export const dict = {
|
|||||||
"dialog.server.add.error": "Verbindung zum Server fehlgeschlagen",
|
"dialog.server.add.error": "Verbindung zum Server fehlgeschlagen",
|
||||||
"dialog.server.add.checking": "Prüfen...",
|
"dialog.server.add.checking": "Prüfen...",
|
||||||
"dialog.server.add.button": "Server hinzufügen",
|
"dialog.server.add.button": "Server hinzufügen",
|
||||||
|
"dialog.server.add.name": "Servername (optional)",
|
||||||
|
"dialog.server.add.namePlaceholder": "Localhost",
|
||||||
|
"dialog.server.add.username": "Benutzername (optional)",
|
||||||
|
"dialog.server.add.password": "Passwort (optional)",
|
||||||
|
"dialog.server.edit.title": "Server bearbeiten",
|
||||||
"dialog.server.default.title": "Standardserver",
|
"dialog.server.default.title": "Standardserver",
|
||||||
"dialog.server.default.description":
|
"dialog.server.default.description":
|
||||||
"Beim App-Start mit diesem Server verbinden, anstatt einen lokalen Server zu starten. Erfordert Neustart.",
|
"Beim App-Start mit diesem Server verbinden, anstatt einen lokalen Server zu starten. Erfordert Neustart.",
|
||||||
@@ -366,6 +372,7 @@ export const dict = {
|
|||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.bs": "Bosanski",
|
"language.bs": "Bosanski",
|
||||||
"language.th": "ไทย",
|
"language.th": "ไทย",
|
||||||
|
"language.tr": "Türkçe",
|
||||||
"toast.language.title": "Sprache",
|
"toast.language.title": "Sprache",
|
||||||
"toast.language.description": "Zu {{language}} gewechselt",
|
"toast.language.description": "Zu {{language}} gewechselt",
|
||||||
"toast.theme.title": "Thema gewechselt",
|
"toast.theme.title": "Thema gewechselt",
|
||||||
@@ -454,9 +461,13 @@ export const dict = {
|
|||||||
"session.review.change.other": "Änderungen",
|
"session.review.change.other": "Änderungen",
|
||||||
"session.review.loadingChanges": "Lade Änderungen...",
|
"session.review.loadingChanges": "Lade Änderungen...",
|
||||||
"session.review.empty": "Noch keine Änderungen in dieser Sitzung",
|
"session.review.empty": "Noch keine Änderungen in dieser Sitzung",
|
||||||
|
"session.review.noVcs": "Kein Git-Versionskontrollsystem erkannt, Änderungen werden nicht angezeigt",
|
||||||
|
"session.review.noSnapshot":
|
||||||
|
"Snapshot-Tracking ist in der Konfiguration deaktiviert, daher sind Sitzungsänderungen nicht verfügbar",
|
||||||
"session.review.noChanges": "Keine Änderungen",
|
"session.review.noChanges": "Keine Änderungen",
|
||||||
"session.files.selectToOpen": "Datei zum Öffnen auswählen",
|
"session.files.selectToOpen": "Datei zum Öffnen auswählen",
|
||||||
"session.files.all": "Alle Dateien",
|
"session.files.all": "Alle Dateien",
|
||||||
|
"session.files.empty": "Keine Dateien",
|
||||||
"session.files.binaryContent": "Binärdatei (Inhalt kann nicht angezeigt werden)",
|
"session.files.binaryContent": "Binärdatei (Inhalt kann nicht angezeigt werden)",
|
||||||
"session.messages.renderEarlier": "Frühere Nachrichten rendern",
|
"session.messages.renderEarlier": "Frühere Nachrichten rendern",
|
||||||
"session.messages.loadingEarlier": "Lade frühere Nachrichten...",
|
"session.messages.loadingEarlier": "Lade frühere Nachrichten...",
|
||||||
@@ -467,6 +478,17 @@ export const dict = {
|
|||||||
"session.todo.title": "Aufgaben",
|
"session.todo.title": "Aufgaben",
|
||||||
"session.todo.collapse": "Einklappen",
|
"session.todo.collapse": "Einklappen",
|
||||||
"session.todo.expand": "Ausklappen",
|
"session.todo.expand": "Ausklappen",
|
||||||
|
"session.followupDock.summary.one": "{{count}} Nachricht in der Warteschlange",
|
||||||
|
"session.followupDock.summary.other": "{{count}} Nachrichten in der Warteschlange",
|
||||||
|
"session.followupDock.sendNow": "Jetzt senden",
|
||||||
|
"session.followupDock.edit": "Bearbeiten",
|
||||||
|
"session.followupDock.collapse": "Warteschlange einklappen",
|
||||||
|
"session.followupDock.expand": "Warteschlange ausklappen",
|
||||||
|
"session.revertDock.summary.one": "{{count}} zurückgesetzte Nachricht",
|
||||||
|
"session.revertDock.summary.other": "{{count}} zurückgesetzte Nachrichten",
|
||||||
|
"session.revertDock.collapse": "Zurückgesetzte Nachrichten einklappen",
|
||||||
|
"session.revertDock.expand": "Zurückgesetzte Nachrichten ausklappen",
|
||||||
|
"session.revertDock.restore": "Nachricht wiederherstellen",
|
||||||
"session.new.title": "Baue, was du willst",
|
"session.new.title": "Baue, was du willst",
|
||||||
"session.new.worktree.main": "Haupt-Branch",
|
"session.new.worktree.main": "Haupt-Branch",
|
||||||
"session.new.worktree.mainWithBranch": "Haupt-Branch ({{branch}})",
|
"session.new.worktree.mainWithBranch": "Haupt-Branch ({{branch}})",
|
||||||
@@ -553,10 +575,21 @@ export const dict = {
|
|||||||
"settings.general.row.language.description": "Die Anzeigesprache für OpenCode ändern",
|
"settings.general.row.language.description": "Die Anzeigesprache für OpenCode ändern",
|
||||||
"settings.general.row.appearance.title": "Erscheinungsbild",
|
"settings.general.row.appearance.title": "Erscheinungsbild",
|
||||||
"settings.general.row.appearance.description": "Anpassen, wie OpenCode auf Ihrem Gerät aussieht",
|
"settings.general.row.appearance.description": "Anpassen, wie OpenCode auf Ihrem Gerät aussieht",
|
||||||
|
"settings.general.row.colorScheme.title": "Farbschema",
|
||||||
|
"settings.general.row.colorScheme.description":
|
||||||
|
"Wählen Sie, ob OpenCode dem System-, hellen oder dunklen Thema folgt",
|
||||||
"settings.general.row.theme.title": "Thema",
|
"settings.general.row.theme.title": "Thema",
|
||||||
"settings.general.row.theme.description": "Das Thema von OpenCode anpassen.",
|
"settings.general.row.theme.description": "Das Thema von OpenCode anpassen.",
|
||||||
"settings.general.row.font.title": "Schriftart",
|
"settings.general.row.font.title": "Schriftart",
|
||||||
"settings.general.row.font.description": "Die in Codeblöcken verwendete Monospace-Schriftart anpassen",
|
"settings.general.row.font.description": "Die in Codeblöcken verwendete Monospace-Schriftart anpassen",
|
||||||
|
"settings.general.row.followup.title": "Verhalten bei Folgefragen",
|
||||||
|
"settings.general.row.followup.description":
|
||||||
|
"Wählen Sie, ob Folgefragen sofort steuern oder in einer Warteschlange warten",
|
||||||
|
"settings.general.row.followup.option.queue": "Warteschlange",
|
||||||
|
"settings.general.row.followup.option.steer": "Steuern",
|
||||||
|
"settings.general.row.reasoningSummaries.title": "Reasoning-Zusammenfassungen anzeigen",
|
||||||
|
"settings.general.row.reasoningSummaries.description":
|
||||||
|
"Zusammenfassungen des Modell-Reasonings in der Timeline anzeigen",
|
||||||
"settings.general.row.shellToolPartsExpanded.title": "Shell-Tool-Abschnitte ausklappen",
|
"settings.general.row.shellToolPartsExpanded.title": "Shell-Tool-Abschnitte ausklappen",
|
||||||
"settings.general.row.shellToolPartsExpanded.description":
|
"settings.general.row.shellToolPartsExpanded.description":
|
||||||
"Shell-Tool-Abschnitte standardmäßig in der Timeline ausgeklappt anzeigen",
|
"Shell-Tool-Abschnitte standardmäßig in der Timeline ausgeklappt anzeigen",
|
||||||
@@ -766,4 +799,80 @@ export const dict = {
|
|||||||
"common.time.daysAgo.short": "vor {{count}} Tg",
|
"common.time.daysAgo.short": "vor {{count}} Tg",
|
||||||
"settings.providers.connected.environmentDescription": "Verbunden aus Ihren Umgebungsvariablen",
|
"settings.providers.connected.environmentDescription": "Verbunden aus Ihren Umgebungsvariablen",
|
||||||
"settings.providers.custom.description": "Fügen Sie einen OpenAI-kompatiblen Anbieter per Basis-URL hinzu.",
|
"settings.providers.custom.description": "Fügen Sie einen OpenAI-kompatiblen Anbieter per Basis-URL hinzu.",
|
||||||
|
|
||||||
|
"app.server.unreachable": "Konnte {{server}} nicht erreichen",
|
||||||
|
"app.server.retrying": "Automatische erneute Verbindung...",
|
||||||
|
"app.server.otherServers": "Andere Server",
|
||||||
|
"dialog.server.add.usernamePlaceholder": "Benutzername",
|
||||||
|
"dialog.server.add.passwordPlaceholder": "Passwort",
|
||||||
|
"server.row.noUsername": "Kein Benutzername",
|
||||||
|
"session.review.noVcs.createGit.title": "Git-Repository erstellen",
|
||||||
|
"session.review.noVcs.createGit.description":
|
||||||
|
"Änderungen in diesem Projekt verfolgen, überprüfen und rückgängig machen",
|
||||||
|
"session.review.noVcs.createGit.actionLoading": "Git-Repository wird erstellt...",
|
||||||
|
"session.review.noVcs.createGit.action": "Git-Repository erstellen",
|
||||||
|
"session.todo.progress": "{{done}} von {{total}} Aufgaben erledigt",
|
||||||
|
"session.question.progress": "{{current}} von {{total}} Fragen",
|
||||||
|
"session.header.open.finder": "Finder",
|
||||||
|
"session.header.open.fileExplorer": "Datei-Explorer",
|
||||||
|
"session.header.open.fileManager": "Dateimanager",
|
||||||
|
"session.header.open.app.vscode": "VS Code",
|
||||||
|
"session.header.open.app.cursor": "Cursor",
|
||||||
|
"session.header.open.app.zed": "Zed",
|
||||||
|
"session.header.open.app.textmate": "TextMate",
|
||||||
|
"session.header.open.app.antigravity": "Antigravity",
|
||||||
|
"session.header.open.app.terminal": "Terminal",
|
||||||
|
"session.header.open.app.iterm2": "iTerm2",
|
||||||
|
"session.header.open.app.ghostty": "Ghostty",
|
||||||
|
"session.header.open.app.warp": "Warp",
|
||||||
|
"session.header.open.app.xcode": "Xcode",
|
||||||
|
"session.header.open.app.androidStudio": "Android Studio",
|
||||||
|
"session.header.open.app.powershell": "PowerShell",
|
||||||
|
"session.header.open.app.sublimeText": "Sublime Text",
|
||||||
|
"debugBar.ariaLabel": "Entwicklungs-Leistungsdiagnose",
|
||||||
|
"debugBar.na": "n.v.",
|
||||||
|
"debugBar.nav.label": "NAV",
|
||||||
|
"debugBar.nav.tip":
|
||||||
|
"Letzter abgeschlossener Routenübergang, der eine Sitzungsseite berührt, gemessen vom Start des Routers bis zum ersten Rendern nach dem Einschwingen.",
|
||||||
|
"debugBar.fps.label": "FPS",
|
||||||
|
"debugBar.fps.tip": "Gleitende Bilder pro Sekunde in den letzten 5 Sekunden.",
|
||||||
|
"debugBar.frame.label": "FRAME",
|
||||||
|
"debugBar.frame.tip": "Schlechteste Frame-Zeit in den letzten 5 Sekunden.",
|
||||||
|
"debugBar.jank.label": "JANK",
|
||||||
|
"debugBar.jank.tip": "Frames über 32ms in den letzten 5 Sekunden.",
|
||||||
|
"debugBar.long.label": "LONG",
|
||||||
|
"debugBar.long.tip": "Blockierte Zeit und Anzahl langer Aufgaben in den letzten 5 Sekunden. Max Aufgabe: {{max}}.",
|
||||||
|
"debugBar.delay.label": "DELAY",
|
||||||
|
"debugBar.delay.tip": "Schlechteste beobachtete Eingabeverzögerung in den letzten 5 Sekunden.",
|
||||||
|
"debugBar.inp.label": "INP",
|
||||||
|
"debugBar.inp.tip":
|
||||||
|
"Ungefähre Interaktionsdauer in den letzten 5 Sekunden. Dies ist INP-ähnlich, nicht das offizielle Web Vitals INP.",
|
||||||
|
"debugBar.cls.label": "CLS",
|
||||||
|
"debugBar.cls.tip": "Kumulative Layoutverschiebung für die aktuelle App-Lebensdauer.",
|
||||||
|
"debugBar.mem.label": "MEM",
|
||||||
|
"debugBar.mem.tipUnavailable": "Verwendeter JS-Heap vs Heap-Limit. Nur Chromium.",
|
||||||
|
"debugBar.mem.tip": "Verwendeter JS-Heap vs Heap-Limit. {{used}} von {{limit}}.",
|
||||||
|
"common.key.ctrl": "Strg",
|
||||||
|
"common.key.alt": "Alt",
|
||||||
|
"common.key.shift": "Umschalt",
|
||||||
|
"common.key.meta": "Meta",
|
||||||
|
"common.key.space": "Leertaste",
|
||||||
|
"common.key.backspace": "Rücktaste",
|
||||||
|
"common.key.enter": "Eingabe",
|
||||||
|
"common.key.tab": "Tab",
|
||||||
|
"common.key.delete": "Entf",
|
||||||
|
"common.key.home": "Pos1",
|
||||||
|
"common.key.end": "Ende",
|
||||||
|
"common.key.pageUp": "Bild auf",
|
||||||
|
"common.key.pageDown": "Bild ab",
|
||||||
|
"common.key.insert": "Einfg",
|
||||||
|
"common.unknown": "unbekannt",
|
||||||
|
"error.page.circular": "[Zirkulär]",
|
||||||
|
"error.globalSDK.noServerAvailable": "Kein Server verfügbar",
|
||||||
|
"error.globalSDK.serverNotAvailable": "Server nicht verfügbar",
|
||||||
|
"error.childStore.persistedCacheCreateFailed": "Dauerhafter Cache konnte nicht erstellt werden",
|
||||||
|
"error.childStore.persistedProjectMetadataCreateFailed": "Dauerhafte Projektmetadaten konnten nicht erstellt werden",
|
||||||
|
"error.childStore.persistedProjectIconCreateFailed": "Dauerhaftes Projekticon konnte nicht erstellt werden",
|
||||||
|
"error.childStore.storeCreateFailed": "Speicher konnte nicht erstellt werden",
|
||||||
|
"terminal.connectionLost.abnormalClose": "WebSocket abnormal geschlossen: {{code}}",
|
||||||
} satisfies Partial<Record<Keys, string>>
|
} satisfies Partial<Record<Keys, string>>
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ export const dict = {
|
|||||||
|
|
||||||
"prompt.popover.emptyResults": "No matching results",
|
"prompt.popover.emptyResults": "No matching results",
|
||||||
"prompt.popover.emptyCommands": "No matching commands",
|
"prompt.popover.emptyCommands": "No matching commands",
|
||||||
"prompt.dropzone.label": "Drop images or PDFs here",
|
"prompt.dropzone.label": "Drop images, PDFs, or text files here",
|
||||||
"prompt.dropzone.file.label": "Drop to @mention file",
|
"prompt.dropzone.file.label": "Drop to @mention file",
|
||||||
"prompt.slash.badge.custom": "custom",
|
"prompt.slash.badge.custom": "custom",
|
||||||
"prompt.slash.badge.skill": "skill",
|
"prompt.slash.badge.skill": "skill",
|
||||||
@@ -278,8 +278,8 @@ export const dict = {
|
|||||||
"prompt.action.send": "Send",
|
"prompt.action.send": "Send",
|
||||||
"prompt.action.stop": "Stop",
|
"prompt.action.stop": "Stop",
|
||||||
|
|
||||||
"prompt.toast.pasteUnsupported.title": "Unsupported paste",
|
"prompt.toast.pasteUnsupported.title": "Unsupported attachment",
|
||||||
"prompt.toast.pasteUnsupported.description": "Only images or PDFs can be pasted here.",
|
"prompt.toast.pasteUnsupported.description": "Only images, PDFs, or text files can be attached here.",
|
||||||
"prompt.toast.modelAgentRequired.title": "Select an agent and model",
|
"prompt.toast.modelAgentRequired.title": "Select an agent and model",
|
||||||
"prompt.toast.modelAgentRequired.description": "Choose an agent and model before sending a prompt.",
|
"prompt.toast.modelAgentRequired.description": "Choose an agent and model before sending a prompt.",
|
||||||
"prompt.toast.worktreeCreateFailed.title": "Failed to create worktree",
|
"prompt.toast.worktreeCreateFailed.title": "Failed to create worktree",
|
||||||
@@ -306,6 +306,10 @@ export const dict = {
|
|||||||
"dialog.directory.search.placeholder": "Search folders",
|
"dialog.directory.search.placeholder": "Search folders",
|
||||||
"dialog.directory.empty": "No folders found",
|
"dialog.directory.empty": "No folders found",
|
||||||
|
|
||||||
|
"app.server.unreachable": "Could not reach {{server}}",
|
||||||
|
"app.server.retrying": "Retrying automatically...",
|
||||||
|
"app.server.otherServers": "Other servers",
|
||||||
|
|
||||||
"dialog.server.title": "Servers",
|
"dialog.server.title": "Servers",
|
||||||
"dialog.server.description": "Switch which OpenCode server this app connects to.",
|
"dialog.server.description": "Switch which OpenCode server this app connects to.",
|
||||||
"dialog.server.search.placeholder": "Search servers",
|
"dialog.server.search.placeholder": "Search servers",
|
||||||
@@ -319,7 +323,9 @@ export const dict = {
|
|||||||
"dialog.server.add.name": "Server name (optional)",
|
"dialog.server.add.name": "Server name (optional)",
|
||||||
"dialog.server.add.namePlaceholder": "Localhost",
|
"dialog.server.add.namePlaceholder": "Localhost",
|
||||||
"dialog.server.add.username": "Username (optional)",
|
"dialog.server.add.username": "Username (optional)",
|
||||||
|
"dialog.server.add.usernamePlaceholder": "username",
|
||||||
"dialog.server.add.password": "Password (optional)",
|
"dialog.server.add.password": "Password (optional)",
|
||||||
|
"dialog.server.add.passwordPlaceholder": "password",
|
||||||
"dialog.server.edit.title": "Edit server",
|
"dialog.server.edit.title": "Edit server",
|
||||||
"dialog.server.default.title": "Default server",
|
"dialog.server.default.title": "Default server",
|
||||||
"dialog.server.default.description":
|
"dialog.server.default.description":
|
||||||
@@ -335,6 +341,7 @@ export const dict = {
|
|||||||
"dialog.server.menu.delete": "Delete",
|
"dialog.server.menu.delete": "Delete",
|
||||||
"dialog.server.current": "Current Server",
|
"dialog.server.current": "Current Server",
|
||||||
"dialog.server.status.default": "Default",
|
"dialog.server.status.default": "Default",
|
||||||
|
"server.row.noUsername": "no username",
|
||||||
|
|
||||||
"dialog.project.edit.title": "Edit project",
|
"dialog.project.edit.title": "Edit project",
|
||||||
"dialog.project.edit.name": "Name",
|
"dialog.project.edit.name": "Name",
|
||||||
@@ -456,6 +463,7 @@ export const dict = {
|
|||||||
"error.page.action.checking": "Checking...",
|
"error.page.action.checking": "Checking...",
|
||||||
"error.page.action.checkUpdates": "Check for updates",
|
"error.page.action.checkUpdates": "Check for updates",
|
||||||
"error.page.action.updateTo": "Update to {{version}}",
|
"error.page.action.updateTo": "Update to {{version}}",
|
||||||
|
"error.page.circular": "[Circular]",
|
||||||
"error.page.report.prefix": "Please report this error to the OpenCode team",
|
"error.page.report.prefix": "Please report this error to the OpenCode team",
|
||||||
"error.page.report.discord": "on Discord",
|
"error.page.report.discord": "on Discord",
|
||||||
"error.page.version": "Version: {{version}}",
|
"error.page.version": "Version: {{version}}",
|
||||||
@@ -464,6 +472,12 @@ export const dict = {
|
|||||||
"Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?",
|
"Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?",
|
||||||
|
|
||||||
"error.globalSync.connectFailed": "Could not connect to server. Is there a server running at `{{url}}`?",
|
"error.globalSync.connectFailed": "Could not connect to server. Is there a server running at `{{url}}`?",
|
||||||
|
"error.globalSDK.noServerAvailable": "No server available",
|
||||||
|
"error.globalSDK.serverNotAvailable": "Server not available",
|
||||||
|
"error.childStore.persistedCacheCreateFailed": "Failed to create persisted cache",
|
||||||
|
"error.childStore.persistedProjectMetadataCreateFailed": "Failed to create persisted project metadata",
|
||||||
|
"error.childStore.persistedProjectIconCreateFailed": "Failed to create persisted project icon",
|
||||||
|
"error.childStore.storeCreateFailed": "Failed to create store",
|
||||||
"directory.error.invalidUrl": "Invalid directory in URL.",
|
"directory.error.invalidUrl": "Invalid directory in URL.",
|
||||||
|
|
||||||
"error.chain.unknown": "Unknown error",
|
"error.chain.unknown": "Unknown error",
|
||||||
@@ -512,6 +526,10 @@ export const dict = {
|
|||||||
"session.review.loadingChanges": "Loading changes...",
|
"session.review.loadingChanges": "Loading changes...",
|
||||||
"session.review.empty": "No changes in this session yet",
|
"session.review.empty": "No changes in this session yet",
|
||||||
"session.review.noVcs": "No Git Version Control System detected, changes not displayed",
|
"session.review.noVcs": "No Git Version Control System detected, changes not displayed",
|
||||||
|
"session.review.noVcs.createGit.title": "Create a Git repository",
|
||||||
|
"session.review.noVcs.createGit.description": "Track, review, and undo changes in this project",
|
||||||
|
"session.review.noVcs.createGit.actionLoading": "Creating Git repository...",
|
||||||
|
"session.review.noVcs.createGit.action": "Create Git repository",
|
||||||
"session.review.noSnapshot": "Snapshot tracking is disabled in config, so session changes are unavailable",
|
"session.review.noSnapshot": "Snapshot tracking is disabled in config, so session changes are unavailable",
|
||||||
"session.review.noChanges": "No changes",
|
"session.review.noChanges": "No changes",
|
||||||
|
|
||||||
@@ -530,6 +548,14 @@ export const dict = {
|
|||||||
"session.todo.title": "Todos",
|
"session.todo.title": "Todos",
|
||||||
"session.todo.collapse": "Collapse",
|
"session.todo.collapse": "Collapse",
|
||||||
"session.todo.expand": "Expand",
|
"session.todo.expand": "Expand",
|
||||||
|
"session.todo.progress": "{{done}} of {{total}} todos completed",
|
||||||
|
"session.question.progress": "{{current}} of {{total}} questions",
|
||||||
|
"session.followupDock.summary.one": "{{count}} queued message",
|
||||||
|
"session.followupDock.summary.other": "{{count}} queued messages",
|
||||||
|
"session.followupDock.sendNow": "Send now",
|
||||||
|
"session.followupDock.edit": "Edit",
|
||||||
|
"session.followupDock.collapse": "Collapse queued messages",
|
||||||
|
"session.followupDock.expand": "Expand queued messages",
|
||||||
"session.revertDock.summary.one": "{{count}} rolled back message",
|
"session.revertDock.summary.one": "{{count}} rolled back message",
|
||||||
"session.revertDock.summary.other": "{{count}} rolled back messages",
|
"session.revertDock.summary.other": "{{count}} rolled back messages",
|
||||||
"session.revertDock.collapse": "Collapse rolled back messages",
|
"session.revertDock.collapse": "Collapse rolled back messages",
|
||||||
@@ -549,6 +575,22 @@ export const dict = {
|
|||||||
"session.header.open.ariaLabel": "Open in {{app}}",
|
"session.header.open.ariaLabel": "Open in {{app}}",
|
||||||
"session.header.open.menu": "Open options",
|
"session.header.open.menu": "Open options",
|
||||||
"session.header.open.copyPath": "Copy path",
|
"session.header.open.copyPath": "Copy path",
|
||||||
|
"session.header.open.finder": "Finder",
|
||||||
|
"session.header.open.fileExplorer": "File Explorer",
|
||||||
|
"session.header.open.fileManager": "File Manager",
|
||||||
|
"session.header.open.app.vscode": "VS Code",
|
||||||
|
"session.header.open.app.cursor": "Cursor",
|
||||||
|
"session.header.open.app.zed": "Zed",
|
||||||
|
"session.header.open.app.textmate": "TextMate",
|
||||||
|
"session.header.open.app.antigravity": "Antigravity",
|
||||||
|
"session.header.open.app.terminal": "Terminal",
|
||||||
|
"session.header.open.app.iterm2": "iTerm2",
|
||||||
|
"session.header.open.app.ghostty": "Ghostty",
|
||||||
|
"session.header.open.app.warp": "Warp",
|
||||||
|
"session.header.open.app.xcode": "Xcode",
|
||||||
|
"session.header.open.app.androidStudio": "Android Studio",
|
||||||
|
"session.header.open.app.powershell": "PowerShell",
|
||||||
|
"session.header.open.app.sublimeText": "Sublime Text",
|
||||||
|
|
||||||
"status.popover.trigger": "Status",
|
"status.popover.trigger": "Status",
|
||||||
"status.popover.ariaLabel": "Server configurations",
|
"status.popover.ariaLabel": "Server configurations",
|
||||||
@@ -581,6 +623,7 @@ export const dict = {
|
|||||||
"terminal.title.numbered": "Terminal {{number}}",
|
"terminal.title.numbered": "Terminal {{number}}",
|
||||||
"terminal.close": "Close terminal",
|
"terminal.close": "Close terminal",
|
||||||
"terminal.connectionLost.title": "Connection Lost",
|
"terminal.connectionLost.title": "Connection Lost",
|
||||||
|
"terminal.connectionLost.abnormalClose": "WebSocket closed abnormally: {{code}}",
|
||||||
"terminal.connectionLost.description":
|
"terminal.connectionLost.description":
|
||||||
"The terminal connection was interrupted. This can happen when the server restarts.",
|
"The terminal connection was interrupted. This can happen when the server restarts.",
|
||||||
|
|
||||||
@@ -598,6 +641,21 @@ export const dict = {
|
|||||||
"common.edit": "Edit",
|
"common.edit": "Edit",
|
||||||
"common.loadMore": "Load more",
|
"common.loadMore": "Load more",
|
||||||
"common.key.esc": "ESC",
|
"common.key.esc": "ESC",
|
||||||
|
"common.key.ctrl": "Ctrl",
|
||||||
|
"common.key.alt": "Alt",
|
||||||
|
"common.key.shift": "Shift",
|
||||||
|
"common.key.meta": "Meta",
|
||||||
|
"common.key.space": "Space",
|
||||||
|
"common.key.backspace": "Backspace",
|
||||||
|
"common.key.enter": "Enter",
|
||||||
|
"common.key.tab": "Tab",
|
||||||
|
"common.key.delete": "Delete",
|
||||||
|
"common.key.home": "Home",
|
||||||
|
"common.key.end": "End",
|
||||||
|
"common.key.pageUp": "Page Up",
|
||||||
|
"common.key.pageDown": "Page Down",
|
||||||
|
"common.key.insert": "Insert",
|
||||||
|
"common.unknown": "unknown",
|
||||||
|
|
||||||
"common.time.justNow": "Just now",
|
"common.time.justNow": "Just now",
|
||||||
"common.time.minutesAgo.short": "{{count}}m ago",
|
"common.time.minutesAgo.short": "{{count}}m ago",
|
||||||
@@ -617,6 +675,30 @@ export const dict = {
|
|||||||
"sidebar.project.viewAllSessions": "View all sessions",
|
"sidebar.project.viewAllSessions": "View all sessions",
|
||||||
"sidebar.project.clearNotifications": "Clear notifications",
|
"sidebar.project.clearNotifications": "Clear notifications",
|
||||||
|
|
||||||
|
"debugBar.ariaLabel": "Development performance diagnostics",
|
||||||
|
"debugBar.na": "n/a",
|
||||||
|
"debugBar.nav.label": "NAV",
|
||||||
|
"debugBar.nav.tip":
|
||||||
|
"Last completed route transition touching a session page, measured from router start until the first paint after it settles.",
|
||||||
|
"debugBar.fps.label": "FPS",
|
||||||
|
"debugBar.fps.tip": "Rolling frames per second over the last 5 seconds.",
|
||||||
|
"debugBar.frame.label": "FRAME",
|
||||||
|
"debugBar.frame.tip": "Worst frame time over the last 5 seconds.",
|
||||||
|
"debugBar.jank.label": "JANK",
|
||||||
|
"debugBar.jank.tip": "Frames over 32ms in the last 5 seconds.",
|
||||||
|
"debugBar.long.label": "LONG",
|
||||||
|
"debugBar.long.tip": "Blocked time and long-task count in the last 5 seconds. Max task: {{max}}.",
|
||||||
|
"debugBar.delay.label": "DELAY",
|
||||||
|
"debugBar.delay.tip": "Worst observed input delay in the last 5 seconds.",
|
||||||
|
"debugBar.inp.label": "INP",
|
||||||
|
"debugBar.inp.tip":
|
||||||
|
"Approximate interaction duration over the last 5 seconds. This is INP-like, not the official Web Vitals INP.",
|
||||||
|
"debugBar.cls.label": "CLS",
|
||||||
|
"debugBar.cls.tip": "Cumulative layout shift for the current app lifetime.",
|
||||||
|
"debugBar.mem.label": "MEM",
|
||||||
|
"debugBar.mem.tipUnavailable": "Used JS heap vs heap limit. Chromium only.",
|
||||||
|
"debugBar.mem.tip": "Used JS heap vs heap limit. {{used}} of {{limit}}.",
|
||||||
|
|
||||||
"app.name.desktop": "OpenCode Desktop",
|
"app.name.desktop": "OpenCode Desktop",
|
||||||
|
|
||||||
"settings.section.desktop": "Desktop",
|
"settings.section.desktop": "Desktop",
|
||||||
@@ -638,10 +720,16 @@ export const dict = {
|
|||||||
"settings.general.row.language.description": "Change the display language for OpenCode",
|
"settings.general.row.language.description": "Change the display language for OpenCode",
|
||||||
"settings.general.row.appearance.title": "Appearance",
|
"settings.general.row.appearance.title": "Appearance",
|
||||||
"settings.general.row.appearance.description": "Customise how OpenCode looks on your device",
|
"settings.general.row.appearance.description": "Customise how OpenCode looks on your device",
|
||||||
|
"settings.general.row.colorScheme.title": "Color scheme",
|
||||||
|
"settings.general.row.colorScheme.description": "Choose whether OpenCode follows the system, light, or dark theme",
|
||||||
"settings.general.row.theme.title": "Theme",
|
"settings.general.row.theme.title": "Theme",
|
||||||
"settings.general.row.theme.description": "Customise how OpenCode is themed.",
|
"settings.general.row.theme.description": "Customise how OpenCode is themed.",
|
||||||
"settings.general.row.font.title": "Font",
|
"settings.general.row.font.title": "Font",
|
||||||
"settings.general.row.font.description": "Customise the mono font used in code blocks",
|
"settings.general.row.font.description": "Customise the mono font used in code blocks",
|
||||||
|
"settings.general.row.followup.title": "Follow-up behavior",
|
||||||
|
"settings.general.row.followup.description": "Choose whether follow-up prompts steer immediately or wait in a queue",
|
||||||
|
"settings.general.row.followup.option.queue": "Queue",
|
||||||
|
"settings.general.row.followup.option.steer": "Steer",
|
||||||
"settings.general.row.reasoningSummaries.title": "Show reasoning summaries",
|
"settings.general.row.reasoningSummaries.title": "Show reasoning summaries",
|
||||||
"settings.general.row.reasoningSummaries.description": "Display model reasoning summaries in the timeline",
|
"settings.general.row.reasoningSummaries.description": "Display model reasoning summaries in the timeline",
|
||||||
"settings.general.row.shellToolPartsExpanded.title": "Expand shell tool parts",
|
"settings.general.row.shellToolPartsExpanded.title": "Expand shell tool parts",
|
||||||
|
|||||||
+110
-4
@@ -113,6 +113,7 @@ export const dict = {
|
|||||||
"dialog.model.empty": "Sin resultados de modelos",
|
"dialog.model.empty": "Sin resultados de modelos",
|
||||||
"dialog.model.manage": "Gestionar modelos",
|
"dialog.model.manage": "Gestionar modelos",
|
||||||
"dialog.model.manage.description": "Personalizar qué modelos aparecen en el selector de modelos.",
|
"dialog.model.manage.description": "Personalizar qué modelos aparecen en el selector de modelos.",
|
||||||
|
"dialog.model.manage.provider.toggle": "Alternar todos los modelos de {{provider}}",
|
||||||
|
|
||||||
"dialog.model.unpaid.freeModels.title": "Modelos gratuitos proporcionados por OpenCode",
|
"dialog.model.unpaid.freeModels.title": "Modelos gratuitos proporcionados por OpenCode",
|
||||||
"dialog.model.unpaid.addMore.title": "Añadir más modelos de proveedores populares",
|
"dialog.model.unpaid.addMore.title": "Añadir más modelos de proveedores populares",
|
||||||
@@ -262,7 +263,7 @@ export const dict = {
|
|||||||
|
|
||||||
"prompt.popover.emptyResults": "Sin resultados coincidentes",
|
"prompt.popover.emptyResults": "Sin resultados coincidentes",
|
||||||
"prompt.popover.emptyCommands": "Sin comandos coincidentes",
|
"prompt.popover.emptyCommands": "Sin comandos coincidentes",
|
||||||
"prompt.dropzone.label": "Suelta imágenes o PDFs aquí",
|
"prompt.dropzone.label": "Suelta imágenes, PDFs o archivos de texto aquí",
|
||||||
"prompt.dropzone.file.label": "Suelta para @mencionar archivo",
|
"prompt.dropzone.file.label": "Suelta para @mencionar archivo",
|
||||||
"prompt.slash.badge.custom": "personalizado",
|
"prompt.slash.badge.custom": "personalizado",
|
||||||
"prompt.slash.badge.skill": "skill",
|
"prompt.slash.badge.skill": "skill",
|
||||||
@@ -276,8 +277,8 @@ export const dict = {
|
|||||||
"prompt.action.send": "Enviar",
|
"prompt.action.send": "Enviar",
|
||||||
"prompt.action.stop": "Detener",
|
"prompt.action.stop": "Detener",
|
||||||
|
|
||||||
"prompt.toast.pasteUnsupported.title": "Pegado no soportado",
|
"prompt.toast.pasteUnsupported.title": "Adjunto no compatible",
|
||||||
"prompt.toast.pasteUnsupported.description": "Solo se pueden pegar imágenes o PDFs aquí.",
|
"prompt.toast.pasteUnsupported.description": "Solo se pueden adjuntar imágenes, PDFs o archivos de texto aquí.",
|
||||||
"prompt.toast.modelAgentRequired.title": "Selecciona un agente y modelo",
|
"prompt.toast.modelAgentRequired.title": "Selecciona un agente y modelo",
|
||||||
"prompt.toast.modelAgentRequired.description": "Elige un agente y modelo antes de enviar un prompt.",
|
"prompt.toast.modelAgentRequired.description": "Elige un agente y modelo antes de enviar un prompt.",
|
||||||
"prompt.toast.worktreeCreateFailed.title": "Fallo al crear el árbol de trabajo",
|
"prompt.toast.worktreeCreateFailed.title": "Fallo al crear el árbol de trabajo",
|
||||||
@@ -314,6 +315,11 @@ export const dict = {
|
|||||||
"dialog.server.add.error": "No se pudo conectar al servidor",
|
"dialog.server.add.error": "No se pudo conectar al servidor",
|
||||||
"dialog.server.add.checking": "Comprobando...",
|
"dialog.server.add.checking": "Comprobando...",
|
||||||
"dialog.server.add.button": "Añadir servidor",
|
"dialog.server.add.button": "Añadir servidor",
|
||||||
|
"dialog.server.add.name": "Nombre del servidor (opcional)",
|
||||||
|
"dialog.server.add.namePlaceholder": "Localhost",
|
||||||
|
"dialog.server.add.username": "Nombre de usuario (opcional)",
|
||||||
|
"dialog.server.add.password": "Contraseña (opcional)",
|
||||||
|
"dialog.server.edit.title": "Editar servidor",
|
||||||
"dialog.server.default.title": "Servidor predeterminado",
|
"dialog.server.default.title": "Servidor predeterminado",
|
||||||
"dialog.server.default.description":
|
"dialog.server.default.description":
|
||||||
"Conectar a este servidor al iniciar la app en lugar de iniciar un servidor local. Requiere reinicio.",
|
"Conectar a este servidor al iniciar la app en lugar de iniciar un servidor local. Requiere reinicio.",
|
||||||
@@ -393,6 +399,7 @@ export const dict = {
|
|||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.bs": "Bosanski",
|
"language.bs": "Bosanski",
|
||||||
"language.th": "ไทย",
|
"language.th": "ไทย",
|
||||||
|
"language.tr": "Türkçe",
|
||||||
|
|
||||||
"toast.language.title": "Idioma",
|
"toast.language.title": "Idioma",
|
||||||
"toast.language.description": "Cambiado a {{language}}",
|
"toast.language.description": "Cambiado a {{language}}",
|
||||||
@@ -499,10 +506,14 @@ export const dict = {
|
|||||||
"session.review.change.other": "Cambios",
|
"session.review.change.other": "Cambios",
|
||||||
"session.review.loadingChanges": "Cargando cambios...",
|
"session.review.loadingChanges": "Cargando cambios...",
|
||||||
"session.review.empty": "No hay cambios en esta sesión aún",
|
"session.review.empty": "No hay cambios en esta sesión aún",
|
||||||
|
"session.review.noVcs": "No se detectó Sistema de Control de Versiones Git, los cambios no se muestran",
|
||||||
|
"session.review.noSnapshot":
|
||||||
|
"El seguimiento de instantáneas está deshabilitado en la configuración, por lo que los cambios de sesión no están disponibles",
|
||||||
"session.review.noChanges": "Sin cambios",
|
"session.review.noChanges": "Sin cambios",
|
||||||
|
|
||||||
"session.files.selectToOpen": "Selecciona un archivo para abrir",
|
"session.files.selectToOpen": "Selecciona un archivo para abrir",
|
||||||
"session.files.all": "Todos los archivos",
|
"session.files.all": "Todos los archivos",
|
||||||
|
"session.files.empty": "Sin archivos",
|
||||||
"session.files.binaryContent": "Archivo binario (el contenido no puede ser mostrado)",
|
"session.files.binaryContent": "Archivo binario (el contenido no puede ser mostrado)",
|
||||||
|
|
||||||
"session.messages.renderEarlier": "Renderizar mensajes anteriores",
|
"session.messages.renderEarlier": "Renderizar mensajes anteriores",
|
||||||
@@ -515,6 +526,17 @@ export const dict = {
|
|||||||
"session.todo.title": "Tareas",
|
"session.todo.title": "Tareas",
|
||||||
"session.todo.collapse": "Contraer",
|
"session.todo.collapse": "Contraer",
|
||||||
"session.todo.expand": "Expandir",
|
"session.todo.expand": "Expandir",
|
||||||
|
"session.followupDock.summary.one": "{{count}} mensaje en cola",
|
||||||
|
"session.followupDock.summary.other": "{{count}} mensajes en cola",
|
||||||
|
"session.followupDock.sendNow": "Enviar ahora",
|
||||||
|
"session.followupDock.edit": "Editar",
|
||||||
|
"session.followupDock.collapse": "Contraer mensajes en cola",
|
||||||
|
"session.followupDock.expand": "Expandir mensajes en cola",
|
||||||
|
"session.revertDock.summary.one": "{{count}} mensaje revertido",
|
||||||
|
"session.revertDock.summary.other": "{{count}} mensajes revertidos",
|
||||||
|
"session.revertDock.collapse": "Contraer mensajes revertidos",
|
||||||
|
"session.revertDock.expand": "Expandir mensajes revertidos",
|
||||||
|
"session.revertDock.restore": "Restaurar mensaje",
|
||||||
|
|
||||||
"session.new.title": "Construye lo que quieras",
|
"session.new.title": "Construye lo que quieras",
|
||||||
"session.new.worktree.main": "Rama principal",
|
"session.new.worktree.main": "Rama principal",
|
||||||
@@ -612,11 +634,20 @@ export const dict = {
|
|||||||
"settings.general.row.language.description": "Cambiar el idioma de visualización para OpenCode",
|
"settings.general.row.language.description": "Cambiar el idioma de visualización para OpenCode",
|
||||||
"settings.general.row.appearance.title": "Apariencia",
|
"settings.general.row.appearance.title": "Apariencia",
|
||||||
"settings.general.row.appearance.description": "Personaliza cómo se ve OpenCode en tu dispositivo",
|
"settings.general.row.appearance.description": "Personaliza cómo se ve OpenCode en tu dispositivo",
|
||||||
|
"settings.general.row.colorScheme.title": "Esquema de color",
|
||||||
|
"settings.general.row.colorScheme.description": "Elige si OpenCode sigue el tema del sistema, claro u oscuro",
|
||||||
"settings.general.row.theme.title": "Tema",
|
"settings.general.row.theme.title": "Tema",
|
||||||
"settings.general.row.theme.description": "Personaliza el tema de OpenCode.",
|
"settings.general.row.theme.description": "Personaliza el tema de OpenCode.",
|
||||||
"settings.general.row.font.title": "Fuente",
|
"settings.general.row.font.title": "Fuente",
|
||||||
"settings.general.row.font.description": "Personaliza la fuente monoespaciada usada en bloques de código",
|
"settings.general.row.font.description": "Personaliza la fuente monoespaciada usada en bloques de código",
|
||||||
|
"settings.general.row.followup.title": "Comportamiento de seguimiento",
|
||||||
|
"settings.general.row.followup.description":
|
||||||
|
"Elige si los prompts de seguimiento se dirigen inmediatamente o esperan en una cola",
|
||||||
|
"settings.general.row.followup.option.queue": "Cola",
|
||||||
|
"settings.general.row.followup.option.steer": "Dirigir",
|
||||||
|
"settings.general.row.reasoningSummaries.title": "Mostrar resúmenes de razonamiento",
|
||||||
|
"settings.general.row.reasoningSummaries.description":
|
||||||
|
"Mostrar resúmenes del razonamiento del modelo en la línea de tiempo",
|
||||||
"settings.general.row.shellToolPartsExpanded.title": "Expandir partes de la herramienta shell",
|
"settings.general.row.shellToolPartsExpanded.title": "Expandir partes de la herramienta shell",
|
||||||
"settings.general.row.shellToolPartsExpanded.description":
|
"settings.general.row.shellToolPartsExpanded.description":
|
||||||
"Mostrar las partes de la herramienta shell expandidas por defecto en la línea de tiempo",
|
"Mostrar las partes de la herramienta shell expandidas por defecto en la línea de tiempo",
|
||||||
@@ -840,4 +871,79 @@ export const dict = {
|
|||||||
"common.time.daysAgo.short": "hace {{count}} d",
|
"common.time.daysAgo.short": "hace {{count}} d",
|
||||||
"settings.providers.connected.environmentDescription": "Conectado desde tus variables de entorno",
|
"settings.providers.connected.environmentDescription": "Conectado desde tus variables de entorno",
|
||||||
"settings.providers.custom.description": "Añade un proveedor compatible con OpenAI por su URL base.",
|
"settings.providers.custom.description": "Añade un proveedor compatible con OpenAI por su URL base.",
|
||||||
|
|
||||||
|
"app.server.unreachable": "No se pudo conectar con {{server}}",
|
||||||
|
"app.server.retrying": "Reintentando automáticamente...",
|
||||||
|
"app.server.otherServers": "Otros servidores",
|
||||||
|
"dialog.server.add.usernamePlaceholder": "usuario",
|
||||||
|
"dialog.server.add.passwordPlaceholder": "contraseña",
|
||||||
|
"server.row.noUsername": "sin usuario",
|
||||||
|
"session.review.noVcs.createGit.title": "Crear repositorio Git",
|
||||||
|
"session.review.noVcs.createGit.description": "Rastrea, revisa y deshaz cambios en este proyecto",
|
||||||
|
"session.review.noVcs.createGit.actionLoading": "Creando repositorio Git...",
|
||||||
|
"session.review.noVcs.createGit.action": "Crear repositorio Git",
|
||||||
|
"session.todo.progress": "{{done}} de {{total}} tareas completadas",
|
||||||
|
"session.question.progress": "{{current}} de {{total}} preguntas",
|
||||||
|
"session.header.open.finder": "Finder",
|
||||||
|
"session.header.open.fileExplorer": "Explorador de archivos",
|
||||||
|
"session.header.open.fileManager": "Gestor de archivos",
|
||||||
|
"session.header.open.app.vscode": "VS Code",
|
||||||
|
"session.header.open.app.cursor": "Cursor",
|
||||||
|
"session.header.open.app.zed": "Zed",
|
||||||
|
"session.header.open.app.textmate": "TextMate",
|
||||||
|
"session.header.open.app.antigravity": "Antigravity",
|
||||||
|
"session.header.open.app.terminal": "Terminal",
|
||||||
|
"session.header.open.app.iterm2": "iTerm2",
|
||||||
|
"session.header.open.app.ghostty": "Ghostty",
|
||||||
|
"session.header.open.app.warp": "Warp",
|
||||||
|
"session.header.open.app.xcode": "Xcode",
|
||||||
|
"session.header.open.app.androidStudio": "Android Studio",
|
||||||
|
"session.header.open.app.powershell": "PowerShell",
|
||||||
|
"session.header.open.app.sublimeText": "Sublime Text",
|
||||||
|
"debugBar.ariaLabel": "Diagnóstico de rendimiento de desarrollo",
|
||||||
|
"debugBar.na": "n/d",
|
||||||
|
"debugBar.nav.label": "NAV",
|
||||||
|
"debugBar.nav.tip":
|
||||||
|
"Última transición de ruta completada tocando una página de sesión, medida desde el inicio del router hasta el primer pintado después de asentarse.",
|
||||||
|
"debugBar.fps.label": "FPS",
|
||||||
|
"debugBar.fps.tip": "Cuadros por segundo en los últimos 5 segundos.",
|
||||||
|
"debugBar.frame.label": "FRAME",
|
||||||
|
"debugBar.frame.tip": "Peor tiempo de cuadro en los últimos 5 segundos.",
|
||||||
|
"debugBar.jank.label": "JANK",
|
||||||
|
"debugBar.jank.tip": "Cuadros superiores a 32ms en los últimos 5 segundos.",
|
||||||
|
"debugBar.long.label": "LONG",
|
||||||
|
"debugBar.long.tip": "Tiempo bloqueado y recuento de tareas largas en los últimos 5 segundos. Tarea máx: {{max}}.",
|
||||||
|
"debugBar.delay.label": "DELAY",
|
||||||
|
"debugBar.delay.tip": "Peor retraso de entrada observado en los últimos 5 segundos.",
|
||||||
|
"debugBar.inp.label": "INP",
|
||||||
|
"debugBar.inp.tip":
|
||||||
|
"Duración aproximada de la interacción en los últimos 5 segundos. Esto es similar a INP, no el INP oficial de Web Vitals.",
|
||||||
|
"debugBar.cls.label": "CLS",
|
||||||
|
"debugBar.cls.tip": "Cambio de diseño acumulativo para la vida útil actual de la aplicación.",
|
||||||
|
"debugBar.mem.label": "MEM",
|
||||||
|
"debugBar.mem.tipUnavailable": "Heap JS usado vs límite de heap. Solo Chromium.",
|
||||||
|
"debugBar.mem.tip": "Heap JS usado vs límite de heap. {{used}} de {{limit}}.",
|
||||||
|
"common.key.ctrl": "Ctrl",
|
||||||
|
"common.key.alt": "Alt",
|
||||||
|
"common.key.shift": "Mayús",
|
||||||
|
"common.key.meta": "Meta",
|
||||||
|
"common.key.space": "Espacio",
|
||||||
|
"common.key.backspace": "Retroceso",
|
||||||
|
"common.key.enter": "Intro",
|
||||||
|
"common.key.tab": "Tab",
|
||||||
|
"common.key.delete": "Supr",
|
||||||
|
"common.key.home": "Inicio",
|
||||||
|
"common.key.end": "Fin",
|
||||||
|
"common.key.pageUp": "RePág",
|
||||||
|
"common.key.pageDown": "AvPág",
|
||||||
|
"common.key.insert": "Insert",
|
||||||
|
"common.unknown": "desconocido",
|
||||||
|
"error.page.circular": "[Circular]",
|
||||||
|
"error.globalSDK.noServerAvailable": "Ningún servidor disponible",
|
||||||
|
"error.globalSDK.serverNotAvailable": "Servidor no disponible",
|
||||||
|
"error.childStore.persistedCacheCreateFailed": "Error al crear caché persistente",
|
||||||
|
"error.childStore.persistedProjectMetadataCreateFailed": "Error al crear metadatos de proyecto persistentes",
|
||||||
|
"error.childStore.persistedProjectIconCreateFailed": "Error al crear icono de proyecto persistente",
|
||||||
|
"error.childStore.storeCreateFailed": "Error al crear almacén",
|
||||||
|
"terminal.connectionLost.abnormalClose": "WebSocket cerrado anormalmente: {{code}}",
|
||||||
}
|
}
|
||||||
|
|||||||
+113
-3
@@ -104,6 +104,7 @@ export const dict = {
|
|||||||
"dialog.model.empty": "Aucun résultat de modèle",
|
"dialog.model.empty": "Aucun résultat de modèle",
|
||||||
"dialog.model.manage": "Gérer les modèles",
|
"dialog.model.manage": "Gérer les modèles",
|
||||||
"dialog.model.manage.description": "Personnalisez les modèles qui apparaissent dans le sélecteur.",
|
"dialog.model.manage.description": "Personnalisez les modèles qui apparaissent dans le sélecteur.",
|
||||||
|
"dialog.model.manage.provider.toggle": "Basculer tous les modèles {{provider}}",
|
||||||
"dialog.model.unpaid.freeModels.title": "Modèles gratuits fournis par OpenCode",
|
"dialog.model.unpaid.freeModels.title": "Modèles gratuits fournis par OpenCode",
|
||||||
"dialog.model.unpaid.addMore.title": "Ajouter plus de modèles de fournisseurs populaires",
|
"dialog.model.unpaid.addMore.title": "Ajouter plus de modèles de fournisseurs populaires",
|
||||||
"dialog.provider.viewAll": "Voir plus de fournisseurs",
|
"dialog.provider.viewAll": "Voir plus de fournisseurs",
|
||||||
@@ -243,7 +244,7 @@ export const dict = {
|
|||||||
"prompt.example.25": "Comment fonctionnent les variables d'environnement ici ?",
|
"prompt.example.25": "Comment fonctionnent les variables d'environnement ici ?",
|
||||||
"prompt.popover.emptyResults": "Aucun résultat correspondant",
|
"prompt.popover.emptyResults": "Aucun résultat correspondant",
|
||||||
"prompt.popover.emptyCommands": "Aucune commande correspondante",
|
"prompt.popover.emptyCommands": "Aucune commande correspondante",
|
||||||
"prompt.dropzone.label": "Déposez des images ou des PDF ici",
|
"prompt.dropzone.label": "Déposez des images, des PDF ou des fichiers texte ici",
|
||||||
"prompt.dropzone.file.label": "Déposez pour @mentionner le fichier",
|
"prompt.dropzone.file.label": "Déposez pour @mentionner le fichier",
|
||||||
"prompt.slash.badge.custom": "personnalisé",
|
"prompt.slash.badge.custom": "personnalisé",
|
||||||
"prompt.slash.badge.skill": "skill",
|
"prompt.slash.badge.skill": "skill",
|
||||||
@@ -256,8 +257,9 @@ export const dict = {
|
|||||||
"prompt.attachment.remove": "Supprimer la pièce jointe",
|
"prompt.attachment.remove": "Supprimer la pièce jointe",
|
||||||
"prompt.action.send": "Envoyer",
|
"prompt.action.send": "Envoyer",
|
||||||
"prompt.action.stop": "Arrêter",
|
"prompt.action.stop": "Arrêter",
|
||||||
"prompt.toast.pasteUnsupported.title": "Collage non supporté",
|
"prompt.toast.pasteUnsupported.title": "Pièce jointe non prise en charge",
|
||||||
"prompt.toast.pasteUnsupported.description": "Seules les images ou les PDF peuvent être collés ici.",
|
"prompt.toast.pasteUnsupported.description":
|
||||||
|
"Seules les images, les PDF ou les fichiers texte peuvent être joints ici.",
|
||||||
"prompt.toast.modelAgentRequired.title": "Sélectionnez un agent et un modèle",
|
"prompt.toast.modelAgentRequired.title": "Sélectionnez un agent et un modèle",
|
||||||
"prompt.toast.modelAgentRequired.description": "Choisissez un agent et un modèle avant d'envoyer un message.",
|
"prompt.toast.modelAgentRequired.description": "Choisissez un agent et un modèle avant d'envoyer un message.",
|
||||||
"prompt.toast.worktreeCreateFailed.title": "Échec de la création de l'arbre de travail",
|
"prompt.toast.worktreeCreateFailed.title": "Échec de la création de l'arbre de travail",
|
||||||
@@ -288,6 +290,11 @@ export const dict = {
|
|||||||
"dialog.server.add.error": "Impossible de se connecter au serveur",
|
"dialog.server.add.error": "Impossible de se connecter au serveur",
|
||||||
"dialog.server.add.checking": "Vérification...",
|
"dialog.server.add.checking": "Vérification...",
|
||||||
"dialog.server.add.button": "Ajouter un serveur",
|
"dialog.server.add.button": "Ajouter un serveur",
|
||||||
|
"dialog.server.add.name": "Nom du serveur (optionnel)",
|
||||||
|
"dialog.server.add.namePlaceholder": "Localhost",
|
||||||
|
"dialog.server.add.username": "Nom d'utilisateur (optionnel)",
|
||||||
|
"dialog.server.add.password": "Mot de passe (optionnel)",
|
||||||
|
"dialog.server.edit.title": "Modifier le serveur",
|
||||||
"dialog.server.default.title": "Serveur par défaut",
|
"dialog.server.default.title": "Serveur par défaut",
|
||||||
"dialog.server.default.description":
|
"dialog.server.default.description":
|
||||||
"Se connecter à ce serveur au lancement de l'application au lieu de démarrer un serveur local. Nécessite un redémarrage.",
|
"Se connecter à ce serveur au lancement de l'application au lieu de démarrer un serveur local. Nécessite un redémarrage.",
|
||||||
@@ -360,6 +367,7 @@ export const dict = {
|
|||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.bs": "Bosanski",
|
"language.bs": "Bosanski",
|
||||||
"language.th": "ไทย",
|
"language.th": "ไทย",
|
||||||
|
"language.tr": "Türkçe",
|
||||||
"toast.language.title": "Langue",
|
"toast.language.title": "Langue",
|
||||||
"toast.language.description": "Passé à {{language}}",
|
"toast.language.description": "Passé à {{language}}",
|
||||||
"toast.theme.title": "Thème changé",
|
"toast.theme.title": "Thème changé",
|
||||||
@@ -451,8 +459,12 @@ export const dict = {
|
|||||||
"session.review.loadingChanges": "Chargement des modifications...",
|
"session.review.loadingChanges": "Chargement des modifications...",
|
||||||
"session.review.empty": "Aucune modification dans cette session pour l'instant",
|
"session.review.empty": "Aucune modification dans cette session pour l'instant",
|
||||||
"session.review.noChanges": "Aucune modification",
|
"session.review.noChanges": "Aucune modification",
|
||||||
|
"session.review.noVcs": "Aucun système de contrôle de version Git détecté, modifications non affichées",
|
||||||
|
"session.review.noSnapshot":
|
||||||
|
"Le suivi des instantanés est désactivé dans la configuration, les modifications de session sont donc indisponibles",
|
||||||
"session.files.selectToOpen": "Sélectionnez un fichier à ouvrir",
|
"session.files.selectToOpen": "Sélectionnez un fichier à ouvrir",
|
||||||
"session.files.all": "Tous les fichiers",
|
"session.files.all": "Tous les fichiers",
|
||||||
|
"session.files.empty": "Aucun fichier",
|
||||||
"session.files.binaryContent": "Fichier binaire (le contenu ne peut pas être affiché)",
|
"session.files.binaryContent": "Fichier binaire (le contenu ne peut pas être affiché)",
|
||||||
"session.messages.renderEarlier": "Afficher les messages précédents",
|
"session.messages.renderEarlier": "Afficher les messages précédents",
|
||||||
"session.messages.loadingEarlier": "Chargement des messages précédents...",
|
"session.messages.loadingEarlier": "Chargement des messages précédents...",
|
||||||
@@ -463,6 +475,17 @@ export const dict = {
|
|||||||
"session.todo.title": "Tâches",
|
"session.todo.title": "Tâches",
|
||||||
"session.todo.collapse": "Réduire",
|
"session.todo.collapse": "Réduire",
|
||||||
"session.todo.expand": "Développer",
|
"session.todo.expand": "Développer",
|
||||||
|
"session.followupDock.summary.one": "{{count}} message en file d'attente",
|
||||||
|
"session.followupDock.summary.other": "{{count}} messages en file d'attente",
|
||||||
|
"session.followupDock.sendNow": "Envoyer maintenant",
|
||||||
|
"session.followupDock.edit": "Modifier",
|
||||||
|
"session.followupDock.collapse": "Réduire les messages en file d'attente",
|
||||||
|
"session.followupDock.expand": "Développer les messages en file d'attente",
|
||||||
|
"session.revertDock.summary.one": "{{count}} message annulé",
|
||||||
|
"session.revertDock.summary.other": "{{count}} messages annulés",
|
||||||
|
"session.revertDock.collapse": "Réduire les messages annulés",
|
||||||
|
"session.revertDock.expand": "Développer les messages annulés",
|
||||||
|
"session.revertDock.restore": "Restaurer le message",
|
||||||
"session.new.title": "Créez ce que vous voulez",
|
"session.new.title": "Créez ce que vous voulez",
|
||||||
"session.new.worktree.main": "Branche principale",
|
"session.new.worktree.main": "Branche principale",
|
||||||
"session.new.worktree.mainWithBranch": "Branche principale ({{branch}})",
|
"session.new.worktree.mainWithBranch": "Branche principale ({{branch}})",
|
||||||
@@ -550,10 +573,20 @@ export const dict = {
|
|||||||
"settings.general.row.language.description": "Changer la langue d'affichage pour OpenCode",
|
"settings.general.row.language.description": "Changer la langue d'affichage pour OpenCode",
|
||||||
"settings.general.row.appearance.title": "Apparence",
|
"settings.general.row.appearance.title": "Apparence",
|
||||||
"settings.general.row.appearance.description": "Personnaliser l'apparence d'OpenCode sur votre appareil",
|
"settings.general.row.appearance.description": "Personnaliser l'apparence d'OpenCode sur votre appareil",
|
||||||
|
"settings.general.row.colorScheme.title": "Schéma de couleurs",
|
||||||
|
"settings.general.row.colorScheme.description": "Choisissez si OpenCode suit le thème système, clair ou sombre",
|
||||||
"settings.general.row.theme.title": "Thème",
|
"settings.general.row.theme.title": "Thème",
|
||||||
"settings.general.row.theme.description": "Personnaliser le thème d'OpenCode.",
|
"settings.general.row.theme.description": "Personnaliser le thème d'OpenCode.",
|
||||||
"settings.general.row.font.title": "Police",
|
"settings.general.row.font.title": "Police",
|
||||||
"settings.general.row.font.description": "Personnaliser la police mono utilisée dans les blocs de code",
|
"settings.general.row.font.description": "Personnaliser la police mono utilisée dans les blocs de code",
|
||||||
|
"settings.general.row.followup.title": "Comportement de suivi",
|
||||||
|
"settings.general.row.followup.description":
|
||||||
|
"Choisissez si les messages de suivi dirigent immédiatement ou attendent dans une file d'attente",
|
||||||
|
"settings.general.row.followup.option.queue": "File d'attente",
|
||||||
|
"settings.general.row.followup.option.steer": "Diriger",
|
||||||
|
"settings.general.row.reasoningSummaries.title": "Afficher les résumés de raisonnement",
|
||||||
|
"settings.general.row.reasoningSummaries.description":
|
||||||
|
"Afficher les résumés de raisonnement du modèle dans la chronologie",
|
||||||
"settings.general.row.shellToolPartsExpanded.title": "Développer les parties de l'outil shell",
|
"settings.general.row.shellToolPartsExpanded.title": "Développer les parties de l'outil shell",
|
||||||
"settings.general.row.shellToolPartsExpanded.description":
|
"settings.general.row.shellToolPartsExpanded.description":
|
||||||
"Afficher les parties de l'outil shell développées par défaut dans la chronologie",
|
"Afficher les parties de l'outil shell développées par défaut dans la chronologie",
|
||||||
@@ -764,4 +797,81 @@ export const dict = {
|
|||||||
"common.time.daysAgo.short": "il y a {{count}}j",
|
"common.time.daysAgo.short": "il y a {{count}}j",
|
||||||
"settings.providers.connected.environmentDescription": "Connecté à partir de vos variables d'environnement",
|
"settings.providers.connected.environmentDescription": "Connecté à partir de vos variables d'environnement",
|
||||||
"settings.providers.custom.description": "Ajouter un fournisseur compatible avec OpenAI via l'URL de base.",
|
"settings.providers.custom.description": "Ajouter un fournisseur compatible avec OpenAI via l'URL de base.",
|
||||||
|
|
||||||
|
"app.server.unreachable": "Impossible de joindre {{server}}",
|
||||||
|
"app.server.retrying": "Nouvelle tentative automatique...",
|
||||||
|
"app.server.otherServers": "Autres serveurs",
|
||||||
|
"dialog.server.add.usernamePlaceholder": "nom d'utilisateur",
|
||||||
|
"dialog.server.add.passwordPlaceholder": "mot de passe",
|
||||||
|
"server.row.noUsername": "aucun nom d'utilisateur",
|
||||||
|
"session.review.noVcs.createGit.title": "Créer un dépôt Git",
|
||||||
|
"session.review.noVcs.createGit.description": "Suivre, examiner et annuler les modifications dans ce projet",
|
||||||
|
"session.review.noVcs.createGit.actionLoading": "Création du dépôt Git...",
|
||||||
|
"session.review.noVcs.createGit.action": "Créer un dépôt Git",
|
||||||
|
"session.todo.progress": "{{done}} tâches sur {{total}} terminées",
|
||||||
|
"session.question.progress": "{{current}} questions sur {{total}}",
|
||||||
|
"session.header.open.finder": "Finder",
|
||||||
|
"session.header.open.fileExplorer": "Explorateur de fichiers",
|
||||||
|
"session.header.open.fileManager": "Gestionnaire de fichiers",
|
||||||
|
"session.header.open.app.vscode": "VS Code",
|
||||||
|
"session.header.open.app.cursor": "Cursor",
|
||||||
|
"session.header.open.app.zed": "Zed",
|
||||||
|
"session.header.open.app.textmate": "TextMate",
|
||||||
|
"session.header.open.app.antigravity": "Antigravity",
|
||||||
|
"session.header.open.app.terminal": "Terminal",
|
||||||
|
"session.header.open.app.iterm2": "iTerm2",
|
||||||
|
"session.header.open.app.ghostty": "Ghostty",
|
||||||
|
"session.header.open.app.warp": "Warp",
|
||||||
|
"session.header.open.app.xcode": "Xcode",
|
||||||
|
"session.header.open.app.androidStudio": "Android Studio",
|
||||||
|
"session.header.open.app.powershell": "PowerShell",
|
||||||
|
"session.header.open.app.sublimeText": "Sublime Text",
|
||||||
|
"debugBar.ariaLabel": "Diagnostics de performance de développement",
|
||||||
|
"debugBar.na": "n/a",
|
||||||
|
"debugBar.nav.label": "NAV",
|
||||||
|
"debugBar.nav.tip":
|
||||||
|
"Dernière transition de route terminée touchant une page de session, mesurée du début du routeur jusqu'au premier affichage après stabilisation.",
|
||||||
|
"debugBar.fps.label": "FPS",
|
||||||
|
"debugBar.fps.tip": "Images par seconde glissantes sur les 5 dernières secondes.",
|
||||||
|
"debugBar.frame.label": "FRAME",
|
||||||
|
"debugBar.frame.tip": "Pire temps d'image sur les 5 dernières secondes.",
|
||||||
|
"debugBar.jank.label": "JANK",
|
||||||
|
"debugBar.jank.tip": "Images de plus de 32ms au cours des 5 dernières secondes.",
|
||||||
|
"debugBar.long.label": "LONG",
|
||||||
|
"debugBar.long.tip":
|
||||||
|
"Temps bloqué et nombre de tâches longues au cours des 5 dernières secondes. Tâche max : {{max}}.",
|
||||||
|
"debugBar.delay.label": "DELAY",
|
||||||
|
"debugBar.delay.tip": "Pire délai d'entrée observé au cours des 5 dernières secondes.",
|
||||||
|
"debugBar.inp.label": "INP",
|
||||||
|
"debugBar.inp.tip":
|
||||||
|
"Durée approximative d'interaction au cours des 5 dernières secondes. Ceci est similaire à INP, pas le INP officiel des Web Vitals.",
|
||||||
|
"debugBar.cls.label": "CLS",
|
||||||
|
"debugBar.cls.tip": "Décalage cumulatif de la mise en page pour la durée de vie actuelle de l'application.",
|
||||||
|
"debugBar.mem.label": "MEM",
|
||||||
|
"debugBar.mem.tipUnavailable": "Tas JS utilisé vs limite de tas. Chromium uniquement.",
|
||||||
|
"debugBar.mem.tip": "Tas JS utilisé vs limite de tas. {{used}} sur {{limit}}.",
|
||||||
|
"common.key.ctrl": "Ctrl",
|
||||||
|
"common.key.alt": "Alt",
|
||||||
|
"common.key.shift": "Maj",
|
||||||
|
"common.key.meta": "Méta",
|
||||||
|
"common.key.space": "Espace",
|
||||||
|
"common.key.backspace": "Retour arrière",
|
||||||
|
"common.key.enter": "Entrée",
|
||||||
|
"common.key.tab": "Tab",
|
||||||
|
"common.key.delete": "Suppr",
|
||||||
|
"common.key.home": "Début",
|
||||||
|
"common.key.end": "Fin",
|
||||||
|
"common.key.pageUp": "Page précédente",
|
||||||
|
"common.key.pageDown": "Page suivante",
|
||||||
|
"common.key.insert": "Inser",
|
||||||
|
"common.unknown": "inconnu",
|
||||||
|
"error.page.circular": "[Circulaire]",
|
||||||
|
"error.globalSDK.noServerAvailable": "Aucun serveur disponible",
|
||||||
|
"error.globalSDK.serverNotAvailable": "Serveur non disponible",
|
||||||
|
"error.childStore.persistedCacheCreateFailed": "Échec de la création du cache persistant",
|
||||||
|
"error.childStore.persistedProjectMetadataCreateFailed":
|
||||||
|
"Échec de la création des métadonnées de projet persistantes",
|
||||||
|
"error.childStore.persistedProjectIconCreateFailed": "Échec de la création de l'icône de projet persistante",
|
||||||
|
"error.childStore.storeCreateFailed": "Échec de la création du stockage",
|
||||||
|
"terminal.connectionLost.abnormalClose": "WebSocket fermé anormalement : {{code}}",
|
||||||
}
|
}
|
||||||
|
|||||||
+107
-3
@@ -104,6 +104,7 @@ export const dict = {
|
|||||||
"dialog.model.empty": "モデルが見つかりません",
|
"dialog.model.empty": "モデルが見つかりません",
|
||||||
"dialog.model.manage": "モデルを管理",
|
"dialog.model.manage": "モデルを管理",
|
||||||
"dialog.model.manage.description": "モデルセレクターに表示するモデルをカスタマイズします。",
|
"dialog.model.manage.description": "モデルセレクターに表示するモデルをカスタマイズします。",
|
||||||
|
"dialog.model.manage.provider.toggle": "すべての{{provider}}モデルを切り替え",
|
||||||
"dialog.model.unpaid.freeModels.title": "OpenCodeが提供する無料モデル",
|
"dialog.model.unpaid.freeModels.title": "OpenCodeが提供する無料モデル",
|
||||||
"dialog.model.unpaid.addMore.title": "人気のプロバイダーからモデルを追加",
|
"dialog.model.unpaid.addMore.title": "人気のプロバイダーからモデルを追加",
|
||||||
"dialog.provider.viewAll": "さらにプロバイダーを表示",
|
"dialog.provider.viewAll": "さらにプロバイダーを表示",
|
||||||
@@ -242,7 +243,7 @@ export const dict = {
|
|||||||
"prompt.example.25": "ここでは環境変数はどう機能しますか?",
|
"prompt.example.25": "ここでは環境変数はどう機能しますか?",
|
||||||
"prompt.popover.emptyResults": "一致する結果がありません",
|
"prompt.popover.emptyResults": "一致する結果がありません",
|
||||||
"prompt.popover.emptyCommands": "一致するコマンドがありません",
|
"prompt.popover.emptyCommands": "一致するコマンドがありません",
|
||||||
"prompt.dropzone.label": "画像またはPDFをここにドロップ",
|
"prompt.dropzone.label": "画像、PDF、またはテキストファイルをここにドロップしてください",
|
||||||
"prompt.dropzone.file.label": "ドロップして@メンションファイルを追加",
|
"prompt.dropzone.file.label": "ドロップして@メンションファイルを追加",
|
||||||
"prompt.slash.badge.custom": "カスタム",
|
"prompt.slash.badge.custom": "カスタム",
|
||||||
"prompt.slash.badge.skill": "スキル",
|
"prompt.slash.badge.skill": "スキル",
|
||||||
@@ -255,8 +256,8 @@ export const dict = {
|
|||||||
"prompt.attachment.remove": "添付ファイルを削除",
|
"prompt.attachment.remove": "添付ファイルを削除",
|
||||||
"prompt.action.send": "送信",
|
"prompt.action.send": "送信",
|
||||||
"prompt.action.stop": "停止",
|
"prompt.action.stop": "停止",
|
||||||
"prompt.toast.pasteUnsupported.title": "サポートされていない貼り付け",
|
"prompt.toast.pasteUnsupported.title": "サポートされていない添付ファイル",
|
||||||
"prompt.toast.pasteUnsupported.description": "ここでは画像またはPDFのみ貼り付け可能です。",
|
"prompt.toast.pasteUnsupported.description": "画像、PDF、またはテキストファイルのみ添付できます。",
|
||||||
"prompt.toast.modelAgentRequired.title": "エージェントとモデルを選択",
|
"prompt.toast.modelAgentRequired.title": "エージェントとモデルを選択",
|
||||||
"prompt.toast.modelAgentRequired.description": "プロンプトを送信する前にエージェントとモデルを選択してください。",
|
"prompt.toast.modelAgentRequired.description": "プロンプトを送信する前にエージェントとモデルを選択してください。",
|
||||||
"prompt.toast.worktreeCreateFailed.title": "ワークツリーの作成に失敗しました",
|
"prompt.toast.worktreeCreateFailed.title": "ワークツリーの作成に失敗しました",
|
||||||
@@ -287,6 +288,11 @@ export const dict = {
|
|||||||
"dialog.server.add.error": "サーバーに接続できませんでした",
|
"dialog.server.add.error": "サーバーに接続できませんでした",
|
||||||
"dialog.server.add.checking": "確認中...",
|
"dialog.server.add.checking": "確認中...",
|
||||||
"dialog.server.add.button": "サーバーを追加",
|
"dialog.server.add.button": "サーバーを追加",
|
||||||
|
"dialog.server.add.name": "サーバー名 (オプション)",
|
||||||
|
"dialog.server.add.namePlaceholder": "Localhost",
|
||||||
|
"dialog.server.add.username": "ユーザー名 (オプション)",
|
||||||
|
"dialog.server.add.password": "パスワード (オプション)",
|
||||||
|
"dialog.server.edit.title": "サーバーを編集",
|
||||||
"dialog.server.default.title": "デフォルトサーバー",
|
"dialog.server.default.title": "デフォルトサーバー",
|
||||||
"dialog.server.default.description":
|
"dialog.server.default.description":
|
||||||
"ローカルサーバーを起動する代わりに、アプリ起動時にこのサーバーに接続します。再起動が必要です。",
|
"ローカルサーバーを起動する代わりに、アプリ起動時にこのサーバーに接続します。再起動が必要です。",
|
||||||
@@ -358,6 +364,7 @@ export const dict = {
|
|||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.bs": "Bosanski",
|
"language.bs": "Bosanski",
|
||||||
"language.th": "ไทย",
|
"language.th": "ไทย",
|
||||||
|
"language.tr": "Türkçe",
|
||||||
"toast.language.title": "言語",
|
"toast.language.title": "言語",
|
||||||
"toast.language.description": "{{language}}に切り替えました",
|
"toast.language.description": "{{language}}に切り替えました",
|
||||||
"toast.theme.title": "テーマが切り替わりました",
|
"toast.theme.title": "テーマが切り替わりました",
|
||||||
@@ -444,9 +451,12 @@ export const dict = {
|
|||||||
"session.review.change.other": "変更",
|
"session.review.change.other": "変更",
|
||||||
"session.review.loadingChanges": "変更を読み込み中...",
|
"session.review.loadingChanges": "変更を読み込み中...",
|
||||||
"session.review.empty": "このセッションでの変更はまだありません",
|
"session.review.empty": "このセッションでの変更はまだありません",
|
||||||
|
"session.review.noVcs": "Gitバージョン管理システムが検出されないため、変更は表示されません",
|
||||||
|
"session.review.noSnapshot": "設定でスナップショット追跡が無効になっているため、セッションの変更は利用できません",
|
||||||
"session.review.noChanges": "変更なし",
|
"session.review.noChanges": "変更なし",
|
||||||
"session.files.selectToOpen": "開くファイルを選択",
|
"session.files.selectToOpen": "開くファイルを選択",
|
||||||
"session.files.all": "すべてのファイル",
|
"session.files.all": "すべてのファイル",
|
||||||
|
"session.files.empty": "ファイルなし",
|
||||||
"session.files.binaryContent": "バイナリファイル(内容を表示できません)",
|
"session.files.binaryContent": "バイナリファイル(内容を表示できません)",
|
||||||
"session.messages.renderEarlier": "以前のメッセージを表示",
|
"session.messages.renderEarlier": "以前のメッセージを表示",
|
||||||
"session.messages.loadingEarlier": "以前のメッセージを読み込み中...",
|
"session.messages.loadingEarlier": "以前のメッセージを読み込み中...",
|
||||||
@@ -457,6 +467,17 @@ export const dict = {
|
|||||||
"session.todo.title": "ToDo",
|
"session.todo.title": "ToDo",
|
||||||
"session.todo.collapse": "折りたたむ",
|
"session.todo.collapse": "折りたたむ",
|
||||||
"session.todo.expand": "展開",
|
"session.todo.expand": "展開",
|
||||||
|
"session.followupDock.summary.one": "{{count}} 件のメッセージが待機中",
|
||||||
|
"session.followupDock.summary.other": "{{count}} 件のメッセージが待機中",
|
||||||
|
"session.followupDock.sendNow": "今すぐ送信",
|
||||||
|
"session.followupDock.edit": "編集",
|
||||||
|
"session.followupDock.collapse": "待機中のメッセージを折りたたむ",
|
||||||
|
"session.followupDock.expand": "待機中のメッセージを展開",
|
||||||
|
"session.revertDock.summary.one": "{{count}} 件のロールバックされたメッセージ",
|
||||||
|
"session.revertDock.summary.other": "{{count}} 件のロールバックされたメッセージ",
|
||||||
|
"session.revertDock.collapse": "ロールバックされたメッセージを折りたたむ",
|
||||||
|
"session.revertDock.expand": "ロールバックされたメッセージを展開",
|
||||||
|
"session.revertDock.restore": "メッセージを復元",
|
||||||
"session.new.title": "何でも作る",
|
"session.new.title": "何でも作る",
|
||||||
"session.new.worktree.main": "メインブランチ",
|
"session.new.worktree.main": "メインブランチ",
|
||||||
"session.new.worktree.mainWithBranch": "メインブランチ ({{branch}})",
|
"session.new.worktree.mainWithBranch": "メインブランチ ({{branch}})",
|
||||||
@@ -542,10 +563,19 @@ export const dict = {
|
|||||||
"settings.general.row.language.description": "OpenCodeの表示言語を変更します",
|
"settings.general.row.language.description": "OpenCodeの表示言語を変更します",
|
||||||
"settings.general.row.appearance.title": "外観",
|
"settings.general.row.appearance.title": "外観",
|
||||||
"settings.general.row.appearance.description": "デバイスでのOpenCodeの表示をカスタマイズします",
|
"settings.general.row.appearance.description": "デバイスでのOpenCodeの表示をカスタマイズします",
|
||||||
|
"settings.general.row.colorScheme.title": "配色",
|
||||||
|
"settings.general.row.colorScheme.description": "OpenCodeがシステム、ライト、またはダークテーマに従うかを選択します",
|
||||||
"settings.general.row.theme.title": "テーマ",
|
"settings.general.row.theme.title": "テーマ",
|
||||||
"settings.general.row.theme.description": "OpenCodeのテーマをカスタマイズします。",
|
"settings.general.row.theme.description": "OpenCodeのテーマをカスタマイズします。",
|
||||||
"settings.general.row.font.title": "フォント",
|
"settings.general.row.font.title": "フォント",
|
||||||
"settings.general.row.font.description": "コードブロックで使用する等幅フォントをカスタマイズします",
|
"settings.general.row.font.description": "コードブロックで使用する等幅フォントをカスタマイズします",
|
||||||
|
"settings.general.row.followup.title": "フォローアップの動作",
|
||||||
|
"settings.general.row.followup.description":
|
||||||
|
"フォローアッププロンプトを即座に実行するか、キューで待機させるかを選択します",
|
||||||
|
"settings.general.row.followup.option.queue": "キューに追加",
|
||||||
|
"settings.general.row.followup.option.steer": "即座に実行 (Steer)",
|
||||||
|
"settings.general.row.reasoningSummaries.title": "推論の要約を表示",
|
||||||
|
"settings.general.row.reasoningSummaries.description": "タイムラインにモデルの推論の要約を表示します",
|
||||||
"settings.general.row.shellToolPartsExpanded.title": "shell ツールパーツを展開",
|
"settings.general.row.shellToolPartsExpanded.title": "shell ツールパーツを展開",
|
||||||
"settings.general.row.shellToolPartsExpanded.description":
|
"settings.general.row.shellToolPartsExpanded.description":
|
||||||
"タイムラインで shell ツールパーツをデフォルトで展開して表示します",
|
"タイムラインで shell ツールパーツをデフォルトで展開して表示します",
|
||||||
@@ -753,4 +783,78 @@ export const dict = {
|
|||||||
"common.time.daysAgo.short": "{{count}} 日前",
|
"common.time.daysAgo.short": "{{count}} 日前",
|
||||||
"settings.providers.connected.environmentDescription": "環境変数から接続されました",
|
"settings.providers.connected.environmentDescription": "環境変数から接続されました",
|
||||||
"settings.providers.custom.description": "ベース URL を指定して OpenAI 互換のプロバイダーを追加します。",
|
"settings.providers.custom.description": "ベース URL を指定して OpenAI 互換のプロバイダーを追加します。",
|
||||||
|
|
||||||
|
"app.server.unreachable": "{{server}} に到達できませんでした",
|
||||||
|
"app.server.retrying": "自動的に再試行中...",
|
||||||
|
"app.server.otherServers": "その他のサーバー",
|
||||||
|
"dialog.server.add.usernamePlaceholder": "ユーザー名",
|
||||||
|
"dialog.server.add.passwordPlaceholder": "パスワード",
|
||||||
|
"server.row.noUsername": "ユーザー名なし",
|
||||||
|
"session.review.noVcs.createGit.title": "Git リポジトリを作成",
|
||||||
|
"session.review.noVcs.createGit.description": "このプロジェクトの変更を追跡、レビュー、元に戻す",
|
||||||
|
"session.review.noVcs.createGit.actionLoading": "Git リポジトリを作成中...",
|
||||||
|
"session.review.noVcs.createGit.action": "Git リポジトリを作成",
|
||||||
|
"session.todo.progress": "{{done}} 個中 {{total}} 個の Todo が完了",
|
||||||
|
"session.question.progress": "{{total}} 問中 {{current}} 問",
|
||||||
|
"session.header.open.finder": "Finder",
|
||||||
|
"session.header.open.fileExplorer": "エクスプローラー",
|
||||||
|
"session.header.open.fileManager": "ファイルマネージャー",
|
||||||
|
"session.header.open.app.vscode": "VS Code",
|
||||||
|
"session.header.open.app.cursor": "Cursor",
|
||||||
|
"session.header.open.app.zed": "Zed",
|
||||||
|
"session.header.open.app.textmate": "TextMate",
|
||||||
|
"session.header.open.app.antigravity": "Antigravity",
|
||||||
|
"session.header.open.app.terminal": "ターミナル",
|
||||||
|
"session.header.open.app.iterm2": "iTerm2",
|
||||||
|
"session.header.open.app.ghostty": "Ghostty",
|
||||||
|
"session.header.open.app.warp": "Warp",
|
||||||
|
"session.header.open.app.xcode": "Xcode",
|
||||||
|
"session.header.open.app.androidStudio": "Android Studio",
|
||||||
|
"session.header.open.app.powershell": "PowerShell",
|
||||||
|
"session.header.open.app.sublimeText": "Sublime Text",
|
||||||
|
"debugBar.ariaLabel": "開発パフォーマンス診断",
|
||||||
|
"debugBar.na": "n/a",
|
||||||
|
"debugBar.nav.label": "NAV",
|
||||||
|
"debugBar.nav.tip": "セッションページに触れる最後に完了したルート遷移。ルーター開始から安定後の最初の描画まで測定。",
|
||||||
|
"debugBar.fps.label": "FPS",
|
||||||
|
"debugBar.fps.tip": "過去5秒間のローリングフレーム/秒。",
|
||||||
|
"debugBar.frame.label": "FRAME",
|
||||||
|
"debugBar.frame.tip": "過去5秒間の最悪フレーム時間。",
|
||||||
|
"debugBar.jank.label": "JANK",
|
||||||
|
"debugBar.jank.tip": "過去5秒間で32msを超えたフレーム。",
|
||||||
|
"debugBar.long.label": "LONG",
|
||||||
|
"debugBar.long.tip": "過去5秒間のブロック時間と長時間タスク数。最大タスク: {{max}}。",
|
||||||
|
"debugBar.delay.label": "DELAY",
|
||||||
|
"debugBar.delay.tip": "過去5秒間で観測された最悪の入力遅延。",
|
||||||
|
"debugBar.inp.label": "INP",
|
||||||
|
"debugBar.inp.tip":
|
||||||
|
"過去5秒間の概算インタラクション時間。これは INP に似ていますが、公式の Web Vitals INP ではありません。",
|
||||||
|
"debugBar.cls.label": "CLS",
|
||||||
|
"debugBar.cls.tip": "現在のアプリ寿命の累積レイアウトシフト。",
|
||||||
|
"debugBar.mem.label": "MEM",
|
||||||
|
"debugBar.mem.tipUnavailable": "使用中の JS ヒープ対ヒープ制限。Chromium のみ。",
|
||||||
|
"debugBar.mem.tip": "使用中の JS ヒープ対ヒープ制限。{{limit}} 中 {{used}}。",
|
||||||
|
"common.key.ctrl": "Ctrl",
|
||||||
|
"common.key.alt": "Alt",
|
||||||
|
"common.key.shift": "Shift",
|
||||||
|
"common.key.meta": "Meta",
|
||||||
|
"common.key.space": "Space",
|
||||||
|
"common.key.backspace": "Backspace",
|
||||||
|
"common.key.enter": "Enter",
|
||||||
|
"common.key.tab": "Tab",
|
||||||
|
"common.key.delete": "Delete",
|
||||||
|
"common.key.home": "Home",
|
||||||
|
"common.key.end": "End",
|
||||||
|
"common.key.pageUp": "Page Up",
|
||||||
|
"common.key.pageDown": "Page Down",
|
||||||
|
"common.key.insert": "Insert",
|
||||||
|
"common.unknown": "不明",
|
||||||
|
"error.page.circular": "[循環]",
|
||||||
|
"error.globalSDK.noServerAvailable": "利用可能なサーバーがありません",
|
||||||
|
"error.globalSDK.serverNotAvailable": "サーバーが利用できません",
|
||||||
|
"error.childStore.persistedCacheCreateFailed": "永続キャッシュの作成に失敗しました",
|
||||||
|
"error.childStore.persistedProjectMetadataCreateFailed": "永続プロジェクトメタデータの作成に失敗しました",
|
||||||
|
"error.childStore.persistedProjectIconCreateFailed": "永続プロジェクトアイコンの作成に失敗しました",
|
||||||
|
"error.childStore.storeCreateFailed": "ストアの作成に失敗しました",
|
||||||
|
"terminal.connectionLost.abnormalClose": "WebSocket が異常終了しました: {{code}}",
|
||||||
}
|
}
|
||||||
|
|||||||
+106
-3
@@ -108,6 +108,7 @@ export const dict = {
|
|||||||
"dialog.model.empty": "모델 결과 없음",
|
"dialog.model.empty": "모델 결과 없음",
|
||||||
"dialog.model.manage": "모델 관리",
|
"dialog.model.manage": "모델 관리",
|
||||||
"dialog.model.manage.description": "모델 선택기에 표시할 모델 사용자 지정",
|
"dialog.model.manage.description": "모델 선택기에 표시할 모델 사용자 지정",
|
||||||
|
"dialog.model.manage.provider.toggle": "모든 {{provider}} 모델 토글",
|
||||||
"dialog.model.unpaid.freeModels.title": "OpenCode에서 제공하는 무료 모델",
|
"dialog.model.unpaid.freeModels.title": "OpenCode에서 제공하는 무료 모델",
|
||||||
"dialog.model.unpaid.addMore.title": "인기 공급자의 모델 추가",
|
"dialog.model.unpaid.addMore.title": "인기 공급자의 모델 추가",
|
||||||
"dialog.provider.viewAll": "더 많은 공급자 보기",
|
"dialog.provider.viewAll": "더 많은 공급자 보기",
|
||||||
@@ -246,7 +247,7 @@ export const dict = {
|
|||||||
"prompt.example.25": "여기서 환경 변수는 어떻게 작동하나요?",
|
"prompt.example.25": "여기서 환경 변수는 어떻게 작동하나요?",
|
||||||
"prompt.popover.emptyResults": "일치하는 결과 없음",
|
"prompt.popover.emptyResults": "일치하는 결과 없음",
|
||||||
"prompt.popover.emptyCommands": "일치하는 명령어 없음",
|
"prompt.popover.emptyCommands": "일치하는 명령어 없음",
|
||||||
"prompt.dropzone.label": "이미지나 PDF를 여기에 드롭하세요",
|
"prompt.dropzone.label": "이미지, PDF 또는 텍스트 파일을 이곳에 드롭하세요",
|
||||||
"prompt.dropzone.file.label": "드롭하여 파일 @멘션 추가",
|
"prompt.dropzone.file.label": "드롭하여 파일 @멘션 추가",
|
||||||
"prompt.slash.badge.custom": "사용자 지정",
|
"prompt.slash.badge.custom": "사용자 지정",
|
||||||
"prompt.slash.badge.skill": "스킬",
|
"prompt.slash.badge.skill": "스킬",
|
||||||
@@ -259,8 +260,8 @@ export const dict = {
|
|||||||
"prompt.attachment.remove": "첨부 파일 제거",
|
"prompt.attachment.remove": "첨부 파일 제거",
|
||||||
"prompt.action.send": "전송",
|
"prompt.action.send": "전송",
|
||||||
"prompt.action.stop": "중지",
|
"prompt.action.stop": "중지",
|
||||||
"prompt.toast.pasteUnsupported.title": "지원되지 않는 붙여넣기",
|
"prompt.toast.pasteUnsupported.title": "지원되지 않는 첨부 파일",
|
||||||
"prompt.toast.pasteUnsupported.description": "이미지나 PDF만 붙여넣을 수 있습니다.",
|
"prompt.toast.pasteUnsupported.description": "이미지, PDF 또는 텍스트 파일만 첨부할 수 있습니다.",
|
||||||
"prompt.toast.modelAgentRequired.title": "에이전트 및 모델 선택",
|
"prompt.toast.modelAgentRequired.title": "에이전트 및 모델 선택",
|
||||||
"prompt.toast.modelAgentRequired.description": "프롬프트를 보내기 전에 에이전트와 모델을 선택하세요.",
|
"prompt.toast.modelAgentRequired.description": "프롬프트를 보내기 전에 에이전트와 모델을 선택하세요.",
|
||||||
"prompt.toast.worktreeCreateFailed.title": "작업 트리 생성 실패",
|
"prompt.toast.worktreeCreateFailed.title": "작업 트리 생성 실패",
|
||||||
@@ -291,6 +292,11 @@ export const dict = {
|
|||||||
"dialog.server.add.error": "서버에 연결할 수 없습니다",
|
"dialog.server.add.error": "서버에 연결할 수 없습니다",
|
||||||
"dialog.server.add.checking": "확인 중...",
|
"dialog.server.add.checking": "확인 중...",
|
||||||
"dialog.server.add.button": "서버 추가",
|
"dialog.server.add.button": "서버 추가",
|
||||||
|
"dialog.server.add.name": "서버 이름 (선택 사항)",
|
||||||
|
"dialog.server.add.namePlaceholder": "Localhost",
|
||||||
|
"dialog.server.add.username": "사용자 이름 (선택 사항)",
|
||||||
|
"dialog.server.add.password": "비밀번호 (선택 사항)",
|
||||||
|
"dialog.server.edit.title": "서버 편집",
|
||||||
"dialog.server.default.title": "기본 서버",
|
"dialog.server.default.title": "기본 서버",
|
||||||
"dialog.server.default.description":
|
"dialog.server.default.description":
|
||||||
"로컬 서버를 시작하는 대신 앱 실행 시 이 서버에 연결합니다. 다시 시작해야 합니다.",
|
"로컬 서버를 시작하는 대신 앱 실행 시 이 서버에 연결합니다. 다시 시작해야 합니다.",
|
||||||
@@ -361,6 +367,7 @@ export const dict = {
|
|||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.bs": "Bosanski",
|
"language.bs": "Bosanski",
|
||||||
"language.th": "ไทย",
|
"language.th": "ไทย",
|
||||||
|
"language.tr": "Türkçe",
|
||||||
"toast.language.title": "언어",
|
"toast.language.title": "언어",
|
||||||
"toast.language.description": "{{language}}(으)로 전환됨",
|
"toast.language.description": "{{language}}(으)로 전환됨",
|
||||||
"toast.theme.title": "테마 전환됨",
|
"toast.theme.title": "테마 전환됨",
|
||||||
@@ -446,9 +453,12 @@ export const dict = {
|
|||||||
"session.review.change.other": "변경",
|
"session.review.change.other": "변경",
|
||||||
"session.review.loadingChanges": "변경 사항 로드 중...",
|
"session.review.loadingChanges": "변경 사항 로드 중...",
|
||||||
"session.review.empty": "이 세션에 변경 사항이 아직 없습니다",
|
"session.review.empty": "이 세션에 변경 사항이 아직 없습니다",
|
||||||
|
"session.review.noVcs": "Git 버전 관리 시스템이 감지되지 않아 변경 사항이 표시되지 않습니다",
|
||||||
|
"session.review.noSnapshot": "구성에서 스냅샷 추적이 비활성화되어 있어 세션 변경 사항을 사용할 수 없습니다",
|
||||||
"session.review.noChanges": "변경 없음",
|
"session.review.noChanges": "변경 없음",
|
||||||
"session.files.selectToOpen": "열 파일을 선택하세요",
|
"session.files.selectToOpen": "열 파일을 선택하세요",
|
||||||
"session.files.all": "모든 파일",
|
"session.files.all": "모든 파일",
|
||||||
|
"session.files.empty": "파일 없음",
|
||||||
"session.files.binaryContent": "바이너리 파일 (내용을 표시할 수 없음)",
|
"session.files.binaryContent": "바이너리 파일 (내용을 표시할 수 없음)",
|
||||||
"session.messages.renderEarlier": "이전 메시지 렌더링",
|
"session.messages.renderEarlier": "이전 메시지 렌더링",
|
||||||
"session.messages.loadingEarlier": "이전 메시지 로드 중...",
|
"session.messages.loadingEarlier": "이전 메시지 로드 중...",
|
||||||
@@ -459,6 +469,17 @@ export const dict = {
|
|||||||
"session.todo.title": "할 일",
|
"session.todo.title": "할 일",
|
||||||
"session.todo.collapse": "접기",
|
"session.todo.collapse": "접기",
|
||||||
"session.todo.expand": "펼치기",
|
"session.todo.expand": "펼치기",
|
||||||
|
"session.followupDock.summary.one": "{{count}}개의 대기 중인 메시지",
|
||||||
|
"session.followupDock.summary.other": "{{count}}개의 대기 중인 메시지",
|
||||||
|
"session.followupDock.sendNow": "지금 전송",
|
||||||
|
"session.followupDock.edit": "편집",
|
||||||
|
"session.followupDock.collapse": "대기 중인 메시지 접기",
|
||||||
|
"session.followupDock.expand": "대기 중인 메시지 펼치기",
|
||||||
|
"session.revertDock.summary.one": "{{count}}개의 롤백된 메시지",
|
||||||
|
"session.revertDock.summary.other": "{{count}}개의 롤백된 메시지",
|
||||||
|
"session.revertDock.collapse": "롤백된 메시지 접기",
|
||||||
|
"session.revertDock.expand": "롤백된 메시지 펼치기",
|
||||||
|
"session.revertDock.restore": "메시지 복원",
|
||||||
"session.new.title": "무엇이든 만들기",
|
"session.new.title": "무엇이든 만들기",
|
||||||
"session.new.worktree.main": "메인 브랜치",
|
"session.new.worktree.main": "메인 브랜치",
|
||||||
"session.new.worktree.mainWithBranch": "메인 브랜치 ({{branch}})",
|
"session.new.worktree.mainWithBranch": "메인 브랜치 ({{branch}})",
|
||||||
@@ -543,10 +564,18 @@ export const dict = {
|
|||||||
"settings.general.row.language.description": "OpenCode 표시 언어 변경",
|
"settings.general.row.language.description": "OpenCode 표시 언어 변경",
|
||||||
"settings.general.row.appearance.title": "모양",
|
"settings.general.row.appearance.title": "모양",
|
||||||
"settings.general.row.appearance.description": "기기에서 OpenCode가 보이는 방식 사용자 지정",
|
"settings.general.row.appearance.description": "기기에서 OpenCode가 보이는 방식 사용자 지정",
|
||||||
|
"settings.general.row.colorScheme.title": "색상 테마",
|
||||||
|
"settings.general.row.colorScheme.description": "OpenCode가 시스템, 라이트 또는 다크 테마를 따를지 선택하세요",
|
||||||
"settings.general.row.theme.title": "테마",
|
"settings.general.row.theme.title": "테마",
|
||||||
"settings.general.row.theme.description": "OpenCode 테마 사용자 지정",
|
"settings.general.row.theme.description": "OpenCode 테마 사용자 지정",
|
||||||
"settings.general.row.font.title": "글꼴",
|
"settings.general.row.font.title": "글꼴",
|
||||||
"settings.general.row.font.description": "코드 블록에 사용되는 고정폭 글꼴 사용자 지정",
|
"settings.general.row.font.description": "코드 블록에 사용되는 고정폭 글꼴 사용자 지정",
|
||||||
|
"settings.general.row.followup.title": "후속 조치 동작",
|
||||||
|
"settings.general.row.followup.description": "후속 프롬프트를 즉시 실행할지 대기열에 넣을지 선택하세요",
|
||||||
|
"settings.general.row.followup.option.queue": "대기열",
|
||||||
|
"settings.general.row.followup.option.steer": "조종",
|
||||||
|
"settings.general.row.reasoningSummaries.title": "추론 요약 표시",
|
||||||
|
"settings.general.row.reasoningSummaries.description": "타임라인에 모델 추론 요약 표시",
|
||||||
"settings.general.row.shellToolPartsExpanded.title": "shell 도구 파트 펼치기",
|
"settings.general.row.shellToolPartsExpanded.title": "shell 도구 파트 펼치기",
|
||||||
"settings.general.row.shellToolPartsExpanded.description":
|
"settings.general.row.shellToolPartsExpanded.description":
|
||||||
"타임라인에서 기본적으로 shell 도구 파트를 펼친 상태로 표시합니다",
|
"타임라인에서 기본적으로 shell 도구 파트를 펼친 상태로 표시합니다",
|
||||||
@@ -753,4 +782,78 @@ export const dict = {
|
|||||||
"common.time.daysAgo.short": "{{count}}일 전",
|
"common.time.daysAgo.short": "{{count}}일 전",
|
||||||
"settings.providers.connected.environmentDescription": "환경 변수에서 연결됨",
|
"settings.providers.connected.environmentDescription": "환경 변수에서 연결됨",
|
||||||
"settings.providers.custom.description": "기본 URL로 OpenAI 호환 공급자를 추가합니다.",
|
"settings.providers.custom.description": "기본 URL로 OpenAI 호환 공급자를 추가합니다.",
|
||||||
|
|
||||||
|
"app.server.unreachable": "{{server}}에 연결할 수 없습니다",
|
||||||
|
"app.server.retrying": "자동으로 재시도 중...",
|
||||||
|
"app.server.otherServers": "다른 서버",
|
||||||
|
"dialog.server.add.usernamePlaceholder": "사용자 이름",
|
||||||
|
"dialog.server.add.passwordPlaceholder": "비밀번호",
|
||||||
|
"server.row.noUsername": "사용자 이름 없음",
|
||||||
|
"session.review.noVcs.createGit.title": "Git 저장소 생성",
|
||||||
|
"session.review.noVcs.createGit.description": "이 프로젝트의 변경 사항을 추적, 검토 및 실행 취소",
|
||||||
|
"session.review.noVcs.createGit.actionLoading": "Git 저장소 생성 중...",
|
||||||
|
"session.review.noVcs.createGit.action": "Git 저장소 생성",
|
||||||
|
"session.todo.progress": "{{total}}개의 할 일 중 {{done}}개 완료",
|
||||||
|
"session.question.progress": "{{total}}개의 질문 중 {{current}}개",
|
||||||
|
"session.header.open.finder": "Finder",
|
||||||
|
"session.header.open.fileExplorer": "파일 탐색기",
|
||||||
|
"session.header.open.fileManager": "파일 관리자",
|
||||||
|
"session.header.open.app.vscode": "VS Code",
|
||||||
|
"session.header.open.app.cursor": "Cursor",
|
||||||
|
"session.header.open.app.zed": "Zed",
|
||||||
|
"session.header.open.app.textmate": "TextMate",
|
||||||
|
"session.header.open.app.antigravity": "Antigravity",
|
||||||
|
"session.header.open.app.terminal": "터미널",
|
||||||
|
"session.header.open.app.iterm2": "iTerm2",
|
||||||
|
"session.header.open.app.ghostty": "Ghostty",
|
||||||
|
"session.header.open.app.warp": "Warp",
|
||||||
|
"session.header.open.app.xcode": "Xcode",
|
||||||
|
"session.header.open.app.androidStudio": "Android Studio",
|
||||||
|
"session.header.open.app.powershell": "PowerShell",
|
||||||
|
"session.header.open.app.sublimeText": "Sublime Text",
|
||||||
|
"debugBar.ariaLabel": "개발 성능 진단",
|
||||||
|
"debugBar.na": "해당 없음",
|
||||||
|
"debugBar.nav.label": "NAV",
|
||||||
|
"debugBar.nav.tip":
|
||||||
|
"세션 페이지에 닿은 마지막 완료된 라우트 전환. 라우터 시작부터 정착 후 첫 번째 페인트까지 측정됨.",
|
||||||
|
"debugBar.fps.label": "FPS",
|
||||||
|
"debugBar.fps.tip": "지난 5초간의 초당 프레임 수.",
|
||||||
|
"debugBar.frame.label": "FRAME",
|
||||||
|
"debugBar.frame.tip": "지난 5초간의 최악의 프레임 시간.",
|
||||||
|
"debugBar.jank.label": "JANK",
|
||||||
|
"debugBar.jank.tip": "지난 5초간 32ms를 초과한 프레임.",
|
||||||
|
"debugBar.long.label": "LONG",
|
||||||
|
"debugBar.long.tip": "지난 5초간의 차단된 시간 및 긴 작업 수. 최대 작업: {{max}}.",
|
||||||
|
"debugBar.delay.label": "DELAY",
|
||||||
|
"debugBar.delay.tip": "지난 5초간 관찰된 최악의 입력 지연.",
|
||||||
|
"debugBar.inp.label": "INP",
|
||||||
|
"debugBar.inp.tip": "지난 5초간의 대략적인 상호작용 지속 시간. 이것은 공식 Web Vitals INP가 아닌 INP와 유사합니다.",
|
||||||
|
"debugBar.cls.label": "CLS",
|
||||||
|
"debugBar.cls.tip": "현재 앱 수명 동안의 누적 레이아웃 이동.",
|
||||||
|
"debugBar.mem.label": "MEM",
|
||||||
|
"debugBar.mem.tipUnavailable": "사용된 JS 힙 대 힙 제한. Chromium 전용.",
|
||||||
|
"debugBar.mem.tip": "사용된 JS 힙 대 힙 제한. {{limit}} 중 {{used}}.",
|
||||||
|
"common.key.ctrl": "Ctrl",
|
||||||
|
"common.key.alt": "Alt",
|
||||||
|
"common.key.shift": "Shift",
|
||||||
|
"common.key.meta": "Meta",
|
||||||
|
"common.key.space": "Space",
|
||||||
|
"common.key.backspace": "Backspace",
|
||||||
|
"common.key.enter": "Enter",
|
||||||
|
"common.key.tab": "Tab",
|
||||||
|
"common.key.delete": "Delete",
|
||||||
|
"common.key.home": "Home",
|
||||||
|
"common.key.end": "End",
|
||||||
|
"common.key.pageUp": "Page Up",
|
||||||
|
"common.key.pageDown": "Page Down",
|
||||||
|
"common.key.insert": "Insert",
|
||||||
|
"common.unknown": "알 수 없음",
|
||||||
|
"error.page.circular": "[순환]",
|
||||||
|
"error.globalSDK.noServerAvailable": "사용 가능한 서버 없음",
|
||||||
|
"error.globalSDK.serverNotAvailable": "서버를 사용할 수 없음",
|
||||||
|
"error.childStore.persistedCacheCreateFailed": "영구 캐시 생성 실패",
|
||||||
|
"error.childStore.persistedProjectMetadataCreateFailed": "영구 프로젝트 메타데이터 생성 실패",
|
||||||
|
"error.childStore.persistedProjectIconCreateFailed": "영구 프로젝트 아이콘 생성 실패",
|
||||||
|
"error.childStore.storeCreateFailed": "저장소 생성 실패",
|
||||||
|
"terminal.connectionLost.abnormalClose": "WebSocket이 비정상적으로 닫힘: {{code}}",
|
||||||
}
|
}
|
||||||
|
|||||||
+109
-5
@@ -116,6 +116,7 @@ export const dict = {
|
|||||||
"dialog.model.empty": "Ingen modellresultater",
|
"dialog.model.empty": "Ingen modellresultater",
|
||||||
"dialog.model.manage": "Administrer modeller",
|
"dialog.model.manage": "Administrer modeller",
|
||||||
"dialog.model.manage.description": "Tilpass hvilke modeller som vises i modellvelgeren.",
|
"dialog.model.manage.description": "Tilpass hvilke modeller som vises i modellvelgeren.",
|
||||||
|
"dialog.model.manage.provider.toggle": "Veksle alle {{provider}}-modeller",
|
||||||
|
|
||||||
"dialog.model.unpaid.freeModels.title": "Gratis modeller levert av OpenCode",
|
"dialog.model.unpaid.freeModels.title": "Gratis modeller levert av OpenCode",
|
||||||
"dialog.model.unpaid.addMore.title": "Legg til flere modeller fra populære leverandører",
|
"dialog.model.unpaid.addMore.title": "Legg til flere modeller fra populære leverandører",
|
||||||
@@ -216,7 +217,7 @@ export const dict = {
|
|||||||
|
|
||||||
"common.search.placeholder": "Søk",
|
"common.search.placeholder": "Søk",
|
||||||
"common.goBack": "Gå tilbake",
|
"common.goBack": "Gå tilbake",
|
||||||
"common.goForward": "Navigate forward",
|
"common.goForward": "Gå frem",
|
||||||
"common.loading": "Laster",
|
"common.loading": "Laster",
|
||||||
"common.loading.ellipsis": "...",
|
"common.loading.ellipsis": "...",
|
||||||
"common.cancel": "Avbryt",
|
"common.cancel": "Avbryt",
|
||||||
@@ -265,7 +266,7 @@ export const dict = {
|
|||||||
|
|
||||||
"prompt.popover.emptyResults": "Ingen matchende resultater",
|
"prompt.popover.emptyResults": "Ingen matchende resultater",
|
||||||
"prompt.popover.emptyCommands": "Ingen matchende kommandoer",
|
"prompt.popover.emptyCommands": "Ingen matchende kommandoer",
|
||||||
"prompt.dropzone.label": "Slipp bilder eller PDF-er her",
|
"prompt.dropzone.label": "Slipp bilder, PDF-er eller tekstfiler her",
|
||||||
"prompt.dropzone.file.label": "Slipp for å @nevne fil",
|
"prompt.dropzone.file.label": "Slipp for å @nevne fil",
|
||||||
"prompt.slash.badge.custom": "egendefinert",
|
"prompt.slash.badge.custom": "egendefinert",
|
||||||
"prompt.slash.badge.skill": "skill",
|
"prompt.slash.badge.skill": "skill",
|
||||||
@@ -279,8 +280,8 @@ export const dict = {
|
|||||||
"prompt.action.send": "Send",
|
"prompt.action.send": "Send",
|
||||||
"prompt.action.stop": "Stopp",
|
"prompt.action.stop": "Stopp",
|
||||||
|
|
||||||
"prompt.toast.pasteUnsupported.title": "Liming ikke støttet",
|
"prompt.toast.pasteUnsupported.title": "Ikke støttet vedlegg",
|
||||||
"prompt.toast.pasteUnsupported.description": "Kun bilder eller PDF-er kan limes inn her.",
|
"prompt.toast.pasteUnsupported.description": "Kun bilder, PDF-er eller tekstfiler kan legges ved her.",
|
||||||
"prompt.toast.modelAgentRequired.title": "Velg en agent og modell",
|
"prompt.toast.modelAgentRequired.title": "Velg en agent og modell",
|
||||||
"prompt.toast.modelAgentRequired.description": "Velg en agent og modell før du sender en forespørsel.",
|
"prompt.toast.modelAgentRequired.description": "Velg en agent og modell før du sender en forespørsel.",
|
||||||
"prompt.toast.worktreeCreateFailed.title": "Kunne ikke opprette worktree",
|
"prompt.toast.worktreeCreateFailed.title": "Kunne ikke opprette worktree",
|
||||||
@@ -317,6 +318,11 @@ export const dict = {
|
|||||||
"dialog.server.add.error": "Kunne ikke koble til server",
|
"dialog.server.add.error": "Kunne ikke koble til server",
|
||||||
"dialog.server.add.checking": "Sjekker...",
|
"dialog.server.add.checking": "Sjekker...",
|
||||||
"dialog.server.add.button": "Legg til server",
|
"dialog.server.add.button": "Legg til server",
|
||||||
|
"dialog.server.add.name": "Servernavn (valgfritt)",
|
||||||
|
"dialog.server.add.namePlaceholder": "Localhost",
|
||||||
|
"dialog.server.add.username": "Brukernavn (valgfritt)",
|
||||||
|
"dialog.server.add.password": "Passord (valgfritt)",
|
||||||
|
"dialog.server.edit.title": "Rediger server",
|
||||||
"dialog.server.default.title": "Standardserver",
|
"dialog.server.default.title": "Standardserver",
|
||||||
"dialog.server.default.description":
|
"dialog.server.default.description":
|
||||||
"Koble til denne serveren ved oppstart i stedet for å starte en lokal server. Krever omstart.",
|
"Koble til denne serveren ved oppstart i stedet for å starte en lokal server. Krever omstart.",
|
||||||
@@ -394,6 +400,7 @@ export const dict = {
|
|||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.bs": "Bosanski",
|
"language.bs": "Bosanski",
|
||||||
"language.th": "ไทย",
|
"language.th": "ไทย",
|
||||||
|
"language.tr": "Türkçe",
|
||||||
|
|
||||||
"toast.language.title": "Språk",
|
"toast.language.title": "Språk",
|
||||||
"toast.language.description": "Byttet til {{language}}",
|
"toast.language.description": "Byttet til {{language}}",
|
||||||
@@ -499,10 +506,14 @@ export const dict = {
|
|||||||
"session.review.change.other": "Endringer",
|
"session.review.change.other": "Endringer",
|
||||||
"session.review.loadingChanges": "Laster endringer...",
|
"session.review.loadingChanges": "Laster endringer...",
|
||||||
"session.review.empty": "Ingen endringer i denne sesjonen ennå",
|
"session.review.empty": "Ingen endringer i denne sesjonen ennå",
|
||||||
|
"session.review.noVcs": "Ingen Git-versjonskontrollsystem oppdaget, endringer vises ikke",
|
||||||
|
"session.review.noSnapshot":
|
||||||
|
"Snapshot-sporing er deaktivert i konfigurasjonen, så sesjonsendringer er ikke tilgjengelige",
|
||||||
"session.review.noChanges": "Ingen endringer",
|
"session.review.noChanges": "Ingen endringer",
|
||||||
|
|
||||||
"session.files.selectToOpen": "Velg en fil å åpne",
|
"session.files.selectToOpen": "Velg en fil å åpne",
|
||||||
"session.files.all": "Alle filer",
|
"session.files.all": "Alle filer",
|
||||||
|
"session.files.empty": "Ingen filer",
|
||||||
"session.files.binaryContent": "Binær fil (innhold kan ikke vises)",
|
"session.files.binaryContent": "Binær fil (innhold kan ikke vises)",
|
||||||
|
|
||||||
"session.messages.renderEarlier": "Vis tidligere meldinger",
|
"session.messages.renderEarlier": "Vis tidligere meldinger",
|
||||||
@@ -515,6 +526,17 @@ export const dict = {
|
|||||||
"session.todo.title": "Oppgaver",
|
"session.todo.title": "Oppgaver",
|
||||||
"session.todo.collapse": "Skjul",
|
"session.todo.collapse": "Skjul",
|
||||||
"session.todo.expand": "Utvid",
|
"session.todo.expand": "Utvid",
|
||||||
|
"session.followupDock.summary.one": "{{count}} melding i kø",
|
||||||
|
"session.followupDock.summary.other": "{{count}} meldinger i kø",
|
||||||
|
"session.followupDock.sendNow": "Send nå",
|
||||||
|
"session.followupDock.edit": "Rediger",
|
||||||
|
"session.followupDock.collapse": "Skjul meldinger i kø",
|
||||||
|
"session.followupDock.expand": "Utvid meldinger i kø",
|
||||||
|
"session.revertDock.summary.one": "{{count}} tilbakestilt melding",
|
||||||
|
"session.revertDock.summary.other": "{{count}} tilbakestilte meldinger",
|
||||||
|
"session.revertDock.collapse": "Skjul tilbakestilte meldinger",
|
||||||
|
"session.revertDock.expand": "Utvid tilbakestilte meldinger",
|
||||||
|
"session.revertDock.restore": "Gjenopprett melding",
|
||||||
|
|
||||||
"session.new.title": "Bygg hva som helst",
|
"session.new.title": "Bygg hva som helst",
|
||||||
"session.new.worktree.main": "Hovedgren",
|
"session.new.worktree.main": "Hovedgren",
|
||||||
@@ -612,11 +634,18 @@ export const dict = {
|
|||||||
"settings.general.row.language.description": "Endre visningsspråket for OpenCode",
|
"settings.general.row.language.description": "Endre visningsspråket for OpenCode",
|
||||||
"settings.general.row.appearance.title": "Utseende",
|
"settings.general.row.appearance.title": "Utseende",
|
||||||
"settings.general.row.appearance.description": "Tilpass hvordan OpenCode ser ut på enheten din",
|
"settings.general.row.appearance.description": "Tilpass hvordan OpenCode ser ut på enheten din",
|
||||||
|
"settings.general.row.colorScheme.title": "Fargevalg",
|
||||||
|
"settings.general.row.colorScheme.description": "Velg om OpenCode skal følge systemets, lyst eller mørkt tema",
|
||||||
"settings.general.row.theme.title": "Tema",
|
"settings.general.row.theme.title": "Tema",
|
||||||
"settings.general.row.theme.description": "Tilpass hvordan OpenCode er tematisert.",
|
"settings.general.row.theme.description": "Tilpass hvordan OpenCode er tematisert.",
|
||||||
"settings.general.row.font.title": "Skrift",
|
"settings.general.row.font.title": "Skrift",
|
||||||
"settings.general.row.font.description": "Tilpass mono-skriften som brukes i kodeblokker",
|
"settings.general.row.font.description": "Tilpass mono-skriften som brukes i kodeblokker",
|
||||||
|
"settings.general.row.followup.title": "Oppfølgingsadferd",
|
||||||
|
"settings.general.row.followup.description": "Velg om oppfølgingsspørsmål skal kjøres umiddelbart eller vente i kø",
|
||||||
|
"settings.general.row.followup.option.queue": "Kø",
|
||||||
|
"settings.general.row.followup.option.steer": "Styr",
|
||||||
|
"settings.general.row.reasoningSummaries.title": "Vis resonneringssammendrag",
|
||||||
|
"settings.general.row.reasoningSummaries.description": "Vis sammendrag av modellresonnering i tidslinjen",
|
||||||
"settings.general.row.shellToolPartsExpanded.title": "Utvid shell-verktøydeler",
|
"settings.general.row.shellToolPartsExpanded.title": "Utvid shell-verktøydeler",
|
||||||
"settings.general.row.shellToolPartsExpanded.description": "Vis shell-verktøydeler utvidet som standard i tidslinjen",
|
"settings.general.row.shellToolPartsExpanded.description": "Vis shell-verktøydeler utvidet som standard i tidslinjen",
|
||||||
"settings.general.row.editToolPartsExpanded.title": "Utvid edit-verktøydeler",
|
"settings.general.row.editToolPartsExpanded.title": "Utvid edit-verktøydeler",
|
||||||
@@ -836,4 +865,79 @@ export const dict = {
|
|||||||
"common.time.daysAgo.short": "{{count}} d siden",
|
"common.time.daysAgo.short": "{{count}} d siden",
|
||||||
"settings.providers.connected.environmentDescription": "Koblet til fra miljøvariablene dine",
|
"settings.providers.connected.environmentDescription": "Koblet til fra miljøvariablene dine",
|
||||||
"settings.providers.custom.description": "Legg til en OpenAI-kompatibel leverandør via basis-URL.",
|
"settings.providers.custom.description": "Legg til en OpenAI-kompatibel leverandør via basis-URL.",
|
||||||
|
|
||||||
|
"app.server.unreachable": "Kunne ikke nå {{server}}",
|
||||||
|
"app.server.retrying": "Prøver på nytt automatisk...",
|
||||||
|
"app.server.otherServers": "Andre servere",
|
||||||
|
"dialog.server.add.usernamePlaceholder": "brukernavn",
|
||||||
|
"dialog.server.add.passwordPlaceholder": "passord",
|
||||||
|
"server.row.noUsername": "inget brukernavn",
|
||||||
|
"session.review.noVcs.createGit.title": "Opprett et Git-depot",
|
||||||
|
"session.review.noVcs.createGit.description": "Spor, gjennomgå og angre endringer i dette prosjektet",
|
||||||
|
"session.review.noVcs.createGit.actionLoading": "Oppretter Git-depot...",
|
||||||
|
"session.review.noVcs.createGit.action": "Opprett Git-depot",
|
||||||
|
"session.todo.progress": "{{done}} av {{total}} oppgaver fullført",
|
||||||
|
"session.question.progress": "{{current}} av {{total}} spørsmål",
|
||||||
|
"session.header.open.finder": "Finder",
|
||||||
|
"session.header.open.fileExplorer": "Filutforsker",
|
||||||
|
"session.header.open.fileManager": "Filbehandler",
|
||||||
|
"session.header.open.app.vscode": "VS Code",
|
||||||
|
"session.header.open.app.cursor": "Cursor",
|
||||||
|
"session.header.open.app.zed": "Zed",
|
||||||
|
"session.header.open.app.textmate": "TextMate",
|
||||||
|
"session.header.open.app.antigravity": "Antigravity",
|
||||||
|
"session.header.open.app.terminal": "Terminal",
|
||||||
|
"session.header.open.app.iterm2": "iTerm2",
|
||||||
|
"session.header.open.app.ghostty": "Ghostty",
|
||||||
|
"session.header.open.app.warp": "Warp",
|
||||||
|
"session.header.open.app.xcode": "Xcode",
|
||||||
|
"session.header.open.app.androidStudio": "Android Studio",
|
||||||
|
"session.header.open.app.powershell": "PowerShell",
|
||||||
|
"session.header.open.app.sublimeText": "Sublime Text",
|
||||||
|
"debugBar.ariaLabel": "Utviklingsytelsesdiagnostikk",
|
||||||
|
"debugBar.na": "i/t",
|
||||||
|
"debugBar.nav.label": "NAV",
|
||||||
|
"debugBar.nav.tip":
|
||||||
|
"Siste fullførte ruteovergang som berører en sesjonsside, målt fra ruterstart til første opptegning etter at den har roet seg.",
|
||||||
|
"debugBar.fps.label": "FPS",
|
||||||
|
"debugBar.fps.tip": "Rullende bilder per sekund over de siste 5 sekundene.",
|
||||||
|
"debugBar.frame.label": "FRAME",
|
||||||
|
"debugBar.frame.tip": "Verste bildetid over de siste 5 sekundene.",
|
||||||
|
"debugBar.jank.label": "JANK",
|
||||||
|
"debugBar.jank.tip": "Bilder over 32ms i de siste 5 sekundene.",
|
||||||
|
"debugBar.long.label": "LONG",
|
||||||
|
"debugBar.long.tip": "Blokkert tid og antall lange oppgaver i de siste 5 sekundene. Maks oppgave: {{max}}.",
|
||||||
|
"debugBar.delay.label": "DELAY",
|
||||||
|
"debugBar.delay.tip": "Verste observerte inndataforsinkelse i de siste 5 sekundene.",
|
||||||
|
"debugBar.inp.label": "INP",
|
||||||
|
"debugBar.inp.tip":
|
||||||
|
"Omtrentlig interaksjonsvarighet over de siste 5 sekundene. Dette er INP-lignende, ikke den offisielle Web Vitals INP.",
|
||||||
|
"debugBar.cls.label": "CLS",
|
||||||
|
"debugBar.cls.tip": "Kumulativ layoutforskyvning for gjeldende app-levetid.",
|
||||||
|
"debugBar.mem.label": "MEM",
|
||||||
|
"debugBar.mem.tipUnavailable": "Brukt JS-heap vs heap-grense. Kun Chromium.",
|
||||||
|
"debugBar.mem.tip": "Brukt JS-heap vs heap-grense. {{used}} av {{limit}}.",
|
||||||
|
"common.key.ctrl": "Ctrl",
|
||||||
|
"common.key.alt": "Alt",
|
||||||
|
"common.key.shift": "Shift",
|
||||||
|
"common.key.meta": "Meta",
|
||||||
|
"common.key.space": "Mellomrom",
|
||||||
|
"common.key.backspace": "Backspace",
|
||||||
|
"common.key.enter": "Enter",
|
||||||
|
"common.key.tab": "Tab",
|
||||||
|
"common.key.delete": "Delete",
|
||||||
|
"common.key.home": "Home",
|
||||||
|
"common.key.end": "End",
|
||||||
|
"common.key.pageUp": "Page Up",
|
||||||
|
"common.key.pageDown": "Page Down",
|
||||||
|
"common.key.insert": "Insert",
|
||||||
|
"common.unknown": "ukjent",
|
||||||
|
"error.page.circular": "[Sirkulær]",
|
||||||
|
"error.globalSDK.noServerAvailable": "Ingen server tilgjengelig",
|
||||||
|
"error.globalSDK.serverNotAvailable": "Server ikke tilgjengelig",
|
||||||
|
"error.childStore.persistedCacheCreateFailed": "Kunne ikke opprette vedvarende hurtigbuffer",
|
||||||
|
"error.childStore.persistedProjectMetadataCreateFailed": "Kunne ikke opprette vedvarende prosjektmetadata",
|
||||||
|
"error.childStore.persistedProjectIconCreateFailed": "Kunne ikke opprette vedvarende prosjektikon",
|
||||||
|
"error.childStore.storeCreateFailed": "Kunne ikke opprette lager",
|
||||||
|
"terminal.connectionLost.abnormalClose": "WebSocket lukket unormalt: {{code}}",
|
||||||
} satisfies Partial<Record<Keys, string>>
|
} satisfies Partial<Record<Keys, string>>
|
||||||
|
|||||||
+109
-3
@@ -104,6 +104,7 @@ export const dict = {
|
|||||||
"dialog.model.empty": "Brak wyników modelu",
|
"dialog.model.empty": "Brak wyników modelu",
|
||||||
"dialog.model.manage": "Zarządzaj modelami",
|
"dialog.model.manage": "Zarządzaj modelami",
|
||||||
"dialog.model.manage.description": "Dostosuj, które modele pojawiają się w wyborze modelu.",
|
"dialog.model.manage.description": "Dostosuj, które modele pojawiają się w wyborze modelu.",
|
||||||
|
"dialog.model.manage.provider.toggle": "Przełącz wszystkie modele {{provider}}",
|
||||||
"dialog.model.unpaid.freeModels.title": "Darmowe modele dostarczane przez OpenCode",
|
"dialog.model.unpaid.freeModels.title": "Darmowe modele dostarczane przez OpenCode",
|
||||||
"dialog.model.unpaid.addMore.title": "Dodaj więcej modeli od popularnych dostawców",
|
"dialog.model.unpaid.addMore.title": "Dodaj więcej modeli od popularnych dostawców",
|
||||||
"dialog.provider.viewAll": "Zobacz więcej dostawców",
|
"dialog.provider.viewAll": "Zobacz więcej dostawców",
|
||||||
@@ -244,7 +245,7 @@ export const dict = {
|
|||||||
"prompt.example.25": "Jak działają tutaj zmienne środowiskowe?",
|
"prompt.example.25": "Jak działają tutaj zmienne środowiskowe?",
|
||||||
"prompt.popover.emptyResults": "Brak pasujących wyników",
|
"prompt.popover.emptyResults": "Brak pasujących wyników",
|
||||||
"prompt.popover.emptyCommands": "Brak pasujących poleceń",
|
"prompt.popover.emptyCommands": "Brak pasujących poleceń",
|
||||||
"prompt.dropzone.label": "Upuść obrazy lub pliki PDF tutaj",
|
"prompt.dropzone.label": "Upuść tutaj obrazy, pliki PDF lub pliki tekstowe",
|
||||||
"prompt.dropzone.file.label": "Upuść, aby @wspomnieć plik",
|
"prompt.dropzone.file.label": "Upuść, aby @wspomnieć plik",
|
||||||
"prompt.slash.badge.custom": "własne",
|
"prompt.slash.badge.custom": "własne",
|
||||||
"prompt.slash.badge.skill": "skill",
|
"prompt.slash.badge.skill": "skill",
|
||||||
@@ -257,8 +258,8 @@ export const dict = {
|
|||||||
"prompt.attachment.remove": "Usuń załącznik",
|
"prompt.attachment.remove": "Usuń załącznik",
|
||||||
"prompt.action.send": "Wyślij",
|
"prompt.action.send": "Wyślij",
|
||||||
"prompt.action.stop": "Zatrzymaj",
|
"prompt.action.stop": "Zatrzymaj",
|
||||||
"prompt.toast.pasteUnsupported.title": "Nieobsługiwane wklejanie",
|
"prompt.toast.pasteUnsupported.title": "Nieobsługiwany załącznik",
|
||||||
"prompt.toast.pasteUnsupported.description": "Tylko obrazy lub pliki PDF mogą być tutaj wklejane.",
|
"prompt.toast.pasteUnsupported.description": "Można tutaj załączać tylko obrazy, pliki PDF lub pliki tekstowe.",
|
||||||
"prompt.toast.modelAgentRequired.title": "Wybierz agenta i model",
|
"prompt.toast.modelAgentRequired.title": "Wybierz agenta i model",
|
||||||
"prompt.toast.modelAgentRequired.description": "Wybierz agenta i model przed wysłaniem zapytania.",
|
"prompt.toast.modelAgentRequired.description": "Wybierz agenta i model przed wysłaniem zapytania.",
|
||||||
"prompt.toast.worktreeCreateFailed.title": "Nie udało się utworzyć drzewa roboczego",
|
"prompt.toast.worktreeCreateFailed.title": "Nie udało się utworzyć drzewa roboczego",
|
||||||
@@ -289,6 +290,11 @@ export const dict = {
|
|||||||
"dialog.server.add.error": "Nie można połączyć się z serwerem",
|
"dialog.server.add.error": "Nie można połączyć się z serwerem",
|
||||||
"dialog.server.add.checking": "Sprawdzanie...",
|
"dialog.server.add.checking": "Sprawdzanie...",
|
||||||
"dialog.server.add.button": "Dodaj serwer",
|
"dialog.server.add.button": "Dodaj serwer",
|
||||||
|
"dialog.server.add.name": "Nazwa serwera (opcjonalnie)",
|
||||||
|
"dialog.server.add.namePlaceholder": "Localhost",
|
||||||
|
"dialog.server.add.username": "Nazwa użytkownika (opcjonalnie)",
|
||||||
|
"dialog.server.add.password": "Hasło (opcjonalnie)",
|
||||||
|
"dialog.server.edit.title": "Edytuj serwer",
|
||||||
"dialog.server.default.title": "Domyślny serwer",
|
"dialog.server.default.title": "Domyślny serwer",
|
||||||
"dialog.server.default.description":
|
"dialog.server.default.description":
|
||||||
"Połącz z tym serwerem przy uruchomieniu aplikacji zamiast uruchamiać lokalny serwer. Wymaga restartu.",
|
"Połącz z tym serwerem przy uruchomieniu aplikacji zamiast uruchamiać lokalny serwer. Wymaga restartu.",
|
||||||
@@ -359,6 +365,7 @@ export const dict = {
|
|||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.bs": "Bosanski",
|
"language.bs": "Bosanski",
|
||||||
"language.th": "ไทย",
|
"language.th": "ไทย",
|
||||||
|
"language.tr": "Türkçe",
|
||||||
"toast.language.title": "Język",
|
"toast.language.title": "Język",
|
||||||
"toast.language.description": "Przełączono na {{language}}",
|
"toast.language.description": "Przełączono na {{language}}",
|
||||||
"toast.theme.title": "Przełączono motyw",
|
"toast.theme.title": "Przełączono motyw",
|
||||||
@@ -445,9 +452,12 @@ export const dict = {
|
|||||||
"session.review.change.other": "Zmiany",
|
"session.review.change.other": "Zmiany",
|
||||||
"session.review.loadingChanges": "Ładowanie zmian...",
|
"session.review.loadingChanges": "Ładowanie zmian...",
|
||||||
"session.review.empty": "Brak zmian w tej sesji",
|
"session.review.empty": "Brak zmian w tej sesji",
|
||||||
|
"session.review.noVcs": "Nie wykryto systemu kontroli wersji Git, zmiany nie są wyświetlane",
|
||||||
|
"session.review.noSnapshot": "Śledzenie migawek jest wyłączone w konfiguracji, więc zmiany w sesji są niedostępne",
|
||||||
"session.review.noChanges": "Brak zmian",
|
"session.review.noChanges": "Brak zmian",
|
||||||
"session.files.selectToOpen": "Wybierz plik do otwarcia",
|
"session.files.selectToOpen": "Wybierz plik do otwarcia",
|
||||||
"session.files.all": "Wszystkie pliki",
|
"session.files.all": "Wszystkie pliki",
|
||||||
|
"session.files.empty": "Brak plików",
|
||||||
"session.files.binaryContent": "Plik binarny (zawartość nie może być wyświetlona)",
|
"session.files.binaryContent": "Plik binarny (zawartość nie może być wyświetlona)",
|
||||||
"session.messages.renderEarlier": "Renderuj wcześniejsze wiadomości",
|
"session.messages.renderEarlier": "Renderuj wcześniejsze wiadomości",
|
||||||
"session.messages.loadingEarlier": "Ładowanie wcześniejszych wiadomości...",
|
"session.messages.loadingEarlier": "Ładowanie wcześniejszych wiadomości...",
|
||||||
@@ -458,6 +468,17 @@ export const dict = {
|
|||||||
"session.todo.title": "Zadania",
|
"session.todo.title": "Zadania",
|
||||||
"session.todo.collapse": "Zwiń",
|
"session.todo.collapse": "Zwiń",
|
||||||
"session.todo.expand": "Rozwiń",
|
"session.todo.expand": "Rozwiń",
|
||||||
|
"session.followupDock.summary.one": "{{count}} wiadomość w kolejce",
|
||||||
|
"session.followupDock.summary.other": "{{count}} wiadomości w kolejce",
|
||||||
|
"session.followupDock.sendNow": "Wyślij teraz",
|
||||||
|
"session.followupDock.edit": "Edytuj",
|
||||||
|
"session.followupDock.collapse": "Zwiń wiadomości w kolejce",
|
||||||
|
"session.followupDock.expand": "Rozwiń wiadomości w kolejce",
|
||||||
|
"session.revertDock.summary.one": "{{count}} cofnięta wiadomość",
|
||||||
|
"session.revertDock.summary.other": "{{count}} cofnięte wiadomości",
|
||||||
|
"session.revertDock.collapse": "Zwiń cofnięte wiadomości",
|
||||||
|
"session.revertDock.expand": "Rozwiń cofnięte wiadomości",
|
||||||
|
"session.revertDock.restore": "Przywróć wiadomość",
|
||||||
"session.new.title": "Zbuduj cokolwiek",
|
"session.new.title": "Zbuduj cokolwiek",
|
||||||
"session.new.worktree.main": "Główna gałąź",
|
"session.new.worktree.main": "Główna gałąź",
|
||||||
"session.new.worktree.mainWithBranch": "Główna gałąź ({{branch}})",
|
"session.new.worktree.mainWithBranch": "Główna gałąź ({{branch}})",
|
||||||
@@ -543,10 +564,19 @@ export const dict = {
|
|||||||
"settings.general.row.language.description": "Zmień język wyświetlania dla OpenCode",
|
"settings.general.row.language.description": "Zmień język wyświetlania dla OpenCode",
|
||||||
"settings.general.row.appearance.title": "Wygląd",
|
"settings.general.row.appearance.title": "Wygląd",
|
||||||
"settings.general.row.appearance.description": "Dostosuj wygląd OpenCode na swoim urządzeniu",
|
"settings.general.row.appearance.description": "Dostosuj wygląd OpenCode na swoim urządzeniu",
|
||||||
|
"settings.general.row.colorScheme.title": "Schemat kolorów",
|
||||||
|
"settings.general.row.colorScheme.description":
|
||||||
|
"Wybierz, czy OpenCode ma używać motywu systemowego, jasnego czy ciemnego",
|
||||||
"settings.general.row.theme.title": "Motyw",
|
"settings.general.row.theme.title": "Motyw",
|
||||||
"settings.general.row.theme.description": "Dostosuj motyw OpenCode.",
|
"settings.general.row.theme.description": "Dostosuj motyw OpenCode.",
|
||||||
"settings.general.row.font.title": "Czcionka",
|
"settings.general.row.font.title": "Czcionka",
|
||||||
"settings.general.row.font.description": "Dostosuj czcionkę mono używaną w blokach kodu",
|
"settings.general.row.font.description": "Dostosuj czcionkę mono używaną w blokach kodu",
|
||||||
|
"settings.general.row.followup.title": "Zachowanie kontynuacji",
|
||||||
|
"settings.general.row.followup.description": "Wybierz, czy kontynuacja ma być natychmiastowa, czy czekać w kolejce",
|
||||||
|
"settings.general.row.followup.option.queue": "Kolejka",
|
||||||
|
"settings.general.row.followup.option.steer": "Sterowanie",
|
||||||
|
"settings.general.row.reasoningSummaries.title": "Pokaż podsumowania wnioskowania",
|
||||||
|
"settings.general.row.reasoningSummaries.description": "Wyświetlaj podsumowania wnioskowania modelu na osi czasu",
|
||||||
"settings.general.row.shellToolPartsExpanded.title": "Rozwijaj elementy narzędzia shell",
|
"settings.general.row.shellToolPartsExpanded.title": "Rozwijaj elementy narzędzia shell",
|
||||||
"settings.general.row.shellToolPartsExpanded.description":
|
"settings.general.row.shellToolPartsExpanded.description":
|
||||||
"Domyślnie pokazuj rozwinięte elementy narzędzia shell na osi czasu",
|
"Domyślnie pokazuj rozwinięte elementy narzędzia shell na osi czasu",
|
||||||
@@ -755,4 +785,80 @@ export const dict = {
|
|||||||
"common.time.daysAgo.short": "{{count}} dni temu",
|
"common.time.daysAgo.short": "{{count}} dni temu",
|
||||||
"settings.providers.connected.environmentDescription": "Połączono ze zmiennymi środowiskowymi",
|
"settings.providers.connected.environmentDescription": "Połączono ze zmiennymi środowiskowymi",
|
||||||
"settings.providers.custom.description": "Dodaj dostawcę zgodnego z OpenAI poprzez podstawowy URL.",
|
"settings.providers.custom.description": "Dodaj dostawcę zgodnego z OpenAI poprzez podstawowy URL.",
|
||||||
|
|
||||||
|
"app.server.unreachable": "Nie można połączyć z {{server}}",
|
||||||
|
"app.server.retrying": "Ponawianie automatycznie...",
|
||||||
|
"app.server.otherServers": "Inne serwery",
|
||||||
|
"dialog.server.add.usernamePlaceholder": "nazwa użytkownika",
|
||||||
|
"dialog.server.add.passwordPlaceholder": "hasło",
|
||||||
|
"server.row.noUsername": "brak nazwy użytkownika",
|
||||||
|
"session.review.noVcs.createGit.title": "Utwórz repozytorium Git",
|
||||||
|
"session.review.noVcs.createGit.description": "Śledź, przeglądaj i cofaj zmiany w tym projekcie",
|
||||||
|
"session.review.noVcs.createGit.actionLoading": "Tworzenie repozytorium Git...",
|
||||||
|
"session.review.noVcs.createGit.action": "Utwórz repozytorium Git",
|
||||||
|
"session.todo.progress": "Ukończono {{done}} z {{total}} zadań",
|
||||||
|
"session.question.progress": "{{current}} z {{total}} pytań",
|
||||||
|
"session.header.open.finder": "Finder",
|
||||||
|
"session.header.open.fileExplorer": "Eksplorator plików",
|
||||||
|
"session.header.open.fileManager": "Menedżer plików",
|
||||||
|
"session.header.open.app.vscode": "VS Code",
|
||||||
|
"session.header.open.app.cursor": "Cursor",
|
||||||
|
"session.header.open.app.zed": "Zed",
|
||||||
|
"session.header.open.app.textmate": "TextMate",
|
||||||
|
"session.header.open.app.antigravity": "Antigravity",
|
||||||
|
"session.header.open.app.terminal": "Terminal",
|
||||||
|
"session.header.open.app.iterm2": "iTerm2",
|
||||||
|
"session.header.open.app.ghostty": "Ghostty",
|
||||||
|
"session.header.open.app.warp": "Warp",
|
||||||
|
"session.header.open.app.xcode": "Xcode",
|
||||||
|
"session.header.open.app.androidStudio": "Android Studio",
|
||||||
|
"session.header.open.app.powershell": "PowerShell",
|
||||||
|
"session.header.open.app.sublimeText": "Sublime Text",
|
||||||
|
"debugBar.ariaLabel": "Diagnostyka wydajności deweloperskiej",
|
||||||
|
"debugBar.na": "n.d.",
|
||||||
|
"debugBar.nav.label": "NAV",
|
||||||
|
"debugBar.nav.tip":
|
||||||
|
"Ostatnie zakończone przejście trasy dotykające strony sesji, mierzone od startu routera do pierwszego odrysowania po ustaleniu.",
|
||||||
|
"debugBar.fps.label": "FPS",
|
||||||
|
"debugBar.fps.tip": "Średnia liczba klatek na sekundę w ciągu ostatnich 5 sekund.",
|
||||||
|
"debugBar.frame.label": "FRAME",
|
||||||
|
"debugBar.frame.tip": "Najgorszy czas klatki w ciągu ostatnich 5 sekund.",
|
||||||
|
"debugBar.jank.label": "JANK",
|
||||||
|
"debugBar.jank.tip": "Klatki powyżej 32ms w ciągu ostatnich 5 sekund.",
|
||||||
|
"debugBar.long.label": "LONG",
|
||||||
|
"debugBar.long.tip":
|
||||||
|
"Zablokowany czas i liczba długich zadań w ciągu ostatnich 5 sekund. Maksymalne zadanie: {{max}}.",
|
||||||
|
"debugBar.delay.label": "DELAY",
|
||||||
|
"debugBar.delay.tip": "Najgorsze zaobserwowane opóźnienie wejścia w ciągu ostatnich 5 sekund.",
|
||||||
|
"debugBar.inp.label": "INP",
|
||||||
|
"debugBar.inp.tip":
|
||||||
|
"Przybliżony czas trwania interakcji w ciągu ostatnich 5 sekund. Jest to podobne do INP, a nie oficjalne Web Vitals INP.",
|
||||||
|
"debugBar.cls.label": "CLS",
|
||||||
|
"debugBar.cls.tip": "Skumulowane przesunięcie układu dla bieżącego czasu życia aplikacji.",
|
||||||
|
"debugBar.mem.label": "MEM",
|
||||||
|
"debugBar.mem.tipUnavailable": "Użyta sterta JS vs limit sterty. Tylko Chromium.",
|
||||||
|
"debugBar.mem.tip": "Użyta sterta JS vs limit sterty. {{used}} z {{limit}}.",
|
||||||
|
"common.key.ctrl": "Ctrl",
|
||||||
|
"common.key.alt": "Alt",
|
||||||
|
"common.key.shift": "Shift",
|
||||||
|
"common.key.meta": "Meta",
|
||||||
|
"common.key.space": "Spacja",
|
||||||
|
"common.key.backspace": "Backspace",
|
||||||
|
"common.key.enter": "Enter",
|
||||||
|
"common.key.tab": "Tab",
|
||||||
|
"common.key.delete": "Delete",
|
||||||
|
"common.key.home": "Home",
|
||||||
|
"common.key.end": "End",
|
||||||
|
"common.key.pageUp": "Page Up",
|
||||||
|
"common.key.pageDown": "Page Down",
|
||||||
|
"common.key.insert": "Insert",
|
||||||
|
"common.unknown": "nieznany",
|
||||||
|
"error.page.circular": "[Cykliczne]",
|
||||||
|
"error.globalSDK.noServerAvailable": "Brak dostępnego serwera",
|
||||||
|
"error.globalSDK.serverNotAvailable": "Serwer niedostępny",
|
||||||
|
"error.childStore.persistedCacheCreateFailed": "Nie udało się utworzyć trwałej pamięci podręcznej",
|
||||||
|
"error.childStore.persistedProjectMetadataCreateFailed": "Nie udało się utworzyć trwałych metadanych projektu",
|
||||||
|
"error.childStore.persistedProjectIconCreateFailed": "Nie udało się utworzyć trwałej ikony projektu",
|
||||||
|
"error.childStore.storeCreateFailed": "Nie udało się utworzyć magazynu",
|
||||||
|
"terminal.connectionLost.abnormalClose": "WebSocket zamknięty nieprawidłowo: {{code}}",
|
||||||
}
|
}
|
||||||
|
|||||||
+125
-19
@@ -113,6 +113,7 @@ export const dict = {
|
|||||||
"dialog.model.empty": "Модели не найдены",
|
"dialog.model.empty": "Модели не найдены",
|
||||||
"dialog.model.manage": "Управление моделями",
|
"dialog.model.manage": "Управление моделями",
|
||||||
"dialog.model.manage.description": "Настройте какие модели появляются в выборе модели",
|
"dialog.model.manage.description": "Настройте какие модели появляются в выборе модели",
|
||||||
|
"dialog.model.manage.provider.toggle": "Переключить все модели {{provider}}",
|
||||||
|
|
||||||
"dialog.model.unpaid.freeModels.title": "Бесплатные модели от OpenCode",
|
"dialog.model.unpaid.freeModels.title": "Бесплатные модели от OpenCode",
|
||||||
"dialog.model.unpaid.addMore.title": "Добавьте больше моделей от популярных провайдеров",
|
"dialog.model.unpaid.addMore.title": "Добавьте больше моделей от популярных провайдеров",
|
||||||
@@ -262,7 +263,7 @@ export const dict = {
|
|||||||
|
|
||||||
"prompt.popover.emptyResults": "Нет совпадений",
|
"prompt.popover.emptyResults": "Нет совпадений",
|
||||||
"prompt.popover.emptyCommands": "Нет совпадающих команд",
|
"prompt.popover.emptyCommands": "Нет совпадающих команд",
|
||||||
"prompt.dropzone.label": "Перетащите изображения или PDF сюда",
|
"prompt.dropzone.label": "Перетащите сюда изображения, PDF или текстовые файлы",
|
||||||
"prompt.dropzone.file.label": "Отпустите для @упоминания файла",
|
"prompt.dropzone.file.label": "Отпустите для @упоминания файла",
|
||||||
"prompt.slash.badge.custom": "своё",
|
"prompt.slash.badge.custom": "своё",
|
||||||
"prompt.slash.badge.skill": "навык",
|
"prompt.slash.badge.skill": "навык",
|
||||||
@@ -276,8 +277,8 @@ export const dict = {
|
|||||||
"prompt.action.send": "Отправить",
|
"prompt.action.send": "Отправить",
|
||||||
"prompt.action.stop": "Остановить",
|
"prompt.action.stop": "Остановить",
|
||||||
|
|
||||||
"prompt.toast.pasteUnsupported.title": "Неподдерживаемая вставка",
|
"prompt.toast.pasteUnsupported.title": "Неподдерживаемое вложение",
|
||||||
"prompt.toast.pasteUnsupported.description": "Сюда можно вставлять только изображения или PDF.",
|
"prompt.toast.pasteUnsupported.description": "Здесь можно прикрепить только изображения, PDF или текстовые файлы.",
|
||||||
"prompt.toast.modelAgentRequired.title": "Выберите агента и модель",
|
"prompt.toast.modelAgentRequired.title": "Выберите агента и модель",
|
||||||
"prompt.toast.modelAgentRequired.description": "Выберите агента и модель перед отправкой запроса.",
|
"prompt.toast.modelAgentRequired.description": "Выберите агента и модель перед отправкой запроса.",
|
||||||
"prompt.toast.worktreeCreateFailed.title": "Не удалось создать worktree",
|
"prompt.toast.worktreeCreateFailed.title": "Не удалось создать worktree",
|
||||||
@@ -314,6 +315,11 @@ export const dict = {
|
|||||||
"dialog.server.add.error": "Не удалось подключиться к серверу",
|
"dialog.server.add.error": "Не удалось подключиться к серверу",
|
||||||
"dialog.server.add.checking": "Проверка...",
|
"dialog.server.add.checking": "Проверка...",
|
||||||
"dialog.server.add.button": "Добавить сервер",
|
"dialog.server.add.button": "Добавить сервер",
|
||||||
|
"dialog.server.add.name": "Имя сервера (необязательно)",
|
||||||
|
"dialog.server.add.namePlaceholder": "Localhost",
|
||||||
|
"dialog.server.add.username": "Имя пользователя (необязательно)",
|
||||||
|
"dialog.server.add.password": "Пароль (необязательно)",
|
||||||
|
"dialog.server.edit.title": "Редактировать сервер",
|
||||||
"dialog.server.default.title": "Сервер по умолчанию",
|
"dialog.server.default.title": "Сервер по умолчанию",
|
||||||
"dialog.server.default.description":
|
"dialog.server.default.description":
|
||||||
"Подключаться к этому серверу при запуске приложения вместо запуска локального сервера. Требуется перезапуск.",
|
"Подключаться к этому серверу при запуске приложения вместо запуска локального сервера. Требуется перезапуск.",
|
||||||
@@ -393,6 +399,7 @@ export const dict = {
|
|||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.bs": "Bosanski",
|
"language.bs": "Bosanski",
|
||||||
"language.th": "ไทย",
|
"language.th": "ไทย",
|
||||||
|
"language.tr": "Türkçe",
|
||||||
|
|
||||||
"toast.language.title": "Язык",
|
"toast.language.title": "Язык",
|
||||||
"toast.language.description": "Переключено на {{language}}",
|
"toast.language.description": "Переключено на {{language}}",
|
||||||
@@ -499,9 +506,12 @@ export const dict = {
|
|||||||
"session.review.change.other": "Изменения",
|
"session.review.change.other": "Изменения",
|
||||||
"session.review.loadingChanges": "Загрузка изменений...",
|
"session.review.loadingChanges": "Загрузка изменений...",
|
||||||
"session.review.empty": "Изменений в этой сессии пока нет",
|
"session.review.empty": "Изменений в этой сессии пока нет",
|
||||||
|
"session.review.noVcs": "Система контроля версий Git не обнаружена, изменения не отображаются",
|
||||||
|
"session.review.noSnapshot": "Отслеживание снимков отключено в настройках, поэтому изменения сессии недоступны",
|
||||||
"session.review.noChanges": "Нет изменений",
|
"session.review.noChanges": "Нет изменений",
|
||||||
"session.files.selectToOpen": "Выберите файл, чтобы открыть",
|
"session.files.selectToOpen": "Выберите файл, чтобы открыть",
|
||||||
"session.files.all": "Все файлы",
|
"session.files.all": "Все файлы",
|
||||||
|
"session.files.empty": "Нет файлов",
|
||||||
"session.files.binaryContent": "Двоичный файл (содержимое не может быть отображено)",
|
"session.files.binaryContent": "Двоичный файл (содержимое не может быть отображено)",
|
||||||
"session.messages.renderEarlier": "Показать предыдущие сообщения",
|
"session.messages.renderEarlier": "Показать предыдущие сообщения",
|
||||||
"session.messages.loadingEarlier": "Загрузка предыдущих сообщений...",
|
"session.messages.loadingEarlier": "Загрузка предыдущих сообщений...",
|
||||||
@@ -513,6 +523,17 @@ export const dict = {
|
|||||||
"session.todo.title": "Задачи",
|
"session.todo.title": "Задачи",
|
||||||
"session.todo.collapse": "Свернуть",
|
"session.todo.collapse": "Свернуть",
|
||||||
"session.todo.expand": "Развернуть",
|
"session.todo.expand": "Развернуть",
|
||||||
|
"session.followupDock.summary.one": "{{count}} сообщение в очереди",
|
||||||
|
"session.followupDock.summary.other": "{{count}} сообщений в очереди",
|
||||||
|
"session.followupDock.sendNow": "Отправить сейчас",
|
||||||
|
"session.followupDock.edit": "Редактировать",
|
||||||
|
"session.followupDock.collapse": "Свернуть сообщения в очереди",
|
||||||
|
"session.followupDock.expand": "Развернуть сообщения в очереди",
|
||||||
|
"session.revertDock.summary.one": "{{count}} сообщение возвращено",
|
||||||
|
"session.revertDock.summary.other": "{{count}} сообщений возвращено",
|
||||||
|
"session.revertDock.collapse": "Свернуть возвращённые сообщения",
|
||||||
|
"session.revertDock.expand": "Развернуть возвращённые сообщения",
|
||||||
|
"session.revertDock.restore": "Восстановить сообщение",
|
||||||
|
|
||||||
"session.new.title": "Создавайте что угодно",
|
"session.new.title": "Создавайте что угодно",
|
||||||
"session.new.worktree.main": "Основная ветка",
|
"session.new.worktree.main": "Основная ветка",
|
||||||
@@ -610,10 +631,19 @@ export const dict = {
|
|||||||
"settings.general.row.language.description": "Изменить язык отображения OpenCode",
|
"settings.general.row.language.description": "Изменить язык отображения OpenCode",
|
||||||
"settings.general.row.appearance.title": "Внешний вид",
|
"settings.general.row.appearance.title": "Внешний вид",
|
||||||
"settings.general.row.appearance.description": "Настройте как OpenCode выглядит на вашем устройстве",
|
"settings.general.row.appearance.description": "Настройте как OpenCode выглядит на вашем устройстве",
|
||||||
|
"settings.general.row.colorScheme.title": "Цветовая схема",
|
||||||
|
"settings.general.row.colorScheme.description": "Выберите, следует ли OpenCode системной, светлой или тёмной теме",
|
||||||
"settings.general.row.theme.title": "Тема",
|
"settings.general.row.theme.title": "Тема",
|
||||||
"settings.general.row.theme.description": "Настройте оформление OpenCode.",
|
"settings.general.row.theme.description": "Настройте оформление OpenCode.",
|
||||||
"settings.general.row.font.title": "Шрифт",
|
"settings.general.row.font.title": "Шрифт",
|
||||||
"settings.general.row.font.description": "Настройте моноширинный шрифт для блоков кода",
|
"settings.general.row.font.description": "Настройте моноширинный шрифт для блоков кода",
|
||||||
|
"settings.general.row.followup.title": "Поведение уточняющих вопросов",
|
||||||
|
"settings.general.row.followup.description":
|
||||||
|
"Выберите, отправлять ли уточняющие вопросы сразу или помещать их в очередь",
|
||||||
|
"settings.general.row.followup.option.queue": "Очередь",
|
||||||
|
"settings.general.row.followup.option.steer": "Направлять",
|
||||||
|
"settings.general.row.reasoningSummaries.title": "Показывать сводки рассуждений",
|
||||||
|
"settings.general.row.reasoningSummaries.description": "Отображать сводки рассуждений модели в ленте",
|
||||||
|
|
||||||
"settings.general.row.shellToolPartsExpanded.title": "Разворачивать элементы инструмента shell",
|
"settings.general.row.shellToolPartsExpanded.title": "Разворачивать элементы инструмента shell",
|
||||||
"settings.general.row.shellToolPartsExpanded.description":
|
"settings.general.row.shellToolPartsExpanded.description":
|
||||||
@@ -767,30 +797,31 @@ export const dict = {
|
|||||||
"settings.permissions.tool.glob.description": "Сопоставление файлов по паттернам glob",
|
"settings.permissions.tool.glob.description": "Сопоставление файлов по паттернам glob",
|
||||||
"settings.permissions.tool.grep.title": "Grep",
|
"settings.permissions.tool.grep.title": "Grep",
|
||||||
"settings.permissions.tool.grep.description": "Поиск по содержимому файлов с использованием регулярных выражений",
|
"settings.permissions.tool.grep.description": "Поиск по содержимому файлов с использованием регулярных выражений",
|
||||||
"settings.permissions.tool.list.title": "Список",
|
"settings.permissions.tool.list.title": "List",
|
||||||
"settings.permissions.tool.list.description": "Список файлов в директории",
|
"settings.permissions.tool.list.description": "Список файлов в директории",
|
||||||
"settings.permissions.tool.bash.title": "Bash",
|
"settings.permissions.tool.bash.title": "Bash",
|
||||||
"settings.permissions.tool.bash.description": "Выполнение команд оболочки",
|
"settings.permissions.tool.bash.description": "Запуск команд оболочки",
|
||||||
"settings.permissions.tool.task.title": "Task",
|
"settings.permissions.tool.task.title": "Task",
|
||||||
"settings.permissions.tool.task.description": "Запуск под-агентов",
|
"settings.permissions.tool.task.description": "Запуск подагентов",
|
||||||
"settings.permissions.tool.skill.title": "Skill",
|
"settings.permissions.tool.skill.title": "Skill",
|
||||||
"settings.permissions.tool.skill.description": "Загрузить навык по имени",
|
"settings.permissions.tool.skill.description": "Загрузка навыка по имени",
|
||||||
"settings.permissions.tool.lsp.title": "LSP",
|
"settings.permissions.tool.lsp.title": "LSP",
|
||||||
"settings.permissions.tool.lsp.description": "Выполнение запросов к языковому серверу",
|
"settings.permissions.tool.lsp.description": "Запросы к языковому серверу",
|
||||||
"settings.permissions.tool.todoread.title": "Чтение списка задач",
|
"settings.permissions.tool.todoread.title": "Todo Read",
|
||||||
"settings.permissions.tool.todoread.description": "Чтение списка задач",
|
"settings.permissions.tool.todoread.description": "Чтение списка задач",
|
||||||
"settings.permissions.tool.todowrite.title": "Запись списка задач",
|
"settings.permissions.tool.todowrite.title": "Todo Write",
|
||||||
"settings.permissions.tool.todowrite.description": "Обновление списка задач",
|
"settings.permissions.tool.todowrite.description": "Обновление списка задач",
|
||||||
"settings.permissions.tool.webfetch.title": "Web Fetch",
|
"settings.permissions.tool.webfetch.title": "Web Fetch",
|
||||||
"settings.permissions.tool.webfetch.description": "Получить содержимое по URL",
|
"settings.permissions.tool.webfetch.description": "Получение контента по URL",
|
||||||
"settings.permissions.tool.websearch.title": "Web Search",
|
"settings.permissions.tool.websearch.title": "Web Search",
|
||||||
"settings.permissions.tool.websearch.description": "Поиск в интернете",
|
"settings.permissions.tool.websearch.description": "Поиск в интернете",
|
||||||
"settings.permissions.tool.codesearch.title": "Поиск кода",
|
"settings.permissions.tool.codesearch.title": "Code Search",
|
||||||
"settings.permissions.tool.codesearch.description": "Поиск кода в интернете",
|
"settings.permissions.tool.codesearch.description": "Поиск кода в интернете",
|
||||||
"settings.permissions.tool.external_directory.title": "Внешняя директория",
|
"settings.permissions.tool.external_directory.title": "Внешняя директория",
|
||||||
"settings.permissions.tool.external_directory.description": "Доступ к файлам вне директории проекта",
|
"settings.permissions.tool.external_directory.description": "Доступ к файлам вне директории проекта",
|
||||||
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
"settings.permissions.tool.doom_loop.title": "Doom Loop",
|
||||||
"settings.permissions.tool.doom_loop.description": "Обнаружение повторных вызовов инструментов с одинаковым вводом",
|
"settings.permissions.tool.doom_loop.description":
|
||||||
|
"Обнаружение повторяющихся вызовов инструментов с одинаковыми входными данными",
|
||||||
|
|
||||||
"session.delete.failed.title": "Не удалось удалить сессию",
|
"session.delete.failed.title": "Не удалось удалить сессию",
|
||||||
"session.delete.title": "Удалить сессию",
|
"session.delete.title": "Удалить сессию",
|
||||||
@@ -807,21 +838,21 @@ export const dict = {
|
|||||||
"workspace.reset.failed.title": "Не удалось сбросить рабочее пространство",
|
"workspace.reset.failed.title": "Не удалось сбросить рабочее пространство",
|
||||||
"workspace.reset.success.title": "Рабочее пространство сброшено",
|
"workspace.reset.success.title": "Рабочее пространство сброшено",
|
||||||
"workspace.reset.success.description": "Рабочее пространство теперь соответствует ветке по умолчанию.",
|
"workspace.reset.success.description": "Рабочее пространство теперь соответствует ветке по умолчанию.",
|
||||||
"workspace.error.stillPreparing": "Рабочее пространство всё ещё готовится",
|
"workspace.error.stillPreparing": "Рабочее пространство всё ещё подготавливается",
|
||||||
"workspace.status.checking": "Проверка наличия неслитых изменений...",
|
"workspace.status.checking": "Проверка незафиксированных изменений...",
|
||||||
"workspace.status.error": "Не удалось проверить статус git.",
|
"workspace.status.error": "Не удалось проверить статус git.",
|
||||||
"workspace.status.clean": "Неслитые изменения не обнаружены.",
|
"workspace.status.clean": "Незафиксированных изменений не обнаружено.",
|
||||||
"workspace.status.dirty": "Обнаружены неслитые изменения в этом рабочем пространстве.",
|
"workspace.status.dirty": "В этом рабочем пространстве обнаружены незафиксированные изменения.",
|
||||||
"workspace.delete.title": "Удалить рабочее пространство",
|
"workspace.delete.title": "Удалить рабочее пространство",
|
||||||
"workspace.delete.confirm": 'Удалить рабочее пространство "{{name}}"?',
|
"workspace.delete.confirm": 'Удалить рабочее пространство "{{name}}"?',
|
||||||
"workspace.delete.button": "Удалить рабочее пространство",
|
"workspace.delete.button": "Удалить рабочее пространство",
|
||||||
"workspace.reset.title": "Сбросить рабочее пространство",
|
"workspace.reset.title": "Сбросить рабочее пространство",
|
||||||
"workspace.reset.confirm": 'Сбросить рабочее пространство "{{name}}"?',
|
"workspace.reset.confirm": 'Сбросить рабочее пространство "{{name}}"?',
|
||||||
"workspace.reset.button": "Сбросить рабочее пространство",
|
"workspace.reset.button": "Сбросить рабочее пространство",
|
||||||
"workspace.reset.archived.none": "Никакие активные сессии не будут архивированы.",
|
"workspace.reset.archived.none": "Активные сессии не будут архивированы.",
|
||||||
"workspace.reset.archived.one": "1 сессия будет архивирована.",
|
"workspace.reset.archived.one": "1 сессия будет архивирована.",
|
||||||
"workspace.reset.archived.many": "{{count}} сессий будет архивировано.",
|
"workspace.reset.archived.many": "{{count}} сессий будет архивировано.",
|
||||||
"workspace.reset.note": "Рабочее пространство будет сброшено в соответствие с веткой по умолчанию.",
|
"workspace.reset.note": "Это сбросит рабочее пространство до соответствия ветке по умолчанию.",
|
||||||
"common.open": "Открыть",
|
"common.open": "Открыть",
|
||||||
"dialog.releaseNotes.action.getStarted": "Начать",
|
"dialog.releaseNotes.action.getStarted": "Начать",
|
||||||
"dialog.releaseNotes.action.next": "Далее",
|
"dialog.releaseNotes.action.next": "Далее",
|
||||||
@@ -836,4 +867,79 @@ export const dict = {
|
|||||||
"common.time.daysAgo.short": "{{count}} д назад",
|
"common.time.daysAgo.short": "{{count}} д назад",
|
||||||
"settings.providers.connected.environmentDescription": "Подключено из ваших переменных окружения",
|
"settings.providers.connected.environmentDescription": "Подключено из ваших переменных окружения",
|
||||||
"settings.providers.custom.description": "Добавить провайдера, совместимого с OpenAI, по базовому URL.",
|
"settings.providers.custom.description": "Добавить провайдера, совместимого с OpenAI, по базовому URL.",
|
||||||
|
|
||||||
|
"app.server.unreachable": "Не удалось связаться с {{server}}",
|
||||||
|
"app.server.retrying": "Автоматическая повторная попытка...",
|
||||||
|
"app.server.otherServers": "Другие серверы",
|
||||||
|
"dialog.server.add.usernamePlaceholder": "имя пользователя",
|
||||||
|
"dialog.server.add.passwordPlaceholder": "пароль",
|
||||||
|
"server.row.noUsername": "нет имени пользователя",
|
||||||
|
"session.review.noVcs.createGit.title": "Создать репозиторий Git",
|
||||||
|
"session.review.noVcs.createGit.description": "Отслеживайте, просматривайте и отменяйте изменения в этом проекте",
|
||||||
|
"session.review.noVcs.createGit.actionLoading": "Создание репозитория Git...",
|
||||||
|
"session.review.noVcs.createGit.action": "Создать репозиторий Git",
|
||||||
|
"session.todo.progress": "Выполнено {{done}} из {{total}} задач",
|
||||||
|
"session.question.progress": "{{current}} из {{total}} вопросов",
|
||||||
|
"session.header.open.finder": "Finder",
|
||||||
|
"session.header.open.fileExplorer": "Проводник",
|
||||||
|
"session.header.open.fileManager": "Файловый менеджер",
|
||||||
|
"session.header.open.app.vscode": "VS Code",
|
||||||
|
"session.header.open.app.cursor": "Cursor",
|
||||||
|
"session.header.open.app.zed": "Zed",
|
||||||
|
"session.header.open.app.textmate": "TextMate",
|
||||||
|
"session.header.open.app.antigravity": "Antigravity",
|
||||||
|
"session.header.open.app.terminal": "Терминал",
|
||||||
|
"session.header.open.app.iterm2": "iTerm2",
|
||||||
|
"session.header.open.app.ghostty": "Ghostty",
|
||||||
|
"session.header.open.app.warp": "Warp",
|
||||||
|
"session.header.open.app.xcode": "Xcode",
|
||||||
|
"session.header.open.app.androidStudio": "Android Studio",
|
||||||
|
"session.header.open.app.powershell": "PowerShell",
|
||||||
|
"session.header.open.app.sublimeText": "Sublime Text",
|
||||||
|
"debugBar.ariaLabel": "Диагностика производительности разработки",
|
||||||
|
"debugBar.na": "н/д",
|
||||||
|
"debugBar.nav.label": "NAV",
|
||||||
|
"debugBar.nav.tip":
|
||||||
|
"Последний завершенный переход маршрута, затрагивающий страницу сеанса, измеренный от запуска маршрутизатора до первой отрисовки после стабилизации.",
|
||||||
|
"debugBar.fps.label": "FPS",
|
||||||
|
"debugBar.fps.tip": "Скользящая частота кадров в секунду за последние 5 секунд.",
|
||||||
|
"debugBar.frame.label": "FRAME",
|
||||||
|
"debugBar.frame.tip": "Худшее время кадра за последние 5 секунд.",
|
||||||
|
"debugBar.jank.label": "JANK",
|
||||||
|
"debugBar.jank.tip": "Кадры более 32 мс за последние 5 секунд.",
|
||||||
|
"debugBar.long.label": "LONG",
|
||||||
|
"debugBar.long.tip": "Заблокированное время и количество длинных задач за последние 5 секунд. Макс. задача: {{max}}.",
|
||||||
|
"debugBar.delay.label": "DELAY",
|
||||||
|
"debugBar.delay.tip": "Худшая наблюдаемая задержка ввода за последние 5 секунд.",
|
||||||
|
"debugBar.inp.label": "INP",
|
||||||
|
"debugBar.inp.tip":
|
||||||
|
"Приблизительная продолжительность взаимодействия за последние 5 секунд. Это похоже на INP, а не официальный Web Vitals INP.",
|
||||||
|
"debugBar.cls.label": "CLS",
|
||||||
|
"debugBar.cls.tip": "Кумулятивный сдвиг макета за текущее время жизни приложения.",
|
||||||
|
"debugBar.mem.label": "MEM",
|
||||||
|
"debugBar.mem.tipUnavailable": "Используемая куча JS по сравнению с лимитом кучи. Только Chromium.",
|
||||||
|
"debugBar.mem.tip": "Используемая куча JS по сравнению с лимитом кучи. {{used}} из {{limit}}.",
|
||||||
|
"common.key.ctrl": "Ctrl",
|
||||||
|
"common.key.alt": "Alt",
|
||||||
|
"common.key.shift": "Shift",
|
||||||
|
"common.key.meta": "Meta",
|
||||||
|
"common.key.space": "Пробел",
|
||||||
|
"common.key.backspace": "Backspace",
|
||||||
|
"common.key.enter": "Enter",
|
||||||
|
"common.key.tab": "Tab",
|
||||||
|
"common.key.delete": "Delete",
|
||||||
|
"common.key.home": "Home",
|
||||||
|
"common.key.end": "End",
|
||||||
|
"common.key.pageUp": "Page Up",
|
||||||
|
"common.key.pageDown": "Page Down",
|
||||||
|
"common.key.insert": "Insert",
|
||||||
|
"common.unknown": "неизвестно",
|
||||||
|
"error.page.circular": "[Циклично]",
|
||||||
|
"error.globalSDK.noServerAvailable": "Нет доступного сервера",
|
||||||
|
"error.globalSDK.serverNotAvailable": "Сервер недоступен",
|
||||||
|
"error.childStore.persistedCacheCreateFailed": "Не удалось создать постоянный кэш",
|
||||||
|
"error.childStore.persistedProjectMetadataCreateFailed": "Не удалось создать постоянные метаданные проекта",
|
||||||
|
"error.childStore.persistedProjectIconCreateFailed": "Не удалось создать постоянный значок проекта",
|
||||||
|
"error.childStore.storeCreateFailed": "Не удалось создать хранилище",
|
||||||
|
"terminal.connectionLost.abnormalClose": "WebSocket закрыт аварийно: {{code}}",
|
||||||
}
|
}
|
||||||
|
|||||||
+107
-4
@@ -113,6 +113,7 @@ export const dict = {
|
|||||||
"dialog.model.empty": "ไม่พบผลลัพธ์โมเดล",
|
"dialog.model.empty": "ไม่พบผลลัพธ์โมเดล",
|
||||||
"dialog.model.manage": "จัดการโมเดล",
|
"dialog.model.manage": "จัดการโมเดล",
|
||||||
"dialog.model.manage.description": "ปรับแต่งโมเดลที่จะปรากฏในตัวเลือกโมเดล",
|
"dialog.model.manage.description": "ปรับแต่งโมเดลที่จะปรากฏในตัวเลือกโมเดล",
|
||||||
|
"dialog.model.manage.provider.toggle": "สลับโมเดลทั้งหมดของ {{provider}}",
|
||||||
|
|
||||||
"dialog.model.unpaid.freeModels.title": "โมเดลฟรีที่จัดหาให้โดย OpenCode",
|
"dialog.model.unpaid.freeModels.title": "โมเดลฟรีที่จัดหาให้โดย OpenCode",
|
||||||
"dialog.model.unpaid.addMore.title": "เพิ่มโมเดลเพิ่มเติมจากผู้ให้บริการยอดนิยม",
|
"dialog.model.unpaid.addMore.title": "เพิ่มโมเดลเพิ่มเติมจากผู้ให้บริการยอดนิยม",
|
||||||
@@ -262,7 +263,7 @@ export const dict = {
|
|||||||
|
|
||||||
"prompt.popover.emptyResults": "ไม่พบผลลัพธ์ที่ตรงกัน",
|
"prompt.popover.emptyResults": "ไม่พบผลลัพธ์ที่ตรงกัน",
|
||||||
"prompt.popover.emptyCommands": "ไม่พบคำสั่งที่ตรงกัน",
|
"prompt.popover.emptyCommands": "ไม่พบคำสั่งที่ตรงกัน",
|
||||||
"prompt.dropzone.label": "วางรูปภาพหรือ PDF ที่นี่",
|
"prompt.dropzone.label": "ลากรูปภาพ, PDF หรือไฟล์ข้อความมาวางที่นี่",
|
||||||
"prompt.dropzone.file.label": "วางเพื่อ @กล่าวถึงไฟล์",
|
"prompt.dropzone.file.label": "วางเพื่อ @กล่าวถึงไฟล์",
|
||||||
"prompt.slash.badge.custom": "กำหนดเอง",
|
"prompt.slash.badge.custom": "กำหนดเอง",
|
||||||
"prompt.slash.badge.skill": "ทักษะ",
|
"prompt.slash.badge.skill": "ทักษะ",
|
||||||
@@ -276,8 +277,8 @@ export const dict = {
|
|||||||
"prompt.action.send": "ส่ง",
|
"prompt.action.send": "ส่ง",
|
||||||
"prompt.action.stop": "หยุด",
|
"prompt.action.stop": "หยุด",
|
||||||
|
|
||||||
"prompt.toast.pasteUnsupported.title": "การวางไม่รองรับ",
|
"prompt.toast.pasteUnsupported.title": "ไฟล์แนบที่ไม่รองรับ",
|
||||||
"prompt.toast.pasteUnsupported.description": "สามารถวางรูปภาพหรือ PDF เท่านั้น",
|
"prompt.toast.pasteUnsupported.description": "แนบได้เฉพาะรูปภาพ, PDF หรือไฟล์ข้อความเท่านั้น",
|
||||||
"prompt.toast.modelAgentRequired.title": "เลือกเอเจนต์และโมเดล",
|
"prompt.toast.modelAgentRequired.title": "เลือกเอเจนต์และโมเดล",
|
||||||
"prompt.toast.modelAgentRequired.description": "เลือกเอเจนต์และโมเดลก่อนส่งพร้อมท์",
|
"prompt.toast.modelAgentRequired.description": "เลือกเอเจนต์และโมเดลก่อนส่งพร้อมท์",
|
||||||
"prompt.toast.worktreeCreateFailed.title": "ไม่สามารถสร้าง worktree",
|
"prompt.toast.worktreeCreateFailed.title": "ไม่สามารถสร้าง worktree",
|
||||||
@@ -314,6 +315,11 @@ export const dict = {
|
|||||||
"dialog.server.add.error": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์",
|
"dialog.server.add.error": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์",
|
||||||
"dialog.server.add.checking": "กำลังตรวจสอบ...",
|
"dialog.server.add.checking": "กำลังตรวจสอบ...",
|
||||||
"dialog.server.add.button": "เพิ่มเซิร์ฟเวอร์",
|
"dialog.server.add.button": "เพิ่มเซิร์ฟเวอร์",
|
||||||
|
"dialog.server.add.name": "ชื่อเซิร์ฟเวอร์ (ไม่บังคับ)",
|
||||||
|
"dialog.server.add.namePlaceholder": "Localhost",
|
||||||
|
"dialog.server.add.username": "ชื่อผู้ใช้ (ไม่บังคับ)",
|
||||||
|
"dialog.server.add.password": "รหัสผ่าน (ไม่บังคับ)",
|
||||||
|
"dialog.server.edit.title": "แก้ไขเซิร์ฟเวอร์",
|
||||||
"dialog.server.default.title": "เซิร์ฟเวอร์เริ่มต้น",
|
"dialog.server.default.title": "เซิร์ฟเวอร์เริ่มต้น",
|
||||||
"dialog.server.default.description":
|
"dialog.server.default.description":
|
||||||
"เชื่อมต่อกับเซิร์ฟเวอร์นี้เมื่อเปิดแอปแทนการเริ่มเซิร์ฟเวอร์ในเครื่อง ต้องรีสตาร์ท",
|
"เชื่อมต่อกับเซิร์ฟเวอร์นี้เมื่อเปิดแอปแทนการเริ่มเซิร์ฟเวอร์ในเครื่อง ต้องรีสตาร์ท",
|
||||||
@@ -391,6 +397,7 @@ export const dict = {
|
|||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.bs": "Bosanski",
|
"language.bs": "Bosanski",
|
||||||
"language.th": "ไทย",
|
"language.th": "ไทย",
|
||||||
|
"language.tr": "Türkçe",
|
||||||
|
|
||||||
"toast.language.title": "ภาษา",
|
"toast.language.title": "ภาษา",
|
||||||
"toast.language.description": "สลับไปที่ {{language}}",
|
"toast.language.description": "สลับไปที่ {{language}}",
|
||||||
@@ -494,9 +501,12 @@ export const dict = {
|
|||||||
"session.review.change.other": "การเปลี่ยนแปลง",
|
"session.review.change.other": "การเปลี่ยนแปลง",
|
||||||
"session.review.loadingChanges": "กำลังโหลดการเปลี่ยนแปลง...",
|
"session.review.loadingChanges": "กำลังโหลดการเปลี่ยนแปลง...",
|
||||||
"session.review.empty": "ยังไม่มีการเปลี่ยนแปลงในเซสชันนี้",
|
"session.review.empty": "ยังไม่มีการเปลี่ยนแปลงในเซสชันนี้",
|
||||||
|
"session.review.noVcs": "ไม่ตรวจพบระบบควบคุมเวอร์ชัน Git การเปลี่ยนแปลงจะไม่แสดง",
|
||||||
|
"session.review.noSnapshot": "การติดตามสแนปชอตถูกปิดใช้งานในการกำหนดค่า ดังนั้นการเปลี่ยนแปลงเซสชันจึงไม่พร้อมใช้งาน",
|
||||||
"session.review.noChanges": "ไม่มีการเปลี่ยนแปลง",
|
"session.review.noChanges": "ไม่มีการเปลี่ยนแปลง",
|
||||||
|
|
||||||
"session.files.selectToOpen": "เลือกไฟล์เพื่อเปิด",
|
"session.files.selectToOpen": "เลือกไฟล์เพื่อเปิด",
|
||||||
|
"session.files.empty": "ไม่มีไฟล์",
|
||||||
"session.files.all": "ไฟล์ทั้งหมด",
|
"session.files.all": "ไฟล์ทั้งหมด",
|
||||||
"session.files.binaryContent": "ไฟล์ไบนารี (ไม่สามารถแสดงเนื้อหาได้)",
|
"session.files.binaryContent": "ไฟล์ไบนารี (ไม่สามารถแสดงเนื้อหาได้)",
|
||||||
|
|
||||||
@@ -510,6 +520,17 @@ export const dict = {
|
|||||||
"session.todo.title": "สิ่งที่ต้องทำ",
|
"session.todo.title": "สิ่งที่ต้องทำ",
|
||||||
"session.todo.collapse": "ย่อ",
|
"session.todo.collapse": "ย่อ",
|
||||||
"session.todo.expand": "ขยาย",
|
"session.todo.expand": "ขยาย",
|
||||||
|
"session.followupDock.summary.one": "{{count}} ข้อความในคิว",
|
||||||
|
"session.followupDock.summary.other": "{{count}} ข้อความในคิว",
|
||||||
|
"session.followupDock.sendNow": "ส่งทันที",
|
||||||
|
"session.followupDock.edit": "แก้ไข",
|
||||||
|
"session.followupDock.collapse": "ย่อข้อความในคิว",
|
||||||
|
"session.followupDock.expand": "ขยายข้อความในคิว",
|
||||||
|
"session.revertDock.summary.one": "{{count}} ข้อความที่ถูกย้อนกลับ",
|
||||||
|
"session.revertDock.summary.other": "{{count}} ข้อความที่ถูกย้อนกลับ",
|
||||||
|
"session.revertDock.collapse": "ย่อข้อความที่ถูกย้อนกลับ",
|
||||||
|
"session.revertDock.expand": "ขยายข้อความที่ถูกย้อนกลับ",
|
||||||
|
"session.revertDock.restore": "กู้คืนข้อความ",
|
||||||
|
|
||||||
"session.new.title": "สร้างอะไรก็ได้",
|
"session.new.title": "สร้างอะไรก็ได้",
|
||||||
"session.new.worktree.main": "สาขาหลัก",
|
"session.new.worktree.main": "สาขาหลัก",
|
||||||
@@ -604,11 +625,18 @@ export const dict = {
|
|||||||
"settings.general.row.language.description": "เปลี่ยนภาษาที่แสดงสำหรับ OpenCode",
|
"settings.general.row.language.description": "เปลี่ยนภาษาที่แสดงสำหรับ OpenCode",
|
||||||
"settings.general.row.appearance.title": "รูปลักษณ์",
|
"settings.general.row.appearance.title": "รูปลักษณ์",
|
||||||
"settings.general.row.appearance.description": "ปรับแต่งวิธีการที่ OpenCode มีลักษณะบนอุปกรณ์ของคุณ",
|
"settings.general.row.appearance.description": "ปรับแต่งวิธีการที่ OpenCode มีลักษณะบนอุปกรณ์ของคุณ",
|
||||||
|
"settings.general.row.colorScheme.title": "โทนสี",
|
||||||
|
"settings.general.row.colorScheme.description": "เลือกว่าจะให้ OpenCode ใช้ธีมตามระบบ สว่าง หรือมืด",
|
||||||
"settings.general.row.theme.title": "ธีม",
|
"settings.general.row.theme.title": "ธีม",
|
||||||
"settings.general.row.theme.description": "ปรับแต่งวิธีการที่ OpenCode มีธีม",
|
"settings.general.row.theme.description": "ปรับแต่งวิธีการที่ OpenCode มีธีม",
|
||||||
"settings.general.row.font.title": "ฟอนต์",
|
"settings.general.row.font.title": "ฟอนต์",
|
||||||
"settings.general.row.font.description": "ปรับแต่งฟอนต์โมโนที่ใช้ในบล็อกโค้ด",
|
"settings.general.row.font.description": "ปรับแต่งฟอนต์โมโนที่ใช้ในบล็อกโค้ด",
|
||||||
|
"settings.general.row.followup.title": "พฤติกรรมการติดตามผล",
|
||||||
|
"settings.general.row.followup.description": "เลือกว่าจะให้พร้อมท์ติดตามผลทำงานทันทีหรือรอในคิว",
|
||||||
|
"settings.general.row.followup.option.queue": "คิว",
|
||||||
|
"settings.general.row.followup.option.steer": "นำทาง",
|
||||||
|
"settings.general.row.reasoningSummaries.title": "แสดงสรุปการใช้เหตุผล",
|
||||||
|
"settings.general.row.reasoningSummaries.description": "แสดงสรุปการใช้เหตุผลของโมเดลในไทม์ไลน์",
|
||||||
"settings.general.row.shellToolPartsExpanded.title": "ขยายส่วนเครื่องมือ shell",
|
"settings.general.row.shellToolPartsExpanded.title": "ขยายส่วนเครื่องมือ shell",
|
||||||
"settings.general.row.shellToolPartsExpanded.description": "แสดงส่วนเครื่องมือ shell แบบขยายตามค่าเริ่มต้นในไทม์ไลน์",
|
"settings.general.row.shellToolPartsExpanded.description": "แสดงส่วนเครื่องมือ shell แบบขยายตามค่าเริ่มต้นในไทม์ไลน์",
|
||||||
"settings.general.row.editToolPartsExpanded.title": "ขยายส่วนเครื่องมือ edit",
|
"settings.general.row.editToolPartsExpanded.title": "ขยายส่วนเครื่องมือ edit",
|
||||||
@@ -826,4 +854,79 @@ export const dict = {
|
|||||||
"common.time.daysAgo.short": "{{count}} วันที่แล้ว",
|
"common.time.daysAgo.short": "{{count}} วันที่แล้ว",
|
||||||
"settings.providers.connected.environmentDescription": "เชื่อมต่อจากตัวแปรสภาพแวดล้อมของคุณ",
|
"settings.providers.connected.environmentDescription": "เชื่อมต่อจากตัวแปรสภาพแวดล้อมของคุณ",
|
||||||
"settings.providers.custom.description": "เพิ่มผู้ให้บริการที่รองรับ OpenAI ด้วย URL หลัก",
|
"settings.providers.custom.description": "เพิ่มผู้ให้บริการที่รองรับ OpenAI ด้วย URL หลัก",
|
||||||
|
|
||||||
|
"app.server.unreachable": "ไม่สามารถติดต่อ {{server}}",
|
||||||
|
"app.server.retrying": "กำลังลองใหม่โดยอัตโนมัติ...",
|
||||||
|
"app.server.otherServers": "เซิร์ฟเวอร์อื่น ๆ",
|
||||||
|
"dialog.server.add.usernamePlaceholder": "ชื่อผู้ใช้",
|
||||||
|
"dialog.server.add.passwordPlaceholder": "รหัสผ่าน",
|
||||||
|
"server.row.noUsername": "ไม่มีชื่อผู้ใช้",
|
||||||
|
"session.review.noVcs.createGit.title": "สร้าง Git repository",
|
||||||
|
"session.review.noVcs.createGit.description": "ติดตาม ตรวจสอบ และเลิกทำสิ่งเปลี่ยนแปลงในโปรเจกต์นี้",
|
||||||
|
"session.review.noVcs.createGit.actionLoading": "กำลังสร้าง Git repository...",
|
||||||
|
"session.review.noVcs.createGit.action": "สร้าง Git repository",
|
||||||
|
"session.todo.progress": "เสร็จสิ้น {{done}} จาก {{total}} รายการ",
|
||||||
|
"session.question.progress": "{{current}} จาก {{total}} คำถาม",
|
||||||
|
"session.header.open.finder": "Finder",
|
||||||
|
"session.header.open.fileExplorer": "File Explorer",
|
||||||
|
"session.header.open.fileManager": "File Manager",
|
||||||
|
"session.header.open.app.vscode": "VS Code",
|
||||||
|
"session.header.open.app.cursor": "Cursor",
|
||||||
|
"session.header.open.app.zed": "Zed",
|
||||||
|
"session.header.open.app.textmate": "TextMate",
|
||||||
|
"session.header.open.app.antigravity": "Antigravity",
|
||||||
|
"session.header.open.app.terminal": "Terminal",
|
||||||
|
"session.header.open.app.iterm2": "iTerm2",
|
||||||
|
"session.header.open.app.ghostty": "Ghostty",
|
||||||
|
"session.header.open.app.warp": "Warp",
|
||||||
|
"session.header.open.app.xcode": "Xcode",
|
||||||
|
"session.header.open.app.androidStudio": "Android Studio",
|
||||||
|
"session.header.open.app.powershell": "PowerShell",
|
||||||
|
"session.header.open.app.sublimeText": "Sublime Text",
|
||||||
|
"debugBar.ariaLabel": "การวินิจฉัยประสิทธิภาพการพัฒนา",
|
||||||
|
"debugBar.na": "n/a",
|
||||||
|
"debugBar.nav.label": "NAV",
|
||||||
|
"debugBar.nav.tip":
|
||||||
|
"การเปลี่ยนเส้นทางที่เสร็จสมบูรณ์ล่าสุดที่สัมผัสหน้าเซสชัน วัดจากจุดเริ่มต้นเราเตอร์จนถึงการวาดครั้งแรกหลังจากที่นิ่ง",
|
||||||
|
"debugBar.fps.label": "FPS",
|
||||||
|
"debugBar.fps.tip": "เฟรมต่อวินาทีแบบต่อเนื่องในช่วง 5 วินาทีที่ผ่านมา",
|
||||||
|
"debugBar.frame.label": "FRAME",
|
||||||
|
"debugBar.frame.tip": "เวลาเฟรมที่แย่ที่สุดในช่วง 5 วินาทีที่ผ่านมา",
|
||||||
|
"debugBar.jank.label": "JANK",
|
||||||
|
"debugBar.jank.tip": "เฟรมที่เกิน 32ms ในช่วง 5 วินาทีที่ผ่านมา",
|
||||||
|
"debugBar.long.label": "LONG",
|
||||||
|
"debugBar.long.tip": "เวลาที่ถูกบล็อกและจำนวนงานยาวในช่วง 5 วินาทีที่ผ่านมา งานสูงสุด: {{max}}",
|
||||||
|
"debugBar.delay.label": "DELAY",
|
||||||
|
"debugBar.delay.tip": "ความล่าช้าในการป้อนข้อมูลที่แย่ที่สุดที่สังเกตได้ในช่วง 5 วินาทีที่ผ่านมา",
|
||||||
|
"debugBar.inp.label": "INP",
|
||||||
|
"debugBar.inp.tip":
|
||||||
|
"ระยะเวลาการโต้ตอบโดยประมาณในช่วง 5 วินาทีที่ผ่านมา นี่เป็นเหมือน INP ไม่ใช่ Web Vitals INP อย่างเป็นทางการ",
|
||||||
|
"debugBar.cls.label": "CLS",
|
||||||
|
"debugBar.cls.tip": "การเลื่อนเลย์เอาต์สะสมสำหรับอายุการใช้งานของแอปปัจจุบัน",
|
||||||
|
"debugBar.mem.label": "MEM",
|
||||||
|
"debugBar.mem.tipUnavailable": "JS heap ที่ใช้เทียบกับขีดจำกัด heap เฉพาะ Chromium",
|
||||||
|
"debugBar.mem.tip": "JS heap ที่ใช้เทียบกับขีดจำกัด heap {{used}} จาก {{limit}}",
|
||||||
|
"common.key.ctrl": "Ctrl",
|
||||||
|
"common.key.alt": "Alt",
|
||||||
|
"common.key.shift": "Shift",
|
||||||
|
"common.key.meta": "Meta",
|
||||||
|
"common.key.space": "Space",
|
||||||
|
"common.key.backspace": "Backspace",
|
||||||
|
"common.key.enter": "Enter",
|
||||||
|
"common.key.tab": "Tab",
|
||||||
|
"common.key.delete": "Delete",
|
||||||
|
"common.key.home": "Home",
|
||||||
|
"common.key.end": "End",
|
||||||
|
"common.key.pageUp": "Page Up",
|
||||||
|
"common.key.pageDown": "Page Down",
|
||||||
|
"common.key.insert": "Insert",
|
||||||
|
"common.unknown": "ไม่ทราบ",
|
||||||
|
"error.page.circular": "[วงกลม]",
|
||||||
|
"error.globalSDK.noServerAvailable": "ไม่มีเซิร์ฟเวอร์",
|
||||||
|
"error.globalSDK.serverNotAvailable": "เซิร์ฟเวอร์ไม่พร้อมใช้งาน",
|
||||||
|
"error.childStore.persistedCacheCreateFailed": "ไม่สามารถสร้างแคชถาวร",
|
||||||
|
"error.childStore.persistedProjectMetadataCreateFailed": "ไม่สามารถสร้างเมตาดาต้าโปรเจกต์ถาวร",
|
||||||
|
"error.childStore.persistedProjectIconCreateFailed": "ไม่สามารถสร้างไอคอนโปรเจกต์ถาวร",
|
||||||
|
"error.childStore.storeCreateFailed": "ไม่สามารถสร้างที่เก็บ",
|
||||||
|
"terminal.connectionLost.abnormalClose": "WebSocket ปิดอย่างผิดปกติ: {{code}}",
|
||||||
}
|
}
|
||||||
|
|||||||
+104
-3
@@ -268,7 +268,7 @@ export const dict = {
|
|||||||
|
|
||||||
"prompt.popover.emptyResults": "Eşleşen sonuç yok",
|
"prompt.popover.emptyResults": "Eşleşen sonuç yok",
|
||||||
"prompt.popover.emptyCommands": "Eşleşen komut yok",
|
"prompt.popover.emptyCommands": "Eşleşen komut yok",
|
||||||
"prompt.dropzone.label": "Görsel veya PDF'leri buraya bırakın",
|
"prompt.dropzone.label": "Resimleri, PDF'leri veya metin dosyalarını buraya bırakın",
|
||||||
"prompt.dropzone.file.label": "@bahsetmek için dosyayı bırakın",
|
"prompt.dropzone.file.label": "@bahsetmek için dosyayı bırakın",
|
||||||
"prompt.slash.badge.custom": "özel",
|
"prompt.slash.badge.custom": "özel",
|
||||||
"prompt.slash.badge.skill": "beceri",
|
"prompt.slash.badge.skill": "beceri",
|
||||||
@@ -282,8 +282,8 @@ export const dict = {
|
|||||||
"prompt.action.send": "Gönder",
|
"prompt.action.send": "Gönder",
|
||||||
"prompt.action.stop": "Durdur",
|
"prompt.action.stop": "Durdur",
|
||||||
|
|
||||||
"prompt.toast.pasteUnsupported.title": "Desteklenmeyen yapıştırma",
|
"prompt.toast.pasteUnsupported.title": "Desteklenmeyen ek",
|
||||||
"prompt.toast.pasteUnsupported.description": "Buraya sadece görsel veya PDF yapıştırılabilir.",
|
"prompt.toast.pasteUnsupported.description": "Buraya yalnızca resimler, PDF'ler veya metin dosyaları eklenebilir.",
|
||||||
"prompt.toast.modelAgentRequired.title": "Bir ajan ve model seçin",
|
"prompt.toast.modelAgentRequired.title": "Bir ajan ve model seçin",
|
||||||
"prompt.toast.modelAgentRequired.description": "Komut göndermeden önce bir ajan ve model seçin.",
|
"prompt.toast.modelAgentRequired.description": "Komut göndermeden önce bir ajan ve model seçin.",
|
||||||
"prompt.toast.worktreeCreateFailed.title": "Çalışma ağacı oluşturulamadı",
|
"prompt.toast.worktreeCreateFailed.title": "Çalışma ağacı oluşturulamadı",
|
||||||
@@ -320,6 +320,11 @@ export const dict = {
|
|||||||
"dialog.server.add.error": "Sunucuya bağlanılamadı",
|
"dialog.server.add.error": "Sunucuya bağlanılamadı",
|
||||||
"dialog.server.add.checking": "Kontrol ediliyor...",
|
"dialog.server.add.checking": "Kontrol ediliyor...",
|
||||||
"dialog.server.add.button": "Sunucu ekle",
|
"dialog.server.add.button": "Sunucu ekle",
|
||||||
|
"dialog.server.add.name": "Sunucu adı (isteğe bağlı)",
|
||||||
|
"dialog.server.add.namePlaceholder": "Localhost",
|
||||||
|
"dialog.server.add.username": "Kullanıcı adı (isteğe bağlı)",
|
||||||
|
"dialog.server.add.password": "Şifre (isteğe bağlı)",
|
||||||
|
"dialog.server.edit.title": "Sunucuyu düzenle",
|
||||||
"dialog.server.default.title": "Varsayılan sunucu",
|
"dialog.server.default.title": "Varsayılan sunucu",
|
||||||
"dialog.server.default.description":
|
"dialog.server.default.description":
|
||||||
"Uygulama başlatıldığında yerel sunucu başlatmak yerine bu sunucuya bağlan. Yeniden başlatma gerektirir.",
|
"Uygulama başlatıldığında yerel sunucu başlatmak yerine bu sunucuya bağlan. Yeniden başlatma gerektirir.",
|
||||||
@@ -506,10 +511,13 @@ export const dict = {
|
|||||||
"session.review.loadingChanges": "Değişiklikler yükleniyor...",
|
"session.review.loadingChanges": "Değişiklikler yükleniyor...",
|
||||||
"session.review.empty": "Bu oturumda henüz değişiklik yok",
|
"session.review.empty": "Bu oturumda henüz değişiklik yok",
|
||||||
"session.review.noVcs": "Git VCS algılanamadı, oturum değişiklikleri tespit edilemeyecek",
|
"session.review.noVcs": "Git VCS algılanamadı, oturum değişiklikleri tespit edilemeyecek",
|
||||||
|
"session.review.noSnapshot":
|
||||||
|
"Yapılandırmada anlık görüntü takibi devre dışı bırakıldı, bu nedenle oturum değişiklikleri kullanılamıyor",
|
||||||
"session.review.noChanges": "Değişiklik yok",
|
"session.review.noChanges": "Değişiklik yok",
|
||||||
|
|
||||||
"session.files.selectToOpen": "Açmak için bir dosya seçin",
|
"session.files.selectToOpen": "Açmak için bir dosya seçin",
|
||||||
"session.files.all": "Tüm dosyalar",
|
"session.files.all": "Tüm dosyalar",
|
||||||
|
"session.files.empty": "Dosya yok",
|
||||||
"session.files.binaryContent": "İkili dosya (içerik görüntülenemiyor)",
|
"session.files.binaryContent": "İkili dosya (içerik görüntülenemiyor)",
|
||||||
|
|
||||||
"session.messages.renderEarlier": "Önceki mesajları göster",
|
"session.messages.renderEarlier": "Önceki mesajları göster",
|
||||||
@@ -522,6 +530,17 @@ export const dict = {
|
|||||||
"session.todo.title": "Görevler",
|
"session.todo.title": "Görevler",
|
||||||
"session.todo.collapse": "Daralt",
|
"session.todo.collapse": "Daralt",
|
||||||
"session.todo.expand": "Genişlet",
|
"session.todo.expand": "Genişlet",
|
||||||
|
"session.followupDock.summary.one": "{{count}} sıradaki mesaj",
|
||||||
|
"session.followupDock.summary.other": "{{count}} sıradaki mesaj",
|
||||||
|
"session.followupDock.sendNow": "Şimdi gönder",
|
||||||
|
"session.followupDock.edit": "Düzenle",
|
||||||
|
"session.followupDock.collapse": "Sıradaki mesajları daralt",
|
||||||
|
"session.followupDock.expand": "Sıradaki mesajları genişlet",
|
||||||
|
"session.revertDock.summary.one": "{{count}} geri alınan mesaj",
|
||||||
|
"session.revertDock.summary.other": "{{count}} geri alınan mesaj",
|
||||||
|
"session.revertDock.collapse": "Geri alınan mesajları daralt",
|
||||||
|
"session.revertDock.expand": "Geri alınan mesajları genişlet",
|
||||||
|
"session.revertDock.restore": "Mesajı geri yükle",
|
||||||
|
|
||||||
"session.new.title": "İstediğini yap",
|
"session.new.title": "İstediğini yap",
|
||||||
"session.new.worktree.main": "Ana dal",
|
"session.new.worktree.main": "Ana dal",
|
||||||
@@ -618,10 +637,18 @@ export const dict = {
|
|||||||
"settings.general.row.language.description": "OpenCode'un görünüm dilini değiştirin",
|
"settings.general.row.language.description": "OpenCode'un görünüm dilini değiştirin",
|
||||||
"settings.general.row.appearance.title": "Görünüm",
|
"settings.general.row.appearance.title": "Görünüm",
|
||||||
"settings.general.row.appearance.description": "OpenCode'un cihazınızdaki görünümünü özelleştirin",
|
"settings.general.row.appearance.description": "OpenCode'un cihazınızdaki görünümünü özelleştirin",
|
||||||
|
"settings.general.row.colorScheme.title": "Renk şeması",
|
||||||
|
"settings.general.row.colorScheme.description":
|
||||||
|
"OpenCode'un sistem, açık veya koyu temayı takip etip etmeyeceğini seçin",
|
||||||
"settings.general.row.theme.title": "Tema",
|
"settings.general.row.theme.title": "Tema",
|
||||||
"settings.general.row.theme.description": "OpenCode'un temasını özelleştirin.",
|
"settings.general.row.theme.description": "OpenCode'un temasını özelleştirin.",
|
||||||
"settings.general.row.font.title": "Yazı Tipi",
|
"settings.general.row.font.title": "Yazı Tipi",
|
||||||
"settings.general.row.font.description": "Kod bloklarında kullanılan monospace yazı tipini özelleştirin",
|
"settings.general.row.font.description": "Kod bloklarında kullanılan monospace yazı tipini özelleştirin",
|
||||||
|
"settings.general.row.followup.title": "Takip davranışı",
|
||||||
|
"settings.general.row.followup.description":
|
||||||
|
"Takip komutlarının hemen yönlendirilmesini mi yoksa sırada beklemesini mi istediğinizi seçin",
|
||||||
|
"settings.general.row.followup.option.queue": "Sıra",
|
||||||
|
"settings.general.row.followup.option.steer": "Yönlendir",
|
||||||
"settings.general.row.reasoningSummaries.title": "Akıl yürütme özetlerini göster",
|
"settings.general.row.reasoningSummaries.title": "Akıl yürütme özetlerini göster",
|
||||||
"settings.general.row.reasoningSummaries.description": "Zaman çizelgesinde model akıl yürütme özetlerini görüntüle",
|
"settings.general.row.reasoningSummaries.description": "Zaman çizelgesinde model akıl yürütme özetlerini görüntüle",
|
||||||
"settings.general.row.shellToolPartsExpanded.title": "Kabuk araç bileşenlerini genişlet",
|
"settings.general.row.shellToolPartsExpanded.title": "Kabuk araç bileşenlerini genişlet",
|
||||||
@@ -847,4 +874,78 @@ export const dict = {
|
|||||||
"common.time.daysAgo.short": "{{count}}g önce",
|
"common.time.daysAgo.short": "{{count}}g önce",
|
||||||
"settings.providers.connected.environmentDescription": "Ortam değişkenlerinizden bağlandı",
|
"settings.providers.connected.environmentDescription": "Ortam değişkenlerinizden bağlandı",
|
||||||
"settings.providers.custom.description": "Temel URL üzerinden OpenAI uyumlu bir sağlayıcı ekleyin.",
|
"settings.providers.custom.description": "Temel URL üzerinden OpenAI uyumlu bir sağlayıcı ekleyin.",
|
||||||
|
|
||||||
|
"app.server.unreachable": "{{server}} sunucusuna ulaşılamadı",
|
||||||
|
"app.server.retrying": "Otomatik olarak tekrar deneniyor...",
|
||||||
|
"app.server.otherServers": "Diğer sunucular",
|
||||||
|
"dialog.server.add.usernamePlaceholder": "kullanıcı adı",
|
||||||
|
"dialog.server.add.passwordPlaceholder": "parola",
|
||||||
|
"server.row.noUsername": "kullanıcı adı yok",
|
||||||
|
"session.review.noVcs.createGit.title": "Git deposu oluştur",
|
||||||
|
"session.review.noVcs.createGit.description": "Bu projedeki değişiklikleri takip et, incele ve geri al",
|
||||||
|
"session.review.noVcs.createGit.actionLoading": "Git deposu oluşturuluyor...",
|
||||||
|
"session.review.noVcs.createGit.action": "Git deposu oluştur",
|
||||||
|
"session.todo.progress": "{{total}} görevin {{done}} tanesi tamamlandı",
|
||||||
|
"session.question.progress": "{{total}} sorunun {{current}} tanesi",
|
||||||
|
"session.header.open.finder": "Finder",
|
||||||
|
"session.header.open.fileExplorer": "Dosya Gezgini",
|
||||||
|
"session.header.open.fileManager": "Dosya Yöneticisi",
|
||||||
|
"session.header.open.app.vscode": "VS Code",
|
||||||
|
"session.header.open.app.cursor": "Cursor",
|
||||||
|
"session.header.open.app.zed": "Zed",
|
||||||
|
"session.header.open.app.textmate": "TextMate",
|
||||||
|
"session.header.open.app.antigravity": "Antigravity",
|
||||||
|
"session.header.open.app.terminal": "Terminal",
|
||||||
|
"session.header.open.app.iterm2": "iTerm2",
|
||||||
|
"session.header.open.app.ghostty": "Ghostty",
|
||||||
|
"session.header.open.app.warp": "Warp",
|
||||||
|
"session.header.open.app.xcode": "Xcode",
|
||||||
|
"session.header.open.app.androidStudio": "Android Studio",
|
||||||
|
"session.header.open.app.powershell": "PowerShell",
|
||||||
|
"session.header.open.app.sublimeText": "Sublime Text",
|
||||||
|
"debugBar.ariaLabel": "Geliştirme performansı teşhisi",
|
||||||
|
"debugBar.na": "yok",
|
||||||
|
"debugBar.nav.label": "NAV",
|
||||||
|
"debugBar.nav.tip":
|
||||||
|
"Yönlendirici başlangıcından yerleşme sonrası ilk boyamaya kadar ölçülen, bir oturum sayfasına dokunan son tamamlanmış rota geçişi.",
|
||||||
|
"debugBar.fps.label": "FPS",
|
||||||
|
"debugBar.fps.tip": "Son 5 saniyedeki kayan saniye başına kare sayısı.",
|
||||||
|
"debugBar.frame.label": "FRAME",
|
||||||
|
"debugBar.frame.tip": "Son 5 saniyedeki en kötü kare süresi.",
|
||||||
|
"debugBar.jank.label": "JANK",
|
||||||
|
"debugBar.jank.tip": "Son 5 saniyede 32ms üzerindeki kareler.",
|
||||||
|
"debugBar.long.label": "LONG",
|
||||||
|
"debugBar.long.tip": "Son 5 saniyedeki engellenen süre ve uzun görev sayısı. Maksimum görev: {{max}}.",
|
||||||
|
"debugBar.delay.label": "DELAY",
|
||||||
|
"debugBar.delay.tip": "Son 5 saniyede gözlemlenen en kötü giriş gecikmesi.",
|
||||||
|
"debugBar.inp.label": "INP",
|
||||||
|
"debugBar.inp.tip": "Son 5 saniyedeki yaklaşık etkileşim süresi. Bu INP benzeridir, resmi Web Vitals INP değildir.",
|
||||||
|
"debugBar.cls.label": "CLS",
|
||||||
|
"debugBar.cls.tip": "Mevcut uygulama ömrü için kümülatif düzen kayması.",
|
||||||
|
"debugBar.mem.label": "MEM",
|
||||||
|
"debugBar.mem.tipUnavailable": "Kullanılan JS yığını vs yığın sınırı. Yalnızca Chromium.",
|
||||||
|
"debugBar.mem.tip": "Kullanılan JS yığını vs yığın sınırı. {{limit}} içinde {{used}}.",
|
||||||
|
"common.key.ctrl": "Ctrl",
|
||||||
|
"common.key.alt": "Alt",
|
||||||
|
"common.key.shift": "Shift",
|
||||||
|
"common.key.meta": "Meta",
|
||||||
|
"common.key.space": "Boşluk",
|
||||||
|
"common.key.backspace": "Geri",
|
||||||
|
"common.key.enter": "Enter",
|
||||||
|
"common.key.tab": "Tab",
|
||||||
|
"common.key.delete": "Delete",
|
||||||
|
"common.key.home": "Home",
|
||||||
|
"common.key.end": "End",
|
||||||
|
"common.key.pageUp": "Page Up",
|
||||||
|
"common.key.pageDown": "Page Down",
|
||||||
|
"common.key.insert": "Insert",
|
||||||
|
"common.unknown": "bilinmiyor",
|
||||||
|
"error.page.circular": "[Döngüsel]",
|
||||||
|
"error.globalSDK.noServerAvailable": "Sunucu yok",
|
||||||
|
"error.globalSDK.serverNotAvailable": "Sunucu mevcut değil",
|
||||||
|
"error.childStore.persistedCacheCreateFailed": "Kalıcı önbellek oluşturulamadı",
|
||||||
|
"error.childStore.persistedProjectMetadataCreateFailed": "Kalıcı proje meta verileri oluşturulamadı",
|
||||||
|
"error.childStore.persistedProjectIconCreateFailed": "Kalıcı proje simgesi oluşturulamadı",
|
||||||
|
"error.childStore.storeCreateFailed": "Depo oluşturulamadı",
|
||||||
|
"terminal.connectionLost.abnormalClose": "WebSocket anormal şekilde kapandı: {{code}}",
|
||||||
} satisfies Partial<Record<Keys, string>>
|
} satisfies Partial<Record<Keys, string>>
|
||||||
|
|||||||
+105
-3
@@ -140,6 +140,7 @@ export const dict = {
|
|||||||
"dialog.model.empty": "未找到模型",
|
"dialog.model.empty": "未找到模型",
|
||||||
"dialog.model.manage": "管理模型",
|
"dialog.model.manage": "管理模型",
|
||||||
"dialog.model.manage.description": "自定义模型选择器中显示的模型。",
|
"dialog.model.manage.description": "自定义模型选择器中显示的模型。",
|
||||||
|
"dialog.model.manage.provider.toggle": "切换所有 {{provider}} 模型",
|
||||||
"dialog.model.unpaid.freeModels.title": "OpenCode 提供的免费模型",
|
"dialog.model.unpaid.freeModels.title": "OpenCode 提供的免费模型",
|
||||||
"dialog.model.unpaid.addMore.title": "从热门提供商添加更多模型",
|
"dialog.model.unpaid.addMore.title": "从热门提供商添加更多模型",
|
||||||
|
|
||||||
@@ -282,7 +283,7 @@ export const dict = {
|
|||||||
"prompt.example.25": "这里的环境变量是怎么工作的?",
|
"prompt.example.25": "这里的环境变量是怎么工作的?",
|
||||||
"prompt.popover.emptyResults": "没有匹配的结果",
|
"prompt.popover.emptyResults": "没有匹配的结果",
|
||||||
"prompt.popover.emptyCommands": "没有匹配的命令",
|
"prompt.popover.emptyCommands": "没有匹配的命令",
|
||||||
"prompt.dropzone.label": "将图片或 PDF 拖到这里",
|
"prompt.dropzone.label": "将图片、PDF 或文本文件拖放到此处",
|
||||||
"prompt.dropzone.file.label": "拖放以 @提及文件",
|
"prompt.dropzone.file.label": "拖放以 @提及文件",
|
||||||
"prompt.slash.badge.custom": "自定义",
|
"prompt.slash.badge.custom": "自定义",
|
||||||
"prompt.slash.badge.skill": "技能",
|
"prompt.slash.badge.skill": "技能",
|
||||||
@@ -295,8 +296,8 @@ export const dict = {
|
|||||||
"prompt.attachment.remove": "移除附件",
|
"prompt.attachment.remove": "移除附件",
|
||||||
"prompt.action.send": "发送",
|
"prompt.action.send": "发送",
|
||||||
"prompt.action.stop": "停止",
|
"prompt.action.stop": "停止",
|
||||||
"prompt.toast.pasteUnsupported.title": "不支持的粘贴",
|
"prompt.toast.pasteUnsupported.title": "不支持的附件",
|
||||||
"prompt.toast.pasteUnsupported.description": "这里只能粘贴图片或 PDF 文件。",
|
"prompt.toast.pasteUnsupported.description": "此处仅能附加图片、PDF 或文本文件。",
|
||||||
"prompt.toast.modelAgentRequired.title": "请选择智能体和模型",
|
"prompt.toast.modelAgentRequired.title": "请选择智能体和模型",
|
||||||
"prompt.toast.modelAgentRequired.description": "发送提示前请先选择智能体和模型。",
|
"prompt.toast.modelAgentRequired.description": "发送提示前请先选择智能体和模型。",
|
||||||
"prompt.toast.worktreeCreateFailed.title": "创建工作树失败",
|
"prompt.toast.worktreeCreateFailed.title": "创建工作树失败",
|
||||||
@@ -334,6 +335,11 @@ export const dict = {
|
|||||||
"dialog.server.add.error": "无法连接到服务器",
|
"dialog.server.add.error": "无法连接到服务器",
|
||||||
"dialog.server.add.checking": "检查中...",
|
"dialog.server.add.checking": "检查中...",
|
||||||
"dialog.server.add.button": "添加服务器",
|
"dialog.server.add.button": "添加服务器",
|
||||||
|
"dialog.server.add.name": "服务器名称(可选)",
|
||||||
|
"dialog.server.add.namePlaceholder": "Localhost",
|
||||||
|
"dialog.server.add.username": "用户名(可选)",
|
||||||
|
"dialog.server.add.password": "密码(可选)",
|
||||||
|
"dialog.server.edit.title": "编辑服务器",
|
||||||
"dialog.server.default.title": "默认服务器",
|
"dialog.server.default.title": "默认服务器",
|
||||||
"dialog.server.default.description": "应用启动时连接此服务器,而不是启动本地服务器。需要重启。",
|
"dialog.server.default.description": "应用启动时连接此服务器,而不是启动本地服务器。需要重启。",
|
||||||
"dialog.server.default.none": "未选择服务器",
|
"dialog.server.default.none": "未选择服务器",
|
||||||
@@ -406,6 +412,7 @@ export const dict = {
|
|||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.bs": "Bosanski",
|
"language.bs": "Bosanski",
|
||||||
"language.th": "ไทย",
|
"language.th": "ไทย",
|
||||||
|
"language.tr": "Türkçe",
|
||||||
|
|
||||||
"toast.language.title": "语言",
|
"toast.language.title": "语言",
|
||||||
"toast.language.description": "已切换到{{language}}",
|
"toast.language.description": "已切换到{{language}}",
|
||||||
@@ -497,9 +504,12 @@ export const dict = {
|
|||||||
"session.review.change.other": "更改",
|
"session.review.change.other": "更改",
|
||||||
"session.review.loadingChanges": "正在加载更改...",
|
"session.review.loadingChanges": "正在加载更改...",
|
||||||
"session.review.empty": "此会话暂无更改",
|
"session.review.empty": "此会话暂无更改",
|
||||||
|
"session.review.noVcs": "未检测到 Git 版本控制系统,无法显示更改",
|
||||||
|
"session.review.noSnapshot": "配置中已禁用快照跟踪,因此会话更改不可用",
|
||||||
"session.review.noChanges": "无更改",
|
"session.review.noChanges": "无更改",
|
||||||
"session.files.selectToOpen": "选择要打开的文件",
|
"session.files.selectToOpen": "选择要打开的文件",
|
||||||
"session.files.all": "所有文件",
|
"session.files.all": "所有文件",
|
||||||
|
"session.files.empty": "无文件",
|
||||||
"session.files.binaryContent": "二进制文件(无法显示内容)",
|
"session.files.binaryContent": "二进制文件(无法显示内容)",
|
||||||
"session.messages.renderEarlier": "显示更早的消息",
|
"session.messages.renderEarlier": "显示更早的消息",
|
||||||
"session.messages.loadingEarlier": "正在加载更早的消息...",
|
"session.messages.loadingEarlier": "正在加载更早的消息...",
|
||||||
@@ -510,6 +520,17 @@ export const dict = {
|
|||||||
"session.todo.title": "待办事项",
|
"session.todo.title": "待办事项",
|
||||||
"session.todo.collapse": "折叠",
|
"session.todo.collapse": "折叠",
|
||||||
"session.todo.expand": "展开",
|
"session.todo.expand": "展开",
|
||||||
|
"session.followupDock.summary.one": "{{count}} 条排队消息",
|
||||||
|
"session.followupDock.summary.other": "{{count}} 条排队消息",
|
||||||
|
"session.followupDock.sendNow": "立即发送",
|
||||||
|
"session.followupDock.edit": "编辑",
|
||||||
|
"session.followupDock.collapse": "折叠排队消息",
|
||||||
|
"session.followupDock.expand": "展开排队消息",
|
||||||
|
"session.revertDock.summary.one": "{{count}} 条已回滚消息",
|
||||||
|
"session.revertDock.summary.other": "{{count}} 条已回滚消息",
|
||||||
|
"session.revertDock.collapse": "折叠已回滚消息",
|
||||||
|
"session.revertDock.expand": "展开已回滚消息",
|
||||||
|
"session.revertDock.restore": "恢复消息",
|
||||||
"session.new.title": "构建任何东西",
|
"session.new.title": "构建任何东西",
|
||||||
"session.new.worktree.main": "主分支",
|
"session.new.worktree.main": "主分支",
|
||||||
"session.new.worktree.mainWithBranch": "主分支({{branch}})",
|
"session.new.worktree.mainWithBranch": "主分支({{branch}})",
|
||||||
@@ -604,10 +625,18 @@ export const dict = {
|
|||||||
"settings.general.row.language.description": "更改 OpenCode 的显示语言",
|
"settings.general.row.language.description": "更改 OpenCode 的显示语言",
|
||||||
"settings.general.row.appearance.title": "外观",
|
"settings.general.row.appearance.title": "外观",
|
||||||
"settings.general.row.appearance.description": "自定义 OpenCode 在你的设备上的外观",
|
"settings.general.row.appearance.description": "自定义 OpenCode 在你的设备上的外观",
|
||||||
|
"settings.general.row.colorScheme.title": "配色方案",
|
||||||
|
"settings.general.row.colorScheme.description": "选择 OpenCode 跟随系统、浅色或深色主题",
|
||||||
"settings.general.row.theme.title": "主题",
|
"settings.general.row.theme.title": "主题",
|
||||||
"settings.general.row.theme.description": "自定义 OpenCode 的主题。",
|
"settings.general.row.theme.description": "自定义 OpenCode 的主题。",
|
||||||
"settings.general.row.font.title": "字体",
|
"settings.general.row.font.title": "字体",
|
||||||
"settings.general.row.font.description": "自定义代码块使用的等宽字体",
|
"settings.general.row.font.description": "自定义代码块使用的等宽字体",
|
||||||
|
"settings.general.row.followup.title": "跟进消息行为",
|
||||||
|
"settings.general.row.followup.description": "选择跟进提示是立即引导还是在队列中等待",
|
||||||
|
"settings.general.row.followup.option.queue": "排队",
|
||||||
|
"settings.general.row.followup.option.steer": "引导",
|
||||||
|
"settings.general.row.reasoningSummaries.title": "显示推理摘要",
|
||||||
|
"settings.general.row.reasoningSummaries.description": "在时间线中显示模型推理摘要",
|
||||||
"settings.general.row.shellToolPartsExpanded.title": "展开 shell 工具部分",
|
"settings.general.row.shellToolPartsExpanded.title": "展开 shell 工具部分",
|
||||||
"settings.general.row.shellToolPartsExpanded.description": "默认在时间线中展开 shell 工具部分",
|
"settings.general.row.shellToolPartsExpanded.description": "默认在时间线中展开 shell 工具部分",
|
||||||
"settings.general.row.editToolPartsExpanded.title": "展开编辑工具部分",
|
"settings.general.row.editToolPartsExpanded.title": "展开编辑工具部分",
|
||||||
@@ -824,4 +853,77 @@ export const dict = {
|
|||||||
"common.time.daysAgo.short": "{{count}}天前",
|
"common.time.daysAgo.short": "{{count}}天前",
|
||||||
"settings.providers.connected.environmentDescription": "已通过环境变量连接",
|
"settings.providers.connected.environmentDescription": "已通过环境变量连接",
|
||||||
"settings.providers.custom.description": "通过基础 URL 添加与 OpenAI 兼容的提供商。",
|
"settings.providers.custom.description": "通过基础 URL 添加与 OpenAI 兼容的提供商。",
|
||||||
|
|
||||||
|
"app.server.unreachable": "无法连接到 {{server}}",
|
||||||
|
"app.server.retrying": "正在自动重试...",
|
||||||
|
"app.server.otherServers": "其他服务器",
|
||||||
|
"dialog.server.add.usernamePlaceholder": "用户名",
|
||||||
|
"dialog.server.add.passwordPlaceholder": "密码",
|
||||||
|
"server.row.noUsername": "无用户名",
|
||||||
|
"session.review.noVcs.createGit.title": "创建 Git 仓库",
|
||||||
|
"session.review.noVcs.createGit.description": "在此项目中跟踪、审查和撤消更改",
|
||||||
|
"session.review.noVcs.createGit.actionLoading": "正在创建 Git 仓库...",
|
||||||
|
"session.review.noVcs.createGit.action": "创建 Git 仓库",
|
||||||
|
"session.todo.progress": "已完成 {{done}} 个任务(共 {{total}} 个)",
|
||||||
|
"session.question.progress": "{{current}}/{{total}} 个问题",
|
||||||
|
"session.header.open.finder": "访达",
|
||||||
|
"session.header.open.fileExplorer": "文件资源管理器",
|
||||||
|
"session.header.open.fileManager": "文件管理器",
|
||||||
|
"session.header.open.app.vscode": "VS Code",
|
||||||
|
"session.header.open.app.cursor": "Cursor",
|
||||||
|
"session.header.open.app.zed": "Zed",
|
||||||
|
"session.header.open.app.textmate": "TextMate",
|
||||||
|
"session.header.open.app.antigravity": "Antigravity",
|
||||||
|
"session.header.open.app.terminal": "终端",
|
||||||
|
"session.header.open.app.iterm2": "iTerm2",
|
||||||
|
"session.header.open.app.ghostty": "Ghostty",
|
||||||
|
"session.header.open.app.warp": "Warp",
|
||||||
|
"session.header.open.app.xcode": "Xcode",
|
||||||
|
"session.header.open.app.androidStudio": "Android Studio",
|
||||||
|
"session.header.open.app.powershell": "PowerShell",
|
||||||
|
"session.header.open.app.sublimeText": "Sublime Text",
|
||||||
|
"debugBar.ariaLabel": "开发性能诊断",
|
||||||
|
"debugBar.na": "不适用",
|
||||||
|
"debugBar.nav.label": "NAV",
|
||||||
|
"debugBar.nav.tip": "最后一次完成的涉及会话页面的路由转换,从路由器启动到稳定后的第一次绘制。",
|
||||||
|
"debugBar.fps.label": "FPS",
|
||||||
|
"debugBar.fps.tip": "过去 5 秒内的滚动帧率。",
|
||||||
|
"debugBar.frame.label": "FRAME",
|
||||||
|
"debugBar.frame.tip": "过去 5 秒内最差的帧时间。",
|
||||||
|
"debugBar.jank.label": "JANK",
|
||||||
|
"debugBar.jank.tip": "过去 5 秒内超过 32ms 的帧。",
|
||||||
|
"debugBar.long.label": "LONG",
|
||||||
|
"debugBar.long.tip": "过去 5 秒内的阻塞时间和长任务计数。最大任务:{{max}}。",
|
||||||
|
"debugBar.delay.label": "DELAY",
|
||||||
|
"debugBar.delay.tip": "过去 5 秒内观察到的最差输入延迟。",
|
||||||
|
"debugBar.inp.label": "INP",
|
||||||
|
"debugBar.inp.tip": "过去 5 秒内的近似交互持续时间。这类似于 INP,而非官方的 Web Vitals INP。",
|
||||||
|
"debugBar.cls.label": "CLS",
|
||||||
|
"debugBar.cls.tip": "当前应用生命周期的累积布局偏移。",
|
||||||
|
"debugBar.mem.label": "MEM",
|
||||||
|
"debugBar.mem.tipUnavailable": "使用的 JS 堆与堆限制。仅限 Chromium。",
|
||||||
|
"debugBar.mem.tip": "使用的 JS 堆与堆限制。{{used}} / {{limit}}。",
|
||||||
|
"common.key.ctrl": "Ctrl",
|
||||||
|
"common.key.alt": "Alt",
|
||||||
|
"common.key.shift": "Shift",
|
||||||
|
"common.key.meta": "Meta",
|
||||||
|
"common.key.space": "空格",
|
||||||
|
"common.key.backspace": "退格",
|
||||||
|
"common.key.enter": "回车",
|
||||||
|
"common.key.tab": "Tab",
|
||||||
|
"common.key.delete": "Delete",
|
||||||
|
"common.key.home": "Home",
|
||||||
|
"common.key.end": "End",
|
||||||
|
"common.key.pageUp": "Page Up",
|
||||||
|
"common.key.pageDown": "Page Down",
|
||||||
|
"common.key.insert": "Insert",
|
||||||
|
"common.unknown": "未知",
|
||||||
|
"error.page.circular": "[循环]",
|
||||||
|
"error.globalSDK.noServerAvailable": "无可用服务器",
|
||||||
|
"error.globalSDK.serverNotAvailable": "服务器不可用",
|
||||||
|
"error.childStore.persistedCacheCreateFailed": "创建持久化缓存失败",
|
||||||
|
"error.childStore.persistedProjectMetadataCreateFailed": "创建持久化项目元数据失败",
|
||||||
|
"error.childStore.persistedProjectIconCreateFailed": "创建持久化项目图标失败",
|
||||||
|
"error.childStore.storeCreateFailed": "创建存储失败",
|
||||||
|
"terminal.connectionLost.abnormalClose": "WebSocket 异常关闭:{{code}}",
|
||||||
} satisfies Partial<Record<Keys, string>>
|
} satisfies Partial<Record<Keys, string>>
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ export const dict = {
|
|||||||
"dialog.model.empty": "找不到模型",
|
"dialog.model.empty": "找不到模型",
|
||||||
"dialog.model.manage": "管理模型",
|
"dialog.model.manage": "管理模型",
|
||||||
"dialog.model.manage.description": "自訂模型選擇器中顯示的模型。",
|
"dialog.model.manage.description": "自訂模型選擇器中顯示的模型。",
|
||||||
|
"dialog.model.manage.provider.toggle": "切換所有 {{provider}} 模型",
|
||||||
|
|
||||||
"dialog.model.unpaid.freeModels.title": "OpenCode 提供的免費模型",
|
"dialog.model.unpaid.freeModels.title": "OpenCode 提供的免費模型",
|
||||||
"dialog.model.unpaid.addMore.title": "從熱門提供者新增更多模型",
|
"dialog.model.unpaid.addMore.title": "從熱門提供者新增更多模型",
|
||||||
@@ -262,7 +263,7 @@ export const dict = {
|
|||||||
|
|
||||||
"prompt.popover.emptyResults": "沒有符合的結果",
|
"prompt.popover.emptyResults": "沒有符合的結果",
|
||||||
"prompt.popover.emptyCommands": "沒有符合的命令",
|
"prompt.popover.emptyCommands": "沒有符合的命令",
|
||||||
"prompt.dropzone.label": "將圖片或 PDF 拖到這裡",
|
"prompt.dropzone.label": "將圖片、PDF 或文字檔案拖放到此處",
|
||||||
"prompt.dropzone.file.label": "拖放以 @提及檔案",
|
"prompt.dropzone.file.label": "拖放以 @提及檔案",
|
||||||
"prompt.slash.badge.custom": "自訂",
|
"prompt.slash.badge.custom": "自訂",
|
||||||
"prompt.slash.badge.skill": "技能",
|
"prompt.slash.badge.skill": "技能",
|
||||||
@@ -276,8 +277,8 @@ export const dict = {
|
|||||||
"prompt.action.send": "傳送",
|
"prompt.action.send": "傳送",
|
||||||
"prompt.action.stop": "停止",
|
"prompt.action.stop": "停止",
|
||||||
|
|
||||||
"prompt.toast.pasteUnsupported.title": "不支援的貼上",
|
"prompt.toast.pasteUnsupported.title": "不支援的附件",
|
||||||
"prompt.toast.pasteUnsupported.description": "這裡只能貼上圖片或 PDF 檔案。",
|
"prompt.toast.pasteUnsupported.description": "此處僅能附加圖片、PDF 或文字檔案。",
|
||||||
"prompt.toast.modelAgentRequired.title": "請選擇代理程式和模型",
|
"prompt.toast.modelAgentRequired.title": "請選擇代理程式和模型",
|
||||||
"prompt.toast.modelAgentRequired.description": "傳送提示前請先選擇代理程式和模型。",
|
"prompt.toast.modelAgentRequired.description": "傳送提示前請先選擇代理程式和模型。",
|
||||||
"prompt.toast.worktreeCreateFailed.title": "建立工作樹失敗",
|
"prompt.toast.worktreeCreateFailed.title": "建立工作樹失敗",
|
||||||
@@ -314,6 +315,11 @@ export const dict = {
|
|||||||
"dialog.server.add.error": "無法連線到伺服器",
|
"dialog.server.add.error": "無法連線到伺服器",
|
||||||
"dialog.server.add.checking": "檢查中...",
|
"dialog.server.add.checking": "檢查中...",
|
||||||
"dialog.server.add.button": "新增伺服器",
|
"dialog.server.add.button": "新增伺服器",
|
||||||
|
"dialog.server.add.name": "伺服器名稱(選填)",
|
||||||
|
"dialog.server.add.namePlaceholder": "Localhost",
|
||||||
|
"dialog.server.add.username": "使用者名稱(選填)",
|
||||||
|
"dialog.server.add.password": "密碼(選填)",
|
||||||
|
"dialog.server.edit.title": "編輯伺服器",
|
||||||
"dialog.server.default.title": "預設伺服器",
|
"dialog.server.default.title": "預設伺服器",
|
||||||
"dialog.server.default.description": "應用程式啟動時連線此伺服器,而不是啟動本地伺服器。需要重新啟動。",
|
"dialog.server.default.description": "應用程式啟動時連線此伺服器,而不是啟動本地伺服器。需要重新啟動。",
|
||||||
"dialog.server.default.none": "未選擇伺服器",
|
"dialog.server.default.none": "未選擇伺服器",
|
||||||
@@ -390,6 +396,7 @@ export const dict = {
|
|||||||
"language.br": "Português (Brasil)",
|
"language.br": "Português (Brasil)",
|
||||||
"language.bs": "Bosanski",
|
"language.bs": "Bosanski",
|
||||||
"language.th": "ไทย",
|
"language.th": "ไทย",
|
||||||
|
"language.tr": "Türkçe",
|
||||||
|
|
||||||
"toast.language.title": "語言",
|
"toast.language.title": "語言",
|
||||||
"toast.language.description": "已切換到 {{language}}",
|
"toast.language.description": "已切換到 {{language}}",
|
||||||
@@ -493,8 +500,11 @@ export const dict = {
|
|||||||
"session.review.loadingChanges": "正在載入變更...",
|
"session.review.loadingChanges": "正在載入變更...",
|
||||||
"session.review.empty": "此工作階段暫無變更",
|
"session.review.empty": "此工作階段暫無變更",
|
||||||
"session.review.noChanges": "沒有變更",
|
"session.review.noChanges": "沒有變更",
|
||||||
|
"session.review.noVcs": "未偵測到 Git 版本控制系統,無法顯示變更",
|
||||||
|
"session.review.noSnapshot": "設定中已停用快照追蹤,因此無法使用工作階段變更",
|
||||||
"session.files.selectToOpen": "選取要開啟的檔案",
|
"session.files.selectToOpen": "選取要開啟的檔案",
|
||||||
"session.files.all": "所有檔案",
|
"session.files.all": "所有檔案",
|
||||||
|
"session.files.empty": "沒有檔案",
|
||||||
"session.files.binaryContent": "二進位檔案(無法顯示內容)",
|
"session.files.binaryContent": "二進位檔案(無法顯示內容)",
|
||||||
"session.messages.renderEarlier": "顯示更早的訊息",
|
"session.messages.renderEarlier": "顯示更早的訊息",
|
||||||
"session.messages.loadingEarlier": "正在載入更早的訊息...",
|
"session.messages.loadingEarlier": "正在載入更早的訊息...",
|
||||||
@@ -506,6 +516,17 @@ export const dict = {
|
|||||||
"session.todo.title": "待辦事項",
|
"session.todo.title": "待辦事項",
|
||||||
"session.todo.collapse": "折疊",
|
"session.todo.collapse": "折疊",
|
||||||
"session.todo.expand": "展開",
|
"session.todo.expand": "展開",
|
||||||
|
"session.followupDock.summary.one": "{{count}} 則佇列訊息",
|
||||||
|
"session.followupDock.summary.other": "{{count}} 則佇列訊息",
|
||||||
|
"session.followupDock.sendNow": "立即傳送",
|
||||||
|
"session.followupDock.edit": "編輯",
|
||||||
|
"session.followupDock.collapse": "收合佇列訊息",
|
||||||
|
"session.followupDock.expand": "展開佇列訊息",
|
||||||
|
"session.revertDock.summary.one": "{{count}} 則已回復訊息",
|
||||||
|
"session.revertDock.summary.other": "{{count}} 則已回復訊息",
|
||||||
|
"session.revertDock.collapse": "收合已回復訊息",
|
||||||
|
"session.revertDock.expand": "展開已回復訊息",
|
||||||
|
"session.revertDock.restore": "還原訊息",
|
||||||
|
|
||||||
"session.new.title": "建構任何東西",
|
"session.new.title": "建構任何東西",
|
||||||
"session.new.worktree.main": "主分支",
|
"session.new.worktree.main": "主分支",
|
||||||
@@ -585,8 +606,8 @@ export const dict = {
|
|||||||
"settings.tab.general": "一般",
|
"settings.tab.general": "一般",
|
||||||
"settings.tab.shortcuts": "快速鍵",
|
"settings.tab.shortcuts": "快速鍵",
|
||||||
"settings.desktop.section.wsl": "WSL",
|
"settings.desktop.section.wsl": "WSL",
|
||||||
"settings.desktop.wsl.title": "WSL integration",
|
"settings.desktop.wsl.title": "WSL 整合",
|
||||||
"settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.",
|
"settings.desktop.wsl.description": "在 Windows 上的 WSL 中執行 OpenCode 伺服器。",
|
||||||
|
|
||||||
"settings.general.section.appearance": "外觀",
|
"settings.general.section.appearance": "外觀",
|
||||||
"settings.general.section.notifications": "系統通知",
|
"settings.general.section.notifications": "系統通知",
|
||||||
@@ -599,10 +620,18 @@ export const dict = {
|
|||||||
"settings.general.row.language.description": "變更 OpenCode 的顯示語言",
|
"settings.general.row.language.description": "變更 OpenCode 的顯示語言",
|
||||||
"settings.general.row.appearance.title": "外觀",
|
"settings.general.row.appearance.title": "外觀",
|
||||||
"settings.general.row.appearance.description": "自訂 OpenCode 在你的裝置上的外觀",
|
"settings.general.row.appearance.description": "自訂 OpenCode 在你的裝置上的外觀",
|
||||||
|
"settings.general.row.colorScheme.title": "配色方案",
|
||||||
|
"settings.general.row.colorScheme.description": "選擇 OpenCode 要跟隨系統、淺色或深色主題",
|
||||||
"settings.general.row.theme.title": "主題",
|
"settings.general.row.theme.title": "主題",
|
||||||
"settings.general.row.theme.description": "自訂 OpenCode 的主題。",
|
"settings.general.row.theme.description": "自訂 OpenCode 的主題。",
|
||||||
"settings.general.row.font.title": "字型",
|
"settings.general.row.font.title": "字型",
|
||||||
"settings.general.row.font.description": "自訂程式碼區塊使用的等寬字型",
|
"settings.general.row.font.description": "自訂程式碼區塊使用的等寬字型",
|
||||||
|
"settings.general.row.followup.title": "後續追問行為",
|
||||||
|
"settings.general.row.followup.description": "選擇後續追問提示是立即引導還是進入佇列等待",
|
||||||
|
"settings.general.row.followup.option.queue": "佇列",
|
||||||
|
"settings.general.row.followup.option.steer": "引導",
|
||||||
|
"settings.general.row.reasoningSummaries.title": "顯示推理摘要",
|
||||||
|
"settings.general.row.reasoningSummaries.description": "在時間軸中顯示模型推理摘要",
|
||||||
|
|
||||||
"settings.general.row.shellToolPartsExpanded.title": "展開 shell 工具區塊",
|
"settings.general.row.shellToolPartsExpanded.title": "展開 shell 工具區塊",
|
||||||
"settings.general.row.shellToolPartsExpanded.description": "在時間軸中預設展開 shell 工具區塊",
|
"settings.general.row.shellToolPartsExpanded.description": "在時間軸中預設展開 shell 工具區塊",
|
||||||
@@ -819,4 +848,77 @@ export const dict = {
|
|||||||
"common.time.daysAgo.short": "{{count}}天前",
|
"common.time.daysAgo.short": "{{count}}天前",
|
||||||
"settings.providers.connected.environmentDescription": "已從環境變數連線",
|
"settings.providers.connected.environmentDescription": "已從環境變數連線",
|
||||||
"settings.providers.custom.description": "透過基本 URL 新增與 OpenAI 相容的提供者。",
|
"settings.providers.custom.description": "透過基本 URL 新增與 OpenAI 相容的提供者。",
|
||||||
|
|
||||||
|
"app.server.unreachable": "無法連線至 {{server}}",
|
||||||
|
"app.server.retrying": "正在自動重試...",
|
||||||
|
"app.server.otherServers": "其他伺服器",
|
||||||
|
"dialog.server.add.usernamePlaceholder": "使用者名稱",
|
||||||
|
"dialog.server.add.passwordPlaceholder": "密碼",
|
||||||
|
"server.row.noUsername": "無使用者名稱",
|
||||||
|
"session.review.noVcs.createGit.title": "建立 Git 儲存庫",
|
||||||
|
"session.review.noVcs.createGit.description": "追蹤、檢閱及復原此專案中的變更",
|
||||||
|
"session.review.noVcs.createGit.actionLoading": "正在建立 Git 儲存庫...",
|
||||||
|
"session.review.noVcs.createGit.action": "建立 Git 儲存庫",
|
||||||
|
"session.todo.progress": "已完成 {{done}} 個待辦事項(共 {{total}} 個)",
|
||||||
|
"session.question.progress": "{{current}}/{{total}} 個問題",
|
||||||
|
"session.header.open.finder": "Finder",
|
||||||
|
"session.header.open.fileExplorer": "檔案總管",
|
||||||
|
"session.header.open.fileManager": "檔案管理員",
|
||||||
|
"session.header.open.app.vscode": "VS Code",
|
||||||
|
"session.header.open.app.cursor": "Cursor",
|
||||||
|
"session.header.open.app.zed": "Zed",
|
||||||
|
"session.header.open.app.textmate": "TextMate",
|
||||||
|
"session.header.open.app.antigravity": "Antigravity",
|
||||||
|
"session.header.open.app.terminal": "終端機",
|
||||||
|
"session.header.open.app.iterm2": "iTerm2",
|
||||||
|
"session.header.open.app.ghostty": "Ghostty",
|
||||||
|
"session.header.open.app.warp": "Warp",
|
||||||
|
"session.header.open.app.xcode": "Xcode",
|
||||||
|
"session.header.open.app.androidStudio": "Android Studio",
|
||||||
|
"session.header.open.app.powershell": "PowerShell",
|
||||||
|
"session.header.open.app.sublimeText": "Sublime Text",
|
||||||
|
"debugBar.ariaLabel": "開發效能診斷",
|
||||||
|
"debugBar.na": "不適用",
|
||||||
|
"debugBar.nav.label": "NAV",
|
||||||
|
"debugBar.nav.tip": "最後一次完成的涉及工作階段頁面的路由轉換,從路由器啟動到穩定後的第一次繪製。",
|
||||||
|
"debugBar.fps.label": "FPS",
|
||||||
|
"debugBar.fps.tip": "過去 5 秒內的滾動幀率。",
|
||||||
|
"debugBar.frame.label": "FRAME",
|
||||||
|
"debugBar.frame.tip": "過去 5 秒內最差的幀時間。",
|
||||||
|
"debugBar.jank.label": "JANK",
|
||||||
|
"debugBar.jank.tip": "過去 5 秒內超過 32ms 的幀。",
|
||||||
|
"debugBar.long.label": "LONG",
|
||||||
|
"debugBar.long.tip": "過去 5 秒內的阻塞時間和長任務計數。最大任務:{{max}}。",
|
||||||
|
"debugBar.delay.label": "DELAY",
|
||||||
|
"debugBar.delay.tip": "過去 5 秒內觀察到的最差輸入延遲。",
|
||||||
|
"debugBar.inp.label": "INP",
|
||||||
|
"debugBar.inp.tip": "過去 5 秒內的近似互動持續時間。這類似於 INP,而非官方的 Web Vitals INP。",
|
||||||
|
"debugBar.cls.label": "CLS",
|
||||||
|
"debugBar.cls.tip": "目前應用程式生命週期的累積版面配置位移。",
|
||||||
|
"debugBar.mem.label": "MEM",
|
||||||
|
"debugBar.mem.tipUnavailable": "使用的 JS 堆積與堆積限制。僅限 Chromium。",
|
||||||
|
"debugBar.mem.tip": "使用的 JS 堆積與堆積限制。{{used}} / {{limit}}。",
|
||||||
|
"common.key.ctrl": "Ctrl",
|
||||||
|
"common.key.alt": "Alt",
|
||||||
|
"common.key.shift": "Shift",
|
||||||
|
"common.key.meta": "Meta",
|
||||||
|
"common.key.space": "空白鍵",
|
||||||
|
"common.key.backspace": "退格鍵",
|
||||||
|
"common.key.enter": "Enter",
|
||||||
|
"common.key.tab": "Tab",
|
||||||
|
"common.key.delete": "Delete",
|
||||||
|
"common.key.home": "Home",
|
||||||
|
"common.key.end": "End",
|
||||||
|
"common.key.pageUp": "Page Up",
|
||||||
|
"common.key.pageDown": "Page Down",
|
||||||
|
"common.key.insert": "Insert",
|
||||||
|
"common.unknown": "未知",
|
||||||
|
"error.page.circular": "[循環]",
|
||||||
|
"error.globalSDK.noServerAvailable": "無可用的伺服器",
|
||||||
|
"error.globalSDK.serverNotAvailable": "伺服器無法使用",
|
||||||
|
"error.childStore.persistedCacheCreateFailed": "建立持續性快取失敗",
|
||||||
|
"error.childStore.persistedProjectMetadataCreateFailed": "建立持續性專案中繼資料失敗",
|
||||||
|
"error.childStore.persistedProjectIconCreateFailed": "建立持續性專案圖示失敗",
|
||||||
|
"error.childStore.storeCreateFailed": "建立儲存區失敗",
|
||||||
|
"terminal.connectionLost.abnormalClose": "WebSocket 異常關閉:{{code}}",
|
||||||
} satisfies Partial<Record<Keys, string>>
|
} satisfies Partial<Record<Keys, string>>
|
||||||
|
|||||||
@@ -35,14 +35,14 @@ function isInitError(error: unknown): error is InitError {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeJson(value: unknown): string {
|
function safeJson(value: unknown, circular: string): string {
|
||||||
const seen = new WeakSet<object>()
|
const seen = new WeakSet<object>()
|
||||||
const json = JSON.stringify(
|
const json = JSON.stringify(
|
||||||
value,
|
value,
|
||||||
(_key, val) => {
|
(_key, val) => {
|
||||||
if (typeof val === "bigint") return val.toString()
|
if (typeof val === "bigint") return val.toString()
|
||||||
if (typeof val === "object" && val) {
|
if (typeof val === "object" && val) {
|
||||||
if (seen.has(val)) return "[Circular]"
|
if (seen.has(val)) return circular
|
||||||
seen.add(val)
|
seen.add(val)
|
||||||
}
|
}
|
||||||
return val
|
return val
|
||||||
@@ -54,14 +54,15 @@ function safeJson(value: unknown): string {
|
|||||||
|
|
||||||
function formatInitError(error: InitError, t: Translator): string {
|
function formatInitError(error: InitError, t: Translator): string {
|
||||||
const data = error.data
|
const data = error.data
|
||||||
|
const json = (value: unknown) => safeJson(value, t("error.page.circular"))
|
||||||
switch (error.name) {
|
switch (error.name) {
|
||||||
case "MCPFailed": {
|
case "MCPFailed": {
|
||||||
const name = typeof data.name === "string" ? data.name : ""
|
const name = typeof data.name === "string" ? data.name : ""
|
||||||
return t("error.chain.mcpFailed", { name })
|
return t("error.chain.mcpFailed", { name })
|
||||||
}
|
}
|
||||||
case "ProviderAuthError": {
|
case "ProviderAuthError": {
|
||||||
const providerID = typeof data.providerID === "string" ? data.providerID : "unknown"
|
const providerID = typeof data.providerID === "string" ? data.providerID : t("common.unknown")
|
||||||
const message = typeof data.message === "string" ? data.message : safeJson(data.message)
|
const message = typeof data.message === "string" ? data.message : json(data.message)
|
||||||
return t("error.chain.providerAuthFailed", { provider: providerID, message })
|
return t("error.chain.providerAuthFailed", { provider: providerID, message })
|
||||||
}
|
}
|
||||||
case "APIError": {
|
case "APIError": {
|
||||||
@@ -101,24 +102,24 @@ function formatInitError(error: InitError, t: Translator): string {
|
|||||||
].join("\n")
|
].join("\n")
|
||||||
}
|
}
|
||||||
case "ProviderInitError": {
|
case "ProviderInitError": {
|
||||||
const providerID = typeof data.providerID === "string" ? data.providerID : "unknown"
|
const providerID = typeof data.providerID === "string" ? data.providerID : t("common.unknown")
|
||||||
return t("error.chain.providerInitFailed", { provider: providerID })
|
return t("error.chain.providerInitFailed", { provider: providerID })
|
||||||
}
|
}
|
||||||
case "ConfigJsonError": {
|
case "ConfigJsonError": {
|
||||||
const path = typeof data.path === "string" ? data.path : safeJson(data.path)
|
const path = typeof data.path === "string" ? data.path : json(data.path)
|
||||||
const message = typeof data.message === "string" ? data.message : ""
|
const message = typeof data.message === "string" ? data.message : ""
|
||||||
if (message) return t("error.chain.configJsonInvalidWithMessage", { path, message })
|
if (message) return t("error.chain.configJsonInvalidWithMessage", { path, message })
|
||||||
return t("error.chain.configJsonInvalid", { path })
|
return t("error.chain.configJsonInvalid", { path })
|
||||||
}
|
}
|
||||||
case "ConfigDirectoryTypoError": {
|
case "ConfigDirectoryTypoError": {
|
||||||
const path = typeof data.path === "string" ? data.path : safeJson(data.path)
|
const path = typeof data.path === "string" ? data.path : json(data.path)
|
||||||
const dir = typeof data.dir === "string" ? data.dir : safeJson(data.dir)
|
const dir = typeof data.dir === "string" ? data.dir : json(data.dir)
|
||||||
const suggestion = typeof data.suggestion === "string" ? data.suggestion : safeJson(data.suggestion)
|
const suggestion = typeof data.suggestion === "string" ? data.suggestion : json(data.suggestion)
|
||||||
return t("error.chain.configDirectoryTypo", { dir, path, suggestion })
|
return t("error.chain.configDirectoryTypo", { dir, path, suggestion })
|
||||||
}
|
}
|
||||||
case "ConfigFrontmatterError": {
|
case "ConfigFrontmatterError": {
|
||||||
const path = typeof data.path === "string" ? data.path : safeJson(data.path)
|
const path = typeof data.path === "string" ? data.path : json(data.path)
|
||||||
const message = typeof data.message === "string" ? data.message : safeJson(data.message)
|
const message = typeof data.message === "string" ? data.message : json(data.message)
|
||||||
return t("error.chain.configFrontmatterError", { path, message })
|
return t("error.chain.configFrontmatterError", { path, message })
|
||||||
}
|
}
|
||||||
case "ConfigInvalidError": {
|
case "ConfigInvalidError": {
|
||||||
@@ -126,7 +127,7 @@ function formatInitError(error: InitError, t: Translator): string {
|
|||||||
? data.issues.filter(isIssue).map((issue) => "↳ " + issue.message + " " + issue.path.join("."))
|
? data.issues.filter(isIssue).map((issue) => "↳ " + issue.message + " " + issue.path.join("."))
|
||||||
: []
|
: []
|
||||||
const message = typeof data.message === "string" ? data.message : ""
|
const message = typeof data.message === "string" ? data.message : ""
|
||||||
const path = typeof data.path === "string" ? data.path : safeJson(data.path)
|
const path = typeof data.path === "string" ? data.path : json(data.path)
|
||||||
|
|
||||||
const line = message
|
const line = message
|
||||||
? t("error.chain.configInvalidWithMessage", { path, message })
|
? t("error.chain.configInvalidWithMessage", { path, message })
|
||||||
@@ -135,14 +136,15 @@ function formatInitError(error: InitError, t: Translator): string {
|
|||||||
return [line, ...issues].join("\n")
|
return [line, ...issues].join("\n")
|
||||||
}
|
}
|
||||||
case "UnknownError":
|
case "UnknownError":
|
||||||
return typeof data.message === "string" ? data.message : safeJson(data)
|
return typeof data.message === "string" ? data.message : json(data)
|
||||||
default:
|
default:
|
||||||
if (typeof data.message === "string") return data.message
|
if (typeof data.message === "string") return data.message
|
||||||
return safeJson(data)
|
return json(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatErrorChain(error: unknown, t: Translator, depth = 0, parentMessage?: string): string {
|
function formatErrorChain(error: unknown, t: Translator, depth = 0, parentMessage?: string): string {
|
||||||
|
const json = (value: unknown) => safeJson(value, t("error.page.circular"))
|
||||||
if (!error) return t("error.chain.unknown")
|
if (!error) return t("error.chain.unknown")
|
||||||
|
|
||||||
if (isInitError(error)) {
|
if (isInitError(error)) {
|
||||||
@@ -204,7 +206,7 @@ function formatErrorChain(error: unknown, t: Translator, depth = 0, parentMessag
|
|||||||
}
|
}
|
||||||
|
|
||||||
const indent = depth > 0 ? `\n${CHAIN_SEPARATOR}${t("error.chain.causedBy")}\n` : ""
|
const indent = depth > 0 ? `\n${CHAIN_SEPARATOR}${t("error.chain.causedBy")}\n` : ""
|
||||||
return indent + safeJson(error)
|
return indent + json(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatError(error: unknown, t: Translator): string {
|
function formatError(error: unknown, t: Translator): string {
|
||||||
|
|||||||
@@ -1,16 +1,4 @@
|
|||||||
import {
|
import { batch, createEffect, createMemo, For, on, onCleanup, onMount, ParentProps, Show, untrack } from "solid-js"
|
||||||
batch,
|
|
||||||
createEffect,
|
|
||||||
createMemo,
|
|
||||||
createSignal,
|
|
||||||
For,
|
|
||||||
on,
|
|
||||||
onCleanup,
|
|
||||||
onMount,
|
|
||||||
ParentProps,
|
|
||||||
Show,
|
|
||||||
untrack,
|
|
||||||
} from "solid-js"
|
|
||||||
import { useNavigate, useParams } from "@solidjs/router"
|
import { useNavigate, useParams } from "@solidjs/router"
|
||||||
import { useLayout, LocalProject } from "@/context/layout"
|
import { useLayout, LocalProject } from "@/context/layout"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
@@ -41,8 +29,8 @@ import {
|
|||||||
getSessionPrefetch,
|
getSessionPrefetch,
|
||||||
isSessionPrefetchCurrent,
|
isSessionPrefetchCurrent,
|
||||||
runSessionPrefetch,
|
runSessionPrefetch,
|
||||||
SESSION_PREFETCH_TTL,
|
|
||||||
setSessionPrefetch,
|
setSessionPrefetch,
|
||||||
|
shouldSkipSessionPrefetch,
|
||||||
} from "@/context/global-sync/session-prefetch"
|
} from "@/context/global-sync/session-prefetch"
|
||||||
import { useNotification } from "@/context/notification"
|
import { useNotification } from "@/context/notification"
|
||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
@@ -140,14 +128,20 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
const [state, setState] = createStore({
|
const [state, setState] = createStore({
|
||||||
autoselect: !initialDirectory,
|
autoselect: !initialDirectory,
|
||||||
|
routing: false,
|
||||||
busyWorkspaces: {} as Record<string, boolean>,
|
busyWorkspaces: {} as Record<string, boolean>,
|
||||||
hoverSession: undefined as string | undefined,
|
hoverSession: undefined as string | undefined,
|
||||||
hoverProject: undefined as string | undefined,
|
hoverProject: undefined as string | undefined,
|
||||||
scrollSessionKey: undefined as string | undefined,
|
scrollSessionKey: undefined as string | undefined,
|
||||||
nav: undefined as HTMLElement | undefined,
|
nav: undefined as HTMLElement | undefined,
|
||||||
|
sortNow: Date.now(),
|
||||||
|
sizing: false,
|
||||||
|
peek: undefined as LocalProject | undefined,
|
||||||
|
peeked: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const editor = createInlineEditorController()
|
const editor = createInlineEditorController()
|
||||||
|
let token = 0
|
||||||
const setBusy = (directory: string, value: boolean) => {
|
const setBusy = (directory: string, value: boolean) => {
|
||||||
const key = workspaceKey(directory)
|
const key = workspaceKey(directory)
|
||||||
if (value) {
|
if (value) {
|
||||||
@@ -163,14 +157,13 @@ export default function Layout(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
const isBusy = (directory: string) => !!state.busyWorkspaces[workspaceKey(directory)]
|
const isBusy = (directory: string) => !!state.busyWorkspaces[workspaceKey(directory)]
|
||||||
const navLeave = { current: undefined as number | undefined }
|
const navLeave = { current: undefined as number | undefined }
|
||||||
const [sortNow, setSortNow] = createSignal(Date.now())
|
const sortNow = () => state.sortNow
|
||||||
const [sizing, setSizing] = createSignal(false)
|
|
||||||
let sizet: number | undefined
|
let sizet: number | undefined
|
||||||
let sortNowInterval: ReturnType<typeof setInterval> | undefined
|
let sortNowInterval: ReturnType<typeof setInterval> | undefined
|
||||||
const sortNowTimeout = setTimeout(
|
const sortNowTimeout = setTimeout(
|
||||||
() => {
|
() => {
|
||||||
setSortNow(Date.now())
|
setState("sortNow", Date.now())
|
||||||
sortNowInterval = setInterval(() => setSortNow(Date.now()), 60_000)
|
sortNowInterval = setInterval(() => setState("sortNow", Date.now()), 60_000)
|
||||||
},
|
},
|
||||||
60_000 - (Date.now() % 60_000),
|
60_000 - (Date.now() % 60_000),
|
||||||
)
|
)
|
||||||
@@ -196,7 +189,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const stop = () => setSizing(false)
|
const stop = () => setState("sizing", false)
|
||||||
window.addEventListener("pointerup", stop)
|
window.addEventListener("pointerup", stop)
|
||||||
window.addEventListener("pointercancel", stop)
|
window.addEventListener("pointercancel", stop)
|
||||||
window.addEventListener("blur", stop)
|
window.addEventListener("blur", stop)
|
||||||
@@ -234,8 +227,6 @@ export default function Layout(props: ParentProps) {
|
|||||||
}, 300)
|
}, 300)
|
||||||
}
|
}
|
||||||
|
|
||||||
const [peek, setPeek] = createSignal<LocalProject | undefined>(undefined)
|
|
||||||
const [peeked, setPeeked] = createSignal(false)
|
|
||||||
let peekt: number | undefined
|
let peekt: number | undefined
|
||||||
|
|
||||||
const hoverProjectData = createMemo(() => {
|
const hoverProjectData = createMemo(() => {
|
||||||
@@ -251,17 +242,17 @@ export default function Layout(props: ParentProps) {
|
|||||||
clearTimeout(peekt)
|
clearTimeout(peekt)
|
||||||
peekt = undefined
|
peekt = undefined
|
||||||
}
|
}
|
||||||
setPeek(p)
|
setState("peek", p)
|
||||||
setPeeked(true)
|
setState("peeked", true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setPeeked(false)
|
setState("peeked", false)
|
||||||
if (peek() === undefined) return
|
if (state.peek === undefined) return
|
||||||
if (peekt !== undefined) clearTimeout(peekt)
|
if (peekt !== undefined) clearTimeout(peekt)
|
||||||
peekt = window.setTimeout(() => {
|
peekt = window.setTimeout(() => {
|
||||||
peekt = undefined
|
peekt = undefined
|
||||||
setPeek(undefined)
|
setState("peek", undefined)
|
||||||
}, 180)
|
}, 180)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -272,6 +263,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
const autoselecting = createMemo(() => {
|
const autoselecting = createMemo(() => {
|
||||||
if (params.dir) return false
|
if (params.dir) return false
|
||||||
|
if (state.routing) return true
|
||||||
if (!state.autoselect) return false
|
if (!state.autoselect) return false
|
||||||
if (!pageReady()) return true
|
if (!pageReady()) return true
|
||||||
if (!layoutReady()) return true
|
if (!layoutReady()) return true
|
||||||
@@ -281,12 +273,16 @@ export default function Layout(props: ParentProps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!state.autoselect) return
|
if (!state.autoselect && !state.routing) return
|
||||||
const dir = params.dir
|
const dir = params.dir
|
||||||
if (!dir) return
|
if (!dir) return
|
||||||
const directory = decode64(dir)
|
const directory = decode64(dir)
|
||||||
if (!directory) return
|
if (!directory) return
|
||||||
setState("autoselect", false)
|
token += 1
|
||||||
|
batch(() => {
|
||||||
|
setState("autoselect", false)
|
||||||
|
setState("routing", false)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const editorOpen = editor.editorOpen
|
const editorOpen = editor.editorOpen
|
||||||
@@ -572,23 +568,32 @@ export default function Layout(props: ParentProps) {
|
|||||||
if (!value.ready) return
|
if (!value.ready) return
|
||||||
if (!value.layoutReady) return
|
if (!value.layoutReady) return
|
||||||
if (!state.autoselect) return
|
if (!state.autoselect) return
|
||||||
|
if (state.routing) return
|
||||||
if (value.dir) return
|
if (value.dir) return
|
||||||
|
|
||||||
const last = server.projects.last()
|
const last = server.projects.last()
|
||||||
|
const next =
|
||||||
if (value.list.length === 0) {
|
value.list.length === 0
|
||||||
if (!last) return
|
? last
|
||||||
setState("autoselect", false)
|
: (value.list.find((project) => project.worktree === last)?.worktree ?? value.list[0]?.worktree)
|
||||||
openProject(last, false)
|
|
||||||
navigateToProject(last)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const next = value.list.find((project) => project.worktree === last) ?? value.list[0]
|
|
||||||
if (!next) return
|
if (!next) return
|
||||||
setState("autoselect", false)
|
|
||||||
openProject(next.worktree, false)
|
const id = ++token
|
||||||
navigateToProject(next.worktree)
|
batch(() => {
|
||||||
|
setState("autoselect", false)
|
||||||
|
setState("routing", true)
|
||||||
|
})
|
||||||
|
void navigateToProject(next, () => id === token && !params.dir).then(
|
||||||
|
(navigated) => {
|
||||||
|
if (id !== token) return
|
||||||
|
if (navigated) return
|
||||||
|
setState("routing", false)
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
if (id !== token) return
|
||||||
|
setState("routing", false)
|
||||||
|
},
|
||||||
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -770,9 +775,11 @@ export default function Layout(props: ParentProps) {
|
|||||||
const next = items.map((x) => x.info).filter((m): m is Message => !!m?.id)
|
const next = items.map((x) => x.info).filter((m): m is Message => !!m?.id)
|
||||||
const sorted = mergeByID([], next)
|
const sorted = mergeByID([], next)
|
||||||
const stale = markPrefetched(directory, sessionID)
|
const stale = markPrefetched(directory, sessionID)
|
||||||
|
const cursor = messages.response.headers.get("x-next-cursor") ?? undefined
|
||||||
const meta = {
|
const meta = {
|
||||||
limit: prefetchChunk,
|
limit: sorted.length,
|
||||||
complete: sorted.length < prefetchChunk,
|
cursor,
|
||||||
|
complete: !cursor,
|
||||||
at: Date.now(),
|
at: Date.now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -846,10 +853,12 @@ export default function Layout(props: ParentProps) {
|
|||||||
|
|
||||||
const [store] = globalSync.child(directory, { bootstrap: false })
|
const [store] = globalSync.child(directory, { bootstrap: false })
|
||||||
const cached = untrack(() => {
|
const cached = untrack(() => {
|
||||||
if (store.message[session.id] === undefined) return false
|
|
||||||
const info = getSessionPrefetch(directory, session.id)
|
const info = getSessionPrefetch(directory, session.id)
|
||||||
if (!info) return false
|
return shouldSkipSessionPrefetch({
|
||||||
return Date.now() - info.at < SESSION_PREFETCH_TTL
|
message: store.message[session.id] !== undefined,
|
||||||
|
info,
|
||||||
|
chunk: prefetchChunk,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
if (cached) return
|
if (cached) return
|
||||||
|
|
||||||
@@ -1218,14 +1227,19 @@ export default function Layout(props: ParentProps) {
|
|||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
|
||||||
async function navigateToProject(directory: string | undefined) {
|
async function navigateToProject(directory: string | undefined, live = () => true) {
|
||||||
if (!directory) return
|
if (!directory || !live()) return false
|
||||||
const root = projectRoot(directory)
|
const root = projectRoot(directory)
|
||||||
server.projects.touch(root)
|
const touch = () => {
|
||||||
|
if (!live()) return false
|
||||||
|
layout.projects.open(root)
|
||||||
|
server.projects.touch(root)
|
||||||
|
return true
|
||||||
|
}
|
||||||
const project = layout.projects.list().find((item) => item.worktree === root)
|
const project = layout.projects.list().find((item) => item.worktree === root)
|
||||||
let dirs = project
|
const sandboxes =
|
||||||
? effectiveWorkspaceOrder(root, [root, ...(project.sandboxes ?? [])], store.workspaceOrder[root])
|
project?.sandboxes ?? globalSync.data.project.find((item) => item.worktree === root)?.sandboxes ?? []
|
||||||
: [root]
|
let dirs = effectiveWorkspaceOrder(root, [root, ...sandboxes], store.workspaceOrder[root])
|
||||||
const canOpen = (value: string | undefined) => {
|
const canOpen = (value: string | undefined) => {
|
||||||
if (!value) return false
|
if (!value) return false
|
||||||
return dirs.some((item) => workspaceKey(item) === workspaceKey(value))
|
return dirs.some((item) => workspaceKey(item) === workspaceKey(value))
|
||||||
@@ -1236,13 +1250,16 @@ export default function Layout(props: ParentProps) {
|
|||||||
.list({ directory: root })
|
.list({ directory: root })
|
||||||
.then((x) => x.data ?? [])
|
.then((x) => x.data ?? [])
|
||||||
.catch(() => [] as string[])
|
.catch(() => [] as string[])
|
||||||
|
if (!live()) return false
|
||||||
dirs = effectiveWorkspaceOrder(root, [root, ...listed], store.workspaceOrder[root])
|
dirs = effectiveWorkspaceOrder(root, [root, ...listed], store.workspaceOrder[root])
|
||||||
return canOpen(target)
|
return canOpen(target)
|
||||||
}
|
}
|
||||||
const openSession = async (target: { directory: string; id: string }) => {
|
const openSession = async (target: { directory: string; id: string }) => {
|
||||||
|
if (!live()) return false
|
||||||
if (!canOpen(target.directory)) return false
|
if (!canOpen(target.directory)) return false
|
||||||
const [data] = globalSync.child(target.directory, { bootstrap: false })
|
const [data] = globalSync.child(target.directory, { bootstrap: false })
|
||||||
if (data.session.some((item) => item.id === target.id)) {
|
if (data.session.some((item) => item.id === target.id)) {
|
||||||
|
if (!touch()) return false
|
||||||
setStore("lastProjectSession", root, { directory: target.directory, id: target.id, at: Date.now() })
|
setStore("lastProjectSession", root, { directory: target.directory, id: target.id, at: Date.now() })
|
||||||
navigateWithSidebarReset(`/${base64Encode(target.directory)}/session/${target.id}`)
|
navigateWithSidebarReset(`/${base64Encode(target.directory)}/session/${target.id}`)
|
||||||
return true
|
return true
|
||||||
@@ -1251,8 +1268,10 @@ export default function Layout(props: ParentProps) {
|
|||||||
.get({ sessionID: target.id })
|
.get({ sessionID: target.id })
|
||||||
.then((x) => x.data)
|
.then((x) => x.data)
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
|
if (!live()) return false
|
||||||
if (!resolved?.directory) return false
|
if (!resolved?.directory) return false
|
||||||
if (!canOpen(resolved.directory)) return false
|
if (!canOpen(resolved.directory)) return false
|
||||||
|
if (!touch()) return false
|
||||||
setStore("lastProjectSession", root, { directory: resolved.directory, id: resolved.id, at: Date.now() })
|
setStore("lastProjectSession", root, { directory: resolved.directory, id: resolved.id, at: Date.now() })
|
||||||
navigateWithSidebarReset(`/${base64Encode(resolved.directory)}/session/${resolved.id}`)
|
navigateWithSidebarReset(`/${base64Encode(resolved.directory)}/session/${resolved.id}`)
|
||||||
return true
|
return true
|
||||||
@@ -1261,19 +1280,23 @@ export default function Layout(props: ParentProps) {
|
|||||||
const projectSession = store.lastProjectSession[root]
|
const projectSession = store.lastProjectSession[root]
|
||||||
if (projectSession?.id) {
|
if (projectSession?.id) {
|
||||||
await refreshDirs(projectSession.directory)
|
await refreshDirs(projectSession.directory)
|
||||||
|
if (!live()) return false
|
||||||
const opened = await openSession(projectSession)
|
const opened = await openSession(projectSession)
|
||||||
if (opened) return
|
if (opened) return true
|
||||||
|
if (!live()) return false
|
||||||
clearLastProjectSession(root)
|
clearLastProjectSession(root)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!live()) return false
|
||||||
const latest = latestRootSession(
|
const latest = latestRootSession(
|
||||||
dirs.map((item) => globalSync.child(item, { bootstrap: false })[0]),
|
dirs.map((item) => globalSync.child(item, { bootstrap: false })[0]),
|
||||||
Date.now(),
|
Date.now(),
|
||||||
)
|
)
|
||||||
if (latest && (await openSession(latest))) {
|
if (latest && (await openSession(latest))) {
|
||||||
return
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!live()) return false
|
||||||
const fetched = latestRootSession(
|
const fetched = latestRootSession(
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
dirs.map(async (item) => ({
|
dirs.map(async (item) => ({
|
||||||
@@ -1286,11 +1309,14 @@ export default function Layout(props: ParentProps) {
|
|||||||
),
|
),
|
||||||
Date.now(),
|
Date.now(),
|
||||||
)
|
)
|
||||||
|
if (!live()) return false
|
||||||
if (fetched && (await openSession(fetched))) {
|
if (fetched && (await openSession(fetched))) {
|
||||||
return
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!touch()) return false
|
||||||
navigateWithSidebarReset(`/${base64Encode(root)}/session`)
|
navigateWithSidebarReset(`/${base64Encode(root)}/session`)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigateToSession(session: Session | undefined) {
|
function navigateToSession(session: Session | undefined) {
|
||||||
@@ -1968,7 +1994,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
"flex flex-col min-h-0 min-w-0 box-border rounded-tl-[12px] px-2": true,
|
"flex flex-col min-h-0 min-w-0 box-border rounded-tl-[12px] px-3": true,
|
||||||
"border border-b-0 border-border-weak-base": !merged(),
|
"border border-b-0 border-border-weak-base": !merged(),
|
||||||
"border-l border-t border-border-weaker-base": merged(),
|
"border-l border-t border-border-weaker-base": merged(),
|
||||||
"bg-background-base": merged() || hover(),
|
"bg-background-base": merged() || hover(),
|
||||||
@@ -1983,8 +2009,8 @@ export default function Layout(props: ParentProps) {
|
|||||||
<Show when={panelProps.project}>
|
<Show when={panelProps.project}>
|
||||||
{(p) => (
|
{(p) => (
|
||||||
<>
|
<>
|
||||||
<div class="shrink-0 px-2 py-1">
|
<div class="shrink-0 pl-1 py-1">
|
||||||
<div class="group/project flex items-start justify-between gap-2 p-2 pr-1">
|
<div class="group/project flex items-start justify-between gap-2 py-2 pl-2 pr-0">
|
||||||
<div class="flex flex-col min-w-0">
|
<div class="flex flex-col min-w-0">
|
||||||
<InlineEditor
|
<InlineEditor
|
||||||
id={`project:${projectId()}`}
|
id={`project:${projectId()}`}
|
||||||
@@ -2070,7 +2096,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
when={workspacesEnabled()}
|
when={workspacesEnabled()}
|
||||||
fallback={
|
fallback={
|
||||||
<>
|
<>
|
||||||
<div class="shrink-0 py-4 px-3">
|
<div class="shrink-0 py-4">
|
||||||
<Button
|
<Button
|
||||||
size="large"
|
size="large"
|
||||||
icon="plus-small"
|
icon="plus-small"
|
||||||
@@ -2093,7 +2119,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<div class="shrink-0 py-4 px-3">
|
<div class="shrink-0 py-4">
|
||||||
<Button size="large" icon="plus-small" class="w-full" onClick={() => createWorkspace(p())}>
|
<Button size="large" icon="plus-small" class="w-full" onClick={() => createWorkspace(p())}>
|
||||||
{language.t("workspace.new")}
|
{language.t("workspace.new")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -2166,7 +2192,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
{language.t("command.provider.connect")}
|
{language.t("command.provider.connect")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="large" variant="ghost" onClick={() => setStore("gettingStartedDismissed", true)}>
|
<Button size="large" variant="ghost" onClick={() => setStore("gettingStartedDismissed", true)}>
|
||||||
Not yet
|
{language.t("toast.update.action.notYet")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2203,8 +2229,8 @@ export default function Layout(props: ParentProps) {
|
|||||||
mobile ? (
|
mobile ? (
|
||||||
<SidebarPanel project={currentProject()} mobile />
|
<SidebarPanel project={currentProject()} mobile />
|
||||||
) : (
|
) : (
|
||||||
<Show when={currentProject()} keyed>
|
<Show when={currentProject()}>
|
||||||
{(project) => <SidebarPanel project={project} merged />}
|
<SidebarPanel project={currentProject()} merged />
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -2241,7 +2267,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
>
|
>
|
||||||
<div class="@container w-full h-full contain-strict">{sidebarContent()}</div>
|
<div class="@container w-full h-full contain-strict">{sidebarContent()}</div>
|
||||||
<Show when={layout.sidebar.opened()}>
|
<Show when={layout.sidebar.opened()}>
|
||||||
<div onPointerDown={() => setSizing(true)}>
|
<div onPointerDown={() => setState("sizing", true)}>
|
||||||
<ResizeHandle
|
<ResizeHandle
|
||||||
direction="horizontal"
|
direction="horizontal"
|
||||||
size={layout.sidebar.width()}
|
size={layout.sidebar.width()}
|
||||||
@@ -2249,9 +2275,9 @@ export default function Layout(props: ParentProps) {
|
|||||||
max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64}
|
max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64}
|
||||||
collapseThreshold={244}
|
collapseThreshold={244}
|
||||||
onResize={(w) => {
|
onResize={(w) => {
|
||||||
setSizing(true)
|
setState("sizing", true)
|
||||||
if (sizet !== undefined) clearTimeout(sizet)
|
if (sizet !== undefined) clearTimeout(sizet)
|
||||||
sizet = window.setTimeout(() => setSizing(false), 120)
|
sizet = window.setTimeout(() => setState("sizing", false), 120)
|
||||||
layout.sidebar.resize(w)
|
layout.sidebar.resize(w)
|
||||||
}}
|
}}
|
||||||
onCollapse={layout.sidebar.close}
|
onCollapse={layout.sidebar.close}
|
||||||
@@ -2296,7 +2322,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
"xl:inset-y-0 xl:right-0 xl:left-[var(--main-left)]": true,
|
"xl:inset-y-0 xl:right-0 xl:left-[var(--main-left)]": true,
|
||||||
"z-20": true,
|
"z-20": true,
|
||||||
"transition-[left] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[left] motion-reduce:transition-none":
|
"transition-[left] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[left] motion-reduce:transition-none":
|
||||||
!sizing(),
|
!state.sizing,
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
"--main-left": layout.sidebar.opened() ? `${Math.max(layout.sidebar.width(), 244)}px` : "4rem",
|
"--main-left": layout.sidebar.opened() ? `${Math.max(layout.sidebar.width(), 244)}px` : "4rem",
|
||||||
@@ -2316,11 +2342,11 @@ export default function Layout(props: ParentProps) {
|
|||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
"hidden xl:flex absolute inset-y-0 left-16 z-30": true,
|
"hidden xl:flex absolute inset-y-0 left-16 z-30": true,
|
||||||
"opacity-100 translate-x-0 pointer-events-auto": peeked() && !layout.sidebar.opened(),
|
"opacity-100 translate-x-0 pointer-events-auto": state.peeked && !layout.sidebar.opened(),
|
||||||
"opacity-0 -translate-x-2 pointer-events-none": !peeked() || layout.sidebar.opened(),
|
"opacity-0 -translate-x-2 pointer-events-none": !state.peeked || layout.sidebar.opened(),
|
||||||
"transition-[opacity,transform] motion-reduce:transition-none": true,
|
"transition-[opacity,transform] motion-reduce:transition-none": true,
|
||||||
"duration-180 ease-out": peeked() && !layout.sidebar.opened(),
|
"duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
|
||||||
"duration-120 ease-in": !peeked() || layout.sidebar.opened(),
|
"duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
|
||||||
}}
|
}}
|
||||||
onMouseMove={disarm}
|
onMouseMove={disarm}
|
||||||
onMouseEnter={() => {
|
onMouseEnter={() => {
|
||||||
@@ -2332,19 +2358,19 @@ export default function Layout(props: ParentProps) {
|
|||||||
arm()
|
arm()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show when={peek()} keyed>
|
<Show when={state.peek}>
|
||||||
{(project) => <SidebarPanel project={project} merged={false} />}
|
<SidebarPanel project={state.peek} merged={false} />
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
"hidden xl:block pointer-events-none absolute inset-y-0 right-0 z-25 overflow-hidden": true,
|
"hidden xl:block pointer-events-none absolute inset-y-0 right-0 z-25 overflow-hidden": true,
|
||||||
"opacity-100 translate-x-0": peeked() && !layout.sidebar.opened(),
|
"opacity-100 translate-x-0": state.peeked && !layout.sidebar.opened(),
|
||||||
"opacity-0 -translate-x-2": !peeked() || layout.sidebar.opened(),
|
"opacity-0 -translate-x-2": !state.peeked || layout.sidebar.opened(),
|
||||||
"transition-[opacity,transform] motion-reduce:transition-none": true,
|
"transition-[opacity,transform] motion-reduce:transition-none": true,
|
||||||
"duration-180 ease-out": peeked() && !layout.sidebar.opened(),
|
"duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
|
||||||
"duration-120 ease-in": !peeked() || layout.sidebar.opened(),
|
"duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
|
||||||
}}
|
}}
|
||||||
style={{ left: `calc(4rem + ${Math.max(Math.max(layout.sidebar.width(), 244) - 64, 0)}px)` }}
|
style={{ left: `calc(4rem + ${Math.max(Math.max(layout.sidebar.width(), 244) - 64, 0)}px)` }}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
|
|||||||
import { base64Encode } from "@opencode-ai/util/encode"
|
import { base64Encode } from "@opencode-ai/util/encode"
|
||||||
import { getFilename } from "@opencode-ai/util/path"
|
import { getFilename } from "@opencode-ai/util/path"
|
||||||
import { A, useNavigate, useParams } from "@solidjs/router"
|
import { A, useNavigate, useParams } from "@solidjs/router"
|
||||||
import { type Accessor, createMemo, For, type JSX, Match, onCleanup, Show, Switch } from "solid-js"
|
import { type Accessor, createEffect, createMemo, For, type JSX, on, onCleanup, Show } from "solid-js"
|
||||||
import { getSessionPrefetch, SESSION_PREFETCH_TTL } from "@/context/global-sync/session-prefetch"
|
import { createStore } from "solid-js/store"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout"
|
import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout"
|
||||||
@@ -102,46 +102,94 @@ const SessionRow = (props: {
|
|||||||
warmPress: () => void
|
warmPress: () => void
|
||||||
warmFocus: () => void
|
warmFocus: () => void
|
||||||
cancelHoverPrefetch: () => void
|
cancelHoverPrefetch: () => void
|
||||||
}): JSX.Element => (
|
}): JSX.Element => {
|
||||||
<A
|
const [slot, setSlot] = createStore({
|
||||||
href={`/${props.slug}/session/${props.session.id}`}
|
open: false,
|
||||||
class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] ${props.mobile ? "pr-7" : ""} group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`}
|
show: false,
|
||||||
onPointerDown={props.warmPress}
|
fade: false,
|
||||||
onPointerEnter={props.warmHover}
|
})
|
||||||
onPointerLeave={props.cancelHoverPrefetch}
|
|
||||||
onFocus={props.warmFocus}
|
let f: number | undefined
|
||||||
onClick={() => {
|
const clear = () => {
|
||||||
props.setHoverSession(undefined)
|
if (f !== undefined) window.clearTimeout(f)
|
||||||
if (props.sidebarOpened()) return
|
f = undefined
|
||||||
props.clearHoverProjectSoon()
|
}
|
||||||
}}
|
|
||||||
>
|
onCleanup(clear)
|
||||||
<div class="flex items-center gap-1 w-full">
|
createEffect(
|
||||||
<div
|
on(
|
||||||
class="shrink-0 size-6 flex items-center justify-center"
|
() => props.isWorking(),
|
||||||
style={{ color: props.tint() ?? "var(--icon-interactive-base)" }}
|
(on, prev) => {
|
||||||
>
|
clear()
|
||||||
<Switch fallback={<Icon name="dash" size="small" class="text-icon-weak" />}>
|
if (on) {
|
||||||
<Match when={props.isWorking()}>
|
setSlot({ open: true, show: true, fade: false })
|
||||||
<Spinner class="size-[15px]" />
|
return
|
||||||
</Match>
|
}
|
||||||
<Match when={props.hasPermissions()}>
|
if (prev) {
|
||||||
<div class="size-1.5 rounded-full bg-surface-warning-strong" />
|
setSlot({ open: false, show: true, fade: true })
|
||||||
</Match>
|
f = window.setTimeout(() => setSlot({ show: false, fade: false }), 260)
|
||||||
<Match when={props.hasError()}>
|
return
|
||||||
<div class="size-1.5 rounded-full bg-text-diff-delete-base" />
|
}
|
||||||
</Match>
|
setSlot({ open: false, show: false, fade: false })
|
||||||
<Match when={props.unseenCount() > 0}>
|
},
|
||||||
<div class="size-1.5 rounded-full bg-text-interactive-base" />
|
{ defer: true },
|
||||||
</Match>
|
),
|
||||||
</Switch>
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<A
|
||||||
|
href={`/${props.slug}/session/${props.session.id}`}
|
||||||
|
class={`relative flex items-center min-w-0 text-left w-full focus:outline-none transition-[padding] ${props.mobile ? "pr-7" : ""} group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`}
|
||||||
|
onPointerDown={props.warmPress}
|
||||||
|
onPointerEnter={props.warmHover}
|
||||||
|
onPointerLeave={props.cancelHoverPrefetch}
|
||||||
|
onFocus={props.warmFocus}
|
||||||
|
onClick={() => {
|
||||||
|
props.setHoverSession(undefined)
|
||||||
|
if (props.sidebarOpened()) return
|
||||||
|
props.clearHoverProjectSoon()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Show when={!props.isWorking() && (props.hasPermissions() || props.hasError() || props.unseenCount() > 0)}>
|
||||||
|
<div
|
||||||
|
classList={{
|
||||||
|
"absolute left-0 top-1/2 -translate-y-1/2 size-1.5 rounded-full": true,
|
||||||
|
"bg-surface-warning-strong": props.hasPermissions(),
|
||||||
|
"bg-text-diff-delete-base": !props.hasPermissions() && props.hasError(),
|
||||||
|
"bg-text-interactive-base": !props.hasPermissions() && !props.hasError() && props.unseenCount() > 0,
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div class="flex items-center min-w-0 grow-1">
|
||||||
|
<div
|
||||||
|
class="shrink-0 flex items-center justify-center overflow-hidden transition-[width,margin] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)]"
|
||||||
|
style={{
|
||||||
|
width: slot.open ? "16px" : "0px",
|
||||||
|
"margin-right": slot.open ? "8px" : "0px",
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<Show when={slot.show}>
|
||||||
|
<div
|
||||||
|
class="transition-opacity duration-200 ease-out"
|
||||||
|
classList={{
|
||||||
|
"opacity-0": slot.fade,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Spinner class="size-4" style={{ color: props.tint() ?? "var(--icon-interactive-base)" }} />
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate">
|
||||||
|
{props.session.title}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate">
|
</A>
|
||||||
{props.session.title}
|
)
|
||||||
</span>
|
}
|
||||||
</div>
|
|
||||||
</A>
|
|
||||||
)
|
|
||||||
|
|
||||||
const SessionHoverPreview = (props: {
|
const SessionHoverPreview = (props: {
|
||||||
mobile?: boolean
|
mobile?: boolean
|
||||||
@@ -205,8 +253,18 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
|||||||
})
|
})
|
||||||
const isWorking = createMemo(() => {
|
const isWorking = createMemo(() => {
|
||||||
if (hasPermissions()) return false
|
if (hasPermissions()) return false
|
||||||
|
const pending = (sessionStore.message[props.session.id] ?? []).findLast(
|
||||||
|
(message) =>
|
||||||
|
message.role === "assistant" &&
|
||||||
|
typeof (message as { time?: { completed?: unknown } }).time?.completed !== "number",
|
||||||
|
)
|
||||||
const status = sessionStore.session_status[props.session.id]
|
const status = sessionStore.session_status[props.session.id]
|
||||||
return status?.type === "busy" || status?.type === "retry"
|
return (
|
||||||
|
pending !== undefined ||
|
||||||
|
status?.type === "busy" ||
|
||||||
|
status?.type === "retry" ||
|
||||||
|
(status !== undefined && status.type !== "idle")
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const tint = createMemo(() => {
|
const tint = createMemo(() => {
|
||||||
@@ -228,13 +286,7 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
|||||||
const hoverMessages = createMemo(() =>
|
const hoverMessages = createMemo(() =>
|
||||||
sessionStore.message[props.session.id]?.filter((message): message is UserMessage => message.role === "user"),
|
sessionStore.message[props.session.id]?.filter((message): message is UserMessage => message.role === "user"),
|
||||||
)
|
)
|
||||||
const hoverReady = createMemo(() => {
|
const hoverReady = createMemo(() => hoverMessages() !== undefined)
|
||||||
if (sessionStore.message[props.session.id] === undefined) return false
|
|
||||||
if (props.session.id === params.id) return true
|
|
||||||
const info = getSessionPrefetch(props.session.directory, props.session.id)
|
|
||||||
if (!info) return false
|
|
||||||
return Date.now() - info.at < SESSION_PREFETCH_TTL
|
|
||||||
})
|
|
||||||
const hoverAllowed = createMemo(() => !props.mobile && props.sidebarExpanded())
|
const hoverAllowed = createMemo(() => !props.mobile && props.sidebarExpanded())
|
||||||
const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed())
|
const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed())
|
||||||
const isActive = createMemo(() => props.session.id === params.id)
|
const isActive = createMemo(() => props.session.id === params.id)
|
||||||
@@ -307,7 +359,7 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-session-id={props.session.id}
|
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 pl-3 pr-3 transition-colors
|
||||||
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"
|
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
|
<Show
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ const WorkspaceSessionList = (props: {
|
|||||||
loadMore: () => Promise<void>
|
loadMore: () => Promise<void>
|
||||||
language: ReturnType<typeof useLanguage>
|
language: ReturnType<typeof useLanguage>
|
||||||
}): JSX.Element => (
|
}): JSX.Element => (
|
||||||
<nav class="flex flex-col gap-1 px-3">
|
<nav class="flex flex-col gap-1">
|
||||||
<Show when={props.showNew()}>
|
<Show when={props.showNew()}>
|
||||||
<NewSessionItem
|
<NewSessionItem
|
||||||
slug={props.slug()}
|
slug={props.slug()}
|
||||||
@@ -382,7 +382,7 @@ export const SortableWorkspace = (props: {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Collapsible variant="ghost" open={open()} class="shrink-0" onOpenChange={openWrapper}>
|
<Collapsible variant="ghost" open={open()} class="shrink-0" onOpenChange={openWrapper}>
|
||||||
<div class="px-2 py-1">
|
<div class="py-1">
|
||||||
<div
|
<div
|
||||||
class="group/workspace relative"
|
class="group/workspace relative"
|
||||||
data-component="workspace-item"
|
data-component="workspace-item"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Project, UserMessage } from "@opencode-ai/sdk/v2"
|
import type { Project, UserMessage } from "@opencode-ai/sdk/v2"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import {
|
import {
|
||||||
|
batch,
|
||||||
onCleanup,
|
onCleanup,
|
||||||
Show,
|
Show,
|
||||||
Match,
|
Match,
|
||||||
@@ -34,8 +35,10 @@ import { useLanguage } from "@/context/language"
|
|||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { usePrompt } from "@/context/prompt"
|
import { usePrompt } from "@/context/prompt"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
|
import { useSettings } from "@/context/settings"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useTerminal } from "@/context/terminal"
|
import { useTerminal } from "@/context/terminal"
|
||||||
|
import { type FollowupDraft, sendFollowupDraft } from "@/components/prompt-input/submit"
|
||||||
import { createSessionComposerState, SessionComposerRegion } from "@/pages/session/composer"
|
import { createSessionComposerState, SessionComposerRegion } from "@/pages/session/composer"
|
||||||
import { createOpenReviewFile, createSessionTabs, createSizing, focusTerminalById } from "@/pages/session/helpers"
|
import { createOpenReviewFile, createSessionTabs, createSizing, focusTerminalById } from "@/pages/session/helpers"
|
||||||
import { MessageTimeline } from "@/pages/session/message-timeline"
|
import { MessageTimeline } from "@/pages/session/message-timeline"
|
||||||
@@ -46,15 +49,18 @@ import { SessionSidePanel } from "@/pages/session/session-side-panel"
|
|||||||
import { TerminalPanel } from "@/pages/session/terminal-panel"
|
import { TerminalPanel } from "@/pages/session/terminal-panel"
|
||||||
import { useSessionCommands } from "@/pages/session/use-session-commands"
|
import { useSessionCommands } from "@/pages/session/use-session-commands"
|
||||||
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
|
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
|
||||||
|
import { Identifier } from "@/utils/id"
|
||||||
import { extractPromptFromParts } from "@/utils/prompt"
|
import { extractPromptFromParts } from "@/utils/prompt"
|
||||||
import { same } from "@/utils/same"
|
import { same } from "@/utils/same"
|
||||||
import { formatServerError } from "@/utils/server-errors"
|
import { formatServerError } from "@/utils/server-errors"
|
||||||
|
|
||||||
const emptyUserMessages: UserMessage[] = []
|
const emptyUserMessages: UserMessage[] = []
|
||||||
|
const emptyFollowups: (FollowupDraft & { id: string })[] = []
|
||||||
|
|
||||||
type SessionHistoryWindowInput = {
|
type SessionHistoryWindowInput = {
|
||||||
sessionID: () => string | undefined
|
sessionID: () => string | undefined
|
||||||
messagesReady: () => boolean
|
messagesReady: () => boolean
|
||||||
|
loaded: () => number
|
||||||
visibleUserMessages: () => UserMessage[]
|
visibleUserMessages: () => UserMessage[]
|
||||||
historyMore: () => boolean
|
historyMore: () => boolean
|
||||||
historyLoading: () => boolean
|
historyLoading: () => boolean
|
||||||
@@ -152,23 +158,39 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
|||||||
|
|
||||||
const start = turnStart()
|
const start = turnStart()
|
||||||
const beforeVisible = input.visibleUserMessages().length
|
const beforeVisible = input.visibleUserMessages().length
|
||||||
|
let loaded = input.loaded()
|
||||||
|
|
||||||
if (start > 0) setTurnStart(0)
|
if (start > 0) setTurnStart(0)
|
||||||
|
|
||||||
if (!input.historyMore() || input.historyLoading()) return
|
if (!input.historyMore() || input.historyLoading()) return
|
||||||
|
|
||||||
await input.loadMore(id)
|
let afterVisible = beforeVisible
|
||||||
if (input.sessionID() !== id) return
|
let added = 0
|
||||||
|
|
||||||
const afterVisible = input.visibleUserMessages().length
|
while (true) {
|
||||||
const growth = afterVisible - beforeVisible
|
await input.loadMore(id)
|
||||||
|
if (input.sessionID() !== id) return
|
||||||
|
|
||||||
|
afterVisible = input.visibleUserMessages().length
|
||||||
|
const nextLoaded = input.loaded()
|
||||||
|
const raw = nextLoaded - loaded
|
||||||
|
added += raw
|
||||||
|
loaded = nextLoaded
|
||||||
|
|
||||||
|
if (afterVisible > beforeVisible) break
|
||||||
|
if (raw <= 0) break
|
||||||
|
if (!input.historyMore()) break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (added <= 0) return
|
||||||
if (state.prefetchNoGrowth) setState("prefetchNoGrowth", 0)
|
if (state.prefetchNoGrowth) setState("prefetchNoGrowth", 0)
|
||||||
|
|
||||||
|
const growth = afterVisible - beforeVisible
|
||||||
if (growth <= 0) return
|
if (growth <= 0) return
|
||||||
if (turnStart() !== 0) return
|
if (turnStart() !== 0) return
|
||||||
|
|
||||||
const target = Math.min(afterVisible, Math.max(beforeVisible, renderedUserMessages().length) + turnBatch)
|
const target = Math.min(afterVisible, beforeVisible + turnBatch)
|
||||||
const nextStart = Math.max(0, afterVisible - target)
|
setTurnStart(Math.max(0, afterVisible - target))
|
||||||
preserveScroll(() => setTurnStart(nextStart))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Scroll/prefetch path: fetch older history from server. */
|
/** Scroll/prefetch path: fetch older history from server. */
|
||||||
@@ -187,19 +209,35 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
|||||||
const start = turnStart()
|
const start = turnStart()
|
||||||
const beforeVisible = input.visibleUserMessages().length
|
const beforeVisible = input.visibleUserMessages().length
|
||||||
const beforeRendered = start <= 0 ? beforeVisible : renderedUserMessages().length
|
const beforeRendered = start <= 0 ? beforeVisible : renderedUserMessages().length
|
||||||
|
let loaded = input.loaded()
|
||||||
|
let added = 0
|
||||||
|
let growth = 0
|
||||||
|
|
||||||
await input.loadMore(id)
|
while (true) {
|
||||||
if (input.sessionID() !== id) return
|
await input.loadMore(id)
|
||||||
|
if (input.sessionID() !== id) return
|
||||||
|
|
||||||
|
const nextLoaded = input.loaded()
|
||||||
|
const raw = nextLoaded - loaded
|
||||||
|
added += raw
|
||||||
|
loaded = nextLoaded
|
||||||
|
growth = input.visibleUserMessages().length - beforeVisible
|
||||||
|
|
||||||
|
if (growth > 0) break
|
||||||
|
if (raw <= 0) break
|
||||||
|
if (opts?.prefetch) break
|
||||||
|
if (!input.historyMore()) break
|
||||||
|
}
|
||||||
|
|
||||||
const afterVisible = input.visibleUserMessages().length
|
const afterVisible = input.visibleUserMessages().length
|
||||||
const growth = afterVisible - beforeVisible
|
|
||||||
|
|
||||||
if (opts?.prefetch) {
|
if (opts?.prefetch) {
|
||||||
setState("prefetchNoGrowth", growth > 0 ? 0 : state.prefetchNoGrowth + 1)
|
setState("prefetchNoGrowth", added > 0 ? 0 : state.prefetchNoGrowth + 1)
|
||||||
} else if (growth > 0 && state.prefetchNoGrowth) {
|
} else if (added > 0 && state.prefetchNoGrowth) {
|
||||||
setState("prefetchNoGrowth", 0)
|
setState("prefetchNoGrowth", 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (added <= 0) return
|
||||||
if (growth <= 0) return
|
if (growth <= 0) return
|
||||||
if (turnStart() !== start) return
|
if (turnStart() !== start) return
|
||||||
|
|
||||||
@@ -269,6 +307,7 @@ export default function Page() {
|
|||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
|
const settings = useSettings()
|
||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
const comments = useComments()
|
const comments = useComments()
|
||||||
const terminal = useTerminal()
|
const terminal = useTerminal()
|
||||||
@@ -291,6 +330,7 @@ export default function Page() {
|
|||||||
git: false,
|
git: false,
|
||||||
pendingMessage: undefined as string | undefined,
|
pendingMessage: undefined as string | undefined,
|
||||||
restoring: undefined as string | undefined,
|
restoring: undefined as string | undefined,
|
||||||
|
reverting: false,
|
||||||
reviewSnap: false,
|
reviewSnap: false,
|
||||||
scrollGesture: 0,
|
scrollGesture: 0,
|
||||||
scroll: {
|
scroll: {
|
||||||
@@ -464,6 +504,17 @@ export default function Page() {
|
|||||||
deferRender: false,
|
deferRender: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const [followup, setFollowup] = createStore({
|
||||||
|
items: {} as Record<string, (FollowupDraft & { id: string })[] | undefined>,
|
||||||
|
sending: {} as Record<string, string | undefined>,
|
||||||
|
failed: {} as Record<string, string | undefined>,
|
||||||
|
paused: {} as Record<string, boolean | undefined>,
|
||||||
|
edit: {} as Record<
|
||||||
|
string,
|
||||||
|
{ id: string; prompt: FollowupDraft["prompt"]; context: FollowupDraft["context"] } | undefined
|
||||||
|
>,
|
||||||
|
})
|
||||||
|
|
||||||
createComputed((prev) => {
|
createComputed((prev) => {
|
||||||
const key = sessionKey()
|
const key = sessionKey()
|
||||||
if (key !== prev) {
|
if (key !== prev) {
|
||||||
@@ -905,13 +956,15 @@ export default function Page() {
|
|||||||
return (
|
return (
|
||||||
<div class={input.emptyClass}>
|
<div class={input.emptyClass}>
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<div class="text-14-medium text-text-strong">Create a Git repository</div>
|
<div class="text-14-medium text-text-strong">{language.t("session.review.noVcs.createGit.title")}</div>
|
||||||
<div class="text-14-regular text-text-base max-w-md" style={{ "line-height": "var(--line-height-normal)" }}>
|
<div class="text-14-regular text-text-base max-w-md" style={{ "line-height": "var(--line-height-normal)" }}>
|
||||||
Track, review, and undo changes in this project
|
{language.t("session.review.noVcs.createGit.description")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button size="large" disabled={ui.git} onClick={initGit}>
|
<Button size="large" disabled={ui.git} onClick={initGit}>
|
||||||
{ui.git ? "Creating Git repository..." : "Create Git repository"}
|
{ui.git
|
||||||
|
? language.t("session.review.noVcs.createGit.actionLoading")
|
||||||
|
: language.t("session.review.noVcs.createGit.action")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -1143,6 +1196,7 @@ export default function Page() {
|
|||||||
|
|
||||||
let scrollStateFrame: number | undefined
|
let scrollStateFrame: number | undefined
|
||||||
let scrollStateTarget: HTMLDivElement | undefined
|
let scrollStateTarget: HTMLDivElement | undefined
|
||||||
|
let fillFrame: number | undefined
|
||||||
|
|
||||||
const updateScrollState = (el: HTMLDivElement) => {
|
const updateScrollState = (el: HTMLDivElement) => {
|
||||||
const max = el.scrollHeight - el.clientHeight
|
const max = el.scrollHeight - el.clientHeight
|
||||||
@@ -1190,10 +1244,14 @@ export default function Page() {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let fill = () => {}
|
||||||
|
|
||||||
const setScrollRef = (el: HTMLDivElement | undefined) => {
|
const setScrollRef = (el: HTMLDivElement | undefined) => {
|
||||||
scroller = el
|
scroller = el
|
||||||
autoScroll.scrollRef(el)
|
autoScroll.scrollRef(el)
|
||||||
if (el) scheduleScrollState(el)
|
if (!el) return
|
||||||
|
scheduleScrollState(el)
|
||||||
|
fill()
|
||||||
}
|
}
|
||||||
|
|
||||||
const markUserScroll = () => {
|
const markUserScroll = () => {
|
||||||
@@ -1205,12 +1263,14 @@ export default function Page() {
|
|||||||
() => {
|
() => {
|
||||||
const el = scroller
|
const el = scroller
|
||||||
if (el) scheduleScrollState(el)
|
if (el) scheduleScrollState(el)
|
||||||
|
fill()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const historyWindow = createSessionHistoryWindow({
|
const historyWindow = createSessionHistoryWindow({
|
||||||
sessionID: () => params.id,
|
sessionID: () => params.id,
|
||||||
messagesReady,
|
messagesReady,
|
||||||
|
loaded: () => messages().length,
|
||||||
visibleUserMessages,
|
visibleUserMessages,
|
||||||
historyMore,
|
historyMore,
|
||||||
historyLoading,
|
historyLoading,
|
||||||
@@ -1219,6 +1279,45 @@ export default function Page() {
|
|||||||
scroller: () => scroller,
|
scroller: () => scroller,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
fill = () => {
|
||||||
|
if (fillFrame !== undefined) return
|
||||||
|
|
||||||
|
fillFrame = requestAnimationFrame(() => {
|
||||||
|
fillFrame = undefined
|
||||||
|
|
||||||
|
if (!params.id || !messagesReady()) return
|
||||||
|
if (autoScroll.userScrolled() || historyLoading()) return
|
||||||
|
|
||||||
|
const el = scroller
|
||||||
|
if (!el) return
|
||||||
|
if (el.scrollHeight > el.clientHeight + 1) return
|
||||||
|
if (historyWindow.turnStart() <= 0 && !historyMore()) return
|
||||||
|
|
||||||
|
void historyWindow.loadAndReveal()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
() =>
|
||||||
|
[
|
||||||
|
params.id,
|
||||||
|
messagesReady(),
|
||||||
|
historyWindow.turnStart(),
|
||||||
|
historyMore(),
|
||||||
|
historyLoading(),
|
||||||
|
autoScroll.userScrolled(),
|
||||||
|
visibleUserMessages().length,
|
||||||
|
] as const,
|
||||||
|
([id, ready, start, more, loading, scrolled]) => {
|
||||||
|
if (!id || !ready || loading || scrolled) return
|
||||||
|
if (start <= 0 && !more) return
|
||||||
|
fill()
|
||||||
|
},
|
||||||
|
{ defer: true },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
const draft = (id: string) =>
|
const draft = (id: string) =>
|
||||||
extractPromptFromParts(sync.data.part[id] ?? [], {
|
extractPromptFromParts(sync.data.part[id] ?? [], {
|
||||||
directory: sdk.directory,
|
directory: sdk.directory,
|
||||||
@@ -1243,13 +1342,136 @@ export default function Page() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const merge = (next: NonNullable<ReturnType<typeof info>>) =>
|
||||||
|
sync.set("session", (list) => {
|
||||||
|
const idx = list.findIndex((item) => item.id === next.id)
|
||||||
|
if (idx < 0) return list
|
||||||
|
const out = list.slice()
|
||||||
|
out[idx] = next
|
||||||
|
return out
|
||||||
|
})
|
||||||
|
|
||||||
|
const roll = (sessionID: string, next: NonNullable<ReturnType<typeof info>>["revert"]) =>
|
||||||
|
sync.set("session", (list) => {
|
||||||
|
const idx = list.findIndex((item) => item.id === sessionID)
|
||||||
|
if (idx < 0) return list
|
||||||
|
const out = list.slice()
|
||||||
|
out[idx] = { ...out[idx], revert: next }
|
||||||
|
return out
|
||||||
|
})
|
||||||
|
|
||||||
const busy = (sessionID: string) => {
|
const busy = (sessionID: string) => {
|
||||||
if (sync.data.session_status[sessionID]?.type !== "idle") return true
|
if ((sync.data.session_status[sessionID] ?? { type: "idle" as const }).type !== "idle") return true
|
||||||
return (sync.data.message[sessionID] ?? []).some(
|
return (sync.data.message[sessionID] ?? []).some(
|
||||||
(item) => item.role === "assistant" && typeof item.time.completed !== "number",
|
(item) => item.role === "assistant" && typeof item.time.completed !== "number",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const queuedFollowups = createMemo(() => {
|
||||||
|
const id = params.id
|
||||||
|
if (!id) return emptyFollowups
|
||||||
|
return followup.items[id] ?? emptyFollowups
|
||||||
|
})
|
||||||
|
|
||||||
|
const editingFollowup = createMemo(() => {
|
||||||
|
const id = params.id
|
||||||
|
if (!id) return
|
||||||
|
return followup.edit[id]
|
||||||
|
})
|
||||||
|
|
||||||
|
const sendingFollowup = createMemo(() => {
|
||||||
|
const id = params.id
|
||||||
|
if (!id) return
|
||||||
|
return followup.sending[id]
|
||||||
|
})
|
||||||
|
|
||||||
|
const queueEnabled = createMemo(() => {
|
||||||
|
const id = params.id
|
||||||
|
if (!id) return false
|
||||||
|
return settings.general.followup() === "queue" && busy(id) && !composer.blocked()
|
||||||
|
})
|
||||||
|
|
||||||
|
const followupText = (item: FollowupDraft) => {
|
||||||
|
const text = item.prompt
|
||||||
|
.map((part) => {
|
||||||
|
if (part.type === "image") return `[image:${part.filename}]`
|
||||||
|
if (part.type === "file") return `[file:${part.path}]`
|
||||||
|
if (part.type === "agent") return `@${part.name}`
|
||||||
|
return part.content
|
||||||
|
})
|
||||||
|
.join("")
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.find((line) => !!line)
|
||||||
|
|
||||||
|
if (text) return text
|
||||||
|
return `[${language.t("common.attachment")}]`
|
||||||
|
}
|
||||||
|
|
||||||
|
const queueFollowup = (draft: FollowupDraft) => {
|
||||||
|
setFollowup("items", draft.sessionID, (items) => [
|
||||||
|
...(items ?? []),
|
||||||
|
{ id: Identifier.ascending("message"), ...draft },
|
||||||
|
])
|
||||||
|
setFollowup("failed", draft.sessionID, undefined)
|
||||||
|
setFollowup("paused", draft.sessionID, undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
const followupDock = createMemo(() => queuedFollowups().map((item) => ({ id: item.id, text: followupText(item) })))
|
||||||
|
|
||||||
|
const sendFollowup = (sessionID: string, id: string, opts?: { manual?: boolean }) => {
|
||||||
|
const item = (followup.items[sessionID] ?? []).find((entry) => entry.id === id)
|
||||||
|
if (!item) return Promise.resolve()
|
||||||
|
if (followup.sending[sessionID]) return Promise.resolve()
|
||||||
|
|
||||||
|
if (opts?.manual) setFollowup("paused", sessionID, undefined)
|
||||||
|
setFollowup("sending", sessionID, id)
|
||||||
|
setFollowup("failed", sessionID, undefined)
|
||||||
|
|
||||||
|
return sendFollowupDraft({
|
||||||
|
client: sdk.client,
|
||||||
|
sync,
|
||||||
|
globalSync,
|
||||||
|
draft: item,
|
||||||
|
optimisticBusy: item.sessionDirectory === sdk.directory,
|
||||||
|
})
|
||||||
|
.then((ok) => {
|
||||||
|
if (ok === false) return
|
||||||
|
setFollowup("items", sessionID, (items) => (items ?? []).filter((entry) => entry.id !== id))
|
||||||
|
if (opts?.manual) resumeScroll()
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setFollowup("failed", sessionID, id)
|
||||||
|
fail(err)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setFollowup("sending", sessionID, (value) => (value === id ? undefined : value))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const editFollowup = (id: string) => {
|
||||||
|
const sessionID = params.id
|
||||||
|
if (!sessionID) return
|
||||||
|
if (followup.sending[sessionID]) return
|
||||||
|
|
||||||
|
const item = queuedFollowups().find((entry) => entry.id === id)
|
||||||
|
if (!item) return
|
||||||
|
|
||||||
|
setFollowup("items", sessionID, (items) => (items ?? []).filter((entry) => entry.id !== id))
|
||||||
|
setFollowup("failed", sessionID, (value) => (value === id ? undefined : value))
|
||||||
|
setFollowup("edit", sessionID, {
|
||||||
|
id: item.id,
|
||||||
|
prompt: item.prompt,
|
||||||
|
context: item.context,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearFollowupEdit = () => {
|
||||||
|
const id = params.id
|
||||||
|
if (!id) return
|
||||||
|
setFollowup("edit", id, undefined)
|
||||||
|
}
|
||||||
|
|
||||||
const halt = (sessionID: string) =>
|
const halt = (sessionID: string) =>
|
||||||
busy(sessionID) ? sdk.client.session.abort({ sessionID }).catch(() => {}) : Promise.resolve()
|
busy(sessionID) ? sdk.client.session.abort({ sessionID }).catch(() => {}) : Promise.resolve()
|
||||||
|
|
||||||
@@ -1275,42 +1497,77 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const revert = (input: { sessionID: string; messageID: string }) => {
|
const revert = (input: { sessionID: string; messageID: string }) => {
|
||||||
|
if (ui.reverting || ui.restoring) return
|
||||||
|
const prev = prompt.current().slice()
|
||||||
|
const last = info()?.revert
|
||||||
const value = draft(input.messageID)
|
const value = draft(input.messageID)
|
||||||
|
batch(() => {
|
||||||
|
setUi("reverting", true)
|
||||||
|
roll(input.sessionID, { messageID: input.messageID })
|
||||||
|
prompt.set(value)
|
||||||
|
})
|
||||||
return halt(input.sessionID)
|
return halt(input.sessionID)
|
||||||
.then(() => sdk.client.session.revert(input))
|
.then(() => sdk.client.session.revert(input))
|
||||||
.then(() => {
|
.then((result) => {
|
||||||
prompt.set(value)
|
if (result.data) merge(result.data)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
batch(() => {
|
||||||
|
roll(input.sessionID, last)
|
||||||
|
prompt.set(prev)
|
||||||
|
})
|
||||||
|
fail(err)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setUi("reverting", false)
|
||||||
})
|
})
|
||||||
.catch(fail)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const restore = (id: string) => {
|
const restore = (id: string) => {
|
||||||
const sessionID = params.id
|
const sessionID = params.id
|
||||||
if (!sessionID || ui.restoring) return
|
if (!sessionID || ui.restoring || ui.reverting) return
|
||||||
|
|
||||||
const next = userMessages().find((item) => item.id > id)
|
const next = userMessages().find((item) => item.id > id)
|
||||||
setUi("restoring", id)
|
const prev = prompt.current().slice()
|
||||||
|
const last = info()?.revert
|
||||||
|
|
||||||
|
batch(() => {
|
||||||
|
setUi("restoring", id)
|
||||||
|
setUi("reverting", true)
|
||||||
|
roll(sessionID, next ? { messageID: next.id } : undefined)
|
||||||
|
if (next) {
|
||||||
|
prompt.set(draft(next.id))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
prompt.reset()
|
||||||
|
})
|
||||||
|
|
||||||
const task = !next
|
const task = !next
|
||||||
? halt(sessionID)
|
? halt(sessionID).then(() => sdk.client.session.unrevert({ sessionID }))
|
||||||
.then(() => sdk.client.session.unrevert({ sessionID }))
|
: halt(sessionID).then(() =>
|
||||||
.then(() => {
|
sdk.client.session.revert({
|
||||||
prompt.reset()
|
sessionID,
|
||||||
})
|
messageID: next.id,
|
||||||
: halt(sessionID)
|
}),
|
||||||
.then(() =>
|
)
|
||||||
sdk.client.session.revert({
|
|
||||||
sessionID,
|
|
||||||
messageID: next.id,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
prompt.set(draft(next.id))
|
|
||||||
})
|
|
||||||
|
|
||||||
return task.catch(fail).finally(() => {
|
return task
|
||||||
setUi("restoring", (value) => (value === id ? undefined : value))
|
.then((result) => {
|
||||||
})
|
if (result.data) merge(result.data)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
batch(() => {
|
||||||
|
roll(sessionID, last)
|
||||||
|
prompt.set(prev)
|
||||||
|
})
|
||||||
|
fail(err)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
batch(() => {
|
||||||
|
setUi("restoring", (value) => (value === id ? undefined : value))
|
||||||
|
setUi("reverting", false)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const rolled = createMemo(() => {
|
const rolled = createMemo(() => {
|
||||||
@@ -1323,6 +1580,21 @@ export default function Page() {
|
|||||||
|
|
||||||
const actions = { fork, revert }
|
const actions = { fork, revert }
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const sessionID = params.id
|
||||||
|
if (!sessionID) return
|
||||||
|
|
||||||
|
const item = queuedFollowups()[0]
|
||||||
|
if (!item) return
|
||||||
|
if (followup.sending[sessionID]) return
|
||||||
|
if (followup.failed[sessionID] === item.id) return
|
||||||
|
if (followup.paused[sessionID]) return
|
||||||
|
if (composer.blocked()) return
|
||||||
|
if (busy(sessionID)) return
|
||||||
|
|
||||||
|
void sendFollowup(sessionID, item.id)
|
||||||
|
})
|
||||||
|
|
||||||
createResizeObserver(
|
createResizeObserver(
|
||||||
() => promptDock,
|
() => promptDock,
|
||||||
({ height }) => {
|
({ height }) => {
|
||||||
@@ -1341,6 +1613,7 @@ export default function Page() {
|
|||||||
if (stick) autoScroll.forceScrollToBottom()
|
if (stick) autoScroll.forceScrollToBottom()
|
||||||
|
|
||||||
if (el) scheduleScrollState(el)
|
if (el) scheduleScrollState(el)
|
||||||
|
fill()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1374,6 +1647,7 @@ export default function Page() {
|
|||||||
if (diffFrame !== undefined) cancelAnimationFrame(diffFrame)
|
if (diffFrame !== undefined) cancelAnimationFrame(diffFrame)
|
||||||
if (diffTimer !== undefined) window.clearTimeout(diffTimer)
|
if (diffTimer !== undefined) window.clearTimeout(diffTimer)
|
||||||
if (scrollStateFrame !== undefined) cancelAnimationFrame(scrollStateFrame)
|
if (scrollStateFrame !== undefined) cancelAnimationFrame(scrollStateFrame)
|
||||||
|
if (fillFrame !== undefined) cancelAnimationFrame(fillFrame)
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1482,11 +1756,33 @@ export default function Page() {
|
|||||||
resumeScroll()
|
resumeScroll()
|
||||||
}}
|
}}
|
||||||
onResponseSubmit={resumeScroll}
|
onResponseSubmit={resumeScroll}
|
||||||
|
followup={
|
||||||
|
params.id
|
||||||
|
? {
|
||||||
|
queue: queueEnabled,
|
||||||
|
items: followupDock(),
|
||||||
|
sending: sendingFollowup(),
|
||||||
|
edit: editingFollowup(),
|
||||||
|
onQueue: queueFollowup,
|
||||||
|
onAbort: () => {
|
||||||
|
const id = params.id
|
||||||
|
if (!id) return
|
||||||
|
setFollowup("paused", id, true)
|
||||||
|
},
|
||||||
|
onSend: (id) => {
|
||||||
|
void sendFollowup(params.id!, id, { manual: true })
|
||||||
|
},
|
||||||
|
onEdit: editFollowup,
|
||||||
|
onEditLoaded: clearFollowupEdit,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
revert={
|
revert={
|
||||||
rolled().length > 0
|
rolled().length > 0
|
||||||
? {
|
? {
|
||||||
items: rolled(),
|
items: rolled(),
|
||||||
restoring: ui.restoring,
|
restoring: ui.restoring,
|
||||||
|
disabled: ui.reverting,
|
||||||
onRestore: restore,
|
onRestore: restore,
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import { getSessionHandoff, setSessionHandoff } from "@/pages/session/handoff"
|
|||||||
import { useSessionKey } from "@/pages/session/session-layout"
|
import { useSessionKey } from "@/pages/session/session-layout"
|
||||||
import { SessionPermissionDock } from "@/pages/session/composer/session-permission-dock"
|
import { SessionPermissionDock } from "@/pages/session/composer/session-permission-dock"
|
||||||
import { SessionQuestionDock } from "@/pages/session/composer/session-question-dock"
|
import { SessionQuestionDock } from "@/pages/session/composer/session-question-dock"
|
||||||
|
import { SessionFollowupDock } from "@/pages/session/composer/session-followup-dock"
|
||||||
import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock"
|
import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock"
|
||||||
import type { SessionComposerState } from "@/pages/session/composer/session-composer-state"
|
import type { SessionComposerState } from "@/pages/session/composer/session-composer-state"
|
||||||
import { SessionTodoDock } from "@/pages/session/composer/session-todo-dock"
|
import { SessionTodoDock } from "@/pages/session/composer/session-todo-dock"
|
||||||
|
import type { FollowupDraft } from "@/components/prompt-input/submit"
|
||||||
|
|
||||||
export function SessionComposerRegion(props: {
|
export function SessionComposerRegion(props: {
|
||||||
state: SessionComposerState
|
state: SessionComposerState
|
||||||
@@ -21,18 +23,30 @@ export function SessionComposerRegion(props: {
|
|||||||
onNewSessionWorktreeReset: () => void
|
onNewSessionWorktreeReset: () => void
|
||||||
onSubmit: () => void
|
onSubmit: () => void
|
||||||
onResponseSubmit: () => void
|
onResponseSubmit: () => void
|
||||||
|
followup?: {
|
||||||
|
queue: () => boolean
|
||||||
|
items: { id: string; text: string }[]
|
||||||
|
sending?: string
|
||||||
|
edit?: { id: string; prompt: FollowupDraft["prompt"]; context: FollowupDraft["context"] }
|
||||||
|
onQueue: (draft: FollowupDraft) => void
|
||||||
|
onAbort: () => void
|
||||||
|
onSend: (id: string) => void
|
||||||
|
onEdit: (id: string) => void
|
||||||
|
onEditLoaded: () => void
|
||||||
|
}
|
||||||
revert?: {
|
revert?: {
|
||||||
items: { id: string; text: string }[]
|
items: { id: string; text: string }[]
|
||||||
restoring?: string
|
restoring?: string
|
||||||
|
disabled?: boolean
|
||||||
onRestore: (id: string) => void
|
onRestore: (id: string) => void
|
||||||
}
|
}
|
||||||
setPromptDockRef: (el: HTMLDivElement) => void
|
setPromptDockRef: (el: HTMLDivElement) => void
|
||||||
}) {
|
}) {
|
||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const { sessionKey } = useSessionKey()
|
const route = useSessionKey()
|
||||||
|
|
||||||
const handoffPrompt = createMemo(() => getSessionHandoff(sessionKey())?.prompt)
|
const handoffPrompt = createMemo(() => getSessionHandoff(route.sessionKey())?.prompt)
|
||||||
|
|
||||||
const previewPrompt = () =>
|
const previewPrompt = () =>
|
||||||
prompt
|
prompt
|
||||||
@@ -48,7 +62,7 @@ export function SessionComposerRegion(props: {
|
|||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!prompt.ready()) return
|
if (!prompt.ready()) return
|
||||||
setSessionHandoff(sessionKey(), { prompt: previewPrompt() })
|
setSessionHandoff(route.sessionKey(), { prompt: previewPrompt() })
|
||||||
})
|
})
|
||||||
|
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
@@ -71,7 +85,7 @@ export function SessionComposerRegion(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
sessionKey()
|
route.sessionKey()
|
||||||
const ready = props.ready
|
const ready = props.ready
|
||||||
const delay = 140
|
const delay = 140
|
||||||
|
|
||||||
@@ -156,6 +170,7 @@ export function SessionComposerRegion(props: {
|
|||||||
<SessionRevertDock
|
<SessionRevertDock
|
||||||
items={revert.items}
|
items={revert.items}
|
||||||
restoring={revert.restoring}
|
restoring={revert.restoring}
|
||||||
|
disabled={revert.disabled}
|
||||||
onRestore={revert.onRestore}
|
onRestore={revert.onRestore}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -179,8 +194,8 @@ export function SessionComposerRegion(props: {
|
|||||||
>
|
>
|
||||||
<div ref={(el) => setStore("body", el)}>
|
<div ref={(el) => setStore("body", el)}>
|
||||||
<SessionTodoDock
|
<SessionTodoDock
|
||||||
|
sessionID={route.params.id}
|
||||||
todos={props.state.todos()}
|
todos={props.state.todos()}
|
||||||
title={language.t("session.todo.title")}
|
|
||||||
collapseLabel={language.t("session.todo.collapse")}
|
collapseLabel={language.t("session.todo.collapse")}
|
||||||
expandLabel={language.t("session.todo.expand")}
|
expandLabel={language.t("session.todo.expand")}
|
||||||
dockProgress={value()}
|
dockProgress={value()}
|
||||||
@@ -195,7 +210,12 @@ export function SessionComposerRegion(props: {
|
|||||||
"margin-top": `${-36 * value()}px`,
|
"margin-top": `${-36 * value()}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SessionRevertDock items={revert.items} restoring={revert.restoring} onRestore={revert.onRestore} />
|
<SessionRevertDock
|
||||||
|
items={revert.items}
|
||||||
|
restoring={revert.restoring}
|
||||||
|
disabled={revert.disabled}
|
||||||
|
onRestore={revert.onRestore}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
@@ -207,10 +227,23 @@ export function SessionComposerRegion(props: {
|
|||||||
"margin-top": `${-lift()}px`,
|
"margin-top": `${-lift()}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Show when={props.followup?.items.length}>
|
||||||
|
<SessionFollowupDock
|
||||||
|
items={props.followup!.items}
|
||||||
|
sending={props.followup!.sending}
|
||||||
|
onSend={props.followup!.onSend}
|
||||||
|
onEdit={props.followup!.onEdit}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
<PromptInput
|
<PromptInput
|
||||||
ref={props.inputRef}
|
ref={props.inputRef}
|
||||||
newSessionWorktree={props.newSessionWorktree}
|
newSessionWorktree={props.newSessionWorktree}
|
||||||
onNewSessionWorktreeReset={props.onNewSessionWorktreeReset}
|
onNewSessionWorktreeReset={props.onNewSessionWorktreeReset}
|
||||||
|
edit={props.followup?.edit}
|
||||||
|
onEditLoaded={props.followup?.onEditLoaded}
|
||||||
|
shouldQueue={props.followup?.queue}
|
||||||
|
onQueue={props.followup?.onQueue}
|
||||||
|
onAbort={props.followup?.onAbort}
|
||||||
onSubmit={props.onSubmit}
|
onSubmit={props.onSubmit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createEffect, createMemo, on, onCleanup } from "solid-js"
|
import { createEffect, createMemo, on, onCleanup, onMount } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2"
|
import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
@@ -8,6 +8,7 @@ import { useLanguage } from "@/context/language"
|
|||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
|
import { composerDriver, composerEnabled, composerEvent } from "@/testing/session-composer"
|
||||||
import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree"
|
import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree"
|
||||||
|
|
||||||
export const todoState = (input: {
|
export const todoState = (input: {
|
||||||
@@ -47,7 +48,50 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
|
|||||||
return !!permissionRequest() || !!questionRequest()
|
return !!permissionRequest() || !!questionRequest()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const [test, setTest] = createStore({
|
||||||
|
on: false,
|
||||||
|
live: undefined as boolean | undefined,
|
||||||
|
todos: undefined as Todo[] | undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
const pull = () => {
|
||||||
|
const id = params.id
|
||||||
|
if (!id) {
|
||||||
|
setTest({ on: false, live: undefined, todos: undefined })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = composerDriver(id)
|
||||||
|
if (!next) {
|
||||||
|
setTest({ on: false, live: undefined, todos: undefined })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setTest({
|
||||||
|
on: true,
|
||||||
|
live: next.live,
|
||||||
|
todos: next.todos?.map((todo) => ({ ...todo })),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!composerEnabled()) return
|
||||||
|
|
||||||
|
pull()
|
||||||
|
createEffect(on(() => params.id, pull, { defer: true }))
|
||||||
|
|
||||||
|
const onEvent = (event: Event) => {
|
||||||
|
const detail = (event as CustomEvent<{ sessionID?: string }>).detail
|
||||||
|
if (detail?.sessionID !== params.id) return
|
||||||
|
pull()
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener(composerEvent, onEvent)
|
||||||
|
onCleanup(() => window.removeEventListener(composerEvent, onEvent))
|
||||||
|
})
|
||||||
|
|
||||||
const todos = createMemo((): Todo[] => {
|
const todos = createMemo((): Todo[] => {
|
||||||
|
if (test.on && test.todos !== undefined) return test.todos
|
||||||
const id = params.id
|
const id = params.id
|
||||||
if (!id) return []
|
if (!id) return []
|
||||||
return globalSync.data.session_todo[id] ?? []
|
return globalSync.data.session_todo[id] ?? []
|
||||||
@@ -64,7 +108,10 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
|
|||||||
})
|
})
|
||||||
|
|
||||||
const busy = createMemo(() => status().type !== "idle")
|
const busy = createMemo(() => status().type !== "idle")
|
||||||
const live = createMemo(() => busy() || blocked())
|
const live = createMemo(() => {
|
||||||
|
if (test.on && test.live !== undefined) return test.live
|
||||||
|
return busy() || blocked()
|
||||||
|
})
|
||||||
|
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
responding: undefined as string | undefined,
|
responding: undefined as string | undefined,
|
||||||
@@ -116,6 +163,10 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
|
|||||||
|
|
||||||
// Keep stale turn todos from reopening if the model never clears them.
|
// Keep stale turn todos from reopening if the model never clears them.
|
||||||
const clear = () => {
|
const clear = () => {
|
||||||
|
if (test.on && test.todos !== undefined) {
|
||||||
|
setTest("todos", [])
|
||||||
|
return
|
||||||
|
}
|
||||||
const id = params.id
|
const id = params.id
|
||||||
if (!id) return
|
if (!id) return
|
||||||
globalSync.todo.set(id, [])
|
globalSync.todo.set(id, [])
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { For, Show, createMemo } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
|
import { DockTray } from "@opencode-ai/ui/dock-surface"
|
||||||
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
|
||||||
|
export function SessionFollowupDock(props: {
|
||||||
|
items: { id: string; text: string }[]
|
||||||
|
sending?: string
|
||||||
|
onSend: (id: string) => void
|
||||||
|
onEdit: (id: string) => void
|
||||||
|
}) {
|
||||||
|
const language = useLanguage()
|
||||||
|
const [store, setStore] = createStore({
|
||||||
|
collapsed: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggle = () => setStore("collapsed", (value) => !value)
|
||||||
|
const total = createMemo(() => props.items.length)
|
||||||
|
const label = createMemo(() =>
|
||||||
|
language.t(total() === 1 ? "session.followupDock.summary.one" : "session.followupDock.summary.other", {
|
||||||
|
count: total(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const preview = createMemo(() => props.items[0]?.text ?? "")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DockTray
|
||||||
|
data-component="session-followup-dock"
|
||||||
|
style={{
|
||||||
|
"margin-bottom": "-0.875rem",
|
||||||
|
"border-bottom-left-radius": 0,
|
||||||
|
"border-bottom-right-radius": 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="pl-3 pr-2 py-2 flex items-center gap-2"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={toggle}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key !== "Enter" && event.key !== " ") return
|
||||||
|
event.preventDefault()
|
||||||
|
toggle()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="shrink-0 text-13-medium text-text-strong cursor-default">{label()}</span>
|
||||||
|
<Show when={store.collapsed && preview()}>
|
||||||
|
<span class="min-w-0 flex-1 truncate text-13-regular text-text-base cursor-default">{preview()}</span>
|
||||||
|
</Show>
|
||||||
|
<div class="ml-auto shrink-0">
|
||||||
|
<IconButton
|
||||||
|
data-collapsed={store.collapsed ? "true" : "false"}
|
||||||
|
icon="chevron-down"
|
||||||
|
size="normal"
|
||||||
|
variant="ghost"
|
||||||
|
style={{ transform: `rotate(${store.collapsed ? 180 : 0}deg)` }}
|
||||||
|
onMouseDown={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
}}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
toggle()
|
||||||
|
}}
|
||||||
|
aria-label={
|
||||||
|
store.collapsed ? language.t("session.followupDock.expand") : language.t("session.followupDock.collapse")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={store.collapsed}>
|
||||||
|
<div class="h-5" aria-hidden="true" />
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={!store.collapsed}>
|
||||||
|
<div class="px-3 pb-7 flex flex-col gap-1.5 max-h-42 overflow-y-auto no-scrollbar">
|
||||||
|
<For each={props.items}>
|
||||||
|
{(item) => (
|
||||||
|
<div class="flex items-center gap-2 min-w-0 py-1">
|
||||||
|
<span class="min-w-0 flex-1 truncate text-13-regular text-text-strong">{item.text}</span>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="secondary"
|
||||||
|
class="shrink-0"
|
||||||
|
disabled={!!props.sending}
|
||||||
|
onClick={() => props.onSend(item.id)}
|
||||||
|
>
|
||||||
|
{language.t("session.followupDock.sendNow")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="ghost"
|
||||||
|
class="shrink-0"
|
||||||
|
disabled={!!props.sending}
|
||||||
|
onClick={() => props.onEdit(item.id)}
|
||||||
|
>
|
||||||
|
{language.t("session.followupDock.edit")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</DockTray>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||||||
|
|
||||||
const summary = createMemo(() => {
|
const summary = createMemo(() => {
|
||||||
const n = Math.min(store.tab + 1, total())
|
const n = Math.min(store.tab + 1, total())
|
||||||
return `${n} of ${total()} questions`
|
return language.t("session.question.progress", { current: n, total: total() })
|
||||||
})
|
})
|
||||||
|
|
||||||
const last = createMemo(() => store.tab >= total() - 1)
|
const last = createMemo(() => store.tab >= total() - 1)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { For, Show, createMemo } from "solid-js"
|
import { For, Show, createEffect, createMemo } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { DockTray } from "@opencode-ai/ui/dock-surface"
|
import { DockTray } from "@opencode-ai/ui/dock-surface"
|
||||||
@@ -8,11 +8,18 @@ import { useLanguage } from "@/context/language"
|
|||||||
export function SessionRevertDock(props: {
|
export function SessionRevertDock(props: {
|
||||||
items: { id: string; text: string }[]
|
items: { id: string; text: string }[]
|
||||||
restoring?: string
|
restoring?: string
|
||||||
|
disabled?: boolean
|
||||||
onRestore: (id: string) => void
|
onRestore: (id: string) => void
|
||||||
}) {
|
}) {
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
collapsed: false,
|
collapsed: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
props.items.length
|
||||||
|
props.items[0]?.id
|
||||||
|
setStore("collapsed", true)
|
||||||
})
|
})
|
||||||
|
|
||||||
const toggle = () => setStore("collapsed", (value) => !value)
|
const toggle = () => setStore("collapsed", (value) => !value)
|
||||||
@@ -68,16 +75,16 @@ export function SessionRevertDock(props: {
|
|||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={!store.collapsed}>
|
<Show when={!store.collapsed}>
|
||||||
<div class="px-3 pb-11 flex flex-col gap-1.5 max-h-42 overflow-y-auto no-scrollbar">
|
<div class="px-3 pb-7 flex flex-col gap-1.5 max-h-42 overflow-y-auto no-scrollbar">
|
||||||
<For each={props.items}>
|
<For each={props.items}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<div class="flex items-center gap-2 min-w-0 rounded-[10px] border border-border-weak-base bg-background-stronger px-2.5 py-2">
|
<div class="flex items-center gap-2 min-w-0 py-1">
|
||||||
<span class="min-w-0 flex-1 truncate text-13-regular text-text-strong">{item.text}</span>
|
<span class="min-w-0 flex-1 truncate text-13-regular text-text-strong">{item.text}</span>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
class="shrink-0"
|
class="shrink-0"
|
||||||
disabled={!!props.restoring}
|
disabled={props.disabled || !!props.restoring}
|
||||||
onClick={() => props.onRestore(item.id)}
|
onClick={() => props.onRestore(item.id)}
|
||||||
>
|
>
|
||||||
{language.t("session.revertDock.restore")}
|
{language.t("session.revertDock.restore")}
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ import { TextReveal } from "@opencode-ai/ui/text-reveal"
|
|||||||
import { TextStrikethrough } from "@opencode-ai/ui/text-strikethrough"
|
import { TextStrikethrough } from "@opencode-ai/ui/text-strikethrough"
|
||||||
import { Index, createEffect, createMemo, on, onCleanup } from "solid-js"
|
import { Index, createEffect, createMemo, on, onCleanup } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
|
import { composerEnabled, composerProbe } from "@/testing/session-composer"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
|
||||||
|
const doneToken = "\u0000done\u0000"
|
||||||
|
const totalToken = "\u0000total\u0000"
|
||||||
|
|
||||||
function dot(status: Todo["status"]) {
|
function dot(status: Todo["status"]) {
|
||||||
if (status !== "in_progress") return undefined
|
if (status !== "in_progress") return undefined
|
||||||
@@ -35,12 +40,13 @@ function dot(status: Todo["status"]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SessionTodoDock(props: {
|
export function SessionTodoDock(props: {
|
||||||
|
sessionID?: string
|
||||||
todos: Todo[]
|
todos: Todo[]
|
||||||
title: string
|
|
||||||
collapseLabel: string
|
collapseLabel: string
|
||||||
expandLabel: string
|
expandLabel: string
|
||||||
dockProgress: number
|
dockProgress: number
|
||||||
}) {
|
}) {
|
||||||
|
const language = useLanguage()
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
height: 320,
|
height: 320,
|
||||||
@@ -50,7 +56,12 @@ export function SessionTodoDock(props: {
|
|||||||
|
|
||||||
const total = createMemo(() => props.todos.length)
|
const total = createMemo(() => props.todos.length)
|
||||||
const done = createMemo(() => props.todos.filter((todo) => todo.status === "completed").length)
|
const done = createMemo(() => props.todos.filter((todo) => todo.status === "completed").length)
|
||||||
const label = createMemo(() => `${done()} of ${total()} ${props.title.toLowerCase()} completed`)
|
const label = createMemo(() => language.t("session.todo.progress", { done: done(), total: total() }))
|
||||||
|
const progress = createMemo(() =>
|
||||||
|
language
|
||||||
|
.t("session.todo.progress", { done: doneToken, total: totalToken })
|
||||||
|
.split(/(\u0000done\u0000|\u0000total\u0000)/),
|
||||||
|
)
|
||||||
|
|
||||||
const active = createMemo(
|
const active = createMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -69,6 +80,8 @@ export function SessionTodoDock(props: {
|
|||||||
const off = createMemo(() => hide() > 0.98)
|
const off = createMemo(() => hide() > 0.98)
|
||||||
const turn = createMemo(() => Math.max(0, Math.min(1, value())))
|
const turn = createMemo(() => Math.max(0, Math.min(1, value())))
|
||||||
const full = createMemo(() => Math.max(78, store.height))
|
const full = createMemo(() => Math.max(78, store.height))
|
||||||
|
const e2e = composerEnabled()
|
||||||
|
const probe = composerProbe(props.sessionID)
|
||||||
let contentRef: HTMLDivElement | undefined
|
let contentRef: HTMLDivElement | undefined
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
@@ -83,6 +96,23 @@ export function SessionTodoDock(props: {
|
|||||||
onCleanup(() => observer.disconnect())
|
onCleanup(() => observer.disconnect())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!e2e) return
|
||||||
|
|
||||||
|
probe.set({
|
||||||
|
mounted: true,
|
||||||
|
collapsed: store.collapsed,
|
||||||
|
hidden: store.collapsed || off(),
|
||||||
|
count: props.todos.length,
|
||||||
|
states: props.todos.map((todo) => todo.status),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
if (!e2e) return
|
||||||
|
probe.drop()
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DockTray
|
<DockTray
|
||||||
data-component="session-todo-dock"
|
data-component="session-todo-dock"
|
||||||
@@ -106,20 +136,28 @@ export function SessionTodoDock(props: {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="text-14-regular text-text-strong cursor-default inline-flex items-baseline shrink-0 whitespace-nowrap overflow-visible"
|
class="text-14-regular text-text-strong cursor-default inline-flex items-baseline shrink-0 overflow-visible"
|
||||||
aria-label={label()}
|
aria-label={label()}
|
||||||
style={{
|
style={{
|
||||||
"--tool-motion-odometer-ms": "600ms",
|
"--tool-motion-odometer-ms": "600ms",
|
||||||
"--tool-motion-mask": "18%",
|
"--tool-motion-mask": "18%",
|
||||||
"--tool-motion-mask-height": "0px",
|
"--tool-motion-mask-height": "0px",
|
||||||
"--tool-motion-spring-ms": "560ms",
|
"--tool-motion-spring-ms": "560ms",
|
||||||
|
"white-space": "pre",
|
||||||
opacity: `${Math.max(0, Math.min(1, 1 - shut()))}`,
|
opacity: `${Math.max(0, Math.min(1, 1 - shut()))}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AnimatedNumber value={done()} />
|
<Index each={progress()}>
|
||||||
<span class="mx-1">of</span>
|
{(item) =>
|
||||||
<AnimatedNumber value={total()} />
|
item() === doneToken ? (
|
||||||
<span> {props.title.toLowerCase()} completed</span>
|
<AnimatedNumber value={done()} />
|
||||||
|
) : item() === totalToken ? (
|
||||||
|
<AnimatedNumber value={total()} />
|
||||||
|
) : (
|
||||||
|
<span>{item()}</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Index>
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
data-slot="session-todo-preview"
|
data-slot="session-todo-preview"
|
||||||
|
|||||||
@@ -11,15 +11,19 @@ import { InlineInput } from "@opencode-ai/ui/inline-input"
|
|||||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||||
import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
||||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||||
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
import type { AssistantMessage, Message as MessageType, Part, TextPart, UserMessage } from "@opencode-ai/sdk/v2"
|
import type { AssistantMessage, Message as MessageType, Part, TextPart, UserMessage } from "@opencode-ai/sdk/v2"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { Binary } from "@opencode-ai/util/binary"
|
import { Binary } from "@opencode-ai/util/binary"
|
||||||
import { getFilename } from "@opencode-ai/util/path"
|
import { getFilename } from "@opencode-ai/util/path"
|
||||||
|
import { Popover as KobaltePopover } from "@kobalte/core/popover"
|
||||||
import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture"
|
import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture"
|
||||||
import { SessionContextUsage } from "@/components/session-context-usage"
|
import { SessionContextUsage } from "@/components/session-context-usage"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { useSessionKey } from "@/pages/session/session-layout"
|
import { useSessionKey } from "@/pages/session/session-layout"
|
||||||
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
@@ -215,12 +219,14 @@ export function MessageTimeline(props: {
|
|||||||
let touchGesture: number | undefined
|
let touchGesture: number | undefined
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const globalSDK = useGlobalSDK()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const { params, sessionKey } = useSessionKey()
|
const { params, sessionKey } = useSessionKey()
|
||||||
|
const platform = usePlatform()
|
||||||
|
|
||||||
const rendered = createMemo(() => props.renderedUserMessages.map((message) => message.id))
|
const rendered = createMemo(() => props.renderedUserMessages.map((message) => message.id))
|
||||||
const sessionID = createMemo(() => params.id)
|
const sessionID = createMemo(() => params.id)
|
||||||
@@ -298,6 +304,8 @@ export function MessageTimeline(props: {
|
|||||||
return sync.session.get(id)
|
return sync.session.get(id)
|
||||||
})
|
})
|
||||||
const titleValue = createMemo(() => info()?.title)
|
const titleValue = createMemo(() => info()?.title)
|
||||||
|
const shareUrl = createMemo(() => info()?.share?.url)
|
||||||
|
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
|
||||||
const parentID = createMemo(() => info()?.parentID)
|
const parentID = createMemo(() => info()?.parentID)
|
||||||
const showHeader = createMemo(() => !!(titleValue() || parentID()))
|
const showHeader = createMemo(() => !!(titleValue() || parentID()))
|
||||||
const stageCfg = { init: 1, batch: 3 }
|
const stageCfg = { init: 1, batch: 3 }
|
||||||
@@ -314,9 +322,55 @@ export function MessageTimeline(props: {
|
|||||||
saving: false,
|
saving: false,
|
||||||
menuOpen: false,
|
menuOpen: false,
|
||||||
pendingRename: false,
|
pendingRename: false,
|
||||||
|
pendingShare: false,
|
||||||
})
|
})
|
||||||
let titleRef: HTMLInputElement | undefined
|
let titleRef: HTMLInputElement | undefined
|
||||||
|
|
||||||
|
const [share, setShare] = createStore({
|
||||||
|
open: false,
|
||||||
|
dismiss: null as "escape" | "outside" | null,
|
||||||
|
})
|
||||||
|
|
||||||
|
let more: HTMLButtonElement | undefined
|
||||||
|
|
||||||
|
const [req, setReq] = createStore({ share: false, unshare: false })
|
||||||
|
|
||||||
|
const shareSession = () => {
|
||||||
|
const id = sessionID()
|
||||||
|
if (!id || req.share) return
|
||||||
|
if (!shareEnabled()) return
|
||||||
|
setReq("share", true)
|
||||||
|
globalSDK.client.session
|
||||||
|
.share({ sessionID: id, directory: sdk.directory })
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
console.error("Failed to share session", err)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setReq("share", false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const unshareSession = () => {
|
||||||
|
const id = sessionID()
|
||||||
|
if (!id || req.unshare) return
|
||||||
|
if (!shareEnabled()) return
|
||||||
|
setReq("unshare", true)
|
||||||
|
globalSDK.client.session
|
||||||
|
.unshare({ sessionID: id, directory: sdk.directory })
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
console.error("Failed to unshare session", err)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setReq("unshare", false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewShare = () => {
|
||||||
|
const url = shareUrl()
|
||||||
|
if (!url) return
|
||||||
|
platform.openLink(url)
|
||||||
|
}
|
||||||
|
|
||||||
const errorMessage = (err: unknown) => {
|
const errorMessage = (err: unknown) => {
|
||||||
if (err && typeof err === "object" && "data" in err) {
|
if (err && typeof err === "object" && "data" in err) {
|
||||||
const data = (err as { data?: { message?: string } }).data
|
const data = (err as { data?: { message?: string } }).data
|
||||||
@@ -329,7 +383,15 @@ export function MessageTimeline(props: {
|
|||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
sessionKey,
|
sessionKey,
|
||||||
() => setTitle({ draft: "", editing: false, saving: false, menuOpen: false, pendingRename: false }),
|
() =>
|
||||||
|
setTitle({
|
||||||
|
draft: "",
|
||||||
|
editing: false,
|
||||||
|
saving: false,
|
||||||
|
menuOpen: false,
|
||||||
|
pendingRename: false,
|
||||||
|
pendingShare: false,
|
||||||
|
}),
|
||||||
{ defer: true },
|
{ defer: true },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -678,23 +740,42 @@ export function MessageTimeline(props: {
|
|||||||
gutter={4}
|
gutter={4}
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
open={title.menuOpen}
|
open={title.menuOpen}
|
||||||
onOpenChange={(open) => setTitle("menuOpen", open)}
|
onOpenChange={(open) => {
|
||||||
|
setTitle("menuOpen", open)
|
||||||
|
if (open) return
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Trigger
|
<DropdownMenu.Trigger
|
||||||
as={IconButton}
|
as={IconButton}
|
||||||
icon="dot-grid"
|
icon="dot-grid"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
|
class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
|
||||||
|
classList={{
|
||||||
|
"bg-surface-base-active": share.open || title.pendingShare,
|
||||||
|
}}
|
||||||
aria-label={language.t("common.moreOptions")}
|
aria-label={language.t("common.moreOptions")}
|
||||||
|
aria-expanded={title.menuOpen || share.open || title.pendingShare}
|
||||||
|
ref={(el: HTMLButtonElement) => {
|
||||||
|
more = el
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<DropdownMenu.Portal>
|
<DropdownMenu.Portal>
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
style={{ "min-width": "104px" }}
|
style={{ "min-width": "104px" }}
|
||||||
onCloseAutoFocus={(event) => {
|
onCloseAutoFocus={(event) => {
|
||||||
if (!title.pendingRename) return
|
if (title.pendingRename) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
setTitle("pendingRename", false)
|
setTitle("pendingRename", false)
|
||||||
openTitleEditor()
|
openTitleEditor()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (title.pendingShare) {
|
||||||
|
event.preventDefault()
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setShare({ open: true, dismiss: null })
|
||||||
|
setTitle("pendingShare", false)
|
||||||
|
})
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
@@ -705,6 +786,17 @@ export function MessageTimeline(props: {
|
|||||||
>
|
>
|
||||||
<DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
|
<DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
<Show when={shareEnabled()}>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onSelect={() => {
|
||||||
|
setTitle({ pendingShare: true, menuOpen: false })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemLabel>
|
||||||
|
{language.t("session.share.action.share")}
|
||||||
|
</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</Show>
|
||||||
<DropdownMenu.Item onSelect={() => void archiveSession(id())}>
|
<DropdownMenu.Item onSelect={() => void archiveSession(id())}>
|
||||||
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
|
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
@@ -717,6 +809,104 @@ export function MessageTimeline(props: {
|
|||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Portal>
|
</DropdownMenu.Portal>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
|
<KobaltePopover
|
||||||
|
open={share.open}
|
||||||
|
anchorRef={() => more}
|
||||||
|
placement="bottom-end"
|
||||||
|
gutter={4}
|
||||||
|
modal={false}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (open) setShare("dismiss", null)
|
||||||
|
setShare("open", open)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<KobaltePopover.Portal>
|
||||||
|
<KobaltePopover.Content
|
||||||
|
data-component="popover-content"
|
||||||
|
style={{ "min-width": "320px" }}
|
||||||
|
onEscapeKeyDown={(event) => {
|
||||||
|
setShare({ dismiss: "escape", open: false })
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
}}
|
||||||
|
onPointerDownOutside={() => {
|
||||||
|
setShare({ dismiss: "outside", open: false })
|
||||||
|
}}
|
||||||
|
onFocusOutside={() => {
|
||||||
|
setShare({ dismiss: "outside", open: false })
|
||||||
|
}}
|
||||||
|
onCloseAutoFocus={(event) => {
|
||||||
|
if (share.dismiss === "outside") event.preventDefault()
|
||||||
|
setShare("dismiss", null)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="flex flex-col p-3">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<div class="text-13-medium text-text-strong">
|
||||||
|
{language.t("session.share.popover.title")}
|
||||||
|
</div>
|
||||||
|
<div class="text-12-regular text-text-weak">
|
||||||
|
{shareUrl()
|
||||||
|
? language.t("session.share.popover.description.shared")
|
||||||
|
: language.t("session.share.popover.description.unshared")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 flex flex-col gap-2">
|
||||||
|
<Show
|
||||||
|
when={shareUrl()}
|
||||||
|
fallback={
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
variant="primary"
|
||||||
|
class="w-full"
|
||||||
|
onClick={shareSession}
|
||||||
|
disabled={req.share}
|
||||||
|
>
|
||||||
|
{req.share
|
||||||
|
? language.t("session.share.action.publishing")
|
||||||
|
: language.t("session.share.action.publish")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<TextField
|
||||||
|
value={shareUrl() ?? ""}
|
||||||
|
readOnly
|
||||||
|
copyable
|
||||||
|
copyKind="link"
|
||||||
|
tabIndex={-1}
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
variant="secondary"
|
||||||
|
class="w-full shadow-none border border-border-weak-base"
|
||||||
|
onClick={unshareSession}
|
||||||
|
disabled={req.unshare}
|
||||||
|
>
|
||||||
|
{req.unshare
|
||||||
|
? language.t("session.share.action.unpublishing")
|
||||||
|
: language.t("session.share.action.unpublish")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
variant="primary"
|
||||||
|
class="w-full"
|
||||||
|
onClick={viewShare}
|
||||||
|
disabled={req.unshare}
|
||||||
|
>
|
||||||
|
{language.t("session.share.action.view")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</KobaltePopover.Content>
|
||||||
|
</KobaltePopover.Portal>
|
||||||
|
</KobaltePopover>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
@@ -752,12 +942,6 @@ export function MessageTimeline(props: {
|
|||||||
<For each={rendered()}>
|
<For each={rendered()}>
|
||||||
{(messageID) => {
|
{(messageID) => {
|
||||||
const active = createMemo(() => activeMessageID() === messageID)
|
const active = createMemo(() => activeMessageID() === messageID)
|
||||||
const queued = createMemo(() => {
|
|
||||||
if (active()) return false
|
|
||||||
const activeID = activeMessageID()
|
|
||||||
if (activeID) return messageID > activeID
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
const comments = createMemo(() => messageComments(sync.data.part[messageID] ?? []), [], {
|
const comments = createMemo(() => messageComments(sync.data.part[messageID] ?? []), [], {
|
||||||
equals: (a, b) => JSON.stringify(a) === JSON.stringify(b),
|
equals: (a, b) => JSON.stringify(a) === JSON.stringify(b),
|
||||||
})
|
})
|
||||||
@@ -817,7 +1001,6 @@ export function MessageTimeline(props: {
|
|||||||
messageID={messageID}
|
messageID={messageID}
|
||||||
actions={props.actions}
|
actions={props.actions}
|
||||||
active={active()}
|
active={active()}
|
||||||
queued={queued()}
|
|
||||||
status={active() ? sessionStatus() : undefined}
|
status={active() ? sessionStatus() : undefined}
|
||||||
showReasoningSummaries={settings.general.showReasoningSummaries()}
|
showReasoningSummaries={settings.general.showReasoningSummaries()}
|
||||||
shellToolDefaultOpen={settings.general.shellToolPartsExpanded()}
|
shellToolDefaultOpen={settings.general.shellToolPartsExpanded()}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { isDefaultTitle as isDefaultTerminalTitle } from "@/context/terminal-title"
|
||||||
|
|
||||||
export const terminalTabLabel = (input: {
|
export const terminalTabLabel = (input: {
|
||||||
title?: string
|
title?: string
|
||||||
titleNumber?: number
|
titleNumber?: number
|
||||||
@@ -5,9 +7,7 @@ export const terminalTabLabel = (input: {
|
|||||||
}) => {
|
}) => {
|
||||||
const title = input.title ?? ""
|
const title = input.title ?? ""
|
||||||
const number = input.titleNumber ?? 0
|
const number = input.titleNumber ?? 0
|
||||||
const match = title.match(/^Terminal (\d+)$/)
|
const isDefaultTitle = Number.isFinite(number) && number > 0 && isDefaultTerminalTitle(title, number)
|
||||||
const parsed = match ? Number(match[1]) : undefined
|
|
||||||
const isDefaultTitle = Number.isFinite(number) && number > 0 && Number.isFinite(parsed) && parsed === number
|
|
||||||
|
|
||||||
if (title && !isDefaultTitle) return title
|
if (title && !isDefaultTitle) return title
|
||||||
if (number > 0) return input.t("terminal.title.numbered", { number })
|
if (number > 0) return input.t("terminal.title.numbered", { number })
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import type { Todo } from "@opencode-ai/sdk/v2"
|
||||||
|
|
||||||
|
export const composerEvent = "opencode:e2e:composer"
|
||||||
|
|
||||||
|
export type ComposerDriverState = {
|
||||||
|
live?: boolean
|
||||||
|
todos?: Array<Pick<Todo, "content" | "status" | "priority">>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComposerProbeState = {
|
||||||
|
mounted: boolean
|
||||||
|
collapsed: boolean
|
||||||
|
hidden: boolean
|
||||||
|
count: number
|
||||||
|
states: Todo["status"][]
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComposerState = {
|
||||||
|
driver?: ComposerDriverState
|
||||||
|
probe?: ComposerProbeState
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComposerWindow = Window & {
|
||||||
|
__opencode_e2e?: {
|
||||||
|
composer?: {
|
||||||
|
enabled?: boolean
|
||||||
|
sessions?: Record<string, ComposerState>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clone = (driver: ComposerDriverState) => ({
|
||||||
|
live: driver.live,
|
||||||
|
todos: driver.todos?.map((todo) => ({ ...todo })),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const composerEnabled = () => {
|
||||||
|
if (typeof window === "undefined") return false
|
||||||
|
return (window as ComposerWindow).__opencode_e2e?.composer?.enabled === true
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = () => {
|
||||||
|
if (!composerEnabled()) return
|
||||||
|
const state = (window as ComposerWindow).__opencode_e2e?.composer
|
||||||
|
if (!state) return
|
||||||
|
state.sessions ??= {}
|
||||||
|
return state.sessions
|
||||||
|
}
|
||||||
|
|
||||||
|
export const composerDriver = (sessionID?: string) => {
|
||||||
|
if (!sessionID) return
|
||||||
|
const state = root()?.[sessionID]?.driver
|
||||||
|
if (!state) return
|
||||||
|
return clone(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const composerProbe = (sessionID?: string) => {
|
||||||
|
const set = (next: ComposerProbeState) => {
|
||||||
|
if (!sessionID) return
|
||||||
|
const sessions = root()
|
||||||
|
if (!sessions) return
|
||||||
|
const prev = sessions[sessionID] ?? {}
|
||||||
|
sessions[sessionID] = {
|
||||||
|
...prev,
|
||||||
|
probe: {
|
||||||
|
...next,
|
||||||
|
states: [...next.states],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
set,
|
||||||
|
drop() {
|
||||||
|
set({
|
||||||
|
mounted: false,
|
||||||
|
collapsed: false,
|
||||||
|
hidden: true,
|
||||||
|
count: 0,
|
||||||
|
states: [],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,21 +2,28 @@ export const terminalAttr = "data-pty-id"
|
|||||||
|
|
||||||
export type TerminalProbeState = {
|
export type TerminalProbeState = {
|
||||||
connected: boolean
|
connected: boolean
|
||||||
|
connects: number
|
||||||
rendered: string
|
rendered: string
|
||||||
settled: number
|
settled: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TerminalProbeControl = {
|
||||||
|
disconnect?: VoidFunction
|
||||||
|
}
|
||||||
|
|
||||||
export type E2EWindow = Window & {
|
export type E2EWindow = Window & {
|
||||||
__opencode_e2e?: {
|
__opencode_e2e?: {
|
||||||
terminal?: {
|
terminal?: {
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
terminals?: Record<string, TerminalProbeState>
|
terminals?: Record<string, TerminalProbeState>
|
||||||
|
controls?: Record<string, TerminalProbeControl>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const seed = (): TerminalProbeState => ({
|
const seed = (): TerminalProbeState => ({
|
||||||
connected: false,
|
connected: false,
|
||||||
|
connects: 0,
|
||||||
rendered: "",
|
rendered: "",
|
||||||
settled: 0,
|
settled: 0,
|
||||||
})
|
})
|
||||||
@@ -25,15 +32,28 @@ const root = () => {
|
|||||||
if (typeof window === "undefined") return
|
if (typeof window === "undefined") return
|
||||||
const state = (window as E2EWindow).__opencode_e2e?.terminal
|
const state = (window as E2EWindow).__opencode_e2e?.terminal
|
||||||
if (!state?.enabled) return
|
if (!state?.enabled) return
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
const terms = () => {
|
||||||
|
const state = root()
|
||||||
|
if (!state) return
|
||||||
state.terminals ??= {}
|
state.terminals ??= {}
|
||||||
return state.terminals
|
return state.terminals
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const controls = () => {
|
||||||
|
const state = root()
|
||||||
|
if (!state) return
|
||||||
|
state.controls ??= {}
|
||||||
|
return state.controls
|
||||||
|
}
|
||||||
|
|
||||||
export const terminalProbe = (id: string) => {
|
export const terminalProbe = (id: string) => {
|
||||||
const set = (next: Partial<TerminalProbeState>) => {
|
const set = (next: Partial<TerminalProbeState>) => {
|
||||||
const terms = root()
|
const state = terms()
|
||||||
if (!terms) return
|
if (!state) return
|
||||||
terms[id] = { ...(terms[id] ?? seed()), ...next }
|
state[id] = { ...(state[id] ?? seed()), ...next }
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -41,24 +61,37 @@ export const terminalProbe = (id: string) => {
|
|||||||
set(seed())
|
set(seed())
|
||||||
},
|
},
|
||||||
connect() {
|
connect() {
|
||||||
set({ connected: true })
|
const state = terms()
|
||||||
|
if (!state) return
|
||||||
|
const prev = state[id] ?? seed()
|
||||||
|
state[id] = {
|
||||||
|
...prev,
|
||||||
|
connected: true,
|
||||||
|
connects: prev.connects + 1,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
render(data: string) {
|
render(data: string) {
|
||||||
const terms = root()
|
const state = terms()
|
||||||
if (!terms) return
|
if (!state) return
|
||||||
const prev = terms[id] ?? seed()
|
const prev = state[id] ?? seed()
|
||||||
terms[id] = { ...prev, rendered: prev.rendered + data }
|
state[id] = { ...prev, rendered: prev.rendered + data }
|
||||||
},
|
},
|
||||||
settle() {
|
settle() {
|
||||||
const terms = root()
|
const state = terms()
|
||||||
if (!terms) return
|
if (!state) return
|
||||||
const prev = terms[id] ?? seed()
|
const prev = state[id] ?? seed()
|
||||||
terms[id] = { ...prev, settled: prev.settled + 1 }
|
state[id] = { ...prev, settled: prev.settled + 1 }
|
||||||
|
},
|
||||||
|
control(next: Partial<TerminalProbeControl>) {
|
||||||
|
const state = controls()
|
||||||
|
if (!state) return
|
||||||
|
state[id] = { ...(state[id] ?? {}), ...next }
|
||||||
},
|
},
|
||||||
drop() {
|
drop() {
|
||||||
const terms = root()
|
const state = terms()
|
||||||
if (!terms) return
|
if (state) delete state[id]
|
||||||
delete terms[id]
|
const control = controls()
|
||||||
|
if (control) delete control[id]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.2.24",
|
"version": "1.2.25",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -76,6 +76,14 @@ export function IconAlipay(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function IconWechat(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg {...props} viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M9.27099 14.6689C8.9532 14.8312 8.56403 14.7122 8.39132 14.4L8.3477 14.3054L6.53019 10.3069C6.52269 10.2588 6.52269 10.2097 6.53019 10.1615C6.53017 10.0735 6.56564 9.98916 6.62857 9.9276C6.6915 9.86603 6.7766 9.83243 6.86462 9.83438C6.93567 9.83269 7.00508 9.85582 7.06091 9.89981L9.24191 11.4265C9.40329 11.5346 9.59293 11.5928 9.78716 11.5937C9.90424 11.5945 10.0203 11.5723 10.1289 11.5283L20.176 7.02816C18.091 4.72544 15.1103 3.43931 12.0045 3.5022C6.4793 3.5022 2.00098 7.23172 2.00098 11.87C2.06681 14.4052 3.35646 16.7515 5.4615 18.1658C5.6878 18.3326 5.78402 18.6241 5.70141 18.8928L5.25067 20.594C5.22336 20.6714 5.20625 20.7521 5.19978 20.8339C5.19777 20.9232 5.23236 21.0094 5.29552 21.0726C5.35868 21.1358 5.44491 21.1703 5.5342 21.1684C5.60098 21.1645 5.66583 21.1445 5.72322 21.1102L7.90423 19.8452C8.06383 19.7467 8.2474 19.6939 8.43494 19.6925C8.53352 19.6923 8.63157 19.707 8.72574 19.7361C9.78781 20.0363 10.8863 20.188 11.99 20.1869C17.5152 20.1869 22.001 16.4574 22.001 11.8554C22.0108 10.4834 21.6301 9.13687 20.903 7.97326L9.35096 14.6253L9.27099 14.6689Z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function IconChevron(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
export function IconChevron(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
|
||||||
return (
|
return (
|
||||||
<svg {...props} width="8" height="6" viewBox="0 0 8 6" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg {...props} width="8" height="6" viewBox="0 0 8 6" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
|||||||
@@ -541,6 +541,7 @@ export const dict = {
|
|||||||
"workspace.billing.addAction": "إضافة",
|
"workspace.billing.addAction": "إضافة",
|
||||||
"workspace.billing.addBalance": "إضافة رصيد",
|
"workspace.billing.addBalance": "إضافة رصيد",
|
||||||
"workspace.billing.alipay": "Alipay",
|
"workspace.billing.alipay": "Alipay",
|
||||||
|
"workspace.billing.wechat": "WeChat Pay",
|
||||||
"workspace.billing.linkedToStripe": "مرتبط بـ Stripe",
|
"workspace.billing.linkedToStripe": "مرتبط بـ Stripe",
|
||||||
"workspace.billing.manage": "إدارة",
|
"workspace.billing.manage": "إدارة",
|
||||||
"workspace.billing.enable": "تمكين الفوترة",
|
"workspace.billing.enable": "تمكين الفوترة",
|
||||||
|
|||||||
@@ -550,6 +550,7 @@ export const dict = {
|
|||||||
"workspace.billing.addAction": "Adicionar",
|
"workspace.billing.addAction": "Adicionar",
|
||||||
"workspace.billing.addBalance": "Adicionar Saldo",
|
"workspace.billing.addBalance": "Adicionar Saldo",
|
||||||
"workspace.billing.alipay": "Alipay",
|
"workspace.billing.alipay": "Alipay",
|
||||||
|
"workspace.billing.wechat": "WeChat Pay",
|
||||||
"workspace.billing.linkedToStripe": "Vinculado ao Stripe",
|
"workspace.billing.linkedToStripe": "Vinculado ao Stripe",
|
||||||
"workspace.billing.manage": "Gerenciar",
|
"workspace.billing.manage": "Gerenciar",
|
||||||
"workspace.billing.enable": "Ativar Faturamento",
|
"workspace.billing.enable": "Ativar Faturamento",
|
||||||
|
|||||||
@@ -546,6 +546,7 @@ export const dict = {
|
|||||||
"workspace.billing.addAction": "Tilføj",
|
"workspace.billing.addAction": "Tilføj",
|
||||||
"workspace.billing.addBalance": "Tilføj saldo",
|
"workspace.billing.addBalance": "Tilføj saldo",
|
||||||
"workspace.billing.alipay": "Alipay",
|
"workspace.billing.alipay": "Alipay",
|
||||||
|
"workspace.billing.wechat": "WeChat Pay",
|
||||||
"workspace.billing.linkedToStripe": "Forbundet til Stripe",
|
"workspace.billing.linkedToStripe": "Forbundet til Stripe",
|
||||||
"workspace.billing.manage": "Administrer",
|
"workspace.billing.manage": "Administrer",
|
||||||
"workspace.billing.enable": "Aktiver fakturering",
|
"workspace.billing.enable": "Aktiver fakturering",
|
||||||
|
|||||||
@@ -549,6 +549,7 @@ export const dict = {
|
|||||||
"workspace.billing.addAction": "Hinzufügen",
|
"workspace.billing.addAction": "Hinzufügen",
|
||||||
"workspace.billing.addBalance": "Guthaben aufladen",
|
"workspace.billing.addBalance": "Guthaben aufladen",
|
||||||
"workspace.billing.alipay": "Alipay",
|
"workspace.billing.alipay": "Alipay",
|
||||||
|
"workspace.billing.wechat": "WeChat Pay",
|
||||||
"workspace.billing.linkedToStripe": "Mit Stripe verbunden",
|
"workspace.billing.linkedToStripe": "Mit Stripe verbunden",
|
||||||
"workspace.billing.manage": "Verwalten",
|
"workspace.billing.manage": "Verwalten",
|
||||||
"workspace.billing.enable": "Abrechnung aktivieren",
|
"workspace.billing.enable": "Abrechnung aktivieren",
|
||||||
|
|||||||
@@ -541,6 +541,7 @@ export const dict = {
|
|||||||
"workspace.billing.addAction": "Add",
|
"workspace.billing.addAction": "Add",
|
||||||
"workspace.billing.addBalance": "Add Balance",
|
"workspace.billing.addBalance": "Add Balance",
|
||||||
"workspace.billing.alipay": "Alipay",
|
"workspace.billing.alipay": "Alipay",
|
||||||
|
"workspace.billing.wechat": "WeChat Pay",
|
||||||
"workspace.billing.linkedToStripe": "Linked to Stripe",
|
"workspace.billing.linkedToStripe": "Linked to Stripe",
|
||||||
"workspace.billing.manage": "Manage",
|
"workspace.billing.manage": "Manage",
|
||||||
"workspace.billing.enable": "Enable Billing",
|
"workspace.billing.enable": "Enable Billing",
|
||||||
|
|||||||
@@ -550,6 +550,7 @@ export const dict = {
|
|||||||
"workspace.billing.addAction": "Añadir",
|
"workspace.billing.addAction": "Añadir",
|
||||||
"workspace.billing.addBalance": "Añadir Saldo",
|
"workspace.billing.addBalance": "Añadir Saldo",
|
||||||
"workspace.billing.alipay": "Alipay",
|
"workspace.billing.alipay": "Alipay",
|
||||||
|
"workspace.billing.wechat": "WeChat Pay",
|
||||||
"workspace.billing.linkedToStripe": "Vinculado con Stripe",
|
"workspace.billing.linkedToStripe": "Vinculado con Stripe",
|
||||||
"workspace.billing.manage": "Gestionar",
|
"workspace.billing.manage": "Gestionar",
|
||||||
"workspace.billing.enable": "Habilitar Facturación",
|
"workspace.billing.enable": "Habilitar Facturación",
|
||||||
|
|||||||
@@ -552,6 +552,7 @@ export const dict = {
|
|||||||
"workspace.billing.addAction": "Ajouter",
|
"workspace.billing.addAction": "Ajouter",
|
||||||
"workspace.billing.addBalance": "Ajouter un solde",
|
"workspace.billing.addBalance": "Ajouter un solde",
|
||||||
"workspace.billing.alipay": "Alipay",
|
"workspace.billing.alipay": "Alipay",
|
||||||
|
"workspace.billing.wechat": "WeChat Pay",
|
||||||
"workspace.billing.linkedToStripe": "Lié à Stripe",
|
"workspace.billing.linkedToStripe": "Lié à Stripe",
|
||||||
"workspace.billing.manage": "Gérer",
|
"workspace.billing.manage": "Gérer",
|
||||||
"workspace.billing.enable": "Activer la facturation",
|
"workspace.billing.enable": "Activer la facturation",
|
||||||
|
|||||||
@@ -548,6 +548,7 @@ export const dict = {
|
|||||||
"workspace.billing.addAction": "Aggiungi",
|
"workspace.billing.addAction": "Aggiungi",
|
||||||
"workspace.billing.addBalance": "Aggiungi Saldo",
|
"workspace.billing.addBalance": "Aggiungi Saldo",
|
||||||
"workspace.billing.alipay": "Alipay",
|
"workspace.billing.alipay": "Alipay",
|
||||||
|
"workspace.billing.wechat": "WeChat Pay",
|
||||||
"workspace.billing.linkedToStripe": "Collegato a Stripe",
|
"workspace.billing.linkedToStripe": "Collegato a Stripe",
|
||||||
"workspace.billing.manage": "Gestisci",
|
"workspace.billing.manage": "Gestisci",
|
||||||
"workspace.billing.enable": "Abilita Fatturazione",
|
"workspace.billing.enable": "Abilita Fatturazione",
|
||||||
|
|||||||
@@ -547,6 +547,7 @@ export const dict = {
|
|||||||
"workspace.billing.addAction": "追加",
|
"workspace.billing.addAction": "追加",
|
||||||
"workspace.billing.addBalance": "残高を追加",
|
"workspace.billing.addBalance": "残高を追加",
|
||||||
"workspace.billing.alipay": "Alipay",
|
"workspace.billing.alipay": "Alipay",
|
||||||
|
"workspace.billing.wechat": "WeChat Pay",
|
||||||
"workspace.billing.linkedToStripe": "Stripeと連携済み",
|
"workspace.billing.linkedToStripe": "Stripeと連携済み",
|
||||||
"workspace.billing.manage": "管理",
|
"workspace.billing.manage": "管理",
|
||||||
"workspace.billing.enable": "課金を有効にする",
|
"workspace.billing.enable": "課金を有効にする",
|
||||||
|
|||||||
@@ -541,6 +541,7 @@ export const dict = {
|
|||||||
"workspace.billing.addAction": "추가",
|
"workspace.billing.addAction": "추가",
|
||||||
"workspace.billing.addBalance": "잔액 추가",
|
"workspace.billing.addBalance": "잔액 추가",
|
||||||
"workspace.billing.alipay": "Alipay",
|
"workspace.billing.alipay": "Alipay",
|
||||||
|
"workspace.billing.wechat": "WeChat Pay",
|
||||||
"workspace.billing.linkedToStripe": "Stripe에 연결됨",
|
"workspace.billing.linkedToStripe": "Stripe에 연결됨",
|
||||||
"workspace.billing.manage": "관리",
|
"workspace.billing.manage": "관리",
|
||||||
"workspace.billing.enable": "결제 활성화",
|
"workspace.billing.enable": "결제 활성화",
|
||||||
|
|||||||
@@ -547,6 +547,7 @@ export const dict = {
|
|||||||
"workspace.billing.addAction": "Legg til",
|
"workspace.billing.addAction": "Legg til",
|
||||||
"workspace.billing.addBalance": "Legg til saldo",
|
"workspace.billing.addBalance": "Legg til saldo",
|
||||||
"workspace.billing.alipay": "Alipay",
|
"workspace.billing.alipay": "Alipay",
|
||||||
|
"workspace.billing.wechat": "WeChat Pay",
|
||||||
"workspace.billing.linkedToStripe": "Koblet til Stripe",
|
"workspace.billing.linkedToStripe": "Koblet til Stripe",
|
||||||
"workspace.billing.manage": "Administrer",
|
"workspace.billing.manage": "Administrer",
|
||||||
"workspace.billing.enable": "Aktiver fakturering",
|
"workspace.billing.enable": "Aktiver fakturering",
|
||||||
|
|||||||
@@ -548,6 +548,7 @@ export const dict = {
|
|||||||
"workspace.billing.addAction": "Dodaj",
|
"workspace.billing.addAction": "Dodaj",
|
||||||
"workspace.billing.addBalance": "Doładuj saldo",
|
"workspace.billing.addBalance": "Doładuj saldo",
|
||||||
"workspace.billing.alipay": "Alipay",
|
"workspace.billing.alipay": "Alipay",
|
||||||
|
"workspace.billing.wechat": "WeChat Pay",
|
||||||
"workspace.billing.linkedToStripe": "Połączono ze Stripe",
|
"workspace.billing.linkedToStripe": "Połączono ze Stripe",
|
||||||
"workspace.billing.manage": "Zarządzaj",
|
"workspace.billing.manage": "Zarządzaj",
|
||||||
"workspace.billing.enable": "Włącz rozliczenia",
|
"workspace.billing.enable": "Włącz rozliczenia",
|
||||||
|
|||||||
@@ -554,6 +554,7 @@ export const dict = {
|
|||||||
"workspace.billing.addAction": "Пополнить",
|
"workspace.billing.addAction": "Пополнить",
|
||||||
"workspace.billing.addBalance": "Пополнить баланс",
|
"workspace.billing.addBalance": "Пополнить баланс",
|
||||||
"workspace.billing.alipay": "Alipay",
|
"workspace.billing.alipay": "Alipay",
|
||||||
|
"workspace.billing.wechat": "WeChat Pay",
|
||||||
"workspace.billing.linkedToStripe": "Привязано к Stripe",
|
"workspace.billing.linkedToStripe": "Привязано к Stripe",
|
||||||
"workspace.billing.manage": "Управление",
|
"workspace.billing.manage": "Управление",
|
||||||
"workspace.billing.enable": "Включить оплату",
|
"workspace.billing.enable": "Включить оплату",
|
||||||
|
|||||||
@@ -543,6 +543,7 @@ export const dict = {
|
|||||||
"workspace.billing.addAction": "เพิ่ม",
|
"workspace.billing.addAction": "เพิ่ม",
|
||||||
"workspace.billing.addBalance": "เพิ่มยอดคงเหลือ",
|
"workspace.billing.addBalance": "เพิ่มยอดคงเหลือ",
|
||||||
"workspace.billing.alipay": "Alipay",
|
"workspace.billing.alipay": "Alipay",
|
||||||
|
"workspace.billing.wechat": "WeChat Pay",
|
||||||
"workspace.billing.linkedToStripe": "เชื่อมโยงกับ Stripe",
|
"workspace.billing.linkedToStripe": "เชื่อมโยงกับ Stripe",
|
||||||
"workspace.billing.manage": "จัดการ",
|
"workspace.billing.manage": "จัดการ",
|
||||||
"workspace.billing.enable": "เปิดใช้งานการเรียกเก็บเงิน",
|
"workspace.billing.enable": "เปิดใช้งานการเรียกเก็บเงิน",
|
||||||
|
|||||||
@@ -550,6 +550,7 @@ export const dict = {
|
|||||||
"workspace.billing.addAction": "Ekle",
|
"workspace.billing.addAction": "Ekle",
|
||||||
"workspace.billing.addBalance": "Bakiye Ekle",
|
"workspace.billing.addBalance": "Bakiye Ekle",
|
||||||
"workspace.billing.alipay": "Alipay",
|
"workspace.billing.alipay": "Alipay",
|
||||||
|
"workspace.billing.wechat": "WeChat Pay",
|
||||||
"workspace.billing.linkedToStripe": "Stripe'a bağlı",
|
"workspace.billing.linkedToStripe": "Stripe'a bağlı",
|
||||||
"workspace.billing.manage": "Yönet",
|
"workspace.billing.manage": "Yönet",
|
||||||
"workspace.billing.enable": "Faturalandırmayı Etkinleştir",
|
"workspace.billing.enable": "Faturalandırmayı Etkinleştir",
|
||||||
|
|||||||
@@ -524,6 +524,7 @@ export const dict = {
|
|||||||
"workspace.billing.addAction": "充值",
|
"workspace.billing.addAction": "充值",
|
||||||
"workspace.billing.addBalance": "充值余额",
|
"workspace.billing.addBalance": "充值余额",
|
||||||
"workspace.billing.alipay": "支付宝",
|
"workspace.billing.alipay": "支付宝",
|
||||||
|
"workspace.billing.wechat": "微信支付",
|
||||||
"workspace.billing.linkedToStripe": "已关联 Stripe",
|
"workspace.billing.linkedToStripe": "已关联 Stripe",
|
||||||
"workspace.billing.manage": "管理",
|
"workspace.billing.manage": "管理",
|
||||||
"workspace.billing.enable": "启用计费",
|
"workspace.billing.enable": "启用计费",
|
||||||
|
|||||||
@@ -524,6 +524,7 @@ export const dict = {
|
|||||||
"workspace.billing.addAction": "儲值",
|
"workspace.billing.addAction": "儲值",
|
||||||
"workspace.billing.addBalance": "儲值餘額",
|
"workspace.billing.addBalance": "儲值餘額",
|
||||||
"workspace.billing.alipay": "支付寶",
|
"workspace.billing.alipay": "支付寶",
|
||||||
|
"workspace.billing.wechat": "微信支付",
|
||||||
"workspace.billing.linkedToStripe": "已連結 Stripe",
|
"workspace.billing.linkedToStripe": "已連結 Stripe",
|
||||||
"workspace.billing.manage": "管理",
|
"workspace.billing.manage": "管理",
|
||||||
"workspace.billing.enable": "啟用帳務",
|
"workspace.billing.enable": "啟用帳務",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { createMemo, Match, Show, Switch, createEffect } from "solid-js"
|
|||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { Billing } from "@opencode-ai/console-core/billing.js"
|
import { Billing } from "@opencode-ai/console-core/billing.js"
|
||||||
import { withActor } from "~/context/auth.withActor"
|
import { withActor } from "~/context/auth.withActor"
|
||||||
import { IconAlipay, IconCreditCard, IconStripe } from "~/component/icon"
|
import { IconAlipay, IconCreditCard, IconStripe, IconWechat } from "~/component/icon"
|
||||||
import styles from "./billing-section.module.css"
|
import styles from "./billing-section.module.css"
|
||||||
import { createCheckoutUrl, formatBalance, queryBillingInfo } from "../../common"
|
import { createCheckoutUrl, formatBalance, queryBillingInfo } from "../../common"
|
||||||
import { useI18n } from "~/context/i18n"
|
import { useI18n } from "~/context/i18n"
|
||||||
@@ -208,6 +208,9 @@ export function BillingSection() {
|
|||||||
<Match when={billingInfo()?.paymentMethodType === "alipay"}>
|
<Match when={billingInfo()?.paymentMethodType === "alipay"}>
|
||||||
<IconAlipay style={{ width: "24px", height: "24px" }} />
|
<IconAlipay style={{ width: "24px", height: "24px" }} />
|
||||||
</Match>
|
</Match>
|
||||||
|
<Match when={billingInfo()?.paymentMethodType === "wechat_pay"}>
|
||||||
|
<IconWechat style={{ width: "24px", height: "24px" }} />
|
||||||
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
<div data-slot="card-details">
|
<div data-slot="card-details">
|
||||||
@@ -224,6 +227,9 @@ export function BillingSection() {
|
|||||||
<Match when={billingInfo()?.paymentMethodType === "alipay"}>
|
<Match when={billingInfo()?.paymentMethodType === "alipay"}>
|
||||||
<span data-slot="type">{i18n.t("workspace.billing.alipay")}</span>
|
<span data-slot="type">{i18n.t("workspace.billing.alipay")}</span>
|
||||||
</Match>
|
</Match>
|
||||||
|
<Match when={billingInfo()?.paymentMethodType === "wechat_pay"}>
|
||||||
|
<span data-slot="type">{i18n.t("workspace.billing.wechat")}</span>
|
||||||
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -136,6 +136,11 @@ export async function handler(
|
|||||||
...createBodyConverter(opts.format, providerInfo.format)(body),
|
...createBodyConverter(opts.format, providerInfo.format)(body),
|
||||||
model: providerInfo.model,
|
model: providerInfo.model,
|
||||||
...(providerInfo.payloadModifier ?? {}),
|
...(providerInfo.payloadModifier ?? {}),
|
||||||
|
...Object.fromEntries(
|
||||||
|
Object.entries(providerInfo.payloadMappings ?? {})
|
||||||
|
.map(([k, v]) => [k, input.request.headers.get(v)])
|
||||||
|
.filter(([_k, v]) => !!v),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
authInfo?.workspaceID,
|
authInfo?.workspaceID,
|
||||||
),
|
),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user