diff options
| author | Adam <[email protected]> | 2026-03-25 06:23:25 -0500 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-03-25 06:25:05 -0500 |
| commit | 898456a25cf2edbfc4ae4961b37424f633419dd6 (patch) | |
| tree | 626207d26b6c338136b069bbaaa09d67a45ddde3 /packages/app/src/context | |
| parent | 53d0b58ebf3468bd161dcfcdc67cd66b6508e9f8 (diff) | |
| download | opencode-898456a25cf2edbfc4ae4961b37424f633419dd6.tar.gz opencode-898456a25cf2edbfc4ae4961b37424f633419dd6.zip | |
Revert "fix(app): startup efficiency"
Diffstat (limited to 'packages/app/src/context')
| -rw-r--r-- | packages/app/src/context/global-sync.tsx | 58 | ||||
| -rw-r--r-- | packages/app/src/context/global-sync/bootstrap.ts | 337 | ||||
| -rw-r--r-- | packages/app/src/context/language.tsx | 144 | ||||
| -rw-r--r-- | packages/app/src/context/notification.tsx | 6 | ||||
| -rw-r--r-- | packages/app/src/context/settings.tsx | 13 | ||||
| -rw-r--r-- | packages/app/src/context/sync.tsx | 7 | ||||
| -rw-r--r-- | packages/app/src/context/terminal-title.ts | 51 |
7 files changed, 285 insertions, 331 deletions
diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index cbd08e99f..2d1e50135 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -9,7 +9,17 @@ import type { } from "@opencode-ai/sdk/v2/client" import { showToast } from "@opencode-ai/ui/toast" import { getFilename } from "@opencode-ai/util/path" -import { createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js" +import { + createContext, + getOwner, + Match, + onCleanup, + onMount, + type ParentProps, + Switch, + untrack, + useContext, +} from "solid-js" import { createStore, produce, reconcile } from "solid-js/store" import { useLanguage } from "@/context/language" import { Persist, persisted } from "@/utils/persist" @@ -70,8 +80,6 @@ function createGlobalSync() { let active = true let projectWritten = false - let bootedAt = 0 - let bootingRoot = false onCleanup(() => { active = false @@ -250,11 +258,6 @@ function createGlobalSync() { const sdk = sdkFor(directory) await bootstrapDirectory({ directory, - global: { - config: globalStore.config, - project: globalStore.project, - provider: globalStore.provider, - }, sdk, store: child[0], setStore: child[1], @@ -275,20 +278,15 @@ function createGlobalSync() { const unsub = globalSDK.event.listen((e) => { const directory = e.name const event = e.details - const recent = bootingRoot || Date.now() - bootedAt < 1500 if (directory === "global") { applyGlobalEvent({ event, project: globalStore.project, - refresh: () => { - if (recent) return - queue.refresh() - }, + refresh: queue.refresh, setGlobalProject: setProjects, }) if (event.type === "server.connected" || event.type === "global.disposed") { - if (recent) return for (const directory of Object.keys(children.children)) { queue.push(directory) } @@ -327,19 +325,17 @@ function createGlobalSync() { }) async function bootstrap() { - bootingRoot = true - try { - await bootstrapGlobal({ - globalSDK: globalSDK.client, - requestFailedTitle: language.t("common.requestFailed"), - translate: language.t, - formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }), - setGlobalStore: setBootStore, - }) - bootedAt = Date.now() - } finally { - bootingRoot = false - } + await bootstrapGlobal({ + globalSDK: globalSDK.client, + connectErrorTitle: language.t("dialog.server.add.error"), + connectErrorDescription: language.t("error.globalSync.connectFailed", { + url: globalSDK.url, + }), + requestFailedTitle: language.t("common.requestFailed"), + translate: language.t, + formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }), + setGlobalStore: setBootStore, + }) } onMount(() => { @@ -396,7 +392,13 @@ const GlobalSyncContext = createContext<ReturnType<typeof createGlobalSync>>() export function GlobalSyncProvider(props: ParentProps) { const value = createGlobalSync() - return <GlobalSyncContext.Provider value={value}>{props.children}</GlobalSyncContext.Provider> + return ( + <Switch> + <Match when={value.ready}> + <GlobalSyncContext.Provider value={value}>{props.children}</GlobalSyncContext.Provider> + </Match> + </Switch> + ) } export function useGlobalSync() { diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index 47be3abcb..13494b7ad 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -31,102 +31,73 @@ type GlobalStore = { reload: undefined | "pending" | "complete" } -function waitForPaint() { - return new Promise<void>((resolve) => { - let done = false - const finish = () => { - if (done) return - done = true - resolve() - } - const timer = setTimeout(finish, 50) - if (typeof requestAnimationFrame !== "function") return - requestAnimationFrame(() => { - clearTimeout(timer) - finish() - }) - }) -} - -function errors(list: PromiseSettledResult<unknown>[]) { - return list.filter((item): item is PromiseRejectedResult => item.status === "rejected").map((item) => item.reason) -} - -function runAll(list: Array<() => Promise<unknown>>) { - return Promise.allSettled(list.map((item) => item())) -} - -function showErrors(input: { - errors: unknown[] - title: string - translate: (key: string, vars?: Record<string, string | number>) => string - formatMoreCount: (count: number) => string -}) { - if (input.errors.length === 0) return - const message = formatServerError(input.errors[0], input.translate) - const more = input.errors.length > 1 ? input.formatMoreCount(input.errors.length - 1) : "" - showToast({ - variant: "error", - title: input.title, - description: message + more, - }) -} - export async function bootstrapGlobal(input: { globalSDK: OpencodeClient + connectErrorTitle: string + connectErrorDescription: string requestFailedTitle: string translate: (key: string, vars?: Record<string, string | number>) => string formatMoreCount: (count: number) => string setGlobalStore: SetStoreFunction<GlobalStore> }) { - const fast = [ - () => - retry(() => - input.globalSDK.path.get().then((x) => { - input.setGlobalStore("path", x.data!) - }), - ), - () => - retry(() => - input.globalSDK.global.config.get().then((x) => { - input.setGlobalStore("config", x.data!) - }), - ), - () => - retry(() => - input.globalSDK.provider.list().then((x) => { - input.setGlobalStore("provider", normalizeProviderList(x.data!)) - }), - ), - ] + const health = await input.globalSDK.global + .health() + .then((x) => x.data) + .catch(() => undefined) + if (!health?.healthy) { + showToast({ + variant: "error", + title: input.connectErrorTitle, + description: input.connectErrorDescription, + }) + input.setGlobalStore("ready", true) + return + } - const slow = [ - () => - retry(() => - input.globalSDK.project.list().then((x) => { - const projects = (x.data ?? []) - .filter((p) => !!p?.id) - .filter((p) => !!p.worktree && !p.worktree.includes("opencode-test")) - .slice() - .sort((a, b) => cmp(a.id, b.id)) - input.setGlobalStore("project", projects) - }), - ), + const tasks = [ + retry(() => + input.globalSDK.path.get().then((x) => { + input.setGlobalStore("path", x.data!) + }), + ), + retry(() => + input.globalSDK.global.config.get().then((x) => { + input.setGlobalStore("config", x.data!) + }), + ), + retry(() => + input.globalSDK.project.list().then((x) => { + const projects = (x.data ?? []) + .filter((p) => !!p?.id) + .filter((p) => !!p.worktree && !p.worktree.includes("opencode-test")) + .slice() + .sort((a, b) => cmp(a.id, b.id)) + input.setGlobalStore("project", projects) + }), + ), + retry(() => + input.globalSDK.provider.list().then((x) => { + input.setGlobalStore("provider", normalizeProviderList(x.data!)) + }), + ), + retry(() => + input.globalSDK.provider.auth().then((x) => { + input.setGlobalStore("provider_auth", x.data ?? {}) + }), + ), ] - showErrors({ - errors: errors(await runAll(fast)), - title: input.requestFailedTitle, - translate: input.translate, - formatMoreCount: input.formatMoreCount, - }) - await waitForPaint() - showErrors({ - errors: errors(await runAll(slow)), - title: input.requestFailedTitle, - translate: input.translate, - formatMoreCount: input.formatMoreCount, - }) + const results = await Promise.allSettled(tasks) + const errors = results.filter((r): r is PromiseRejectedResult => r.status === "rejected").map((r) => r.reason) + if (errors.length) { + const message = formatServerError(errors[0], input.translate) + const more = errors.length > 1 ? input.formatMoreCount(errors.length - 1) : "" + showToast({ + variant: "error", + title: input.requestFailedTitle, + description: message + more, + }) + } input.setGlobalStore("ready", true) } @@ -140,10 +111,6 @@ function groupBySession<T extends { id: string; sessionID: string }>(input: T[]) }, {}) } -function projectID(directory: string, projects: Project[]) { - return projects.find((project) => project.worktree === directory || project.sandboxes?.includes(directory))?.id -} - export async function bootstrapDirectory(input: { directory: string sdk: OpencodeClient @@ -152,130 +119,88 @@ export async function bootstrapDirectory(input: { vcsCache: VcsCache loadSessions: (directory: string) => Promise<void> | void translate: (key: string, vars?: Record<string, string | number>) => string - global: { - config: Config - project: Project[] - provider: ProviderListResponse - } }) { - const loading = input.store.status !== "complete" - const seededProject = projectID(input.directory, input.global.project) - if (seededProject) input.setStore("project", seededProject) - if (input.store.provider.all.length === 0 && input.global.provider.all.length > 0) { - input.setStore("provider", input.global.provider) - } - if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) { - input.setStore("config", input.global.config) - } - if (loading) input.setStore("status", "partial") - - const fast = [ - () => - seededProject - ? Promise.resolve() - : retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)), - () => retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", x.data ?? []))), - () => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))), - () => - retry(() => - input.sdk.path.get().then((x) => { - input.setStore("path", x.data!) - const next = projectID(x.data?.directory ?? input.directory, input.global.project) - if (next) input.setStore("project", next) - }), - ), - () => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))), - () => - retry(() => - input.sdk.vcs.get().then((x) => { - const next = x.data ?? input.store.vcs - input.setStore("vcs", next) - if (next?.branch) input.vcsCache.setStore("value", next) - }), - ), - () => retry(() => input.sdk.command.list().then((x) => input.setStore("command", x.data ?? []))), - () => - retry(() => - input.sdk.permission.list().then((x) => { - const grouped = groupBySession( - (x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID), - ) - batch(() => { - for (const sessionID of Object.keys(input.store.permission)) { - if (grouped[sessionID]) continue - input.setStore("permission", sessionID, []) - } - for (const [sessionID, permissions] of Object.entries(grouped)) { - input.setStore( - "permission", - sessionID, - reconcile( - permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)), - { key: "id" }, - ), - ) - } - }) - }), - ), - () => - retry(() => - input.sdk.question.list().then((x) => { - const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID)) - batch(() => { - for (const sessionID of Object.keys(input.store.question)) { - if (grouped[sessionID]) continue - input.setStore("question", sessionID, []) - } - for (const [sessionID, questions] of Object.entries(grouped)) { - input.setStore( - "question", - sessionID, - reconcile( - questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)), - { key: "id" }, - ), - ) - } - }) - }), - ), - ] - - const slow = [ - () => - retry(() => - input.sdk.provider.list().then((x) => { - input.setStore("provider", normalizeProviderList(x.data!)) - }), - ), - () => Promise.resolve(input.loadSessions(input.directory)), - () => retry(() => input.sdk.mcp.status().then((x) => input.setStore("mcp", x.data!))), - () => retry(() => input.sdk.lsp.status().then((x) => input.setStore("lsp", x.data!))), - ] - - const errs = errors(await runAll(fast)) - if (errs.length > 0) { - console.error("Failed to bootstrap instance", errs[0]) - const project = getFilename(input.directory) - showToast({ - variant: "error", - title: input.translate("toast.project.reloadFailed.title", { project }), - description: formatServerError(errs[0], input.translate), - }) + if (input.store.status !== "complete") input.setStore("status", "loading") + + const blockingRequests = { + project: () => input.sdk.project.current().then((x) => input.setStore("project", x.data!.id)), + provider: () => + input.sdk.provider.list().then((x) => { + input.setStore("provider", normalizeProviderList(x.data!)) + }), + agent: () => input.sdk.app.agents().then((x) => input.setStore("agent", x.data ?? [])), + config: () => input.sdk.config.get().then((x) => input.setStore("config", x.data!)), } - await waitForPaint() - const slowErrs = errors(await runAll(slow)) - if (slowErrs.length > 0) { - console.error("Failed to finish bootstrap instance", slowErrs[0]) + try { + await Promise.all(Object.values(blockingRequests).map((p) => retry(p))) + } catch (err) { + console.error("Failed to bootstrap instance", err) const project = getFilename(input.directory) showToast({ variant: "error", title: input.translate("toast.project.reloadFailed.title", { project }), - description: formatServerError(slowErrs[0], input.translate), + description: formatServerError(err, input.translate), }) + input.setStore("status", "partial") + return } - if (loading && errs.length === 0 && slowErrs.length === 0) input.setStore("status", "complete") + if (input.store.status !== "complete") input.setStore("status", "partial") + + Promise.all([ + input.sdk.path.get().then((x) => input.setStore("path", x.data!)), + input.sdk.command.list().then((x) => input.setStore("command", x.data ?? [])), + input.sdk.session.status().then((x) => input.setStore("session_status", x.data!)), + input.loadSessions(input.directory), + input.sdk.mcp.status().then((x) => input.setStore("mcp", x.data!)), + input.sdk.lsp.status().then((x) => input.setStore("lsp", x.data!)), + input.sdk.vcs.get().then((x) => { + const next = x.data ?? input.store.vcs + input.setStore("vcs", next) + if (next?.branch) input.vcsCache.setStore("value", next) + }), + input.sdk.permission.list().then((x) => { + const grouped = groupBySession( + (x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID), + ) + batch(() => { + for (const sessionID of Object.keys(input.store.permission)) { + if (grouped[sessionID]) continue + input.setStore("permission", sessionID, []) + } + for (const [sessionID, permissions] of Object.entries(grouped)) { + input.setStore( + "permission", + sessionID, + reconcile( + permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)), + { key: "id" }, + ), + ) + } + }) + }), + input.sdk.question.list().then((x) => { + const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID)) + batch(() => { + for (const sessionID of Object.keys(input.store.question)) { + if (grouped[sessionID]) continue + input.setStore("question", sessionID, []) + } + for (const [sessionID, questions] of Object.entries(grouped)) { + input.setStore( + "question", + sessionID, + reconcile( + questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)), + { key: "id" }, + ), + ) + } + }) + }), + ]).then(() => { + input.setStore("status", "complete") + }) } diff --git a/packages/app/src/context/language.tsx b/packages/app/src/context/language.tsx index 51dc09cd7..b1edd541c 100644 --- a/packages/app/src/context/language.tsx +++ b/packages/app/src/context/language.tsx @@ -1,10 +1,42 @@ import * as i18n from "@solid-primitives/i18n" -import { createEffect, createMemo, createResource } from "solid-js" +import { createEffect, createMemo } from "solid-js" import { createStore } from "solid-js/store" import { createSimpleContext } from "@opencode-ai/ui/context" import { Persist, persisted } from "@/utils/persist" import { dict as en } from "@/i18n/en" +import { dict as zh } from "@/i18n/zh" +import { dict as zht } from "@/i18n/zht" +import { dict as ko } from "@/i18n/ko" +import { dict as de } from "@/i18n/de" +import { dict as es } from "@/i18n/es" +import { dict as fr } from "@/i18n/fr" +import { dict as da } from "@/i18n/da" +import { dict as ja } from "@/i18n/ja" +import { dict as pl } from "@/i18n/pl" +import { dict as ru } from "@/i18n/ru" +import { dict as ar } from "@/i18n/ar" +import { dict as no } from "@/i18n/no" +import { dict as br } from "@/i18n/br" +import { dict as th } from "@/i18n/th" +import { dict as bs } from "@/i18n/bs" +import { dict as tr } from "@/i18n/tr" import { dict as uiEn } from "@opencode-ai/ui/i18n/en" +import { dict as uiZh } from "@opencode-ai/ui/i18n/zh" +import { dict as uiZht } from "@opencode-ai/ui/i18n/zht" +import { dict as uiKo } from "@opencode-ai/ui/i18n/ko" +import { dict as uiDe } from "@opencode-ai/ui/i18n/de" +import { dict as uiEs } from "@opencode-ai/ui/i18n/es" +import { dict as uiFr } from "@opencode-ai/ui/i18n/fr" +import { dict as uiDa } from "@opencode-ai/ui/i18n/da" +import { dict as uiJa } from "@opencode-ai/ui/i18n/ja" +import { dict as uiPl } from "@opencode-ai/ui/i18n/pl" +import { dict as uiRu } from "@opencode-ai/ui/i18n/ru" +import { dict as uiAr } from "@opencode-ai/ui/i18n/ar" +import { dict as uiNo } from "@opencode-ai/ui/i18n/no" +import { dict as uiBr } from "@opencode-ai/ui/i18n/br" +import { dict as uiTh } from "@opencode-ai/ui/i18n/th" +import { dict as uiBs } from "@opencode-ai/ui/i18n/bs" +import { dict as uiTr } from "@opencode-ai/ui/i18n/tr" export type Locale = | "en" @@ -27,7 +59,6 @@ export type Locale = type RawDictionary = typeof en & typeof uiEn type Dictionary = i18n.Flatten<RawDictionary> -type Source = { dict: Record<string, string> } function cookie(locale: Locale) { return `oc_locale=${encodeURIComponent(locale)}; Path=/; Max-Age=31536000; SameSite=Lax` @@ -94,43 +125,24 @@ const LABEL_KEY: Record<Locale, keyof Dictionary> = { } const base = i18n.flatten({ ...en, ...uiEn }) -const dicts = new Map<Locale, Dictionary>([["en", base]]) - -const merge = (app: Promise<Source>, ui: Promise<Source>) => - Promise.all([app, ui]).then(([a, b]) => ({ ...base, ...i18n.flatten({ ...a.dict, ...b.dict }) }) as Dictionary) - -const loaders: Record<Exclude<Locale, "en">, () => Promise<Dictionary>> = { - zh: () => merge(import("@/i18n/zh"), import("@opencode-ai/ui/i18n/zh")), - zht: () => merge(import("@/i18n/zht"), import("@opencode-ai/ui/i18n/zht")), - ko: () => merge(import("@/i18n/ko"), import("@opencode-ai/ui/i18n/ko")), - de: () => merge(import("@/i18n/de"), import("@opencode-ai/ui/i18n/de")), - es: () => merge(import("@/i18n/es"), import("@opencode-ai/ui/i18n/es")), - fr: () => merge(import("@/i18n/fr"), import("@opencode-ai/ui/i18n/fr")), - da: () => merge(import("@/i18n/da"), import("@opencode-ai/ui/i18n/da")), - ja: () => merge(import("@/i18n/ja"), import("@opencode-ai/ui/i18n/ja")), - pl: () => merge(import("@/i18n/pl"), import("@opencode-ai/ui/i18n/pl")), - ru: () => merge(import("@/i18n/ru"), import("@opencode-ai/ui/i18n/ru")), - ar: () => merge(import("@/i18n/ar"), import("@opencode-ai/ui/i18n/ar")), - 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")), - bs: () => merge(import("@/i18n/bs"), import("@opencode-ai/ui/i18n/bs")), - tr: () => merge(import("@/i18n/tr"), import("@opencode-ai/ui/i18n/tr")), -} - -function loadDict(locale: Locale) { - const hit = dicts.get(locale) - if (hit) return Promise.resolve(hit) - if (locale === "en") return Promise.resolve(base) - const load = loaders[locale] - return load().then((next: Dictionary) => { - dicts.set(locale, next) - return next - }) -} - -export function loadLocaleDict(locale: Locale) { - return loadDict(locale).then(() => undefined) +const DICT: Record<Locale, Dictionary> = { + en: base, + zh: { ...base, ...i18n.flatten({ ...zh, ...uiZh }) }, + zht: { ...base, ...i18n.flatten({ ...zht, ...uiZht }) }, + ko: { ...base, ...i18n.flatten({ ...ko, ...uiKo }) }, + de: { ...base, ...i18n.flatten({ ...de, ...uiDe }) }, + es: { ...base, ...i18n.flatten({ ...es, ...uiEs }) }, + fr: { ...base, ...i18n.flatten({ ...fr, ...uiFr }) }, + da: { ...base, ...i18n.flatten({ ...da, ...uiDa }) }, + ja: { ...base, ...i18n.flatten({ ...ja, ...uiJa }) }, + pl: { ...base, ...i18n.flatten({ ...pl, ...uiPl }) }, + ru: { ...base, ...i18n.flatten({ ...ru, ...uiRu }) }, + ar: { ...base, ...i18n.flatten({ ...ar, ...uiAr }) }, + no: { ...base, ...i18n.flatten({ ...no, ...uiNo }) }, + br: { ...base, ...i18n.flatten({ ...br, ...uiBr }) }, + th: { ...base, ...i18n.flatten({ ...th, ...uiTh }) }, + bs: { ...base, ...i18n.flatten({ ...bs, ...uiBs }) }, + tr: { ...base, ...i18n.flatten({ ...tr, ...uiTr }) }, } const localeMatchers: Array<{ locale: Locale; match: (language: string) => boolean }> = [ @@ -156,6 +168,27 @@ const localeMatchers: Array<{ locale: Locale; match: (language: string) => boole { locale: "tr", match: (language) => language.startsWith("tr") }, ] +type ParityKey = "command.session.previous.unseen" | "command.session.next.unseen" +const PARITY_CHECK: Record<Exclude<Locale, "en">, Record<ParityKey, string>> = { + zh, + zht, + ko, + de, + es, + fr, + da, + ja, + pl, + ru, + ar, + no, + br, + th, + bs, + tr, +} +void PARITY_CHECK + function detectLocale(): Locale { if (typeof navigator !== "object") return "en" @@ -170,48 +203,27 @@ function detectLocale(): Locale { return "en" } -export function normalizeLocale(value: string): Locale { +function normalizeLocale(value: string): Locale { return LOCALES.includes(value as Locale) ? (value as Locale) : "en" } -function readStoredLocale() { - if (typeof localStorage !== "object") return - try { - const raw = localStorage.getItem("opencode.global.dat:language") - if (!raw) return - const next = JSON.parse(raw) as { locale?: string } - if (typeof next?.locale !== "string") return - return normalizeLocale(next.locale) - } catch { - return - } -} - -const warm = readStoredLocale() ?? detectLocale() -if (warm !== "en") void loadDict(warm) - export const { use: useLanguage, provider: LanguageProvider } = createSimpleContext({ name: "Language", - init: (props: { locale?: Locale }) => { - const initial = props.locale ?? readStoredLocale() ?? detectLocale() + init: () => { const [store, setStore, _, ready] = persisted( Persist.global("language", ["language.v1"]), createStore({ - locale: initial, + locale: detectLocale() as Locale, }), ) const locale = createMemo<Locale>(() => normalizeLocale(store.locale)) + console.log("locale", locale()) const intl = createMemo(() => INTL[locale()]) - const [dict] = createResource(locale, loadDict, { - initialValue: dicts.get(initial) ?? base, - }) + const dict = createMemo<Dictionary>(() => DICT[locale()]) - const t = i18n.translator(() => dict() ?? base, i18n.resolveTemplate) as ( - key: keyof Dictionary, - params?: Record<string, string | number | boolean>, - ) => string + const t = i18n.translator(dict, i18n.resolveTemplate) const label = (value: Locale) => t(LABEL_KEY[value]) diff --git a/packages/app/src/context/notification.tsx b/packages/app/src/context/notification.tsx index 281a1ef33..04bc2fdaa 100644 --- a/packages/app/src/context/notification.tsx +++ b/packages/app/src/context/notification.tsx @@ -12,7 +12,7 @@ import { base64Encode } from "@opencode-ai/util/encode" import { decode64 } from "@/utils/base64" import { EventSessionError } from "@opencode-ai/sdk/v2" import { Persist, persisted } from "@/utils/persist" -import { playSoundById } from "@/utils/sound" +import { playSound, soundSrc } from "@/utils/sound" type NotificationBase = { directory?: string @@ -234,7 +234,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi if (session.parentID) return if (settings.sounds.agentEnabled()) { - void playSoundById(settings.sounds.agent()) + playSound(soundSrc(settings.sounds.agent())) } append({ @@ -263,7 +263,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi if (session?.parentID) return if (settings.sounds.errorsEnabled()) { - void playSoundById(settings.sounds.errors()) + playSound(soundSrc(settings.sounds.errors())) } const error = "error" in event.properties ? event.properties.error : undefined diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx index eddd752eb..48788fe8e 100644 --- a/packages/app/src/context/settings.tsx +++ b/packages/app/src/context/settings.tsx @@ -104,13 +104,6 @@ function withFallback<T>(read: () => T | undefined, fallback: T) { return createMemo(() => read() ?? fallback) } -let font: Promise<typeof import("@opencode-ai/ui/font-loader")> | undefined - -function loadFont() { - font ??= import("@opencode-ai/ui/font-loader") - return font -} - export const { use: useSettings, provider: SettingsProvider } = createSimpleContext({ name: "Settings", init: () => { @@ -118,11 +111,7 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont createEffect(() => { if (typeof document === "undefined") return - const id = store.appearance?.font ?? defaultSettings.appearance.font - if (id !== defaultSettings.appearance.font) { - void loadFont().then((x) => x.ensureMonoFont(id)) - } - document.documentElement.style.setProperty("--font-family-mono", monoFontFamily(id)) + document.documentElement.style.setProperty("--font-family-mono", monoFontFamily(store.appearance?.font)) }) return { diff --git a/packages/app/src/context/sync.tsx b/packages/app/src/context/sync.tsx index bbf4fc5ec..66b889e2a 100644 --- a/packages/app/src/context/sync.tsx +++ b/packages/app/src/context/sync.tsx @@ -180,8 +180,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ return globalSync.child(directory) } const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/") - const initialMessagePageSize = 80 - const historyMessagePageSize = 200 + const messagePageSize = 200 const inflight = new Map<string, Promise<void>>() const inflightDiff = new Map<string, Promise<void>>() const inflightTodo = new Map<string, Promise<void>>() @@ -464,7 +463,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const cached = store.message[sessionID] !== undefined && meta.limit[key] !== undefined if (cached && hasSession && !opts?.force) return - const limit = meta.limit[key] ?? initialMessagePageSize + const limit = meta.limit[key] ?? messagePageSize const sessionReq = hasSession && !opts?.force ? Promise.resolve() @@ -561,7 +560,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const [, setStore] = globalSync.child(directory) touch(directory, setStore, sessionID) const key = keyFor(directory, sessionID) - const step = count ?? historyMessagePageSize + const step = count ?? messagePageSize if (meta.loading[key]) return if (meta.complete[key]) return const before = meta.cursor[key] diff --git a/packages/app/src/context/terminal-title.ts b/packages/app/src/context/terminal-title.ts index c8b18f421..3e8fa9af2 100644 --- a/packages/app/src/context/terminal-title.ts +++ b/packages/app/src/context/terminal-title.ts @@ -1,18 +1,45 @@ -const template = "Terminal {{number}}" +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 = [ - template, - "محطة طرفية {{number}}", - "Терминал {{number}}", - "ターミナル {{number}}", - "터미널 {{number}}", - "เทอร์มินัล {{number}}", - "终端 {{number}}", - "終端機 {{number}}", -] +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 template.replace("{{number}}", String(number)) + return en["terminal.title.numbered"].replace("{{number}}", String(number)) } export function isDefaultTitle(title: string, number: number) { |
