Compare commits

...

5 Commits

Author SHA1 Message Date
Kit Langton a86b653005 feat(tui): diffuse unread tab glow on resolve 2026-08-13 23:05:07 -04:00
Kit Langton fec4f20736 perf(core): load MCP client lazily (#42468) 2026-08-13 22:48:09 -04:00
opencode-agent[bot] 49d07ffe5f fix(core): use file times for tool output cleanup (#42450)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-08-13 21:44:16 -05:00
opencode-agent[bot] 28f6968dda fix(www): point edit links to v2 (#42472)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-08-13 21:42:46 -05:00
opencode-agent[bot] 979ac810af feat(app): add Hebrew locale (#42475)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
2026-08-14 12:31:48 +10:00
17 changed files with 1672 additions and 35 deletions
+48
View File
@@ -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`.
+2 -1
View File
@@ -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", () => {
+3
View File
@@ -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
+8 -3
View File
@@ -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(
+14 -6
View File
@@ -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 })
}
})
+4 -3
View File
@@ -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)
+20
View File
@@ -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) }
+36 -17
View File
@@ -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>
+50 -4
View File
@@ -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,
+199
View File
@@ -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}} הצגות",
}
+1 -1
View File
@@ -19,7 +19,7 @@ export default defineConfig({
github: {
owner: "anomalyco",
repo: "opencode",
branch: "dev",
branch: "v2",
dir: "packages/www",
},
theme: {
+6
View File
@@ -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")
})
+1
View File
@@ -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",