wip(app): i18n
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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"]}
|
||||
|
||||
@@ -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"]}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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"]}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user