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>