diff --git a/AGENTS.md b/AGENTS.md index 750aeff1de..0769523011 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,9 +13,9 @@ - Keep things in one function unless composable or reusable - Avoid `try`/`catch` where possible - Avoid using the `any` type -- Use Bun APIs when possible (e.g., `Bun.file()` instead of `fs.existsSync()`) -- For sync file reads, use `readFileSync` from `fs` (Bun.file is async-only) -- Avoid generated file artifacts - prefer build-time `define` globals for bundled data +- Prefer single word variable names where possible +- Use Bun APIs when possible, like `Bun.file()` +- Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity ### Naming diff --git a/STATS.md b/STATS.md index a8be315651..4aa9a37b82 100644 --- a/STATS.md +++ b/STATS.md @@ -211,3 +211,4 @@ | 2026-01-23 | 6,096,236 (+329,896) | 2,096,235 (+66,748) | 8,192,471 (+396,644) | | 2026-01-24 | 6,371,019 (+274,783) | 2,156,870 (+60,635) | 8,527,889 (+335,418) | | 2026-01-25 | 6,639,082 (+268,063) | 2,187,853 (+30,983) | 8,826,935 (+299,046) | +| 2026-01-26 | 6,941,620 (+302,538) | 2,232,115 (+44,262) | 9,173,735 (+346,800) | diff --git a/packages/app/src/components/dialog-settings.tsx b/packages/app/src/components/dialog-settings.tsx index dbbc8fa7ad..9dd6efd688 100644 --- a/packages/app/src/components/dialog-settings.tsx +++ b/packages/app/src/components/dialog-settings.tsx @@ -21,34 +21,32 @@ export const DialogSettings: Component = () => { -
-
- {language.t("settings.section.desktop")} -
- - - {language.t("settings.tab.general")} - - - - {language.t("settings.tab.shortcuts")} - +
+
+
+
+ {language.t("settings.section.desktop")} +
+ + + {language.t("settings.tab.general")} + + + + {language.t("settings.tab.shortcuts")} + +
+
+ +
+ {language.t("settings.section.server")} +
+ + + {language.t("settings.providers.title")} + +
+
@@ -56,31 +54,6 @@ export const DialogSettings: Component = () => { v{platform.version}
- {/* Server */} - {/* */} - {/* */} - {/* Permissions */} - {/* */} - {/* */} - {/* */} - {/* Providers */} - {/* */} - {/* */} - {/* */} - {/* Models */} - {/* */} - {/* */} - {/* */} - {/* Agents */} - {/* */} - {/* */} - {/* */} - {/* Commands */} - {/* */} - {/* */} - {/* */} - {/* MCP */} - {/* */} @@ -88,12 +61,9 @@ export const DialogSettings: Component = () => { - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} + + + {/* */} {/* */} {/* */} diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index 7b6ca19392..aec6d6c4f0 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -1,14 +1,154 @@ -import { Component } from "solid-js" +import { Button } from "@opencode-ai/ui/button" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { ProviderIcon } from "@opencode-ai/ui/provider-icon" +import { Tag } from "@opencode-ai/ui/tag" +import { showToast } from "@opencode-ai/ui/toast" +import type { IconName } from "@opencode-ai/ui/icons/provider" +import { popularProviders, useProviders } from "@/hooks/use-providers" +import { createMemo, type Component, For, Show } from "solid-js" import { useLanguage } from "@/context/language" +import { useGlobalSDK } from "@/context/global-sdk" +import { DialogConnectProvider } from "./dialog-connect-provider" +import { DialogSelectProvider } from "./dialog-select-provider" + +type ProviderSource = "env" | "api" | "config" | "custom" +type ProviderMeta = { source?: ProviderSource } export const SettingsProviders: Component = () => { + const dialog = useDialog() const language = useLanguage() + const globalSDK = useGlobalSDK() + const providers = useProviders() + + const connected = createMemo(() => providers.connected()) + const popular = createMemo(() => { + const connectedIDs = new Set(connected().map((p) => p.id)) + const items = providers + .popular() + .filter((p) => !connectedIDs.has(p.id)) + .slice() + items.sort((a, b) => popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)) + return items + }) + + const source = (item: unknown) => (item as ProviderMeta).source + + const type = (item: unknown) => { + const current = source(item) + if (current === "env") return language.t("settings.providers.tag.environment") + if (current === "api") return language.t("provider.connect.method.apiKey") + if (current === "config") return language.t("settings.providers.tag.config") + if (current === "custom") return language.t("settings.providers.tag.custom") + return language.t("settings.providers.tag.other") + } + + const canDisconnect = (item: unknown) => source(item) !== "env" + + const disconnect = async (providerID: string, name: string) => { + await globalSDK.client.auth + .remove({ providerID }) + .then(async () => { + await globalSDK.client.global.dispose() + showToast({ + variant: "success", + icon: "circle-check", + title: language.t("provider.disconnect.toast.disconnected.title", { provider: name }), + description: language.t("provider.disconnect.toast.disconnected.description", { provider: name }), + }) + }) + .catch((err: unknown) => { + const message = err instanceof Error ? err.message : String(err) + showToast({ title: language.t("common.requestFailed"), description: message }) + }) + } return ( -
-
-

{language.t("settings.providers.title")}

-

{language.t("settings.providers.description")}

+
+
+
+

{language.t("settings.providers.title")}

+
+
+ +
+
+

{language.t("settings.providers.section.connected")}

+
+ 0} + fallback={ +
+ {language.t("settings.providers.connected.empty")} +
+ } + > + + {(item) => ( +
+
+ + {item.name} + {type(item)} +
+ + + +
+ )} +
+
+
+
+ +
+

{language.t("settings.providers.section.popular")}

+
+ + {(item) => ( +
+
+ + {item.name} + + {language.t("dialog.provider.tag.recommended")} + + +
{language.t("dialog.provider.anthropic.note")}
+
+ +
{language.t("dialog.provider.openai.note")}
+
+ +
{language.t("dialog.provider.copilot.note")}
+
+
+ +
+ )} +
+
+ + +
) diff --git a/packages/app/src/context/command.tsx b/packages/app/src/context/command.tsx index 39bd11a1bb..dc5228b82d 100644 --- a/packages/app/src/context/command.tsx +++ b/packages/app/src/context/command.tsx @@ -24,6 +24,15 @@ function normalizeKey(key: string) { return key.toLowerCase() } +function signature(key: string, ctrl: boolean, meta: boolean, shift: boolean, alt: boolean) { + const mask = (ctrl ? 1 : 0) | (meta ? 2 : 0) | (shift ? 4 : 0) | (alt ? 8 : 0) + return `${key}:${mask}` +} + +function signatureFromEvent(event: KeyboardEvent) { + return signature(normalizeKey(event.key), event.ctrlKey, event.metaKey, event.shiftKey, event.altKey) +} + export type KeybindConfig = string export interface Keybind { @@ -223,6 +232,30 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex const suspended = () => suspendCount() > 0 + const palette = createMemo(() => { + const config = settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND + const keybinds = parseKeybind(config) + return new Set(keybinds.map((kb) => signature(kb.key, kb.ctrl, kb.meta, kb.shift, kb.alt))) + }) + + const keymap = createMemo(() => { + const map = new Map() + for (const option of options()) { + if (option.id.startsWith(SUGGESTED_PREFIX)) continue + if (option.disabled) continue + if (!option.keybind) continue + + const keybinds = parseKeybind(option.keybind) + for (const kb of keybinds) { + if (!kb.key) continue + const sig = signature(kb.key, kb.ctrl, kb.meta, kb.shift, kb.alt) + if (map.has(sig)) continue + map.set(sig, option) + } + } + return map + }) + const run = (id: string, source?: "palette" | "keybind" | "slash") => { for (const option of options()) { if (option.id === id || option.id === "suggested." + id) { @@ -239,24 +272,18 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex const handleKeyDown = (event: KeyboardEvent) => { if (suspended() || dialog.active) return - const paletteKeybinds = parseKeybind(settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND) - if (matchKeybind(paletteKeybinds, event)) { + const sig = signatureFromEvent(event) + + if (palette().has(sig)) { event.preventDefault() showPalette() return } - for (const option of options()) { - if (option.disabled) continue - if (!option.keybind) continue - - const keybinds = parseKeybind(option.keybind) - if (matchKeybind(keybinds, event)) { - event.preventDefault() - option.onSelect?.("keybind") - return - } - } + const option = keymap().get(sig) + if (!option) return + event.preventDefault() + option.onSelect?.("keybind") } onMount(() => { diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index 1023d8df33..5bb6f92ec5 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -218,7 +218,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( } function enrich(project: { worktree: string; expanded: boolean }) { - const [childStore] = globalSync.child(project.worktree) + const [childStore] = globalSync.child(project.worktree, { bootstrap: false }) const projectID = childStore.project const metadata = projectID ? globalSync.data.project.find((x) => x.id === projectID) diff --git a/packages/app/src/context/notification.tsx b/packages/app/src/context/notification.tsx index 58e7fbf83a..5e35f6ac03 100644 --- a/packages/app/src/context/notification.tsx +++ b/packages/app/src/context/notification.tsx @@ -1,5 +1,5 @@ import { createStore } from "solid-js/store" -import { createEffect, onCleanup } from "solid-js" +import { createEffect, createMemo, onCleanup } from "solid-js" import { useParams } from "@solidjs/router" import { createSimpleContext } from "@opencode-ai/ui/context" import { useGlobalSDK } from "./global-sdk" @@ -52,6 +52,15 @@ export const { use: useNotification, provider: NotificationProvider } = createSi const settings = useSettings() const language = useLanguage() + const empty: Notification[] = [] + + const currentDirectory = createMemo(() => { + if (!params.dir) return + return base64Decode(params.dir) + }) + + const currentSession = createMemo(() => params.id) + const [store, setStore, _, ready] = persisted( Persist.global("notification", ["notification.v1"]), createStore({ @@ -72,13 +81,59 @@ export const { use: useNotification, provider: NotificationProvider } = createSi setStore("list", (list) => pruneNotifications([...list, notification])) } + const index = createMemo(() => { + const sessionAll = new Map() + const sessionUnseen = new Map() + const projectAll = new Map() + const projectUnseen = new Map() + + for (const notification of store.list) { + const session = notification.session + if (session) { + const list = sessionAll.get(session) + if (list) list.push(notification) + else sessionAll.set(session, [notification]) + if (!notification.viewed) { + const unseen = sessionUnseen.get(session) + if (unseen) unseen.push(notification) + else sessionUnseen.set(session, [notification]) + } + } + + const directory = notification.directory + if (directory) { + const list = projectAll.get(directory) + if (list) list.push(notification) + else projectAll.set(directory, [notification]) + if (!notification.viewed) { + const unseen = projectUnseen.get(directory) + if (unseen) unseen.push(notification) + else projectUnseen.set(directory, [notification]) + } + } + } + + return { + session: { + all: sessionAll, + unseen: sessionUnseen, + }, + project: { + all: projectAll, + unseen: projectUnseen, + }, + } + }) + const unsub = globalSDK.event.listen((e) => { - const directory = e.name const event = e.details + if (event.type !== "session.idle" && event.type !== "session.error") return + + const directory = e.name const time = Date.now() - const activeDirectory = params.dir ? base64Decode(params.dir) : undefined - const activeSession = params.id const viewed = (sessionID?: string) => { + const activeDirectory = currentDirectory() + const activeSession = currentSession() if (!activeDirectory) return false if (!activeSession) return false if (!sessionID) return false @@ -88,7 +143,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi switch (event.type) { case "session.idle": { const sessionID = event.properties.sessionID - const [syncStore] = globalSync.child(directory) + const [syncStore] = globalSync.child(directory, { bootstrap: false }) const match = Binary.search(syncStore.session, sessionID, (s) => s.id) const session = match.found ? syncStore.session[match.index] : undefined if (session?.parentID) break @@ -115,7 +170,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi } case "session.error": { const sessionID = event.properties.sessionID - const [syncStore] = globalSync.child(directory) + const [syncStore] = globalSync.child(directory, { bootstrap: false }) const match = sessionID ? Binary.search(syncStore.session, sessionID, (s) => s.id) : undefined const session = sessionID && match?.found ? syncStore.session[match.index] : undefined if (session?.parentID) break @@ -148,10 +203,10 @@ export const { use: useNotification, provider: NotificationProvider } = createSi ready, session: { all(session: string) { - return store.list.filter((n) => n.session === session) + return index().session.all.get(session) ?? empty }, unseen(session: string) { - return store.list.filter((n) => n.session === session && !n.viewed) + return index().session.unseen.get(session) ?? empty }, markViewed(session: string) { setStore("list", (n) => n.session === session, "viewed", true) @@ -159,10 +214,10 @@ export const { use: useNotification, provider: NotificationProvider } = createSi }, project: { all(directory: string) { - return store.list.filter((n) => n.directory === directory) + return index().project.all.get(directory) ?? empty }, unseen(directory: string) { - return store.list.filter((n) => n.directory === directory && !n.viewed) + return index().project.unseen.get(directory) ?? empty }, markViewed(directory: string) { setStore("list", (n) => n.directory === directory, "viewed", true) diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 99e516a0a4..9fcd8ba6db 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -100,7 +100,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "نماذج مجانية مقدمة من OpenCode", "dialog.model.unpaid.addMore.title": "إضافة المزيد من النماذج من موفرين مشهورين", - "dialog.provider.viewAll": "عرض جميع الموفرين", + "dialog.provider.viewAll": "عرض المزيد من الموفرين", "provider.connect.title": "اتصال {{provider}}", "provider.connect.title.anthropicProMax": "تسجيل الدخول باستخدام Claude Pro/Max", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index 93dc2f1feb..a0c904dfbc 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -100,7 +100,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "Modelos gratuitos fornecidos pelo OpenCode", "dialog.model.unpaid.addMore.title": "Adicionar mais modelos de provedores populares", - "dialog.provider.viewAll": "Ver todos os provedores", + "dialog.provider.viewAll": "Ver mais provedores", "provider.connect.title": "Conectar {{provider}}", "provider.connect.title.anthropicProMax": "Entrar com Claude Pro/Max", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index b2f0a9afe7..4dc4a2cfb2 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -98,7 +98,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "Gratis modeller leveret af OpenCode", "dialog.model.unpaid.addMore.title": "Tilføj flere modeller fra populære udbydere", - "dialog.provider.viewAll": "Vis alle udbydere", + "dialog.provider.viewAll": "Vis flere udbydere", "provider.connect.title": "Forbind {{provider}}", "provider.connect.title.anthropicProMax": "Log ind med Claude Pro/Max", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index 42f628d5ed..69bf1fb494 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -102,7 +102,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "Kostenlose Modelle von OpenCode", "dialog.model.unpaid.addMore.title": "Weitere Modelle von beliebten Anbietern hinzufügen", - "dialog.provider.viewAll": "Alle Anbieter anzeigen", + "dialog.provider.viewAll": "Mehr Anbieter anzeigen", "provider.connect.title": "{{provider}} verbinden", "provider.connect.title.anthropicProMax": "Mit Claude Pro/Max anmelden", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index b32f034855..770b021364 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -100,7 +100,7 @@ export const dict = { "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", + "dialog.provider.viewAll": "Show more providers", "provider.connect.title": "Connect {{provider}}", "provider.connect.title.anthropicProMax": "Login with Claude Pro/Max", @@ -137,6 +137,9 @@ export const dict = { "provider.connect.toast.connected.title": "{{provider}} connected", "provider.connect.toast.connected.description": "{{provider}} models are now available to use.", + "provider.disconnect.toast.disconnected.title": "{{provider}} disconnected", + "provider.disconnect.toast.disconnected.description": "{{provider}} models are no longer available.", + "model.tag.free": "Free", "model.tag.latest": "Latest", "model.provider.anthropic": "Anthropic", @@ -159,6 +162,8 @@ export const dict = { "common.loading": "Loading", "common.loading.ellipsis": "...", "common.cancel": "Cancel", + "common.connect": "Connect", + "common.disconnect": "Disconnect", "common.submit": "Submit", "common.save": "Save", "common.saving": "Saving...", @@ -491,6 +496,7 @@ export const dict = { "sidebar.project.viewAllSessions": "View all sessions", "settings.section.desktop": "Desktop", + "settings.section.server": "Server", "settings.tab.general": "General", "settings.tab.shortcuts": "Shortcuts", @@ -599,6 +605,13 @@ export const dict = { "settings.providers.title": "Providers", "settings.providers.description": "Provider settings will be configurable here.", + "settings.providers.section.connected": "Connected providers", + "settings.providers.connected.empty": "No connected providers", + "settings.providers.section.popular": "Popular providers", + "settings.providers.tag.environment": "Environment", + "settings.providers.tag.config": "Config", + "settings.providers.tag.custom": "Custom", + "settings.providers.tag.other": "Other", "settings.models.title": "Models", "settings.models.description": "Model settings will be configurable here.", "settings.agents.title": "Agents", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index 1039a2d3a4..c715bc048b 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -98,7 +98,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "Modelos gratuitos proporcionados por OpenCode", "dialog.model.unpaid.addMore.title": "Añadir más modelos de proveedores populares", - "dialog.provider.viewAll": "Ver todos los proveedores", + "dialog.provider.viewAll": "Ver más proveedores", "provider.connect.title": "Conectar {{provider}}", "provider.connect.title.anthropicProMax": "Iniciar sesión con Claude Pro/Max", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index 09eeea44c4..8bd8dba25b 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -98,7 +98,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "Modèles gratuits fournis par OpenCode", "dialog.model.unpaid.addMore.title": "Ajouter plus de modèles de fournisseurs populaires", - "dialog.provider.viewAll": "Voir tous les fournisseurs", + "dialog.provider.viewAll": "Voir plus de fournisseurs", "provider.connect.title": "Connecter {{provider}}", "provider.connect.title.anthropicProMax": "Connexion avec Claude Pro/Max", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index 821c6ccdb1..5b98f5aa92 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -98,7 +98,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "OpenCodeが提供する無料モデル", "dialog.model.unpaid.addMore.title": "人気のプロバイダーからモデルを追加", - "dialog.provider.viewAll": "すべてのプロバイダーを表示", + "dialog.provider.viewAll": "さらにプロバイダーを表示", "provider.connect.title": "{{provider}}を接続", "provider.connect.title.anthropicProMax": "Claude Pro/Maxでログイン", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index ddd00e763d..a016cd34a4 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -102,7 +102,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "OpenCode에서 제공하는 무료 모델", "dialog.model.unpaid.addMore.title": "인기 공급자의 모델 추가", - "dialog.provider.viewAll": "모든 공급자 보기", + "dialog.provider.viewAll": "더 많은 공급자 보기", "provider.connect.title": "{{provider}} 연결", "provider.connect.title.anthropicProMax": "Claude Pro/Max로 로그인", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index 3262d3e04c..153ee04122 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -103,7 +103,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "Gratis modeller levert av OpenCode", "dialog.model.unpaid.addMore.title": "Legg til flere modeller fra populære leverandører", - "dialog.provider.viewAll": "Vis alle leverandører", + "dialog.provider.viewAll": "Vis flere leverandører", "provider.connect.title": "Koble til {{provider}}", "provider.connect.title.anthropicProMax": "Logg inn med Claude Pro/Max", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 7af9d21798..db10262847 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -100,7 +100,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "Darmowe modele dostarczane przez OpenCode", "dialog.model.unpaid.addMore.title": "Dodaj więcej modeli od popularnych dostawców", - "dialog.provider.viewAll": "Zobacz wszystkich dostawców", + "dialog.provider.viewAll": "Zobacz więcej dostawców", "provider.connect.title": "Połącz {{provider}}", "provider.connect.title.anthropicProMax": "Zaloguj się z Claude Pro/Max", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index d7fa135fa0..d8b94cb107 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -100,7 +100,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "Бесплатные модели от OpenCode", "dialog.model.unpaid.addMore.title": "Добавьте больше моделей от популярных провайдеров", - "dialog.provider.viewAll": "Посмотреть всех провайдеров", + "dialog.provider.viewAll": "Показать больше провайдеров", "provider.connect.title": "Подключить {{provider}}", "provider.connect.title.anthropicProMax": "Войти с помощью Claude Pro/Max", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index e2b7df0d10..cfecb739d8 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -102,7 +102,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "OpenCode 提供的免费模型", "dialog.model.unpaid.addMore.title": "从热门提供商添加更多模型", - "dialog.provider.viewAll": "查看全部提供商", + "dialog.provider.viewAll": "查看更多提供商", "provider.connect.title": "连接 {{provider}}", "provider.connect.title.anthropicProMax": "使用 Claude Pro/Max 登录", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index 9973b443b3..050c160cdf 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -102,7 +102,7 @@ export const dict = { "dialog.model.unpaid.freeModels.title": "OpenCode 提供的免費模型", "dialog.model.unpaid.addMore.title": "從熱門提供者新增更多模型", - "dialog.provider.viewAll": "查看全部提供者", + "dialog.provider.viewAll": "查看更多提供者", "provider.connect.title": "連線 {{provider}}", "provider.connect.title.anthropicProMax": "使用 Claude Pro/Max 登入", diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index 0c04c07671..10f7dac530 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -23,6 +23,11 @@ export default function Home() { const server = useServer() const language = useLanguage() const homedir = createMemo(() => sync.data.path.home) + const recent = createMemo(() => { + return sync.data.project + .toSorted((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created)) + .slice(0, 5) + }) function openProject(directory: string) { layout.projects.open(directory) @@ -84,11 +89,7 @@ export default function Home() {
    - (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created)) - .slice(0, 5)} - > + {(project) => (