mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-13 23:09:50 -04:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a86b653005 | |||
| fec4f20736 | |||
| 49d07ffe5f | |||
| 28f6968dda | |||
| 979ac810af |
@@ -0,0 +1,48 @@
|
||||
# he Glossary
|
||||
|
||||
## Sources
|
||||
|
||||
- Hebrew Academy approved IT terminology: https://terms.hebrew-academy.org.il/Millonim/ShowMillon?KodMillon=192
|
||||
- Firefox Hebrew localization corpus: https://github.com/mozilla-l10n/firefox-l10n/tree/main/he
|
||||
- KDE Hebrew localization team and corpus: https://l10n.kde.org/team-infos.php?teamcode=he
|
||||
- Community-maintained VS Code Hebrew language pack: https://github.com/AMAARETS/vscode-language-pack-he
|
||||
- Microsoft Hebrew developer documentation for Git terminology: https://learn.microsoft.com/he-il/power-platform/alm/tutorials/github-actions-deploy
|
||||
- W3C guidance for bidirectional text: https://www.w3.org/International/articles/strings-and-bidi/
|
||||
|
||||
## Do Not Translate (Locale Additions)
|
||||
|
||||
- `OpenCode` (preserve casing in prose and UI copy)
|
||||
- `API`, `MCP`, `LSP`, `OAuth`, `Git`, model names, and provider names
|
||||
- Commands, flags, keyboard shortcuts, file paths, URLs, identifiers, hashes, and code literals
|
||||
- Keep `commit` and `diff` when they name the exact Git artifact or operation
|
||||
|
||||
## Preferred Terms
|
||||
|
||||
| English / Context | Preferred | Notes |
|
||||
| ----------------- | ------------- | ------------------------------------------------------------------------- |
|
||||
| session | `הפעלה` | Use `שיחה` only when the source specifically means a chat or conversation |
|
||||
| workspace | `סביבת עבודה` | |
|
||||
| terminal | `מסוף` | Prefer the established Hebrew term over transliteration |
|
||||
| command | `פקודה` | |
|
||||
| provider | `ספק` | Use `ספק מודלים` where the bare noun is ambiguous |
|
||||
| model | `מודל` | |
|
||||
| API key | `מפתח API` | Keep the acronym in Latin letters |
|
||||
| plugin | `תוסף` | |
|
||||
| repository | `מאגר` | Use `מאגר Git` where context is ambiguous |
|
||||
| branch | `ענף` | |
|
||||
| context | `הקשר` | Use `חלון הקשר` for context window |
|
||||
| tokens | `אסימונים` | |
|
||||
|
||||
## Guidance
|
||||
|
||||
- Prefer natural modern Israeli Hebrew over word-for-word translation or obscure coined terms.
|
||||
- Use short action verbs for controls and translate complete phrases in context.
|
||||
- Keep recognized developer acronyms and exact Git vocabulary in Latin script instead of phonetic transliteration.
|
||||
- Treat embedded code, paths, commands, shortcuts, hashes, model IDs, and other Latin technical artifacts as LTR content inside the RTL interface.
|
||||
- Keep recurring concepts consistent and do not collapse session, chat, run, and launch into one Hebrew term.
|
||||
|
||||
## Avoid
|
||||
|
||||
- Avoid transliterations such as `טרמינל`, `פלאגין`, and `קומנד` when `מסוף`, `תוסף`, and `פקודה` are clear.
|
||||
- Avoid translating `commit` as `התחייבות`.
|
||||
- Avoid inventing Hebrew expansions for `API`, `MCP`, or `LSP`.
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
export type Locale = DesktopNativeLocale
|
||||
export type Direction = "ltr" | "rtl"
|
||||
|
||||
const RTL_LOCALES: ReadonlySet<Locale> = new Set(["ar", "ur", "pa", "fa", "dv"])
|
||||
const RTL_LOCALES: ReadonlySet<Locale> = new Set(["ar", "he", "ur", "pa", "fa", "dv"])
|
||||
|
||||
function localeDirection(locale: Locale): Direction {
|
||||
return RTL_LOCALES.has(locale) ? "rtl" : "ltr"
|
||||
@@ -73,6 +73,7 @@ const loaders: Record<Exclude<Locale, "en">, () => Promise<Dictionary>> = {
|
||||
ru: () => merge(import("@/i18n/ru"), import("@opencode-ai/ui/i18n/ru")),
|
||||
uk: () => merge(import("@/i18n/uk"), import("@opencode-ai/ui/i18n/uk")),
|
||||
ar: () => merge(import("@/i18n/ar"), import("@opencode-ai/ui/i18n/ar")),
|
||||
he: () => merge(import("@/i18n/he"), import("@opencode-ai/ui/i18n/he")),
|
||||
no: () => merge(import("@/i18n/no"), import("@opencode-ai/ui/i18n/no")),
|
||||
br: () => merge(import("@/i18n/br"), import("@opencode-ai/ui/i18n/br")),
|
||||
th: () => merge(import("@/i18n/th"), import("@opencode-ai/ui/i18n/th")),
|
||||
|
||||
@@ -29,6 +29,7 @@ describe("desktop native translations", () => {
|
||||
"Українська",
|
||||
"Bosanski",
|
||||
"العربية",
|
||||
"עברית",
|
||||
"Norsk",
|
||||
"Português (Brasil)",
|
||||
"ไทย",
|
||||
@@ -133,6 +134,11 @@ describe("desktop native locale detection", () => {
|
||||
expect(detectDesktopNativeLocale(["nb-NO"])).toBe("no")
|
||||
expect(detectDesktopNativeLocale(["nn-NO"])).toBe("no")
|
||||
})
|
||||
|
||||
test("recognizes Hebrew language tags", () => {
|
||||
expect(detectDesktopNativeLocale(["he"])).toBe("he")
|
||||
expect(detectDesktopNativeLocale(["he-IL"])).toBe("he")
|
||||
})
|
||||
})
|
||||
|
||||
describe("desktop native ICU data", () => {
|
||||
|
||||
@@ -13,6 +13,7 @@ export const DESKTOP_NATIVE_LOCALES = [
|
||||
"uk",
|
||||
"bs",
|
||||
"ar",
|
||||
"he",
|
||||
"no",
|
||||
"br",
|
||||
"th",
|
||||
@@ -80,6 +81,7 @@ export const DESKTOP_NATIVE_LABELS: Record<DesktopNativeLocale, string> = {
|
||||
uk: "Українська",
|
||||
bs: "Bosanski",
|
||||
ar: "العربية",
|
||||
he: "עברית",
|
||||
no: "Norsk",
|
||||
br: "Português (Brasil)",
|
||||
th: "ไทย",
|
||||
@@ -145,6 +147,7 @@ export const DESKTOP_NATIVE_LOCALE_TAGS: Record<DesktopNativeLocale, string> = {
|
||||
uk: "uk",
|
||||
bs: "bs",
|
||||
ar: "ar",
|
||||
he: "he-IL",
|
||||
no: "nb-NO",
|
||||
br: "pt-BR",
|
||||
th: "th",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,8 +19,7 @@ import { KeyedMutex } from "../effect/keyed-mutex.js"
|
||||
import { Location } from "../location.js"
|
||||
import { waitForAbort } from "@opencode-ai/util/process"
|
||||
import { State } from "../state.js"
|
||||
import { MCPClient } from "./client.js"
|
||||
import { MCPOAuth } from "./oauth.js"
|
||||
import type { MCPClient } from "./client.js"
|
||||
|
||||
export const ServerName = Schema.String.pipe(Schema.brand("MCP.ServerName"))
|
||||
export type ServerName = typeof ServerName.Type
|
||||
@@ -245,7 +244,11 @@ export const layer = (options?: Options) =>
|
||||
draft.method.update({
|
||||
integrationID,
|
||||
method: { id: methodID, type: "oauth", label: name },
|
||||
authorize: () => MCPOAuth.authorize({ name, config: remote, methodID }),
|
||||
authorize: () =>
|
||||
Effect.gen(function* () {
|
||||
const { MCPOAuth } = yield* Effect.promise(() => import("./oauth.js"))
|
||||
return yield* MCPOAuth.authorize({ name, config: remote, methodID })
|
||||
}),
|
||||
})
|
||||
})
|
||||
.pipe(Scope.provide(scope))
|
||||
@@ -264,6 +267,7 @@ export const layer = (options?: Options) =>
|
||||
// opens a browser, so an auth-gated connect ends in UnauthorizedError -> needs_auth rather than a redirect.
|
||||
const connectProvider = Effect.fnUntraced(function* (entry: ServerEntry) {
|
||||
if (entry.config.type !== "remote" || !entry.integrationID) return undefined
|
||||
const { MCPOAuth } = yield* Effect.promise(() => import("./oauth.js"))
|
||||
const remote = entry.config
|
||||
const oauth = remote.oauth || undefined
|
||||
const base = {
|
||||
@@ -505,6 +509,7 @@ export const layer = (options?: Options) =>
|
||||
const scope = yield* Scope.fork(root)
|
||||
entry.scope = scope
|
||||
const authProvider = yield* connectProvider(entry)
|
||||
const { MCPClient } = yield* Effect.promise(() => import("./client.js"))
|
||||
// List tools as part of connect so a failure here marks the server failed rather than
|
||||
// leaving it connected with a silently empty tool list and no path to recover.
|
||||
const result = yield* MCPClient.connect(
|
||||
|
||||
@@ -2,7 +2,7 @@ export * as ToolOutput from "./tool-output.js"
|
||||
|
||||
import path from "path"
|
||||
import type { Tool } from "@opencode-ai/schema/tool"
|
||||
import { Context, Duration, Effect, Layer, Schedule } from "effect"
|
||||
import { Context, Duration, Effect, Layer, Option, Schedule } from "effect"
|
||||
import { makeGlobalNode, makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
@@ -24,15 +24,23 @@ export interface Interface {
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ToolOutput") {}
|
||||
|
||||
const cleanup = Effect.fn("ToolOutput.cleanup")(function* (fs: FSUtil.Interface, directory: string) {
|
||||
const cutoff = Identifier.timestamp(Identifier.create("tool", "ascending", Date.now() - Duration.toMillis(RETENTION)))
|
||||
const cutoff = Date.now() - Duration.toMillis(RETENTION)
|
||||
const entries = yield* fs.readDirectory(directory).pipe(
|
||||
Effect.map((entries) => entries.filter((entry) => /^tool_[0-9a-f]{12}/.test(entry))),
|
||||
Effect.catch(() => Effect.succeed([])),
|
||||
)
|
||||
for (const entry of entries) {
|
||||
if (Identifier.timestamp(entry) >= cutoff) continue
|
||||
yield* fs.remove(path.join(directory, entry)).pipe(Effect.catch(() => Effect.void))
|
||||
}
|
||||
yield* Effect.forEach(
|
||||
entries,
|
||||
(entry) =>
|
||||
Effect.gen(function* () {
|
||||
const file = path.join(directory, entry)
|
||||
const info = yield* fs.stat(file).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||
const mtime = info && Option.getOrUndefined(info.mtime)
|
||||
if (!mtime || mtime.getTime() >= cutoff) return
|
||||
yield* fs.remove(file).pipe(Effect.catch(() => Effect.void))
|
||||
}),
|
||||
{ concurrency: 8, discard: true },
|
||||
)
|
||||
})
|
||||
|
||||
const layer = Layer.effect(
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { mkdtemp, rm } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
|
||||
const root = path.resolve(import.meta.dir, "../../..")
|
||||
|
||||
test("loads the MCP SDK only when connecting or authorizing", async () => {
|
||||
const temporary = await mkdtemp(path.join(import.meta.dir, ".mcp-import-boundary-"))
|
||||
const metafile = path.join(temporary, "meta.json")
|
||||
try {
|
||||
const result = Bun.spawn(
|
||||
[
|
||||
process.execPath,
|
||||
"build",
|
||||
"packages/core/src/mcp/index.ts",
|
||||
"--target=node",
|
||||
"--format=esm",
|
||||
"--packages=bundle",
|
||||
"--splitting",
|
||||
`--metafile=${metafile}`,
|
||||
`--outdir=${path.join(temporary, "out")}`,
|
||||
],
|
||||
{ cwd: root, stdout: "pipe", stderr: "pipe" },
|
||||
)
|
||||
const [exitCode, stdout, stderr] = await Promise.all([
|
||||
result.exited,
|
||||
new Response(result.stdout).text(),
|
||||
new Response(result.stderr).text(),
|
||||
])
|
||||
if (exitCode !== 0) throw new Error(stdout + stderr)
|
||||
|
||||
const metadata = await Bun.file(metafile).json()
|
||||
const imports = metadata.inputs["packages/core/src/mcp/index.ts"].imports
|
||||
const lazy = imports.filter(
|
||||
(item: { original?: string }) => item.original === "./client.js" || item.original === "./oauth.js",
|
||||
)
|
||||
expect(new Set(lazy.map((item: { original: string }) => item.original))).toEqual(
|
||||
new Set(["./client.js", "./oauth.js"]),
|
||||
)
|
||||
expect(lazy.every((item: { kind: string }) => item.kind === "dynamic-import")).toBe(true)
|
||||
} finally {
|
||||
await rm(temporary, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
@@ -143,15 +143,16 @@ describe("ToolOutput", () => {
|
||||
),
|
||||
)
|
||||
|
||||
it.live("removes expired managed files", () =>
|
||||
it.live("uses file modification time when IDs wrap", () =>
|
||||
withStore((output, fs, root) =>
|
||||
Effect.gen(function* () {
|
||||
const directory = path.join(root, ToolOutput.DIRECTORY)
|
||||
const old = path.join(directory, Identifier.create("tool", "ascending", Date.now() - 8 * 24 * 60 * 60 * 1_000))
|
||||
const recent = path.join(directory, Identifier.ascending("tool"))
|
||||
const old = path.join(directory, Identifier.create("tool", "ascending", 2 ** 36 - 1))
|
||||
const recent = path.join(directory, Identifier.create("tool", "ascending", 2 ** 36 + 1))
|
||||
yield* fs.ensureDir(directory)
|
||||
yield* fs.writeFileString(old, "old")
|
||||
yield* fs.writeFileString(recent, "recent")
|
||||
yield* fs.utimes(old, new Date(), new Date(Date.now() - 8 * 24 * 60 * 60 * 1_000))
|
||||
yield* output.cleanup()
|
||||
expect(yield* fs.exists(old)).toBe(false)
|
||||
expect(yield* fs.exists(recent)).toBe(true)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
export const dict = {
|
||||
"desktop.menu.checkForUpdates": "בדוק אם יש עדכונים...",
|
||||
"desktop.menu.reloadWebview": "טען מחדש את תצוגת האינטרנט",
|
||||
"desktop.menu.restart": "הפעל מחדש",
|
||||
"desktop.dialog.chooseFolder": "בחירת תיקייה",
|
||||
"desktop.dialog.chooseFile": "בחירת קובץ",
|
||||
"desktop.dialog.saveFile": "שמירת קובץ",
|
||||
"desktop.updater.checkFailed.title": "בדיקת העדכונים נכשלה",
|
||||
"desktop.updater.checkFailed.message": "לא ניתן לבדוק אם קיימים עדכונים",
|
||||
"desktop.updater.none.title": "אין עדכון זמין",
|
||||
"desktop.updater.none.message": "כבר מותקנת הגרסה העדכנית ביותר של OpenCode",
|
||||
"desktop.updater.downloadFailed.title": "העדכון נכשל",
|
||||
"desktop.updater.downloadFailed.message": "הורדת העדכון נכשלה",
|
||||
"desktop.updater.downloaded.title": "העדכון הורד",
|
||||
"desktop.updater.downloaded.prompt": "גרסה {{version}} של OpenCode הורדה. להתקין אותה ולהפעיל מחדש את היישום?",
|
||||
"desktop.updater.installFailed.title": "העדכון נכשל",
|
||||
"desktop.updater.installFailed.message": "התקנת העדכון נכשלה",
|
||||
"desktop.error.dev.rootNotFound":
|
||||
"רכיב השורש לא נמצא. האם שכחת להוסיף אותו ל-index.html, או שיש טעות בשם מאפיין ה-id?",
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import { dict as desktopPl } from "./pl"
|
||||
import { dict as desktopRu } from "./ru"
|
||||
import { dict as desktopUk } from "./uk"
|
||||
import { dict as desktopAr } from "./ar"
|
||||
import { dict as desktopHe } from "./he"
|
||||
import { dict as desktopNo } from "./no"
|
||||
import { dict as desktopBr } from "./br"
|
||||
import { dict as desktopBs } from "./bs"
|
||||
@@ -126,6 +127,7 @@ function build(locale: Locale): Dictionary {
|
||||
if (locale === "ru") return { ...base, ...i18n.flatten(desktopRu) }
|
||||
if (locale === "uk") return { ...base, ...i18n.flatten(desktopUk) }
|
||||
if (locale === "ar") return { ...base, ...i18n.flatten(desktopAr) }
|
||||
if (locale === "he") return { ...base, ...i18n.flatten(desktopHe) }
|
||||
if (locale === "no") return { ...base, ...i18n.flatten(desktopNo) }
|
||||
if (locale === "br") return { ...base, ...i18n.flatten(desktopBr) }
|
||||
if (locale === "bs") return { ...base, ...i18n.flatten(desktopBs) }
|
||||
|
||||
@@ -71,8 +71,8 @@ export type SessionTabsController = Pick<ContextController, "tabs" | "current" |
|
||||
status(sessionID: string): SessionTabsStatus
|
||||
}
|
||||
const NEW_SESSION_TAB: SessionTab = { sessionID: "new", title: NEW_SESSION_TAB_TITLE }
|
||||
const glowTextColor = (base: RGBA, glow: RGBA, index: number, width: number) =>
|
||||
tint(base, glow, 0.12 * unreadGlowIntensity(index, width))
|
||||
const glowTextColor = (base: RGBA, glow: RGBA, index: number, width: number, level = 1) =>
|
||||
tint(base, glow, 0.12 * unreadGlowIntensity(index, width) * level)
|
||||
|
||||
function createNumberIgnition(runs: () => boolean, prompt: () => number, animations: () => boolean) {
|
||||
const ignition = createAnimatable({ level: 0 }, { enabled: animations, transition: tween({ duration: 0.7 }) })
|
||||
@@ -430,11 +430,7 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
hovered() === tab.sessionID && !selected()
|
||||
? foreground()
|
||||
: tint(idleNumber(), tint(theme.text.default, pulseBackground(), 0.25), Number(selected()))
|
||||
const color = status().attention
|
||||
? theme.text.feedback.warning.default
|
||||
: status().unread === "error"
|
||||
? theme.text.feedback.error.default
|
||||
: tint(base, accent(), Number(complete()))
|
||||
const color = tint(base, glowHue(), numberGlow.value().level)
|
||||
const runningColor = runs() ? activeNumber() : color
|
||||
return sweepLevel() === 0
|
||||
? tint(runningColor, theme.text.default, numberIgnition.value().level)
|
||||
@@ -445,10 +441,13 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
return selected() ? theme.text.default : theme.text.subdued
|
||||
}
|
||||
const complete = () => status().complete
|
||||
// Latched so a resolving glow fades out in the hue it lit with instead of snapping to accent.
|
||||
let lastGlowHue: RGBA | undefined
|
||||
const glowHue = () => {
|
||||
if (status().attention) return theme.text.feedback.warning.default
|
||||
if (status().unread === "error") return theme.text.feedback.error.default
|
||||
return accent()
|
||||
if (status().attention) return (lastGlowHue = theme.text.feedback.warning.default)
|
||||
if (status().unread === "error") return (lastGlowHue = theme.text.feedback.error.default)
|
||||
if (status().unread !== undefined) return (lastGlowHue = accent())
|
||||
return lastGlowHue ?? accent()
|
||||
}
|
||||
const pulseColor = createMemo(() => tint(pulseBackground(), theme.text.default, 0.25))
|
||||
const flashColor = createMemo(() => tint(pulseBackground(), theme.text.default, 0.7))
|
||||
@@ -462,6 +461,21 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
? fadeTitleColor(detailColor(), pulseBackground(), index, visibleDetailParts().length, 0)
|
||||
: detailColor()
|
||||
const glows = () => status().glows
|
||||
// Text tints ride an eased level so they diffuse away with the background glow instead of snapping.
|
||||
const titleGlow = createAnimatable(
|
||||
{ level: 0 },
|
||||
{ enabled: animations, transition: tween({ duration: 0.4 }) },
|
||||
)
|
||||
createEffect(() => titleGlow.animate({ level: glows() ? 1 : 0 }))
|
||||
const numberGlow = createAnimatable(
|
||||
{ level: 0 },
|
||||
{ enabled: animations, transition: tween({ duration: 0.4 }) },
|
||||
)
|
||||
createEffect(() =>
|
||||
numberGlow.animate({
|
||||
level: status().attention || status().unread === "error" || complete() ? 1 : 0,
|
||||
}),
|
||||
)
|
||||
const previous = createMemo(() => items()[index() - 1])
|
||||
const previousStatus = createMemo(() => {
|
||||
const tab = previous()
|
||||
@@ -472,17 +486,22 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
const previousGlows = () => previousStatus().glows
|
||||
const previousRuns = () => previousStatus().runs
|
||||
const indicatorWidth = 10
|
||||
let lastPreviousGlowHue: RGBA | undefined
|
||||
const previousGlowHue = () => {
|
||||
if (previousStatus().attention) return theme.text.feedback.warning.default
|
||||
if (previousStatus().unread === "error") return theme.text.feedback.error.default
|
||||
return accent()
|
||||
if (previousStatus().attention) return (lastPreviousGlowHue = theme.text.feedback.warning.default)
|
||||
if (previousStatus().unread === "error")
|
||||
return (lastPreviousGlowHue = theme.text.feedback.error.default)
|
||||
if (previousStatus().unread !== undefined) return (lastPreviousGlowHue = accent())
|
||||
return lastPreviousGlowHue ?? accent()
|
||||
}
|
||||
const separatorUpperColor = createMemo(() => tint(theme.background.default, previousGlowHue(), 0.1))
|
||||
const separatorLowerColor = createMemo(() => tint(theme.background.default, glowHue(), 0.12))
|
||||
const titleColor = (index: number) => {
|
||||
const color = glows()
|
||||
? glowTextColor(foreground(), glowColor(), 1 + numberWidth() + index, width())
|
||||
: foreground()
|
||||
const level = titleGlow.value().level
|
||||
const color =
|
||||
level === 0
|
||||
? foreground()
|
||||
: glowTextColor(foreground(), glowColor(), 1 + numberWidth() + index, width(), level)
|
||||
return titleFades()
|
||||
? fadeTitleColor(
|
||||
color,
|
||||
@@ -628,7 +647,7 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
selectable={false}
|
||||
attributes={selected() ? TextAttributes.BOLD : undefined}
|
||||
>
|
||||
<Show when={glows() || titleFades()} fallback={visibleTitle()}>
|
||||
<Show when={titleGlow.value().level > 0 || titleFades()} fallback={visibleTitle()}>
|
||||
<For each={visibleTitleParts()}>
|
||||
{(character, index) => <span style={{ fg: titleColor(index()) }}>{character}</span>}
|
||||
</For>
|
||||
|
||||
@@ -47,6 +47,9 @@ const GLOW_IGNITION_DURATION = 600
|
||||
const GLOW_IGNITION_PEAK = 1.5
|
||||
const GLOW_IGNITION_ATTACK = 0.3
|
||||
const GLOW_FADE_OUT = 200
|
||||
const GLOW_RELEASE_DURATION = 900
|
||||
const GLOW_RELEASE_ATTACK = 0.12
|
||||
const GLOW_RELEASE_PEAK = 1.25
|
||||
const GLOW_TAIL = 12
|
||||
const GLOW_OPACITY = 0.16
|
||||
const DEFAULT_FOREGROUND = RGBA.defaultForeground()
|
||||
@@ -75,6 +78,8 @@ export const unreadGlowIntensity = (index: number, width: number, maximumTail =
|
||||
const tail = Math.min(maximumTail, Math.max(1, width - 2))
|
||||
return glowIntensityAt(index, tail)
|
||||
}
|
||||
/** How far a resolving glow has diffused: the resting tail spreads across the full width as it thins away. */
|
||||
const glowReleaseSpread = (progress: number, tail: number, width: number) => tail + smootherstep(progress) * width
|
||||
export function blendTabPulseColor(
|
||||
output: RGBA,
|
||||
background: RGBA,
|
||||
@@ -148,6 +153,10 @@ class Envelope {
|
||||
return this.clock !== undefined
|
||||
}
|
||||
|
||||
get progress() {
|
||||
return this.clock === undefined ? undefined : this.clock / this.duration
|
||||
}
|
||||
|
||||
level() {
|
||||
return this.clock === undefined ? 0 : this.scale * this.shape(this.clock / this.duration)
|
||||
}
|
||||
@@ -178,7 +187,19 @@ class PulseState {
|
||||
private edgeFlash = new Envelope(EDGE_FLASH_DURATION, (progress) => attackDecay(progress, EDGE_FLASH_ATTACK, 1, 0))
|
||||
private ignition = new Envelope(GLOW_IGNITION_DURATION, glowIgnitionLevel)
|
||||
private glowOff = new Envelope(GLOW_FADE_OUT, fadeOut)
|
||||
private envelopes = [this.runAttack, this.runFade, this.completionPulse, this.edgeFlash, this.ignition, this.glowOff]
|
||||
// A resolving glow's send-off: a gentle swell that decays while the spatial profile diffuses outward.
|
||||
private release = new Envelope(GLOW_RELEASE_DURATION, (progress) =>
|
||||
attackDecay(progress, GLOW_RELEASE_ATTACK, GLOW_RELEASE_PEAK, 0),
|
||||
)
|
||||
private envelopes = [
|
||||
this.runAttack,
|
||||
this.runFade,
|
||||
this.completionPulse,
|
||||
this.edgeFlash,
|
||||
this.ignition,
|
||||
this.glowOff,
|
||||
this.release,
|
||||
]
|
||||
|
||||
constructor(options: PulseStateOptions) {
|
||||
this.enabled = options.enabled
|
||||
@@ -211,6 +232,14 @@ class PulseState {
|
||||
return this.ignition.active ? this.ignition.level() : 1
|
||||
}
|
||||
|
||||
get releaseLevel() {
|
||||
return this.release.level()
|
||||
}
|
||||
|
||||
get releaseProgress() {
|
||||
return this.release.progress
|
||||
}
|
||||
|
||||
setEnabled(value: boolean) {
|
||||
if (value === this.enabled) return false
|
||||
this.enabled = value
|
||||
@@ -266,11 +295,16 @@ class PulseState {
|
||||
|
||||
setGlow(value: boolean) {
|
||||
if (value === this.glow) return false
|
||||
if (this.enabled && !value) this.glowOff.start(this.glowLevel)
|
||||
if (this.enabled && !value) {
|
||||
// Resolving the glow sends it off: residual glow drains while the release diffuses outward.
|
||||
this.glowOff.start(this.glowLevel)
|
||||
this.release.restart(Math.max(1, this.glowLevel))
|
||||
}
|
||||
this.glow = value
|
||||
this.ignition.stop()
|
||||
if (this.enabled && value) {
|
||||
this.glowOff.stop()
|
||||
this.release.stop()
|
||||
this.ignition.start()
|
||||
}
|
||||
return true
|
||||
@@ -523,16 +557,20 @@ class TabPulseRenderable extends Renderable {
|
||||
const completion = this.inner.completion
|
||||
const flash = this.inner.flash
|
||||
const glowLevel = this.inner.glowLevel
|
||||
const releaseLevel = this.inner.releaseLevel
|
||||
const outerRunning = this.outer.running
|
||||
const outerCompletion = this.outer.completion
|
||||
const outerFlash = this.outer.flash
|
||||
const outerGlowLevel = this.outer.glowLevel
|
||||
const outerReleaseLevel = this.outer.releaseLevel
|
||||
if (
|
||||
glowLevel === 0 &&
|
||||
releaseLevel === 0 &&
|
||||
running === 0 &&
|
||||
completion === 0 &&
|
||||
flash === 0 &&
|
||||
outerGlowLevel === 0 &&
|
||||
outerReleaseLevel === 0 &&
|
||||
outerRunning === 0 &&
|
||||
outerCompletion === 0 &&
|
||||
outerFlash === 0
|
||||
@@ -551,6 +589,8 @@ class TabPulseRenderable extends Renderable {
|
||||
)
|
||||
const glowTail = Math.min(this._glowTail, Math.max(1, this.width - 2))
|
||||
const outerGlowTail = Math.min(this._outerGlowTail, Math.max(1, this.width - 2))
|
||||
const releaseSpread = glowReleaseSpread(this.inner.releaseProgress ?? 0, glowTail, this.width)
|
||||
const outerReleaseSpread = glowReleaseSpread(this.outer.releaseProgress ?? 0, outerGlowTail, this.width)
|
||||
const flashTail = this._flashTail === undefined ? undefined : Math.min(this._flashTail, Math.max(1, this.width - 2))
|
||||
const outerFlashTail =
|
||||
this._outerFlashTail === undefined ? undefined : Math.min(this._outerFlashTail, Math.max(1, this.width - 2))
|
||||
@@ -581,7 +621,10 @@ class TabPulseRenderable extends Renderable {
|
||||
this._color,
|
||||
this._flashColor,
|
||||
this._completionColor,
|
||||
glowLevel === 0 ? 0 : glowIntensityAt(index, glowTail) * GLOW_OPACITY * glowLevel,
|
||||
Math.max(
|
||||
glowLevel === 0 ? 0 : glowIntensityAt(index, glowTail) * GLOW_OPACITY * glowLevel,
|
||||
releaseLevel === 0 ? 0 : glowIntensityAt(index, releaseSpread) * GLOW_OPACITY * releaseLevel,
|
||||
),
|
||||
sweep,
|
||||
flashTail === undefined ? flash : flash * tabFlashIntensity(index, flashTail),
|
||||
completion,
|
||||
@@ -597,7 +640,10 @@ class TabPulseRenderable extends Renderable {
|
||||
this._outerColor,
|
||||
this._outerFlashColor,
|
||||
this._outerCompletionColor,
|
||||
outerGlowLevel === 0 ? 0 : glowIntensityAt(index, outerGlowTail) * GLOW_OPACITY * outerGlowLevel,
|
||||
Math.max(
|
||||
outerGlowLevel === 0 ? 0 : glowIntensityAt(index, outerGlowTail) * GLOW_OPACITY * outerGlowLevel,
|
||||
outerReleaseLevel === 0 ? 0 : glowIntensityAt(index, outerReleaseSpread) * GLOW_OPACITY * outerReleaseLevel,
|
||||
),
|
||||
outerSweep,
|
||||
outerFlashTail === undefined ? outerFlash : outerFlash * tabFlashIntensity(index, outerFlashTail),
|
||||
outerCompletion,
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
export const dict = {
|
||||
"ui.sessionReview.title": "שינויים בהפעלה",
|
||||
"ui.sessionReview.title.git": "שינויים ב-Git",
|
||||
"ui.sessionReview.title.branch": "שינויים בענף",
|
||||
"ui.sessionReview.title.lastTurn": "שינויים בתור האחרון",
|
||||
"ui.sessionReview.diffStyle.unified": "מאוחד",
|
||||
"ui.sessionReview.diffStyle.split": "מפוצל",
|
||||
"ui.sessionReview.expandAll": "הרחב הכל",
|
||||
"ui.sessionReview.collapseAll": "כווץ הכל",
|
||||
"ui.sessionReview.change.added": "נוסף",
|
||||
"ui.sessionReview.change.removed": "הוסר",
|
||||
"ui.sessionReview.change.modified": "שונה",
|
||||
"ui.sessionReview.image.loading": "טוען...",
|
||||
"ui.sessionReview.image.placeholder": "תמונה",
|
||||
"ui.sessionReview.largeDiff.title": "ה-diff גדול מדי להצגה",
|
||||
"ui.sessionReview.largeDiff.meta": "המגבלה היא {{limit}} שורות שהשתנו. כעת יש {{current}} שורות שהשתנו.",
|
||||
"ui.sessionReview.largeDiff.renderAnyway": "הצג בכל זאת",
|
||||
"ui.sessionReviewV2.expandMode": "הרחבה או כיווץ של ה-diff",
|
||||
"ui.sessionReviewV2.filterFiles": "סינון קבצים",
|
||||
"ui.sessionReviewV2.toggleSidebar": "הצגה או הסתרה של עץ הקבצים",
|
||||
"ui.sessionReviewV2.showAllLines": "הצג את כל השורות",
|
||||
"ui.sessionReviewV2.hideNonDiffLines": "הסתרת שורות שאינן חלק מה-diff",
|
||||
"ui.sessionReviewV2.unifiedDiff": "diff מאוחד",
|
||||
"ui.sessionReviewV2.splitDiff": "diff מפוצל",
|
||||
"ui.sessionReviewV2.previousFile": "הקובץ הקודם",
|
||||
"ui.sessionReviewV2.nextFile": "הקובץ הבא",
|
||||
"ui.sessionReviewV2.diffView": "תצוגת diff",
|
||||
"ui.sessionReviewV2.empty.noGit.title": "אין שינויים במעקב",
|
||||
"ui.sessionReviewV2.empty.noGit.description": "עקוב, סקור ובטל שינויים בפרויקט הזה",
|
||||
"ui.sessionReviewV2.empty.noGit.action": "צור מאגר Git",
|
||||
"ui.sessionReviewV2.empty.noGit.actionLoading": "יוצר מאגר Git...",
|
||||
"ui.sessionReviewV2.empty.changes.title": "אין עדיין שינויים בקובץ",
|
||||
"ui.sessionReviewV2.empty.changes.description": "שינויים בפרויקט יופיעו כאן",
|
||||
"ui.sessionReview.openFile": "פתיחת קובץ",
|
||||
"ui.sessionReview.selection.line": "שורה {{line}}",
|
||||
"ui.sessionReview.selection.lines": "שורות {{start}}-{{end}}",
|
||||
"ui.fileMedia.kind.image": "תמונה",
|
||||
"ui.fileMedia.kind.audio": "אודיו",
|
||||
"ui.fileMedia.state.removed": "הוסר קובץ {{kind}}.",
|
||||
"ui.fileMedia.state.loading": "טוען {{kind}}...",
|
||||
"ui.fileMedia.state.error": "לא ניתן לטעון {{kind}}.",
|
||||
"ui.fileMedia.state.unavailable": "תצוגה מקדימה של {{kind}} אינה זמינה.",
|
||||
"ui.fileMedia.binary.title": "קובץ בינארי",
|
||||
"ui.fileMedia.binary.description.path": "{{path}} הוא קובץ בינארי.",
|
||||
"ui.fileMedia.binary.description.default": "תוכן בינארי",
|
||||
"ui.lineComment.label.prefix": "תגובה על ",
|
||||
"ui.lineComment.label.suffix": "",
|
||||
"ui.lineComment.editorLabel.prefix": "תגובה על ",
|
||||
"ui.lineComment.editorLabel.suffix": "",
|
||||
"ui.lineComment.placeholder": "הוסף תגובה",
|
||||
"ui.lineComment.contextPlaceholder": "הוסף הקשר לשינוי זה",
|
||||
"ui.lineComment.submit": "שלח תגובה",
|
||||
"ui.lineComment.cancel": "בטל",
|
||||
"ui.sessionTurn.steps.show": "הצג צעדים",
|
||||
"ui.sessionTurn.steps.hide": "הסתר צעדים",
|
||||
"ui.sessionTurn.summary.response": "תגובה",
|
||||
"ui.sessionTurn.diff.showMore": "הצג שינויים נוספים ({{count}})",
|
||||
"ui.sessionTurn.diffs.changed.one": "{{count}} קובץ השתנה",
|
||||
"ui.sessionTurn.diffs.changed.other": "{{count}} קבצים השתנו",
|
||||
"ui.sessionTurn.diffs.showAll": "הצג הכל",
|
||||
"ui.sessionTurn.diffs.showLess": "הצג פחות",
|
||||
"ui.sessionTurn.diffs.more": "+{{count}} קבצים נוספים",
|
||||
"ui.sessionTurn.retry.retrying": "מנסה שוב",
|
||||
"ui.sessionTurn.retry.inSeconds": "בעוד {{seconds}} שניות",
|
||||
"ui.sessionTurn.retry.attempt": "ניסיון #{{attempt}}",
|
||||
"ui.sessionTurn.retry.attemptLine": "{{line}} - ניסיון #{{attempt}}",
|
||||
"ui.sessionTurn.retry.geminiHot": "העומס על Gemini גבוה מדי כרגע",
|
||||
"ui.sessionTurn.error.freeUsageExceeded": "חריגה מהשימוש בחינם",
|
||||
"ui.sessionTurn.error.addCredits": "הוסף קרדיטים",
|
||||
"dialog.usageExceeded.freeTier.title": "הגעת למגבלה החינמית",
|
||||
"dialog.usageExceeded.freeTier.description":
|
||||
"הירשם ל-OpenCode Go לקבלת גישה אמינה למודלי הקוד הפתוח הטובים ביותר, החל מ-$5 לחודש.",
|
||||
"dialog.usageExceeded.freeTier.actionLabel": "הירשם",
|
||||
"dialog.usageExceeded.accountRateLimit.title": "הגעת למגבלת Go",
|
||||
"dialog.usageExceeded.accountRateLimit.description":
|
||||
"הגעת למגבלת השימוש. כדי להמשיך להשתמש במודל זה כעת, הפעל שימוש מהיתרה הזמינה שלך",
|
||||
"dialog.usageExceeded.accountRateLimit.actionLabel": "פתח את ההגדרות",
|
||||
"ui.sessionTurn.status.delegating": "מעביר את העבודה לסוכן אחר",
|
||||
"ui.sessionTurn.status.planning": "תכנון השלבים הבאים",
|
||||
"ui.sessionTurn.status.gatheringContext": "בודק את הפרויקט",
|
||||
"ui.sessionTurn.status.gatheredContext": "בדיקת הפרויקט הסתיימה",
|
||||
"ui.sessionTurn.status.searchingCodebase": "חיפוש בבסיס הקוד",
|
||||
"ui.sessionTurn.status.searchingWeb": "חיפוש באינטרנט",
|
||||
"ui.sessionTurn.status.makingEdits": "ביצוע עריכות",
|
||||
"ui.sessionTurn.status.runningCommands": "הפעלת פקודות",
|
||||
"ui.sessionTurn.status.thinking": "חושב",
|
||||
"ui.sessionTurn.status.thinkingWithTopic": "חשיבה - {{topic}}",
|
||||
"ui.sessionTurn.status.gatheringThoughts": "מגבש מחשבות",
|
||||
"ui.sessionTurn.status.consideringNextSteps": "בוחן את הצעדים הבאים",
|
||||
"ui.messagePart.diagnostic.error": "שגיאה",
|
||||
"ui.messagePart.title.edit": "ערוך",
|
||||
"ui.messagePart.title.write": "כתוב",
|
||||
"ui.messagePart.option.typeOwnAnswer": "הקלד את התשובה שלך",
|
||||
"ui.messagePart.review.title": "בדיקת התשובות",
|
||||
"ui.messagePart.questions.dismissed": "השאלות נדחו",
|
||||
"ui.messagePart.compaction": "ההפעלה נדחסה",
|
||||
"ui.messagePart.context.read.one": "{{count}} קריאה",
|
||||
"ui.messagePart.context.read.other": "{{count}} קריאות",
|
||||
"ui.messagePart.context.search.one": "{{count}} חיפוש",
|
||||
"ui.messagePart.context.search.other": "{{count}} חיפושים",
|
||||
"ui.messagePart.context.list.one": "{{count}} הצגה",
|
||||
"ui.messagePart.context.list.other": "{{count}} הצגות",
|
||||
"ui.list.loading": "טוען",
|
||||
"ui.list.empty": "אין תוצאות",
|
||||
"ui.list.clearFilter": "נקה מסנן",
|
||||
"ui.list.emptyWithFilter.prefix": "אין תוצאות עבור",
|
||||
"ui.list.emptyWithFilter.suffix": "",
|
||||
"ui.fileSearch.placeholder": "מצא",
|
||||
"ui.fileSearch.previousMatch": "התוצאה הקודמת",
|
||||
"ui.fileSearch.nextMatch": "התוצאה הבאה",
|
||||
"ui.fileSearch.close": "סגור חיפוש",
|
||||
"ui.messageNav.newMessage": "הודעה חדשה",
|
||||
"ui.promptInput.noMatchingItems": "אין פריטים תואמים",
|
||||
"ui.promptInput.commands": "פקודות",
|
||||
"ui.promptInput.dropFiles": "שחרר קבצים לצירוף",
|
||||
"ui.promptInput.removeAttachment": "הסר את הקובץ המצורף",
|
||||
"ui.promptInput.label": "פרומפט",
|
||||
"ui.promptInput.placeholder.shell": "הזן פקודת מעטפת...",
|
||||
"ui.promptInput.placeholder.normal": "שאל כל דבר, {{slash}} עבור פקודות, {{at}} עבור הקשר...",
|
||||
"ui.promptInput.add": "הוסף תמונות וקבצים",
|
||||
"ui.promptInput.attachments": "תמונות וקבצים",
|
||||
"ui.promptInput.context": "הקשר",
|
||||
"ui.promptInput.shell": "פקודת מעטפת",
|
||||
"ui.promptInput.chooseAgent": "בחר סוכן",
|
||||
"ui.promptInput.chooseModel": "בחר מודל",
|
||||
"ui.promptInput.chooseVariant": "בחר גרסה של מודל",
|
||||
"ui.promptInput.send": "שלח",
|
||||
"ui.promptInput.stop": "עצור",
|
||||
"ui.tabs.close": "סגור כרטיסייה",
|
||||
"ui.textField.copyToClipboard": "העתק ללוח",
|
||||
"ui.textField.copyLink": "העתק קישור",
|
||||
"ui.textField.copied": "הועתק",
|
||||
"ui.imagePreview.alt": "תצוגה מקדימה של תמונה",
|
||||
"ui.scrollView.ariaLabel": "תוכן שניתן לגלול",
|
||||
"ui.tool.read": "קרא",
|
||||
"ui.tool.loaded": "טעון",
|
||||
"ui.tool.list": "רשימה",
|
||||
"ui.tool.glob": "Glob",
|
||||
"ui.tool.grep": "Grep",
|
||||
"ui.tool.task": "משימה",
|
||||
"ui.tool.webfetch": "Webfetch",
|
||||
"ui.tool.websearch": "Web Search",
|
||||
"ui.tool.websearch.provider": "{{provider}} Web Search",
|
||||
"ui.tool.shell": "Shell",
|
||||
"ui.tool.patch": "Patch",
|
||||
"ui.tool.questions": "שאלות",
|
||||
"ui.tool.questions.numbered": "שאלות {{number}}",
|
||||
"ui.tool.agent": "סוכן {{type}}",
|
||||
"ui.tool.agent.default": "סוכן",
|
||||
"ui.tool.skill": "מיומנות",
|
||||
"ui.basicTool.called": "בוצעה קריאה אל `{{tool}}`",
|
||||
"ui.toolErrorCard.failed": "נכשל",
|
||||
"ui.toolErrorCard.copyError": "שגיאת העתקה",
|
||||
"ui.common.file.one": "קובץ",
|
||||
"ui.common.file.other": "קבצים",
|
||||
"ui.common.question.one": "שאלה",
|
||||
"ui.common.question.other": "שאלות",
|
||||
"ui.common.add": "הוסף",
|
||||
"ui.common.clear": "נקה",
|
||||
"ui.common.file": "קובץ",
|
||||
"ui.common.back": "חזרה",
|
||||
"ui.common.cancel": "בטל",
|
||||
"ui.common.confirm": "אשר",
|
||||
"ui.common.dismiss": "סגור",
|
||||
"ui.common.close": "סגור",
|
||||
"ui.common.next": "הבא",
|
||||
"ui.common.submit": "שלח",
|
||||
"ui.common.showMore": "הצג עוד",
|
||||
"ui.permission.deny": "דחה",
|
||||
"ui.permission.allowAlways": "אפשר תמיד",
|
||||
"ui.permission.allowOnce": "אפשר פעם אחת",
|
||||
"ui.message.expand": "הרחב את ההודעה",
|
||||
"ui.message.collapse": "כווץ הודעה",
|
||||
"ui.message.copy": "העתק",
|
||||
"ui.message.copyMessage": "העתק הודעה",
|
||||
"ui.message.forkMessage": "יצירת הפעלה חדשה מכאן",
|
||||
"ui.message.revertMessage": "ביטול ההודעה",
|
||||
"ui.message.copyResponse": "העתק את התגובה",
|
||||
"ui.message.copied": "הועתק",
|
||||
"ui.message.duration.seconds": "{{count}}s",
|
||||
"ui.message.duration.minutesSeconds": "{{minutes}}m {{seconds}}s",
|
||||
"ui.message.interrupted": "נקטע",
|
||||
"ui.message.queued": "בתור",
|
||||
"ui.message.attachment.alt": "קובץ מצורף",
|
||||
"ui.patch.action.deleted": "נמחק",
|
||||
"ui.patch.action.created": "נוצר",
|
||||
"ui.patch.action.moved": "הועבר",
|
||||
"ui.patch.action.patched": "עודכן",
|
||||
"ui.question.subtitle.answered": "שאלות שנענו: {{count}}",
|
||||
"ui.question.answer.none": "(אין תשובה)",
|
||||
"ui.question.review.notAnswered": "(ללא תשובה)",
|
||||
"ui.question.multiHint": "בחר את כל התשובות המתאימות",
|
||||
"ui.question.singleHint": "בחר תשובה אחת",
|
||||
"ui.question.custom.placeholder": "הקלד את תשובתך...",
|
||||
"ui.sessionTurn.diffs.changed.two": "{{count}} קבצים השתנו",
|
||||
"ui.messagePart.context.read.two": "{{count}} קריאות",
|
||||
"ui.messagePart.context.search.two": "{{count}} חיפושים",
|
||||
"ui.messagePart.context.list.two": "{{count}} הצגות",
|
||||
}
|
||||
@@ -19,7 +19,7 @@ export default defineConfig({
|
||||
github: {
|
||||
owner: "anomalyco",
|
||||
repo: "opencode",
|
||||
branch: "dev",
|
||||
branch: "v2",
|
||||
dir: "packages/www",
|
||||
},
|
||||
theme: {
|
||||
|
||||
@@ -75,10 +75,16 @@ describe("translate app", () => {
|
||||
"packages/ui/src/i18n/dv.ts",
|
||||
"packages/desktop/src/renderer/i18n/dv.ts",
|
||||
])
|
||||
expect(targetFiles("he")).toEqual([
|
||||
"packages/app/src/i18n/he.ts",
|
||||
"packages/ui/src/i18n/he.ts",
|
||||
"packages/desktop/src/renderer/i18n/he.ts",
|
||||
])
|
||||
})
|
||||
|
||||
test("maps product locale codes to their glossaries", () => {
|
||||
expect(glossaryFile("fr")).toBe(".opencode/glossary/fr.md")
|
||||
expect(glossaryFile("he")).toBe(".opencode/glossary/he.md")
|
||||
expect(glossaryFile("zh")).toBe(".opencode/glossary/zh-cn.md")
|
||||
expect(glossaryFile("zht")).toBe(".opencode/glossary/zh-tw.md")
|
||||
})
|
||||
|
||||
@@ -14,6 +14,7 @@ const locales = DESKTOP_NATIVE_LOCALES.filter((locale): locale is Locale => loca
|
||||
|
||||
const languages = {
|
||||
ar: "Arabic",
|
||||
he: "Hebrew",
|
||||
br: "Brazilian Portuguese",
|
||||
bs: "Bosnian",
|
||||
da: "Danish",
|
||||
|
||||
Reference in New Issue
Block a user