diff --git a/packages/app/src/api/releases.ts b/packages/app/src/api/releases.ts new file mode 100644 index 00000000000..45de7cb12dc --- /dev/null +++ b/packages/app/src/api/releases.ts @@ -0,0 +1,50 @@ +import type { Platform } from "@/context/platform" + +const REPO = "anomalyco/opencode" +const GITHUB_API_URL = `https://api.github.com/repos/${REPO}/releases` +const PER_PAGE = 30 +const CACHE_TTL = 1000 * 60 * 30 +const CACHE_KEY = "opencode.releases" + +type Release = { + tag: string + body: string + date: string +} + +function loadCache() { + const raw = localStorage.getItem(CACHE_KEY) + return raw ? JSON.parse(raw) : null +} + +function saveCache(data: { releases: Release[]; timestamp: number }) { + localStorage.setItem(CACHE_KEY, JSON.stringify(data)) +} + +export async function fetchReleases(platform: Platform): Promise<{ releases: Release[] }> { + const now = Date.now() + const cached = loadCache() + + if (cached && now - cached.timestamp < CACHE_TTL) { + return { releases: cached.releases } + } + + const fetcher = platform.fetch ?? fetch + const res = await fetcher(`${GITHUB_API_URL}?per_page=${PER_PAGE}`, { + headers: { Accept: "application/vnd.github.v3+json" }, + }).then((r) => (r.ok ? r.json() : Promise.reject(new Error("Failed to load")))) + + const releases = (Array.isArray(res) ? res : []).map((r) => ({ + tag: r.tag_name ?? "Unknown", + body: (r.body ?? "") + .replace(/#(\d+)/g, (_: string, id: string) => `[#${id}](https://github.com/anomalyco/opencode/pull/${id})`) + .replace(/@([a-zA-Z0-9_-]+)/g, (_: string, u: string) => `[@${u}](https://github.com/${u})`), + date: r.published_at ?? "", + })) + + saveCache({ releases, timestamp: now }) + + return { releases } +} + +export type { Release } diff --git a/packages/app/src/components/dialog-changelog.css b/packages/app/src/components/dialog-changelog.css new file mode 100644 index 00000000000..68fd9281345 --- /dev/null +++ b/packages/app/src/components/dialog-changelog.css @@ -0,0 +1,150 @@ +.dialog-changelog { + min-height: 500px; + display: flex; + flex-direction: column; +} + +.dialog-changelog [data-slot="dialog-body"] { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; + min-height: 0; +} + +.dialog-changelog-list { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; + min-height: 0; +} + +.dialog-changelog-list [data-slot="list-scroll"] { + flex: 1; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: var(--border-weak-base) transparent; +} + +.dialog-changelog-list [data-slot="list-scroll"]::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +.dialog-changelog-list [data-slot="list-scroll"]::-webkit-scrollbar-track { + background: transparent; + border-radius: 5px; +} + +.dialog-changelog-list [data-slot="list-scroll"]::-webkit-scrollbar-thumb { + background: var(--border-weak-base); + border-radius: 5px; + border: 3px solid transparent; + background-clip: padding-box; +} + +.dialog-changelog-list [data-slot="list-scroll"]::-webkit-scrollbar-thumb:hover { + background: var(--border-weak-base); +} + +.dialog-changelog-header { + padding: 8px 12px 8px 8px; + display: flex; + align-items: baseline; + gap: 8px; + position: sticky; + top: 0; + z-index: 10; + background: var(--surface-raised-stronger-non-alpha); +} + +.dialog-changelog-header::after { + content: ""; + position: absolute; + top: 100%; + left: 0; + right: 0; + height: 16px; + background: linear-gradient(to bottom, var(--surface-raised-stronger-non-alpha), transparent); + pointer-events: none; + opacity: 0; + transition: opacity 0.15s ease; +} + +.dialog-changelog-header[data-stuck="true"]::after { + opacity: 1; +} + + + +.dialog-changelog-version { + font-size: 20px; + font-weight: 600; +} + +.dialog-changelog-date { + font-size: 12px; + font-weight: 400; + color: var(--text-weak); +} + +.dialog-changelog-list [data-slot="list-item"] { + margin-bottom: 32px; + padding: 0; + border: none; + background: transparent; + cursor: default; + display: block; + text-align: left; +} + +.dialog-changelog-list [data-slot="list-item"]:hover { + background: transparent; +} + +.dialog-changelog-list [data-slot="list-item"]:focus { + outline: none; +} + +.dialog-changelog-list [data-slot="list-item"]:focus-visible { + outline: 2px solid var(--focus-base); + outline-offset: 2px; +} + +.dialog-changelog-content { + padding: 0 8px 24px; +} + +.dialog-changelog-markdown h2 { + border-bottom: 1px solid var(--border-weak-base); + padding-bottom: 4px; + margin: 32px 0 12px 0; + font-size: 14px; + font-weight: 500; + text-transform: capitalize; +} + +.dialog-changelog-markdown h2:first-child { + margin-top: 16px; +} + +.dialog-changelog-markdown a.external-link { + color: var(--text-interactive-base); + font-weight: 500; +} + +.dialog-changelog-markdown a.external-link[href^="https://github.com/anomalyco/opencode/pull/"], +.dialog-changelog-markdown a.external-link[href^="https://github.com/anomalyco/opencode/issues/"], +.dialog-changelog-markdown a.external-link[href^="https://github.com/"] +{ + border-radius: 3px; + padding: 0 2px; +} + +.dialog-changelog-markdown a.external-link[href^="https://github.com/anomalyco/opencode/pull/"]:hover, +.dialog-changelog-markdown a.external-link[href^="https://github.com/anomalyco/opencode/issues/"]:hover, +.dialog-changelog-markdown a.external-link[href^="https://github.com/"]:hover +{ + background: var(--surface-weak-base); +} diff --git a/packages/app/src/components/dialog-changelog.tsx b/packages/app/src/components/dialog-changelog.tsx new file mode 100644 index 00000000000..321a667bcca --- /dev/null +++ b/packages/app/src/components/dialog-changelog.tsx @@ -0,0 +1,40 @@ +import { createResource, Suspense, ErrorBoundary, Show } from "solid-js" +import { Dialog } from "@opencode-ai/ui/dialog" +import { useLanguage } from "@/context/language" +import { usePlatform } from "@/context/platform" +import { fetchReleases } from "@/api/releases" +import { ReleaseList } from "@/components/release-list" + +export function DialogChangelog() { + const language = useLanguage() + const platform = usePlatform() + const [data] = createResource(() => fetchReleases(platform)) + + return ( + +
+ ( +

+ {e instanceof Error ? e.message : "Failed to load changelog"} +

+ )} + > + {language.t("common.loading")}...

}> + 0} + fallback={

{language.t("common.noReleasesFound")}

} + > + {}} + /> +
+
+
+
+
+ ) +} diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index b530aff532f..53ade67d85a 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -459,4 +459,4 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil ) -} +} \ No newline at end of file diff --git a/packages/app/src/components/dialog-settings.tsx b/packages/app/src/components/dialog-settings.tsx index 83cea131f5d..6fd02101a51 100644 --- a/packages/app/src/components/dialog-settings.tsx +++ b/packages/app/src/components/dialog-settings.tsx @@ -2,16 +2,24 @@ import { Component } from "solid-js" import { Dialog } from "@opencode-ai/ui/dialog" import { Tabs } from "@opencode-ai/ui/tabs" import { Icon } from "@opencode-ai/ui/icon" +import { Button } from "@opencode-ai/ui/button" import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" +import { useDialog } from "@opencode-ai/ui/context/dialog" import { SettingsGeneral } from "./settings-general" import { SettingsKeybinds } from "./settings-keybinds" import { SettingsProviders } from "./settings-providers" import { SettingsModels } from "./settings-models" +import { DialogChangelog } from "@/components/dialog-changelog" export const DialogSettings: Component = () => { const language = useLanguage() const platform = usePlatform() + const dialog = useDialog() + + function handleShowChangelog() { + dialog.show(() => ) + } return ( @@ -52,6 +60,12 @@ export const DialogSettings: Component = () => {
{language.t("app.name.desktop")} v{platform.version} +
diff --git a/packages/app/src/components/release-list.tsx b/packages/app/src/components/release-list.tsx new file mode 100644 index 00000000000..7de9314dda9 --- /dev/null +++ b/packages/app/src/components/release-list.tsx @@ -0,0 +1,61 @@ +import { Component } from "solid-js" +import { List } from "@opencode-ai/ui/list" +import { Markdown } from "@opencode-ai/ui/markdown" +import { Button } from "@opencode-ai/ui/button" +import { Tag } from "@opencode-ai/ui/tag" +import { useLanguage } from "@/context/language" +import { getRelativeTime } from "@/utils/time" + +type Release = { + tag: string + body: string + date: string +} + +interface ReleaseListProps { + releases: Release[] + hasMore: boolean + loadingMore: boolean + onLoadMore: () => void +} + +export const ReleaseList: Component = (props) => { + const language = useLanguage() + + return ( + x.tag} + search={false} + emptyMessage="No releases found" + loadingMessage={language.t("common.loading")} + class="flex-1 min-h-0 overflow-hidden flex flex-col [&_[data-slot=list-scroll]]:session-scroller [&_[data-slot=list-item]]:block [&_[data-slot=list-item]]:p-0 [&_[data-slot=list-item]]:border-0 [&_[data-slot=list-item]]:bg-transparent [&_[data-slot=list-item]]:text-left [&_[data-slot=list-item]]:cursor-default [&_[data-slot=list-item]]:hover:bg-transparent [&_[data-slot=list-item]]:focus:outline-none" + add={{ + render: () => + props.hasMore ? ( +
+ +
+ ) : null, + }} + > + {(item) => ( +
+
+ {item.tag} + {item.date ? getRelativeTime(item.date, language.t) : ""} + {item.tag === props.releases[0]?.tag && {language.t("changelog.tag.latest")}} +
+
+ +
+
+ )} +
+ ) +} diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 16f2fbf4925..413fb54fd91 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -506,6 +506,10 @@ export const dict = { "common.close": "إغلاق", "common.edit": "تحرير", "common.loadMore": "تحميل المزيد", + "common.changelog": "التغييرات", + "common.noReleasesFound": "لم يتم العثور على إصدارات", + "changelog.tag.latest": "الأحدث", + "common.key.esc": "ESC", "sidebar.menu.toggle": "تبديل القائمة", "sidebar.nav.projectsAndSessions": "المشاريع والجلسات", @@ -748,4 +752,4 @@ export const dict = { "common.time.daysAgo.short": "قبل {{count}} ي", "settings.providers.connected.environmentDescription": "متصل من متغيرات البيئة الخاصة بك", "settings.providers.custom.description": "أضف مزود متوافق مع OpenAI بواسطة عنوان URL الأساسي.", -} +} \ No newline at end of file diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index 26cf433e0e3..c85555dbdc9 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -512,6 +512,9 @@ export const dict = { "common.close": "Fechar", "common.edit": "Editar", "common.loadMore": "Carregar mais", + "common.changelog": "Novidades", + "common.noReleasesFound": "Nenhuma release encontrada", + "changelog.tag.latest": "Mais recente", "common.key.esc": "ESC", "sidebar.menu.toggle": "Alternar menu", "sidebar.nav.projectsAndSessions": "Projetos e sessões", diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index 6c8198bd715..b38b5ca3bfa 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -572,6 +572,9 @@ export const dict = { "common.close": "Zatvori", "common.edit": "Uredi", "common.loadMore": "Učitaj još", + "common.changelog": "Novosti", + "common.noReleasesFound": "Nema pronađenih verzija", + "changelog.tag.latest": "Najnovije", "common.key.esc": "ESC", "sidebar.menu.toggle": "Prikaži/sakrij meni", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index 11da681760a..0e712c1e31d 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -568,6 +568,9 @@ export const dict = { "common.close": "Luk", "common.edit": "Rediger", "common.loadMore": "Indlæs flere", + "common.changelog": "Nyheder", + "common.noReleasesFound": "Ingen versioner fundet", + "changelog.tag.latest": "Seneste", "common.key.esc": "ESC", "sidebar.menu.toggle": "Skift menu", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index 51b9ec35315..bc785851075 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -520,6 +520,10 @@ export const dict = { "common.close": "Schließen", "common.edit": "Bearbeiten", "common.loadMore": "Mehr laden", + "common.changelog": "Neuerungen", + "common.noReleasesFound": "Keine Versionen gefunden", + "changelog.tag.latest": "Neueste", + "common.key.esc": "ESC", "sidebar.menu.toggle": "Menü umschalten", "sidebar.nav.projectsAndSessions": "Projekte und Sitzungen", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 7e95fd739df..897526f167f 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -589,12 +589,14 @@ export const dict = { "common.close": "Close", "common.edit": "Edit", "common.loadMore": "Load more", - "common.key.esc": "ESC", - + "common.changelog": "Changelog", + "common.noReleasesFound": "No releases found", "common.time.justNow": "Just now", "common.time.minutesAgo.short": "{{count}}m ago", "common.time.hoursAgo.short": "{{count}}h ago", "common.time.daysAgo.short": "{{count}}d ago", + "changelog.tag.latest": "Latest", + "common.key.esc": "ESC", "sidebar.menu.toggle": "Toggle menu", "sidebar.nav.projectsAndSessions": "Projects and sessions", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index 2665a808508..ff6b8864b25 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -575,6 +575,10 @@ export const dict = { "common.close": "Cerrar", "common.edit": "Editar", "common.loadMore": "Cargar más", + "common.changelog": "Novedades", + "common.noReleasesFound": "No se encontraron versiones", + "changelog.tag.latest": "Último", + "common.key.esc": "ESC", "sidebar.menu.toggle": "Alternar menú", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index 1e67db19333..24ef3753bab 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -516,6 +516,10 @@ export const dict = { "common.close": "Fermer", "common.edit": "Modifier", "common.loadMore": "Charger plus", + "common.changelog": "Nouveautés", + "common.noReleasesFound": "Aucune version trouvée", + "changelog.tag.latest": "Dernier", + "common.key.esc": "ESC", "sidebar.menu.toggle": "Basculer le menu", "sidebar.nav.projectsAndSessions": "Projets et sessions", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index ecd38d33249..45567b2129e 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -510,6 +510,10 @@ export const dict = { "common.close": "閉じる", "common.edit": "編集", "common.loadMore": "さらに読み込む", + "common.changelog": "更新履歴", + "common.noReleasesFound": "バージョンが見つかりません", + "changelog.tag.latest": "最新", + "common.key.esc": "ESC", "sidebar.menu.toggle": "メニューを切り替え", "sidebar.nav.projectsAndSessions": "プロジェクトとセッション", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index 8f54b8abdc7..66c77cbd8f8 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -511,6 +511,10 @@ export const dict = { "common.close": "닫기", "common.edit": "편집", "common.loadMore": "더 불러오기", + "common.changelog": "새로운 기능", + "common.noReleasesFound": "버전을 찾을 수 없음", + "changelog.tag.latest": "최신", + "common.key.esc": "ESC", "sidebar.menu.toggle": "메뉴 토글", "sidebar.nav.projectsAndSessions": "프로젝트 및 세션", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index 0c94046eb06..7fa10179c5e 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -575,6 +575,9 @@ export const dict = { "common.close": "Lukk", "common.edit": "Rediger", "common.loadMore": "Last flere", + "common.changelog": "Nyheter", + "common.noReleasesFound": "Ingen versjoner funnet", + "changelog.tag.latest": "Siste", "common.key.esc": "ESC", "sidebar.menu.toggle": "Veksle meny", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 59c0513be62..34981e92240 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -511,6 +511,9 @@ export const dict = { "common.close": "Zamknij", "common.edit": "Edytuj", "common.loadMore": "Załaduj więcej", + "common.changelog": "Nowości", + "common.noReleasesFound": "Nie znaleziono wersji", + "changelog.tag.latest": "Najnowszy", "common.key.esc": "ESC", "sidebar.menu.toggle": "Przełącz menu", "sidebar.nav.projectsAndSessions": "Projekty i sesje", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index 2071eaae7b9..d1c0f794ad4 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -573,6 +573,9 @@ export const dict = { "common.close": "Закрыть", "common.edit": "Редактировать", "common.loadMore": "Загрузить ещё", + "common.changelog": "Что нового", + "common.noReleasesFound": "Версии не найдены", + "changelog.tag.latest": "Последний", "common.key.esc": "ESC", "sidebar.menu.toggle": "Переключить меню", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index 9871555536f..74f3b948039 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -567,6 +567,9 @@ export const dict = { "common.close": "ปิด", "common.edit": "แก้ไข", "common.loadMore": "โหลดเพิ่มเติม", + "common.changelog": "อัปเดต", + "common.noReleasesFound": "ไม่พบเวอร์ชัน", + "changelog.tag.latest": "ล่าสุด", "common.key.esc": "ESC", "sidebar.menu.toggle": "สลับเมนู", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index e72d4c0e3be..473569146dd 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -566,6 +566,10 @@ export const dict = { "common.close": "关闭", "common.edit": "编辑", "common.loadMore": "加载更多", + "common.changelog": "更新日志", + "common.noReleasesFound": "未找到版本", + "changelog.tag.latest": "最新", + "common.key.esc": "ESC", "sidebar.menu.toggle": "切换菜单", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index 70421dfe103..0db7b0b38d6 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -563,6 +563,9 @@ export const dict = { "common.close": "關閉", "common.edit": "編輯", "common.loadMore": "載入更多", + "common.changelog": "更新日誌", + "common.noReleasesFound": "未找到版本", + "changelog.tag.latest": "最新", "common.key.esc": "ESC", "sidebar.menu.toggle": "切換選單", diff --git a/packages/app/src/utils/time.ts b/packages/app/src/utils/time.ts index d183e10807d..212c6c9161b 100644 --- a/packages/app/src/utils/time.ts +++ b/packages/app/src/utils/time.ts @@ -19,4 +19,4 @@ export function getRelativeTime(dateString: string, t: Translate): string { if (diffMinutes < 60) return t("common.time.minutesAgo.short", { count: diffMinutes }) if (diffHours < 24) return t("common.time.hoursAgo.short", { count: diffHours }) return t("common.time.daysAgo.short", { count: diffDays }) -} +} \ No newline at end of file