Skip to content
3 changes: 3 additions & 0 deletions packages/app/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ThemeProvider } from "@opencode-ai/ui/theme"
import { GlobalSyncProvider } from "@/context/global-sync"
import { PermissionProvider } from "@/context/permission"
import { LayoutProvider } from "@/context/layout"
import { LanguageProvider } from "@/context/language"
import { GlobalSDKProvider } from "@/context/global-sdk"
import { ServerProvider, useServer } from "@/context/server"
import { TerminalProvider } from "@/context/terminal"
Expand Down Expand Up @@ -78,6 +79,7 @@ export function AppInterface(props: { defaultUrl?: string }) {
return (
<ServerProvider defaultUrl={defaultServerUrl()}>
<ServerKey>
<LanguageProvider>
<GlobalSDKProvider>
<GlobalSyncProvider>
<Router
Expand Down Expand Up @@ -121,6 +123,7 @@ export function AppInterface(props: { defaultUrl?: string }) {
</Router>
</GlobalSyncProvider>
</GlobalSDKProvider>
</LanguageProvider>
</ServerKey>
</ServerProvider>
)
Expand Down
70 changes: 40 additions & 30 deletions packages/app/src/components/dialog-connect-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { Link } from "@/components/link"
import { useGlobalSDK } from "@/context/global-sdk"
import { useGlobalSync } from "@/context/global-sync"
import { usePlatform } from "@/context/platform"
import { useLanguage } from "@/context/language"
import { DialogSelectModel } from "./dialog-select-model"
import { DialogSelectProvider } from "./dialog-select-provider"

Expand All @@ -25,13 +26,14 @@ export function DialogConnectProvider(props: { provider: string }) {
const globalSync = useGlobalSync()
const globalSDK = useGlobalSDK()
const platform = usePlatform()
const language = useLanguage()
const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === props.provider)!)
const methods = createMemo(
() =>
globalSync.data.provider_auth[props.provider] ?? [
{
type: "api",
label: "API key",
label: language.t("provider.apiKey"),
},
],
)
Expand Down Expand Up @@ -112,8 +114,8 @@ export function DialogConnectProvider(props: { provider: string }) {
showToast({
variant: "success",
icon: "circle-check",
title: `${provider().name} connected`,
description: `${provider().name} models are now available to use.`,
title: language.t("provider.connectedTitle", { provider: provider().name }),
description: language.t("provider.connectedDescription", { provider: provider().name }),
})
}

Expand Down Expand Up @@ -142,16 +144,18 @@ export function DialogConnectProvider(props: { provider: string }) {
<div class="text-16-medium text-text-strong">
<Switch>
<Match when={props.provider === "anthropic" && method()?.label?.toLowerCase().includes("max")}>
Login with Claude Pro/Max
{language.t("provider.loginClaude")}
</Match>
<Match when={true}>Connect {provider().name}</Match>
<Match when={true}>{language.t("provider.connectTo", { provider: provider().name })}</Match>
</Switch>
</div>
</div>
<div class="px-2.5 pb-10 flex flex-col gap-6">
<Switch>
<Match when={store.methodIndex === undefined}>
<div class="text-14-regular text-text-base">Select login method for {provider().name}.</div>
<div class="text-14-regular text-text-base">
{language.t("provider.selectMethod", { provider: provider().name })}
</div>
<div class="">
<List
ref={(ref) => {
Expand Down Expand Up @@ -179,15 +183,15 @@ export function DialogConnectProvider(props: { provider: string }) {
<div class="text-14-regular text-text-base">
<div class="flex items-center gap-x-2">
<Spinner />
<span>Authorization in progress...</span>
<span>{language.t("provider.authInProgress")}</span>
</div>
</div>
</Match>
<Match when={store.state === "error"}>
<div class="text-14-regular text-text-base">
<div class="flex items-center gap-x-2">
<Icon name="circle-ban-sign" class="text-icon-critical-base" />
<span>Authorization failed: {store.error}</span>
<span>{language.t("provider.authFailed", { error: store.error ?? "" })}</span>
</div>
</div>
</Match>
Expand All @@ -206,7 +210,7 @@ export function DialogConnectProvider(props: { provider: string }) {
const apiKey = formData.get("apiKey") as string

if (!apiKey?.trim()) {
setFormStore("error", "API key is required")
setFormStore("error", language.t("provider.apiKeyRequired"))
return
}

Expand All @@ -227,42 +231,40 @@ export function DialogConnectProvider(props: { provider: string }) {
<Match when={provider().id === "opencode"}>
<div class="flex flex-col gap-4">
<div class="text-14-regular text-text-base">
OpenCode Zen gives you access to a curated set of reliable optimized models for coding
agents.
{language.t("provider.opencodeZenIntro")}
</div>
<div class="text-14-regular text-text-base">
With a single API key you'll get access to models such as Claude, GPT, Gemini, GLM and more.
{language.t("provider.opencodeZenModels")}
</div>
<div class="text-14-regular text-text-base">
Visit{" "}
{language.t("provider.opencodeZenVisit")}{" "}
<Link href="https://opencode.ai/zen" tabIndex={-1}>
opencode.ai/zen
</Link>{" "}
to collect your API key.
{language.t("provider.opencodeZenCollect")}
</div>
</div>
</Match>
<Match when={true}>
<div class="text-14-regular text-text-base">
Enter your {provider().name} API key to connect your account and use {provider().name} models
in OpenCode.
{language.t("provider.enterApiKey", { provider: provider().name })}
</div>
</Match>
</Switch>
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
<TextField
autofocus
type="text"
label={`${provider().name} API key`}
placeholder="API key"
label={language.t("provider.apiKeyLabel", { provider: provider().name })}
placeholder={language.t("provider.apiKey")}
name="apiKey"
value={formStore.value}
onChange={setFormStore.bind(null, "value")}
validationState={formStore.error ? "invalid" : undefined}
error={formStore.error}
/>
<Button class="w-auto" type="submit" size="large" variant="primary">
Submit
{language.t("common.submit")}
</Button>
</form>
</div>
Expand Down Expand Up @@ -292,7 +294,7 @@ export function DialogConnectProvider(props: { provider: string }) {
const code = formData.get("code") as string

if (!code?.trim()) {
setFormStore("error", "Authorization code is required")
setFormStore("error", language.t("provider.authCodeRequired"))
return
}

Expand All @@ -306,29 +308,30 @@ export function DialogConnectProvider(props: { provider: string }) {
await complete()
return
}
setFormStore("error", "Invalid authorization code")
setFormStore("error", language.t("provider.authCodeInvalid"))
}

return (
<div class="flex flex-col gap-6">
<div class="text-14-regular text-text-base">
Visit <Link href={store.authorization!.url}>this link</Link> to collect your authorization
code to connect your account and use {provider().name} models in OpenCode.
{language.t("provider.visitLinkToAuthorize")}{" "}
<Link href={store.authorization!.url}>{language.t("provider.thisLink")}</Link>{" "}
{language.t("provider.collectAuthCode", { provider: provider().name })}
</div>
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
<TextField
autofocus
type="text"
label={`${method()?.label} authorization code`}
placeholder="Authorization code"
label={language.t("provider.authCodeLabel", { method: method()?.label ?? "" })}
placeholder={language.t("provider.authCode")}
name="code"
value={formStore.value}
onChange={setFormStore.bind(null, "value")}
validationState={formStore.error ? "invalid" : undefined}
error={formStore.error}
/>
<Button class="w-auto" type="submit" size="large" variant="primary">
Submit
{language.t("common.submit")}
</Button>
</form>
</div>
Expand Down Expand Up @@ -361,13 +364,20 @@ export function DialogConnectProvider(props: { provider: string }) {
return (
<div class="flex flex-col gap-6">
<div class="text-14-regular text-text-base">
Visit <Link href={store.authorization!.url}>this link</Link> and enter the code below to
connect your account and use {provider().name} models in OpenCode.
{language.t("provider.visitLinkToAuthorize")}{" "}
<Link href={store.authorization!.url}>{language.t("provider.thisLink")}</Link>{" "}
{language.t("provider.enterCodeBelow", { provider: provider().name })}
</div>
<TextField label="Confirmation code" class="font-mono" value={code()} readOnly copyable />
<TextField
label={language.t("provider.confirmationCode")}
class="font-mono"
value={code()}
readOnly
copyable
/>
<div class="text-14-regular text-text-base flex items-center gap-4">
<Spinner />
<span>Waiting for authorization...</span>
<span>{language.t("provider.waitingForAuth")}</span>
</div>
</div>
)
Expand Down
16 changes: 9 additions & 7 deletions packages/app/src/components/dialog-edit-project.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import { useGlobalSDK } from "@/context/global-sdk"
import { type LocalProject, getAvatarColors } from "@/context/layout"
import { getFilename } from "@opencode-ai/util/path"
import { Avatar } from "@opencode-ai/ui/avatar"
import { useLanguage } from "@/context/language"

const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const

export function DialogEditProject(props: { project: LocalProject }) {
const dialog = useDialog()
const globalSDK = useGlobalSDK()
const language = useLanguage()

const folderName = createMemo(() => getFilename(props.project.worktree))
const defaultName = createMemo(() => props.project.name || folderName())
Expand Down Expand Up @@ -81,20 +83,20 @@ export function DialogEditProject(props: { project: LocalProject }) {
}

return (
<Dialog title="Edit project" class="w-full max-w-[480px] mx-auto">
<Dialog title={language.t("dialog.editProject")} class="w-full max-w-[480px] mx-auto">
<form onSubmit={handleSubmit} class="flex flex-col gap-6 p-6 pt-0">
<div class="flex flex-col gap-4">
<TextField
autofocus
type="text"
label="Name"
label={language.t("project.name")}
placeholder={folderName()}
value={store.name}
onChange={(v) => setStore("name", v)}
/>

<div class="flex flex-col gap-2">
<label class="text-12-medium text-text-weak">Icon</label>
<label class="text-12-medium text-text-weak">{language.t("project.icon")}</label>
<div class="flex gap-3 items-start">
<div class="relative" onMouseEnter={() => setIconHover(true)} onMouseLeave={() => setIconHover(false)}>
<div
Expand Down Expand Up @@ -127,7 +129,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
</div>
}
>
<img src={store.iconUrl} alt="Project icon" class="size-full object-cover" />
<img src={store.iconUrl} alt={language.t("project.iconAlt")} class="size-full object-cover" />
</Show>
</div>
<div
Expand Down Expand Up @@ -178,7 +180,7 @@ export function DialogEditProject(props: { project: LocalProject }) {

<Show when={!store.iconUrl}>
<div class="flex flex-col gap-2">
<label class="text-12-medium text-text-weak">Color</label>
<label class="text-12-medium text-text-weak">{language.t("project.color")}</label>
<div class="flex gap-1.5">
<For each={AVATAR_COLOR_KEYS}>
{(color) => (
Expand Down Expand Up @@ -208,10 +210,10 @@ export function DialogEditProject(props: { project: LocalProject }) {

<div class="flex justify-end gap-2">
<Button type="button" variant="ghost" size="large" onClick={() => dialog.close()}>
Cancel
{language.t("common.cancel")}
</Button>
<Button type="submit" variant="primary" size="large" disabled={store.saving}>
{store.saving ? "Saving..." : "Save"}
{store.saving ? language.t("common.saving") : language.t("common.save")}
</Button>
</div>
</form>
Expand Down
8 changes: 5 additions & 3 deletions packages/app/src/components/dialog-fork.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { List } from "@opencode-ai/ui/list"
import { extractPromptFromParts } from "@/utils/prompt"
import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client"
import { base64Encode } from "@opencode-ai/util/encode"
import { useLanguage } from "@/context/language"

interface ForkableMessage {
id: string
Expand All @@ -27,6 +28,7 @@ export const DialogFork: Component = () => {
const sdk = useSDK()
const prompt = usePrompt()
const dialog = useDialog()
const language = useLanguage()

const messages = createMemo((): ForkableMessage[] => {
const sessionID = params.id
Expand Down Expand Up @@ -73,11 +75,11 @@ export const DialogFork: Component = () => {
}

return (
<Dialog title="Fork from message">
<Dialog title={language.t("session.forkTitle")}>
<List
class="flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0"
search={{ placeholder: "Search", autofocus: true }}
emptyMessage="No messages to fork from"
search={{ placeholder: language.t("common.search"), autofocus: true }}
emptyMessage={language.t("session.noForkMessages")}
key={(x) => x.id}
items={messages}
filterKeys={["text"]}
Expand Down
8 changes: 5 additions & 3 deletions packages/app/src/components/dialog-manage-models.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import { Switch } from "@opencode-ai/ui/switch"
import type { Component } from "solid-js"
import { useLocal } from "@/context/local"
import { popularProviders } from "@/hooks/use-providers"
import { useLanguage } from "@/context/language"

export const DialogManageModels: Component = () => {
const local = useLocal()
const language = useLanguage()
return (
<Dialog title="Manage models" description="Customize which models appear in the model selector.">
<Dialog title={language.t("model.manageTitle")} description={language.t("model.manageDescription")}>
<List
search={{ placeholder: "Search models", autofocus: true }}
emptyMessage="No model results"
search={{ placeholder: language.t("model.search"), autofocus: true }}
emptyMessage={language.t("model.empty")}
key={(x) => `${x?.provider?.id}:${x?.id}`}
items={local.model.list()}
filterKeys={["provider.name", "name", "id"]}
Expand Down
8 changes: 5 additions & 3 deletions packages/app/src/components/dialog-select-directory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { createMemo } from "solid-js"
import { useGlobalSDK } from "@/context/global-sdk"
import { useGlobalSync } from "@/context/global-sync"
import { useLanguage } from "@/context/language"

interface DialogSelectDirectoryProps {
title?: string
Expand All @@ -17,6 +18,7 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
const sync = useGlobalSync()
const sdk = useGlobalSDK()
const dialog = useDialog()
const language = useLanguage()

const home = createMemo(() => sync.data.path.home)
const root = createMemo(() => sync.data.path.home || sync.data.path.directory)
Expand Down Expand Up @@ -81,10 +83,10 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
}

return (
<Dialog title={props.title ?? "Open project"}>
<Dialog title={props.title ?? language.t("project.open")}>
<List
search={{ placeholder: "Search folders", autofocus: true }}
emptyMessage="No folders found"
search={{ placeholder: language.t("dialog.searchFolders"), autofocus: true }}
emptyMessage={language.t("dialog.noFolders")}
items={directories}
key={(x) => x}
onSelect={(path) => {
Expand Down
Loading