wip(app): i18n

This commit is contained in:
Adam
2026-01-20 05:40:44 -06:00
parent 0470717c7f
commit 92beae1410
18 changed files with 692 additions and 227 deletions

View File

@@ -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())
@@ -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.project.edit.title")} 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("dialog.project.edit.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("dialog.project.edit.icon")}</label>
<div class="flex gap-3 items-start">
<div class="relative" onMouseEnter={() => setIconHover(true)} onMouseLeave={() => setIconHover(false)}>
<div
@@ -128,7 +130,11 @@ 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("dialog.project.edit.icon.alt")}
class="size-full object-cover"
/>
</Show>
</div>
<div
@@ -172,14 +178,15 @@ export function DialogEditProject(props: { project: LocalProject }) {
</div>
<input id="icon-upload" type="file" accept="image/*" class="hidden" onChange={handleInputChange} />
<div class="flex flex-col gap-1.5 text-12-regular text-text-weak self-center">
<span>Recommended size 128x128px</span>
<span>{language.t("dialog.project.edit.icon.hint")}</span>
<span>{language.t("dialog.project.edit.icon.recommended")}</span>
</div>
</div>
</div>
<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("dialog.project.edit.color")}</label>
<div class="flex gap-1.5">
<For each={AVATAR_COLOR_KEYS}>
{(color) => (
@@ -209,10 +216,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>

View File

@@ -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
@@ -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
@@ -73,11 +75,11 @@ export const DialogFork: Component = () => {
}
return (
<Dialog title="Fork from message">
<Dialog title={language.t("command.session.fork")}>
<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.placeholder"), autofocus: true }}
emptyMessage={language.t("dialog.fork.empty")}
key={(x) => x.id}
items={messages}
filterKeys={["text"]}

View File

@@ -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("dialog.model.manage")} description={language.t("dialog.model.manage.description")}>
<List
search={{ placeholder: "Search models", autofocus: true }}
emptyMessage="No model results"
search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true }}
emptyMessage={language.t("dialog.model.empty")}
key={(x) => `${x?.provider?.id}:${x?.id}`}
items={local.model.list()}
filterKeys={["provider.name", "name", "id"]}

View File

@@ -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
@@ -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)
@@ -81,10 +83,11 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
}
return (
<Dialog title={props.title ?? "Open project"}>
<Dialog title={props.title ?? language.t("command.project.open")}>
<List
search={{ placeholder: "Search folders", autofocus: true }}
emptyMessage="No folders found"
search={{ placeholder: language.t("dialog.directory.search.placeholder"), autofocus: true }}
emptyMessage={language.t("dialog.directory.empty")}
loadingMessage={language.t("common.loading")}
items={directories}
key={(x) => x}
onSelect={(path) => {

View File

@@ -9,6 +9,7 @@ import { createMemo, createSignal, onCleanup, Show } from "solid-js"
import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
import { useLayout } from "@/context/layout"
import { useFile } from "@/context/file"
import { useLanguage } from "@/context/language"
type EntryType = "command" | "file"
@@ -18,13 +19,14 @@ type Entry = {
title: string
description?: string
keybind?: string
category: "Commands" | "Files"
category: string
option?: CommandOption
path?: string
}
export function DialogSelectFile() {
const command = useCommand()
const language = useLanguage()
const layout = useLayout()
const file = useFile()
const dialog = useDialog()
@@ -56,7 +58,7 @@ export function DialogSelectFile() {
title: option.title,
description: option.description,
keybind: option.keybind,
category: "Commands",
category: language.t("palette.group.commands"),
option,
})
@@ -64,7 +66,7 @@ export function DialogSelectFile() {
id: "file:" + path,
type: "file",
title: path,
category: "Files",
category: language.t("palette.group.files"),
path,
})
@@ -143,8 +145,14 @@ export function DialogSelectFile() {
return (
<Dialog class="pt-3 pb-0 !max-h-[480px]">
<List
search={{ placeholder: "Search files and commands", autofocus: true, hideIcon: true, class: "pl-3 pr-2 !mb-0" }}
emptyMessage="No results found"
search={{
placeholder: language.t("palette.search.placeholder"),
autofocus: true,
hideIcon: true,
class: "pl-3 pr-2 !mb-0",
}}
emptyMessage={language.t("palette.empty")}
loadingMessage={language.t("common.loading")}
items={items}
key={(item) => item.id}
filterKeys={["title", "description", "category"]}

View File

@@ -4,10 +4,12 @@ import { useSDK } from "@/context/sdk"
import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
import { Switch } from "@opencode-ai/ui/switch"
import { useLanguage } from "@/context/language"
export const DialogSelectMcp: Component = () => {
const sync = useSync()
const sdk = useSDK()
const language = useLanguage()
const [loading, setLoading] = createSignal<string | null>(null)
const items = createMemo(() =>
@@ -34,10 +36,13 @@ export const DialogSelectMcp: Component = () => {
const totalCount = createMemo(() => items().length)
return (
<Dialog title="MCPs" description={`${enabledCount()} of ${totalCount()} enabled`}>
<Dialog
title={language.t("dialog.mcp.title")}
description={language.t("dialog.mcp.description", { enabled: enabledCount(), total: totalCount() })}
>
<List
search={{ placeholder: "Search", autofocus: true }}
emptyMessage="No MCPs configured"
search={{ placeholder: language.t("common.search.placeholder"), autofocus: true }}
emptyMessage={language.t("dialog.mcp.empty")}
key={(x) => x?.name ?? ""}
items={items}
filterKeys={["name", "status"]}
@@ -60,16 +65,16 @@ export const DialogSelectMcp: Component = () => {
<div class="flex items-center gap-2">
<span class="truncate">{i.name}</span>
<Show when={status() === "connected"}>
<span class="text-11-regular text-text-weaker">connected</span>
<span class="text-11-regular text-text-weaker">{language.t("mcp.status.connected")}</span>
</Show>
<Show when={status() === "failed"}>
<span class="text-11-regular text-text-weaker">failed</span>
<span class="text-11-regular text-text-weaker">{language.t("mcp.status.failed")}</span>
</Show>
<Show when={status() === "needs_auth"}>
<span class="text-11-regular text-text-weaker">needs auth</span>
<span class="text-11-regular text-text-weaker">{language.t("mcp.status.needs_auth")}</span>
</Show>
<Show when={status() === "disabled"}>
<span class="text-11-regular text-text-weaker">disabled</span>
<span class="text-11-regular text-text-weaker">{language.t("mcp.status.disabled")}</span>
</Show>
<Show when={loading() === i.name}>
<span class="text-11-regular text-text-weak">...</span>

View File

@@ -10,11 +10,13 @@ import { useLocal } from "@/context/local"
import { popularProviders, useProviders } from "@/hooks/use-providers"
import { DialogConnectProvider } from "./dialog-connect-provider"
import { DialogSelectProvider } from "./dialog-select-provider"
import { useLanguage } from "@/context/language"
export const DialogSelectModelUnpaid: Component = () => {
const local = useLocal()
const dialog = useDialog()
const providers = useProviders()
const language = useLanguage()
let listRef: ListRef | undefined
const handleKey = (e: KeyboardEvent) => {
@@ -30,9 +32,9 @@ export const DialogSelectModelUnpaid: Component = () => {
})
return (
<Dialog title="Select model">
<Dialog title={language.t("dialog.model.select.title")}>
<div class="flex flex-col gap-3 px-2.5">
<div class="text-14-medium text-text-base px-2.5">Free models provided by OpenCode</div>
<div class="text-14-medium text-text-base px-2.5">{language.t("dialog.model.unpaid.freeModels.title")}</div>
<List
ref={(ref) => (listRef = ref)}
items={local.model.list}
@@ -48,9 +50,9 @@ export const DialogSelectModelUnpaid: Component = () => {
{(i) => (
<div class="w-full flex items-center gap-x-2.5">
<span>{i.name}</span>
<Tag>Free</Tag>
<Tag>{language.t("model.tag.free")}</Tag>
<Show when={i.latest}>
<Tag>Latest</Tag>
<Tag>{language.t("model.tag.latest")}</Tag>
</Show>
</div>
)}
@@ -60,9 +62,9 @@ export const DialogSelectModelUnpaid: Component = () => {
</div>
<div class="px-1.5 pb-1.5">
<div class="w-full rounded-sm border border-border-weak-base bg-surface-raised-base">
<div class="w-full flex flex-col items-start gap-4 px-1.5 pt-4 pb-4">
<div class="px-2 text-14-medium text-text-base">Add more models from popular providers</div>
<div class="w-full">
<div class="w-full flex flex-col items-start gap-4 px-1.5 pt-4 pb-4">
<div class="px-2 text-14-medium text-text-base">{language.t("dialog.model.unpaid.addMore.title")}</div>
<div class="w-full">
<List
class="w-full px-0"
key={(x) => x?.id}
@@ -83,10 +85,10 @@ export const DialogSelectModelUnpaid: Component = () => {
<ProviderIcon data-slot="list-item-extra-icon" id={i.id as IconName} />
<span>{i.name}</span>
<Show when={i.id === "opencode"}>
<Tag>Recommended</Tag>
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
</Show>
<Show when={i.id === "anthropic"}>
<div class="text-14-regular text-text-weak">Connect with Claude Pro/Max or API key</div>
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.anthropic.note")}</div>
</Show>
</div>
)}
@@ -99,7 +101,7 @@ export const DialogSelectModelUnpaid: Component = () => {
dialog.show(() => <DialogSelectProvider />)
}}
>
View all providers
{language.t("dialog.provider.viewAll")}
</Button>
</div>
</div>

View File

@@ -9,6 +9,7 @@ import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
import { DialogSelectProvider } from "./dialog-select-provider"
import { DialogManageModels } from "./dialog-manage-models"
import { useLanguage } from "@/context/language"
const ModelList: Component<{
provider?: string
@@ -16,6 +17,7 @@ const ModelList: Component<{
onSelect: () => void
}> = (props) => {
const local = useLocal()
const language = useLanguage()
const models = createMemo(() =>
local.model
@@ -27,8 +29,8 @@ const ModelList: Component<{
return (
<List
class={`flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0 ${props.class ?? ""}`}
search={{ placeholder: "Search models", autofocus: true }}
emptyMessage="No model results"
search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true }}
emptyMessage={language.t("dialog.model.empty")}
key={(x) => `${x.provider.id}:${x.id}`}
items={models}
current={local.model.current()}
@@ -55,10 +57,10 @@ const ModelList: Component<{
<div class="w-full flex items-center gap-x-2 text-13-regular">
<span class="truncate">{i.name}</span>
<Show when={i.provider.id === "opencode" && (!i.cost || i.cost?.input === 0)}>
<Tag>Free</Tag>
<Tag>{language.t("model.tag.free")}</Tag>
</Show>
<Show when={i.latest}>
<Tag>Latest</Tag>
<Tag>{language.t("model.tag.latest")}</Tag>
</Show>
</div>
)}
@@ -71,13 +73,14 @@ export const ModelSelectorPopover: Component<{
children: JSX.Element
}> = (props) => {
const [open, setOpen] = createSignal(false)
const language = useLanguage()
return (
<Kobalte open={open()} onOpenChange={setOpen} placement="top-start" gutter={8}>
<Kobalte.Trigger as="div">{props.children}</Kobalte.Trigger>
<Kobalte.Portal>
<Kobalte.Content class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden">
<Kobalte.Title class="sr-only">Select model</Kobalte.Title>
<Kobalte.Title class="sr-only">{language.t("dialog.model.select.title")}</Kobalte.Title>
<ModelList provider={props.provider} onSelect={() => setOpen(false)} class="p-1" />
</Kobalte.Content>
</Kobalte.Portal>
@@ -87,10 +90,11 @@ export const ModelSelectorPopover: Component<{
export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
const dialog = useDialog()
const language = useLanguage()
return (
<Dialog
title="Select model"
title={language.t("dialog.model.select.title")}
action={
<Button
class="h-7 -my-1 text-14-medium"
@@ -98,7 +102,7 @@ export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
tabIndex={-1}
onClick={() => dialog.show(() => <DialogSelectProvider />)}
>
Connect provider
{language.t("command.provider.connect")}
</Button>
}
>
@@ -108,7 +112,7 @@ export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
class="ml-3 mt-5 mb-6 text-text-base self-start"
onClick={() => dialog.show(() => <DialogManageModels />)}
>
Manage models
{language.t("dialog.model.manage")}
</Button>
</Dialog>
)

View File

@@ -7,28 +7,38 @@ import { Tag } from "@opencode-ai/ui/tag"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { IconName } from "@opencode-ai/ui/icons/provider"
import { DialogConnectProvider } from "./dialog-connect-provider"
import { useLanguage } from "@/context/language"
export const DialogSelectProvider: Component = () => {
const dialog = useDialog()
const providers = useProviders()
const language = useLanguage()
const popularGroup = () => language.t("dialog.provider.group.popular")
const otherGroup = () => language.t("dialog.provider.group.other")
return (
<Dialog title="Connect provider">
<Dialog title={language.t("command.provider.connect")}>
<List
search={{ placeholder: "Search providers", autofocus: true }}
search={{ placeholder: language.t("dialog.provider.search.placeholder"), autofocus: true }}
emptyMessage={language.t("dialog.provider.empty")}
activeIcon="plus-small"
key={(x) => x?.id}
items={providers.all}
items={() => {
language.locale()
return providers.all()
}}
filterKeys={["id", "name"]}
groupBy={(x) => (popularProviders.includes(x.id) ? "Popular" : "Other")}
groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())}
sortBy={(a, b) => {
if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
return a.name.localeCompare(b.name)
}}
sortGroupsBy={(a, b) => {
if (a.category === "Popular" && b.category !== "Popular") return -1
if (b.category === "Popular" && a.category !== "Popular") return 1
const popular = popularGroup()
if (a.category === popular && b.category !== popular) return -1
if (b.category === popular && a.category !== popular) return 1
return 0
}}
onSelect={(x) => {
@@ -41,10 +51,10 @@ export const DialogSelectProvider: Component = () => {
<ProviderIcon data-slot="list-item-extra-icon" id={i.id as IconName} />
<span>{i.name}</span>
<Show when={i.id === "opencode"}>
<Tag>Recommended</Tag>
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
</Show>
<Show when={i.id === "anthropic"}>
<div class="text-14-regular text-text-weak">Connect with Claude Pro/Max or API key</div>
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.anthropic.note")}</div>
</Show>
</div>
)}

View File

@@ -10,6 +10,7 @@ import { normalizeServerUrl, serverDisplayName, useServer } from "@/context/serv
import { usePlatform } from "@/context/platform"
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
import { useNavigate } from "@solidjs/router"
import { useLanguage } from "@/context/language"
type ServerStatus = { healthy: boolean; version?: string }
@@ -30,6 +31,7 @@ export function DialogSelectServer() {
const dialog = useDialog()
const server = useServer()
const platform = usePlatform()
const language = useLanguage()
const [store, setStore] = createStore({
url: "",
adding: false,
@@ -109,7 +111,7 @@ export function DialogSelectServer() {
setStore("adding", false)
if (!result.healthy) {
setStore("error", "Could not connect to server")
setStore("error", language.t("dialog.server.add.error"))
return
}
@@ -122,11 +124,11 @@ export function DialogSelectServer() {
}
return (
<Dialog title="Servers" description="Switch which OpenCode server this app connects to.">
<Dialog title={language.t("dialog.server.title")} description={language.t("dialog.server.description")}>
<div class="flex flex-col gap-4 pb-4">
<List
search={{ placeholder: "Search servers", autofocus: true }}
emptyMessage="No servers yet"
search={{ placeholder: language.t("dialog.server.search.placeholder"), autofocus: true }}
emptyMessage={language.t("dialog.server.empty")}
items={sortedItems}
key={(x) => x}
current={current()}
@@ -168,14 +170,14 @@ export function DialogSelectServer() {
<div class="mt-6 px-3 flex flex-col gap-1.5">
<div class="px-3">
<h3 class="text-14-regular text-text-weak">Add a server</h3>
<h3 class="text-14-regular text-text-weak">{language.t("dialog.server.add.title")}</h3>
</div>
<form onSubmit={handleSubmit}>
<div class="flex items-start gap-2">
<div class="flex-1 min-w-0 h-auto">
<TextField
type="text"
label="Server URL"
label={language.t("dialog.server.add.url")}
hideLabel
placeholder="http://localhost:4096"
value={store.url}
@@ -188,7 +190,7 @@ export function DialogSelectServer() {
/>
</div>
<Button type="submit" variant="secondary" icon="plus-small" size="large" disabled={store.adding}>
{store.adding ? "Checking..." : "Add"}
{store.adding ? language.t("dialog.server.add.checking") : language.t("dialog.server.add.button")}
</Button>
</div>
</form>
@@ -197,9 +199,9 @@ export function DialogSelectServer() {
<Show when={isDesktop}>
<div class="mt-6 px-3 flex flex-col gap-1.5">
<div class="px-3">
<h3 class="text-14-regular text-text-weak">Default server</h3>
<h3 class="text-14-regular text-text-weak">{language.t("dialog.server.default.title")}</h3>
<p class="text-12-regular text-text-weak mt-1">
Connect to this server on app launch instead of starting a local server. Requires restart.
{language.t("dialog.server.default.description")}
</p>
</div>
<div class="flex items-center gap-2 px-3 py-2">
@@ -208,7 +210,7 @@ export function DialogSelectServer() {
fallback={
<Show
when={server.url}
fallback={<span class="text-14-regular text-text-weak">No server selected</span>}
fallback={<span class="text-14-regular text-text-weak">{language.t("dialog.server.default.none")}</span>}
>
<Button
variant="secondary"
@@ -218,7 +220,7 @@ export function DialogSelectServer() {
defaultUrlActions.refetch(server.url)
}}
>
Set current server as default
{language.t("dialog.server.default.set")}
</Button>
</Show>
}
@@ -234,7 +236,7 @@ export function DialogSelectServer() {
defaultUrlActions.refetch()
}}
>
Clear
{language.t("dialog.server.default.clear")}
</Button>
</Show>
</div>

View File

@@ -49,6 +49,7 @@ import { Persist, persisted } from "@/utils/persist"
import { Identifier } from "@/utils/id"
import { SessionContextUsage } from "@/components/session-context-usage"
import { usePermission } from "@/context/permission"
import { useLanguage } from "@/context/language"
import { useGlobalSync } from "@/context/global-sync"
import { usePlatform } from "@/context/platform"
import { createOpencodeClient, type Message, type Part } from "@opencode-ai/sdk/v2/client"
@@ -118,6 +119,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const providers = useProviders()
const command = useCommand()
const permission = usePermission()
const language = useLanguage()
let editorRef!: HTMLDivElement
let fileInputRef!: HTMLInputElement
let scrollRef!: HTMLDivElement
@@ -1560,8 +1562,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<Show when={!prompt.dirty()}>
<div class="absolute top-0 inset-x-0 px-5 py-3 pr-12 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate">
{store.mode === "shell"
? "Enter shell command..."
: `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`}
? language.t("prompt.placeholder.shell")
: language.t("prompt.placeholder.normal", { example: PLACEHOLDERS[store.placeholder] })}
</div>
</Show>
</div>
@@ -1571,12 +1573,16 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<Match when={store.mode === "shell"}>
<div class="flex items-center gap-2 px-2 h-6">
<Icon name="console" size="small" class="text-icon-primary" />
<span class="text-12-regular text-text-primary">Shell</span>
<span class="text-12-regular text-text-weak">esc to exit</span>
<span class="text-12-regular text-text-primary">{language.t("prompt.mode.shell")}</span>
<span class="text-12-regular text-text-weak">{language.t("prompt.mode.shell.exit")}</span>
</div>
</Match>
<Match when={store.mode === "normal"}>
<TooltipKeybind placement="top" title="Cycle agent" keybind={command.keybind("agent.cycle")}>
<TooltipKeybind
placement="top"
title={language.t("command.agent.cycle")}
keybind={command.keybind("agent.cycle")}
>
<Select
options={local.agent.list().map((agent) => agent.name)}
current={local.agent.current()?.name ?? ""}
@@ -1588,24 +1594,32 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<Show
when={providers.paid().length > 0}
fallback={
<TooltipKeybind placement="top" title="Choose model" keybind={command.keybind("model.choose")}>
<TooltipKeybind
placement="top"
title={language.t("command.model.choose")}
keybind={command.keybind("model.choose")}
>
<Button as="div" variant="ghost" onClick={() => dialog.show(() => <DialogSelectModelUnpaid />)}>
<Show when={local.model.current()?.provider?.id}>
<ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
</Show>
{local.model.current()?.name ?? "Select model"}
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
<Icon name="chevron-down" size="small" />
</Button>
</TooltipKeybind>
}
>
<ModelSelectorPopover>
<TooltipKeybind placement="top" title="Choose model" keybind={command.keybind("model.choose")}>
<TooltipKeybind
placement="top"
title={language.t("command.model.choose")}
keybind={command.keybind("model.choose")}
>
<Button as="div" variant="ghost">
<Show when={local.model.current()?.provider?.id}>
<ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" />
</Show>
{local.model.current()?.name ?? "Select model"}
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
<Icon name="chevron-down" size="small" />
</Button>
</TooltipKeybind>
@@ -1614,7 +1628,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<Show when={local.model.variant.list().length > 0}>
<TooltipKeybind
placement="top"
title="Thinking effort"
title={language.t("command.model.variant.cycle")}
keybind={command.keybind("model.variant.cycle")}
>
<Button
@@ -1622,14 +1636,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
class="text-text-base _hidden group-hover/prompt-input:inline-block capitalize text-12-regular"
onClick={() => local.model.variant.cycle()}
>
{local.model.variant.current() ?? "Default"}
{local.model.variant.current() ?? language.t("common.default")}
</Button>
</TooltipKeybind>
</Show>
<Show when={permission.permissionsEnabled() && params.id}>
<TooltipKeybind
placement="top"
title="Auto-accept edits"
title={language.t("command.permissions.autoaccept.enable")}
keybind={command.keybind("permissions.autoaccept")}
>
<Button

View File

@@ -11,6 +11,7 @@ import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
import { Code } from "@opencode-ai/ui/code"
import { Markdown } from "@opencode-ai/ui/markdown"
import type { AssistantMessage, Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
import { useLanguage } from "@/context/language"
interface SessionContextTabProps {
messages: () => Message[]
@@ -22,6 +23,7 @@ interface SessionContextTabProps {
export function SessionContextTab(props: SessionContextTabProps) {
const params = useParams()
const sync = useSync()
const language = useLanguage()
const ctx = createMemo(() => {
const last = props.messages().findLast((x) => {
@@ -172,7 +174,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
return [
{
key: "system",
label: "System",
label: language.t("context.breakdown.system"),
tokens: tokens.system,
width: pct(tokens.system),
percent: pctLabel(tokens.system),
@@ -180,7 +182,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
},
{
key: "user",
label: "User",
label: language.t("context.breakdown.user"),
tokens: tokens.user,
width: pct(tokens.user),
percent: pctLabel(tokens.user),
@@ -188,7 +190,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
},
{
key: "assistant",
label: "Assistant",
label: language.t("context.breakdown.assistant"),
tokens: tokens.assistant,
width: pct(tokens.assistant),
percent: pctLabel(tokens.assistant),
@@ -196,7 +198,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
},
{
key: "tool",
label: "Tool Calls",
label: language.t("context.breakdown.tool"),
tokens: tokens.tool,
width: pct(tokens.tool),
percent: pctLabel(tokens.tool),
@@ -204,7 +206,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
},
{
key: "other",
label: "Other",
label: language.t("context.breakdown.other"),
tokens: tokens.other,
width: pct(tokens.other),
percent: pctLabel(tokens.other),
@@ -243,22 +245,25 @@ export function SessionContextTab(props: SessionContextTabProps) {
const c = ctx()
const count = counts()
return [
{ label: "Session", value: props.info()?.title ?? params.id ?? "—" },
{ label: "Messages", value: count.all.toLocaleString() },
{ label: "Provider", value: providerLabel() },
{ label: "Model", value: modelLabel() },
{ label: "Context Limit", value: number(c?.limit) },
{ label: "Total Tokens", value: number(c?.total) },
{ label: "Usage", value: percent(c?.usage) },
{ label: "Input Tokens", value: number(c?.input) },
{ label: "Output Tokens", value: number(c?.output) },
{ label: "Reasoning Tokens", value: number(c?.reasoning) },
{ label: "Cache Tokens (read/write)", value: `${number(c?.cacheRead)} / ${number(c?.cacheWrite)}` },
{ label: "User Messages", value: count.user.toLocaleString() },
{ label: "Assistant Messages", value: count.assistant.toLocaleString() },
{ label: "Total Cost", value: cost() },
{ label: "Session Created", value: time(props.info()?.time.created) },
{ label: "Last Activity", value: time(c?.message.time.created) },
{ label: language.t("context.stats.session"), value: props.info()?.title ?? params.id ?? "—" },
{ label: language.t("context.stats.messages"), value: count.all.toLocaleString() },
{ label: language.t("context.stats.provider"), value: providerLabel() },
{ label: language.t("context.stats.model"), value: modelLabel() },
{ label: language.t("context.stats.limit"), value: number(c?.limit) },
{ label: language.t("context.stats.totalTokens"), value: number(c?.total) },
{ label: language.t("context.stats.usage"), value: percent(c?.usage) },
{ label: language.t("context.stats.inputTokens"), value: number(c?.input) },
{ label: language.t("context.stats.outputTokens"), value: number(c?.output) },
{ label: language.t("context.stats.reasoningTokens"), value: number(c?.reasoning) },
{
label: language.t("context.stats.cacheTokens"),
value: `${number(c?.cacheRead)} / ${number(c?.cacheWrite)}`,
},
{ label: language.t("context.stats.userMessages"), value: count.user.toLocaleString() },
{ label: language.t("context.stats.assistantMessages"), value: count.assistant.toLocaleString() },
{ label: language.t("context.stats.totalCost"), value: cost() },
{ label: language.t("context.stats.sessionCreated"), value: time(props.info()?.time.created) },
{ label: language.t("context.stats.lastActivity"), value: time(c?.message.time.created) },
] satisfies { label: string; value: JSX.Element }[]
})
@@ -371,7 +376,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
<Show when={breakdown().length > 0}>
<div class="flex flex-col gap-2">
<div class="text-12-regular text-text-weak">Context Breakdown</div>
<div class="text-12-regular text-text-weak">{language.t("context.breakdown.title")}</div>
<div class="h-2 w-full rounded-full bg-surface-base overflow-hidden flex">
<For each={breakdown()}>
{(segment) => (
@@ -397,7 +402,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
</For>
</div>
<div class="hidden text-11-regular text-text-weaker">
Approximate breakdown of input tokens. "Other" includes tool definitions and overhead.
{language.t("context.breakdown.note")}
</div>
</div>
</Show>
@@ -405,7 +410,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
<Show when={systemPrompt()}>
{(prompt) => (
<div class="flex flex-col gap-2">
<div class="text-12-regular text-text-weak">System Prompt</div>
<div class="text-12-regular text-text-weak">{language.t("context.systemPrompt.title")}</div>
<div class="border border-border-base rounded-md bg-surface-base px-3 py-2">
<Markdown text={prompt()} class="text-12-regular" />
</div>
@@ -414,7 +419,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
</Show>
<div class="flex flex-col gap-2">
<div class="text-12-regular text-text-weak">Raw messages</div>
<div class="text-12-regular text-text-weak">{language.t("context.rawMessages.title")}</div>
<Accordion multiple>
<For each={props.messages()}>{(message) => <RawMessage message={message} />}</For>
</Accordion>

View File

@@ -2,6 +2,7 @@ import { createEffect, createMemo, createSignal, onCleanup, onMount, type Access
import { createStore } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings"
import { Persist, persisted } from "@/utils/persist"
@@ -154,6 +155,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
init: () => {
const dialog = useDialog()
const settings = useSettings()
const language = useLanguage()
const [registrations, setRegistrations] = createSignal<Accessor<CommandOption[]>[]>([])
const [suspendCount, setSuspendCount] = createSignal(0)
@@ -213,7 +215,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
...suggested.map((x) => ({
...x,
id: SUGGESTED_PREFIX + x.id,
category: "Suggested",
category: language.t("command.category.suggested"),
})),
...resolved,
]

View File

@@ -1,9 +1,205 @@
export const dict = {
"command.category.suggested": "Suggested",
"command.category.view": "View",
"command.category.project": "Project",
"command.category.provider": "Provider",
"command.category.server": "Server",
"command.category.session": "Session",
"command.category.theme": "Theme",
"command.category.language": "Language",
"command.category.file": "File",
"command.category.terminal": "Terminal",
"command.category.model": "Model",
"command.category.mcp": "MCP",
"command.category.agent": "Agent",
"command.category.permissions": "Permissions",
"theme.scheme.system": "System",
"theme.scheme.light": "Light",
"theme.scheme.dark": "Dark",
"command.sidebar.toggle": "Toggle sidebar",
"command.project.open": "Open project",
"command.provider.connect": "Connect provider",
"command.server.switch": "Switch server",
"command.session.previous": "Previous session",
"command.session.next": "Next session",
"command.session.archive": "Archive session",
"command.theme.cycle": "Cycle theme",
"command.theme.set": "Use theme: {{theme}}",
"command.theme.scheme.cycle": "Cycle color scheme",
"command.theme.scheme.set": "Use color scheme: {{scheme}}",
"command.language.cycle": "Cycle language",
"command.language.set": "Use language: {{language}}",
"command.session.new": "New session",
"command.file.open": "Open file",
"command.file.open.description": "Search files and commands",
"command.terminal.toggle": "Toggle terminal",
"command.review.toggle": "Toggle review",
"command.terminal.new": "New terminal",
"command.terminal.new.description": "Create a new terminal tab",
"command.steps.toggle": "Toggle steps",
"command.steps.toggle.description": "Show or hide steps for the current message",
"command.message.previous": "Previous message",
"command.message.previous.description": "Go to the previous user message",
"command.message.next": "Next message",
"command.message.next.description": "Go to the next user message",
"command.model.choose": "Choose model",
"command.model.choose.description": "Select a different model",
"command.mcp.toggle": "Toggle MCPs",
"command.mcp.toggle.description": "Toggle MCPs",
"command.agent.cycle": "Cycle agent",
"command.agent.cycle.description": "Switch to the next agent",
"command.agent.cycle.reverse": "Cycle agent backwards",
"command.agent.cycle.reverse.description": "Switch to the previous agent",
"command.model.variant.cycle": "Cycle thinking effort",
"command.model.variant.cycle.description": "Switch to the next effort level",
"command.permissions.autoaccept.enable": "Auto-accept edits",
"command.permissions.autoaccept.disable": "Stop auto-accepting edits",
"command.session.undo": "Undo",
"command.session.undo.description": "Undo the last message",
"command.session.redo": "Redo",
"command.session.redo.description": "Redo the last undone message",
"command.session.compact": "Compact session",
"command.session.compact.description": "Summarize the session to reduce context size",
"command.session.fork": "Fork from message",
"command.session.fork.description": "Create a new session from a previous message",
"command.session.share": "Share session",
"command.session.share.description": "Share this session and copy the URL to clipboard",
"command.session.unshare": "Unshare session",
"command.session.unshare.description": "Stop sharing this session",
"palette.search.placeholder": "Search files and commands",
"palette.empty": "No results found",
"palette.group.commands": "Commands",
"palette.group.files": "Files",
"dialog.provider.search.placeholder": "Search providers",
"dialog.provider.empty": "No providers found",
"dialog.provider.group.popular": "Popular",
"dialog.provider.group.other": "Other",
"dialog.provider.tag.recommended": "Recommended",
"dialog.provider.anthropic.note": "Connect with Claude Pro/Max or API key",
"dialog.model.select.title": "Select model",
"dialog.model.search.placeholder": "Search models",
"dialog.model.empty": "No model results",
"dialog.model.manage": "Manage models",
"dialog.model.manage.description": "Customize which models appear in the model selector.",
"dialog.model.unpaid.freeModels.title": "Free models provided by OpenCode",
"dialog.model.unpaid.addMore.title": "Add more models from popular providers",
"dialog.provider.viewAll": "View all providers",
"model.tag.free": "Free",
"model.tag.latest": "Latest",
"common.search.placeholder": "Search",
"common.loading": "Loading",
"common.cancel": "Cancel",
"common.save": "Save",
"common.saving": "Saving...",
"common.default": "Default",
"prompt.placeholder.shell": "Enter shell command...",
"prompt.placeholder.normal": "Ask anything... \"{{example}}\"",
"prompt.mode.shell": "Shell",
"prompt.mode.shell.exit": "esc to exit",
"dialog.mcp.title": "MCPs",
"dialog.mcp.description": "{{enabled}} of {{total}} enabled",
"dialog.mcp.empty": "No MCPs configured",
"mcp.status.connected": "connected",
"mcp.status.failed": "failed",
"mcp.status.needs_auth": "needs auth",
"mcp.status.disabled": "disabled",
"dialog.fork.empty": "No messages to fork from",
"dialog.directory.search.placeholder": "Search folders",
"dialog.directory.empty": "No folders found",
"dialog.server.title": "Servers",
"dialog.server.description": "Switch which OpenCode server this app connects to.",
"dialog.server.search.placeholder": "Search servers",
"dialog.server.empty": "No servers yet",
"dialog.server.add.title": "Add a server",
"dialog.server.add.url": "Server URL",
"dialog.server.add.error": "Could not connect to server",
"dialog.server.add.checking": "Checking...",
"dialog.server.add.button": "Add",
"dialog.server.default.title": "Default server",
"dialog.server.default.description": "Connect to this server on app launch instead of starting a local server. Requires restart.",
"dialog.server.default.none": "No server selected",
"dialog.server.default.set": "Set current server as default",
"dialog.server.default.clear": "Clear",
"dialog.project.edit.title": "Edit project",
"dialog.project.edit.name": "Name",
"dialog.project.edit.icon": "Icon",
"dialog.project.edit.icon.alt": "Project icon",
"dialog.project.edit.icon.hint": "Click or drag an image",
"dialog.project.edit.icon.recommended": "Recommended: 128x128px",
"dialog.project.edit.color": "Color",
"context.breakdown.title": "Context Breakdown",
"context.breakdown.note": "Approximate breakdown of input tokens. \"Other\" includes tool definitions and overhead.",
"context.breakdown.system": "System",
"context.breakdown.user": "User",
"context.breakdown.assistant": "Assistant",
"context.breakdown.tool": "Tool Calls",
"context.breakdown.other": "Other",
"context.systemPrompt.title": "System Prompt",
"context.rawMessages.title": "Raw messages",
"context.stats.session": "Session",
"context.stats.messages": "Messages",
"context.stats.provider": "Provider",
"context.stats.model": "Model",
"context.stats.limit": "Context Limit",
"context.stats.totalTokens": "Total Tokens",
"context.stats.usage": "Usage",
"context.stats.inputTokens": "Input Tokens",
"context.stats.outputTokens": "Output Tokens",
"context.stats.reasoningTokens": "Reasoning Tokens",
"context.stats.cacheTokens": "Cache Tokens (read/write)",
"context.stats.userMessages": "User Messages",
"context.stats.assistantMessages": "Assistant Messages",
"context.stats.totalCost": "Total Cost",
"context.stats.sessionCreated": "Session Created",
"context.stats.lastActivity": "Last Activity",
"language.en": "English",
"language.zh": "Chinese",
"toast.language.title": "Language",
"toast.language.description": "Switched to {{language}}",
"toast.theme.title": "Theme switched",
"toast.scheme.title": "Color scheme",
"toast.permissions.autoaccept.on.title": "Auto-accepting edits",
"toast.permissions.autoaccept.on.description": "Edit and write permissions will be automatically approved",
"toast.permissions.autoaccept.off.title": "Stopped auto-accepting edits",
"toast.permissions.autoaccept.off.description": "Edit and write permissions will require approval",
"toast.model.none.title": "No model selected",
"toast.model.none.description": "Connect a provider to summarize this session",
"toast.session.share.copyFailed.title": "Failed to copy URL to clipboard",
"toast.session.share.success.title": "Session shared",
"toast.session.share.success.description": "Share URL copied to clipboard!",
"toast.session.share.failed.title": "Failed to share session",
"toast.session.share.failed.description": "An error occurred while sharing the session",
"toast.session.unshare.success.title": "Session unshared",
"toast.session.unshare.success.description": "Session unshared successfully!",
"toast.session.unshare.failed.title": "Failed to unshare session",
"toast.session.unshare.failed.description": "An error occurred while unsharing the session",
}

View File

@@ -3,11 +3,207 @@ import { dict as en } from "./en"
type Keys = keyof typeof en
export const dict = {
"command.category.language": "\u8bed\u8a00",
"command.language.cycle": "\u5207\u6362\u8bed\u8a00",
"command.language.set": "\u4f7f\u7528\u8bed\u8a00: {{language}}",
"language.en": "\u82f1\u8bed",
"language.zh": "\u4e2d\u6587",
"toast.language.title": "\u8bed\u8a00",
"toast.language.description": "\u5df2\u5207\u6362\u5230{{language}}",
"command.category.suggested": "建议",
"command.category.view": "视图",
"command.category.project": "项目",
"command.category.provider": "提供商",
"command.category.server": "服务器",
"command.category.session": "会话",
"command.category.theme": "主题",
"command.category.language": "语言",
"command.category.file": "文件",
"command.category.terminal": "终端",
"command.category.model": "模型",
"command.category.mcp": "MCP",
"command.category.agent": "智能体",
"command.category.permissions": "权限",
"theme.scheme.system": "系统",
"theme.scheme.light": "浅色",
"theme.scheme.dark": "深色",
"command.sidebar.toggle": "切换侧边栏",
"command.project.open": "打开项目",
"command.provider.connect": "连接提供商",
"command.server.switch": "切换服务器",
"command.session.previous": "上一个会话",
"command.session.next": "下一个会话",
"command.session.archive": "归档会话",
"command.theme.cycle": "切换主题",
"command.theme.set": "使用主题: {{theme}}",
"command.theme.scheme.cycle": "切换配色方案",
"command.theme.scheme.set": "使用配色方案: {{scheme}}",
"command.language.cycle": "切换语言",
"command.language.set": "使用语言: {{language}}",
"command.session.new": "新建会话",
"command.file.open": "打开文件",
"command.file.open.description": "搜索文件和命令",
"command.terminal.toggle": "切换终端",
"command.review.toggle": "切换审查",
"command.terminal.new": "新建终端",
"command.terminal.new.description": "创建新的终端标签页",
"command.steps.toggle": "切换步骤",
"command.steps.toggle.description": "显示或隐藏当前消息的步骤",
"command.message.previous": "上一条消息",
"command.message.previous.description": "跳转到上一条用户消息",
"command.message.next": "下一条消息",
"command.message.next.description": "跳转到下一条用户消息",
"command.model.choose": "选择模型",
"command.model.choose.description": "选择不同的模型",
"command.mcp.toggle": "切换 MCPs",
"command.mcp.toggle.description": "切换 MCPs",
"command.agent.cycle": "切换智能体",
"command.agent.cycle.description": "切换到下一个智能体",
"command.agent.cycle.reverse": "反向切换智能体",
"command.agent.cycle.reverse.description": "切换到上一个智能体",
"command.model.variant.cycle": "切换思考强度",
"command.model.variant.cycle.description": "切换到下一个强度等级",
"command.permissions.autoaccept.enable": "自动接受编辑",
"command.permissions.autoaccept.disable": "停止自动接受编辑",
"command.session.undo": "撤销",
"command.session.undo.description": "撤销上一条消息",
"command.session.redo": "重做",
"command.session.redo.description": "重做上一条撤销的消息",
"command.session.compact": "精简会话",
"command.session.compact.description": "总结会话以减少上下文大小",
"command.session.fork": "从消息分叉",
"command.session.fork.description": "从之前的消息创建新会话",
"command.session.share": "分享会话",
"command.session.share.description": "分享此会话并将链接复制到剪贴板",
"command.session.unshare": "取消分享会话",
"command.session.unshare.description": "停止分享此会话",
"palette.search.placeholder": "搜索文件和命令",
"palette.empty": "未找到结果",
"palette.group.commands": "命令",
"palette.group.files": "文件",
"dialog.provider.search.placeholder": "搜索提供商",
"dialog.provider.empty": "未找到提供商",
"dialog.provider.group.popular": "热门",
"dialog.provider.group.other": "其他",
"dialog.provider.tag.recommended": "推荐",
"dialog.provider.anthropic.note": "使用 Claude Pro/Max 或 API 密钥连接",
"dialog.model.select.title": "选择模型",
"dialog.model.search.placeholder": "搜索模型",
"dialog.model.empty": "未找到模型",
"dialog.model.manage": "管理模型",
"dialog.model.manage.description": "自定义模型选择器中显示的模型。",
"dialog.model.unpaid.freeModels.title": "OpenCode 提供的免费模型",
"dialog.model.unpaid.addMore.title": "从热门提供商添加更多模型",
"dialog.provider.viewAll": "查看全部提供商",
"model.tag.free": "免费",
"model.tag.latest": "最新",
"common.search.placeholder": "搜索",
"common.loading": "加载中",
"common.cancel": "取消",
"common.save": "保存",
"common.saving": "保存中...",
"common.default": "默认",
"prompt.placeholder.shell": "输入 shell 命令...",
"prompt.placeholder.normal": "随便问点什么... \"{{example}}\"",
"prompt.mode.shell": "Shell",
"prompt.mode.shell.exit": "按 esc 退出",
"dialog.mcp.title": "MCPs",
"dialog.mcp.description": "已启用 {{enabled}} / {{total}}",
"dialog.mcp.empty": "未配置 MCPs",
"mcp.status.connected": "已连接",
"mcp.status.failed": "失败",
"mcp.status.needs_auth": "需要授权",
"mcp.status.disabled": "已禁用",
"dialog.fork.empty": "没有可用于分叉的消息",
"dialog.directory.search.placeholder": "搜索文件夹",
"dialog.directory.empty": "未找到文件夹",
"dialog.server.title": "服务器",
"dialog.server.description": "切换此应用连接的 OpenCode 服务器。",
"dialog.server.search.placeholder": "搜索服务器",
"dialog.server.empty": "暂无服务器",
"dialog.server.add.title": "添加服务器",
"dialog.server.add.url": "服务器 URL",
"dialog.server.add.error": "无法连接到服务器",
"dialog.server.add.checking": "检查中...",
"dialog.server.add.button": "添加",
"dialog.server.default.title": "默认服务器",
"dialog.server.default.description": "应用启动时连接此服务器,而不是启动本地服务器。需要重启。",
"dialog.server.default.none": "未选择服务器",
"dialog.server.default.set": "将当前服务器设为默认",
"dialog.server.default.clear": "清除",
"dialog.project.edit.title": "编辑项目",
"dialog.project.edit.name": "名称",
"dialog.project.edit.icon": "图标",
"dialog.project.edit.icon.alt": "项目图标",
"dialog.project.edit.icon.hint": "点击或拖拽图片",
"dialog.project.edit.icon.recommended": "建议128x128px",
"dialog.project.edit.color": "颜色",
"context.breakdown.title": "上下文拆分",
"context.breakdown.note": "输入 token 的大致拆分。“其他”包含工具定义和开销。",
"context.breakdown.system": "系统",
"context.breakdown.user": "用户",
"context.breakdown.assistant": "助手",
"context.breakdown.tool": "工具调用",
"context.breakdown.other": "其他",
"context.systemPrompt.title": "系统提示词",
"context.rawMessages.title": "原始消息",
"context.stats.session": "会话",
"context.stats.messages": "消息数",
"context.stats.provider": "提供商",
"context.stats.model": "模型",
"context.stats.limit": "上下文限制",
"context.stats.totalTokens": "总 token",
"context.stats.usage": "使用率",
"context.stats.inputTokens": "输入 token",
"context.stats.outputTokens": "输出 token",
"context.stats.reasoningTokens": "推理 token",
"context.stats.cacheTokens": "缓存 token读/写)",
"context.stats.userMessages": "用户消息",
"context.stats.assistantMessages": "助手消息",
"context.stats.totalCost": "总成本",
"context.stats.sessionCreated": "创建时间",
"context.stats.lastActivity": "最后活动",
"language.en": "英语",
"language.zh": "中文",
"toast.language.title": "语言",
"toast.language.description": "已切换到{{language}}",
"toast.theme.title": "主题已切换",
"toast.scheme.title": "配色方案",
"toast.permissions.autoaccept.on.title": "自动接受编辑",
"toast.permissions.autoaccept.on.description": "编辑和写入权限将自动获批",
"toast.permissions.autoaccept.off.title": "已停止自动接受编辑",
"toast.permissions.autoaccept.off.description": "编辑和写入权限将需要手动批准",
"toast.model.none.title": "未选择模型",
"toast.model.none.description": "请先连接提供商以总结此会话",
"toast.session.share.copyFailed.title": "无法复制链接到剪贴板",
"toast.session.share.success.title": "会话已分享",
"toast.session.share.success.description": "分享链接已复制到剪贴板",
"toast.session.share.failed.title": "分享会话失败",
"toast.session.share.failed.description": "分享会话时发生错误",
"toast.session.unshare.success.title": "已取消分享会话",
"toast.session.unshare.success.description": "会话已成功取消分享",
"toast.session.unshare.failed.title": "取消分享失败",
"toast.session.unshare.failed.description": "取消分享会话时发生错误",
} satisfies Partial<Record<Keys, string>>

View File

@@ -114,11 +114,12 @@ export default function Layout(props: ParentProps) {
const initialDir = params.dir
const availableThemeEntries = createMemo(() => Object.entries(theme.themes()))
const colorSchemeOrder: ColorScheme[] = ["system", "light", "dark"]
const colorSchemeLabel: Record<ColorScheme, string> = {
system: "System",
light: "Light",
dark: "Dark",
const colorSchemeKey: Record<ColorScheme, "theme.scheme.system" | "theme.scheme.light" | "theme.scheme.dark"> = {
system: "theme.scheme.system",
light: "theme.scheme.light",
dark: "theme.scheme.dark",
}
const colorSchemeLabel = (scheme: ColorScheme) => language.t(colorSchemeKey[scheme])
const [editor, setEditor] = createStore({
active: "" as string,
@@ -252,7 +253,7 @@ export default function Layout(props: ParentProps) {
theme.setTheme(nextThemeId)
const nextTheme = theme.themes()[nextThemeId]
showToast({
title: "Theme switched",
title: language.t("toast.theme.title"),
description: nextTheme?.name ?? nextThemeId,
})
}
@@ -265,8 +266,8 @@ export default function Layout(props: ParentProps) {
const next = colorSchemeOrder[nextIndex]
theme.setColorScheme(next)
showToast({
title: "Color scheme",
description: colorSchemeLabel[next],
title: language.t("toast.scheme.title"),
description: colorSchemeLabel(next),
})
}
@@ -827,28 +828,28 @@ export default function Layout(props: ParentProps) {
const commands: CommandOption[] = [
{
id: "sidebar.toggle",
title: "Toggle sidebar",
category: "View",
title: language.t("command.sidebar.toggle"),
category: language.t("command.category.view"),
keybind: "mod+b",
onSelect: () => layout.sidebar.toggle(),
},
{
id: "project.open",
title: "Open project",
category: "Project",
title: language.t("command.project.open"),
category: language.t("command.category.project"),
keybind: "mod+o",
onSelect: () => chooseProject(),
},
{
id: "provider.connect",
title: "Connect provider",
category: "Provider",
title: language.t("command.provider.connect"),
category: language.t("command.category.provider"),
onSelect: () => connectProvider(),
},
{
id: "server.switch",
title: "Switch server",
category: "Server",
title: language.t("command.server.switch"),
category: language.t("command.category.server"),
onSelect: () => openServer(),
},
{
@@ -860,22 +861,22 @@ export default function Layout(props: ParentProps) {
},
{
id: "session.previous",
title: "Previous session",
category: "Session",
title: language.t("command.session.previous"),
category: language.t("command.category.session"),
keybind: "alt+arrowup",
onSelect: () => navigateSessionByOffset(-1),
},
{
id: "session.next",
title: "Next session",
category: "Session",
title: language.t("command.session.next"),
category: language.t("command.category.session"),
keybind: "alt+arrowdown",
onSelect: () => navigateSessionByOffset(1),
},
{
id: "session.archive",
title: "Archive session",
category: "Session",
title: language.t("command.session.archive"),
category: language.t("command.category.session"),
keybind: "mod+shift+backspace",
disabled: !params.dir || !params.id,
onSelect: () => {
@@ -885,8 +886,8 @@ export default function Layout(props: ParentProps) {
},
{
id: "theme.cycle",
title: "Cycle theme",
category: "Theme",
title: language.t("command.theme.cycle"),
category: language.t("command.category.theme"),
keybind: "mod+shift+t",
onSelect: () => cycleTheme(1),
},
@@ -895,8 +896,8 @@ export default function Layout(props: ParentProps) {
for (const [id, definition] of availableThemeEntries()) {
commands.push({
id: `theme.set.${id}`,
title: `Use theme: ${definition.name ?? id}`,
category: "Theme",
title: language.t("command.theme.set", { theme: definition.name ?? id }),
category: language.t("command.category.theme"),
onSelect: () => theme.commitPreview(),
onHighlight: () => {
theme.previewTheme(id)
@@ -907,8 +908,8 @@ export default function Layout(props: ParentProps) {
commands.push({
id: "theme.scheme.cycle",
title: "Cycle color scheme",
category: "Theme",
title: language.t("command.theme.scheme.cycle"),
category: language.t("command.category.theme"),
keybind: "mod+shift+s",
onSelect: () => cycleColorScheme(1),
})
@@ -916,8 +917,8 @@ export default function Layout(props: ParentProps) {
for (const scheme of colorSchemeOrder) {
commands.push({
id: `theme.scheme.${scheme}`,
title: `Use color scheme: ${colorSchemeLabel[scheme]}`,
category: "Theme",
title: language.t("command.theme.scheme.set", { scheme: colorSchemeLabel(scheme) }),
category: language.t("command.category.theme"),
onSelect: () => theme.commitPreview(),
onHighlight: () => {
theme.previewColorScheme(scheme)

View File

@@ -33,6 +33,7 @@ import { DialogSelectModel } from "@/components/dialog-select-model"
import { DialogSelectMcp } from "@/components/dialog-select-mcp"
import { DialogFork } from "@/components/dialog-fork"
import { useCommand } from "@/context/command"
import { useLanguage } from "@/context/language"
import { useNavigate, useParams } from "@solidjs/router"
import { UserMessage } from "@opencode-ai/sdk/v2"
import type { FileDiff } from "@opencode-ai/sdk/v2/client"
@@ -161,6 +162,7 @@ export default function Page() {
const dialog = useDialog()
const codeComponent = useCodeComponent()
const command = useCommand()
const language = useLanguage()
const platform = usePlatform()
const params = useParams()
const navigate = useNavigate()
@@ -433,51 +435,51 @@ export default function Page() {
command.register(() => [
{
id: "session.new",
title: "New session",
category: "Session",
title: language.t("command.session.new"),
category: language.t("command.category.session"),
keybind: "mod+shift+s",
slash: "new",
onSelect: () => navigate(`/${params.dir}/session`),
},
{
id: "file.open",
title: "Open file",
description: "Search files and commands",
category: "File",
title: language.t("command.file.open"),
description: language.t("command.file.open.description"),
category: language.t("command.category.file"),
keybind: "mod+p",
slash: "open",
onSelect: () => dialog.show(() => <DialogSelectFile />),
},
{
id: "terminal.toggle",
title: "Toggle terminal",
title: language.t("command.terminal.toggle"),
description: "",
category: "View",
category: language.t("command.category.view"),
keybind: "ctrl+`",
slash: "terminal",
onSelect: () => view().terminal.toggle(),
},
{
id: "review.toggle",
title: "Toggle review",
title: language.t("command.review.toggle"),
description: "",
category: "View",
category: language.t("command.category.view"),
keybind: "mod+shift+r",
onSelect: () => view().reviewPanel.toggle(),
},
{
id: "terminal.new",
title: "New terminal",
description: "Create a new terminal tab",
category: "Terminal",
title: language.t("command.terminal.new"),
description: language.t("command.terminal.new.description"),
category: language.t("command.category.terminal"),
keybind: "ctrl+alt+t",
onSelect: () => terminal.new(),
},
{
id: "steps.toggle",
title: "Toggle steps",
description: "Show or hide steps for the current message",
category: "View",
title: language.t("command.steps.toggle"),
description: language.t("command.steps.toggle.description"),
category: language.t("command.category.view"),
keybind: "mod+e",
slash: "steps",
disabled: !params.id,
@@ -489,62 +491,62 @@ export default function Page() {
},
{
id: "message.previous",
title: "Previous message",
description: "Go to the previous user message",
category: "Session",
title: language.t("command.message.previous"),
description: language.t("command.message.previous.description"),
category: language.t("command.category.session"),
keybind: "mod+arrowup",
disabled: !params.id,
onSelect: () => navigateMessageByOffset(-1),
},
{
id: "message.next",
title: "Next message",
description: "Go to the next user message",
category: "Session",
title: language.t("command.message.next"),
description: language.t("command.message.next.description"),
category: language.t("command.category.session"),
keybind: "mod+arrowdown",
disabled: !params.id,
onSelect: () => navigateMessageByOffset(1),
},
{
id: "model.choose",
title: "Choose model",
description: "Select a different model",
category: "Model",
title: language.t("command.model.choose"),
description: language.t("command.model.choose.description"),
category: language.t("command.category.model"),
keybind: "mod+'",
slash: "model",
onSelect: () => dialog.show(() => <DialogSelectModel />),
},
{
id: "mcp.toggle",
title: "Toggle MCPs",
description: "Toggle MCPs",
category: "MCP",
title: language.t("command.mcp.toggle"),
description: language.t("command.mcp.toggle.description"),
category: language.t("command.category.mcp"),
keybind: "mod+;",
slash: "mcp",
onSelect: () => dialog.show(() => <DialogSelectMcp />),
},
{
id: "agent.cycle",
title: "Cycle agent",
description: "Switch to the next agent",
category: "Agent",
title: language.t("command.agent.cycle"),
description: language.t("command.agent.cycle.description"),
category: language.t("command.category.agent"),
keybind: "mod+.",
slash: "agent",
onSelect: () => local.agent.move(1),
},
{
id: "agent.cycle.reverse",
title: "Cycle agent backwards",
description: "Switch to the previous agent",
category: "Agent",
title: language.t("command.agent.cycle.reverse"),
description: language.t("command.agent.cycle.reverse.description"),
category: language.t("command.category.agent"),
keybind: "shift+mod+.",
onSelect: () => local.agent.move(-1),
},
{
id: "model.variant.cycle",
title: "Cycle thinking effort",
description: "Switch to the next effort level",
category: "Model",
title: language.t("command.model.variant.cycle"),
description: language.t("command.model.variant.cycle.description"),
category: language.t("command.category.model"),
keybind: "shift+mod+d",
onSelect: () => {
local.model.variant.cycle()
@@ -554,30 +556,31 @@ export default function Page() {
id: "permissions.autoaccept",
title:
params.id && permission.isAutoAccepting(params.id, sdk.directory)
? "Stop auto-accepting edits"
: "Auto-accept edits",
category: "Permissions",
? language.t("command.permissions.autoaccept.disable")
: language.t("command.permissions.autoaccept.enable"),
category: language.t("command.category.permissions"),
keybind: "mod+shift+a",
disabled: !params.id || !permission.permissionsEnabled(),
onSelect: () => {
const sessionID = params.id
if (!sessionID) return
permission.toggleAutoAccept(sessionID, sdk.directory)
const enabled = permission.isAutoAccepting(sessionID, sdk.directory)
showToast({
title: permission.isAutoAccepting(sessionID, sdk.directory)
? "Auto-accepting edits"
: "Stopped auto-accepting edits",
description: permission.isAutoAccepting(sessionID, sdk.directory)
? "Edit and write permissions will be automatically approved"
: "Edit and write permissions will require approval",
title: enabled
? language.t("toast.permissions.autoaccept.on.title")
: language.t("toast.permissions.autoaccept.off.title"),
description: enabled
? language.t("toast.permissions.autoaccept.on.description")
: language.t("toast.permissions.autoaccept.off.description"),
})
},
},
{
id: "session.undo",
title: "Undo",
description: "Undo the last message",
category: "Session",
title: language.t("command.session.undo"),
description: language.t("command.session.undo.description"),
category: language.t("command.category.session"),
slash: "undo",
disabled: !params.id || visibleUserMessages().length === 0,
onSelect: async () => {
@@ -604,9 +607,9 @@ export default function Page() {
},
{
id: "session.redo",
title: "Redo",
description: "Redo the last undone message",
category: "Session",
title: language.t("command.session.redo"),
description: language.t("command.session.redo.description"),
category: language.t("command.category.session"),
slash: "redo",
disabled: !params.id || !info()?.revert?.messageID,
onSelect: async () => {
@@ -633,9 +636,9 @@ export default function Page() {
},
{
id: "session.compact",
title: "Compact session",
description: "Summarize the session to reduce context size",
category: "Session",
title: language.t("command.session.compact"),
description: language.t("command.session.compact.description"),
category: language.t("command.category.session"),
slash: "compact",
disabled: !params.id || visibleUserMessages().length === 0,
onSelect: async () => {
@@ -644,8 +647,8 @@ export default function Page() {
const model = local.model.current()
if (!model) {
showToast({
title: "No model selected",
description: "Connect a provider to summarize this session",
title: language.t("toast.model.none.title"),
description: language.t("toast.model.none.description"),
})
return
}
@@ -658,72 +661,72 @@ export default function Page() {
},
{
id: "session.fork",
title: "Fork from message",
description: "Create a new session from a previous message",
category: "Session",
title: language.t("command.session.fork"),
description: language.t("command.session.fork.description"),
category: language.t("command.category.session"),
slash: "fork",
disabled: !params.id || visibleUserMessages().length === 0,
onSelect: () => dialog.show(() => <DialogFork />),
},
...(sync.data.config.share !== "disabled"
? [
{
id: "session.share",
title: "Share session",
description: "Share this session and copy the URL to clipboard",
category: "Session",
slash: "share",
disabled: !params.id || !!info()?.share?.url,
onSelect: async () => {
{
id: "session.share",
title: language.t("command.session.share"),
description: language.t("command.session.share.description"),
category: language.t("command.category.session"),
slash: "share",
disabled: !params.id || !!info()?.share?.url,
onSelect: async () => {
if (!params.id) return
await sdk.client.session
.share({ sessionID: params.id })
.then((res) => {
navigator.clipboard.writeText(res.data!.share!.url).catch(() =>
showToast({
title: "Failed to copy URL to clipboard",
title: language.t("toast.session.share.copyFailed.title"),
variant: "error",
}),
)
})
.then(() =>
showToast({
title: "Session shared",
description: "Share URL copied to clipboard!",
title: language.t("toast.session.share.success.title"),
description: language.t("toast.session.share.success.description"),
variant: "success",
}),
)
.catch(() =>
showToast({
title: "Failed to share session",
description: "An error occurred while sharing the session",
title: language.t("toast.session.share.failed.title"),
description: language.t("toast.session.share.failed.description"),
variant: "error",
}),
)
},
},
{
id: "session.unshare",
title: "Unshare session",
description: "Stop sharing this session",
category: "Session",
slash: "unshare",
disabled: !params.id || !info()?.share?.url,
onSelect: async () => {
{
id: "session.unshare",
title: language.t("command.session.unshare"),
description: language.t("command.session.unshare.description"),
category: language.t("command.category.session"),
slash: "unshare",
disabled: !params.id || !info()?.share?.url,
onSelect: async () => {
if (!params.id) return
await sdk.client.session
.unshare({ sessionID: params.id })
.then(() =>
showToast({
title: "Session unshared",
description: "Session unshared successfully!",
title: language.t("toast.session.unshare.success.title"),
description: language.t("toast.session.unshare.success.description"),
variant: "success",
}),
)
.catch(() =>
showToast({
title: "Failed to unshare session",
description: "An error occurred while unsharing the session",
title: language.t("toast.session.unshare.failed.title"),
description: language.t("toast.session.unshare.failed.description"),
variant: "error",
}),
)

View File

@@ -16,6 +16,7 @@ export interface ListProps<T> extends FilteredListProps<T> {
class?: string
children: (item: T) => JSX.Element
emptyMessage?: string
loadingMessage?: string
onKeyEvent?: (event: KeyboardEvent, item: T | undefined) => void
onMove?: (item: T | undefined) => void
activeIcon?: IconProps["name"]
@@ -207,8 +208,10 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
fallback={
<div data-slot="list-empty-state">
<div data-slot="list-message">
{props.emptyMessage ?? (grouped.loading ? "Loading" : "No results")} for{" "}
<span data-slot="list-filter">&quot;{filter()}&quot;</span>
{grouped.loading ? props.loadingMessage ?? "Loading" : props.emptyMessage ?? "No results"}
<Show when={!props.emptyMessage && !props.loadingMessage && !!filter()}>
{" "}for <span data-slot="list-filter">&quot;{filter()}&quot;</span>
</Show>
</div>
</div>
}