Skip to content

Commit 4a9b9b1

Browse files
Apply PR #15266: feat(app): changelog with PR links
2 parents 8694e47 + fe0f298 commit 4a9b9b1

23 files changed

Lines changed: 374 additions & 5 deletions

packages/app/src/api/releases.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { Platform } from "@/context/platform"
2+
3+
const REPO = "anomalyco/opencode"
4+
const GITHUB_API_URL = `https://api.github.com/repos/${REPO}/releases`
5+
const PER_PAGE = 30
6+
const CACHE_TTL = 1000 * 60 * 30
7+
const CACHE_KEY = "opencode.releases"
8+
9+
type Release = {
10+
tag: string
11+
body: string
12+
date: string
13+
}
14+
15+
function loadCache() {
16+
const raw = localStorage.getItem(CACHE_KEY)
17+
return raw ? JSON.parse(raw) : null
18+
}
19+
20+
function saveCache(data: { releases: Release[]; timestamp: number }) {
21+
localStorage.setItem(CACHE_KEY, JSON.stringify(data))
22+
}
23+
24+
export async function fetchReleases(platform: Platform): Promise<{ releases: Release[] }> {
25+
const now = Date.now()
26+
const cached = loadCache()
27+
28+
if (cached && now - cached.timestamp < CACHE_TTL) {
29+
return { releases: cached.releases }
30+
}
31+
32+
const fetcher = platform.fetch ?? fetch
33+
const res = await fetcher(`${GITHUB_API_URL}?per_page=${PER_PAGE}`, {
34+
headers: { Accept: "application/vnd.github.v3+json" },
35+
}).then((r) => (r.ok ? r.json() : Promise.reject(new Error("Failed to load"))))
36+
37+
const releases = (Array.isArray(res) ? res : []).map((r) => ({
38+
tag: r.tag_name ?? "Unknown",
39+
body: (r.body ?? "")
40+
.replace(/#(\d+)/g, (_: string, id: string) => `[#${id}](https://github.com/anomalyco/opencode/pull/${id})`)
41+
.replace(/@([a-zA-Z0-9_-]+)/g, (_: string, u: string) => `[@${u}](https://github.com/${u})`),
42+
date: r.published_at ?? "",
43+
}))
44+
45+
saveCache({ releases, timestamp: now })
46+
47+
return { releases }
48+
}
49+
50+
export type { Release }
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
.dialog-changelog {
2+
min-height: 500px;
3+
display: flex;
4+
flex-direction: column;
5+
}
6+
7+
.dialog-changelog [data-slot="dialog-body"] {
8+
flex: 1;
9+
overflow: hidden;
10+
display: flex;
11+
flex-direction: column;
12+
min-height: 0;
13+
}
14+
15+
.dialog-changelog-list {
16+
flex: 1;
17+
overflow: hidden;
18+
display: flex;
19+
flex-direction: column;
20+
min-height: 0;
21+
}
22+
23+
.dialog-changelog-list [data-slot="list-scroll"] {
24+
flex: 1;
25+
overflow-y: auto;
26+
scrollbar-width: thin;
27+
scrollbar-color: var(--border-weak-base) transparent;
28+
}
29+
30+
.dialog-changelog-list [data-slot="list-scroll"]::-webkit-scrollbar {
31+
width: 10px;
32+
height: 10px;
33+
}
34+
35+
.dialog-changelog-list [data-slot="list-scroll"]::-webkit-scrollbar-track {
36+
background: transparent;
37+
border-radius: 5px;
38+
}
39+
40+
.dialog-changelog-list [data-slot="list-scroll"]::-webkit-scrollbar-thumb {
41+
background: var(--border-weak-base);
42+
border-radius: 5px;
43+
border: 3px solid transparent;
44+
background-clip: padding-box;
45+
}
46+
47+
.dialog-changelog-list [data-slot="list-scroll"]::-webkit-scrollbar-thumb:hover {
48+
background: var(--border-weak-base);
49+
}
50+
51+
.dialog-changelog-header {
52+
padding: 8px 12px 8px 8px;
53+
display: flex;
54+
align-items: baseline;
55+
gap: 8px;
56+
position: sticky;
57+
top: 0;
58+
z-index: 10;
59+
background: var(--surface-raised-stronger-non-alpha);
60+
}
61+
62+
.dialog-changelog-header::after {
63+
content: "";
64+
position: absolute;
65+
top: 100%;
66+
left: 0;
67+
right: 0;
68+
height: 16px;
69+
background: linear-gradient(to bottom, var(--surface-raised-stronger-non-alpha), transparent);
70+
pointer-events: none;
71+
opacity: 0;
72+
transition: opacity 0.15s ease;
73+
}
74+
75+
.dialog-changelog-header[data-stuck="true"]::after {
76+
opacity: 1;
77+
}
78+
79+
80+
81+
.dialog-changelog-version {
82+
font-size: 20px;
83+
font-weight: 600;
84+
}
85+
86+
.dialog-changelog-date {
87+
font-size: 12px;
88+
font-weight: 400;
89+
color: var(--text-weak);
90+
}
91+
92+
.dialog-changelog-list [data-slot="list-item"] {
93+
margin-bottom: 32px;
94+
padding: 0;
95+
border: none;
96+
background: transparent;
97+
cursor: default;
98+
display: block;
99+
text-align: left;
100+
}
101+
102+
.dialog-changelog-list [data-slot="list-item"]:hover {
103+
background: transparent;
104+
}
105+
106+
.dialog-changelog-list [data-slot="list-item"]:focus {
107+
outline: none;
108+
}
109+
110+
.dialog-changelog-list [data-slot="list-item"]:focus-visible {
111+
outline: 2px solid var(--focus-base);
112+
outline-offset: 2px;
113+
}
114+
115+
.dialog-changelog-content {
116+
padding: 0 8px 24px;
117+
}
118+
119+
.dialog-changelog-markdown h2 {
120+
border-bottom: 1px solid var(--border-weak-base);
121+
padding-bottom: 4px;
122+
margin: 32px 0 12px 0;
123+
font-size: 14px;
124+
font-weight: 500;
125+
text-transform: capitalize;
126+
}
127+
128+
.dialog-changelog-markdown h2:first-child {
129+
margin-top: 16px;
130+
}
131+
132+
.dialog-changelog-markdown a.external-link {
133+
color: var(--text-interactive-base);
134+
font-weight: 500;
135+
}
136+
137+
.dialog-changelog-markdown a.external-link[href^="https://github.com/anomalyco/opencode/pull/"],
138+
.dialog-changelog-markdown a.external-link[href^="https://github.com/anomalyco/opencode/issues/"],
139+
.dialog-changelog-markdown a.external-link[href^="https://github.com/"]
140+
{
141+
border-radius: 3px;
142+
padding: 0 2px;
143+
}
144+
145+
.dialog-changelog-markdown a.external-link[href^="https://github.com/anomalyco/opencode/pull/"]:hover,
146+
.dialog-changelog-markdown a.external-link[href^="https://github.com/anomalyco/opencode/issues/"]:hover,
147+
.dialog-changelog-markdown a.external-link[href^="https://github.com/"]:hover
148+
{
149+
background: var(--surface-weak-base);
150+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { createResource, Suspense, ErrorBoundary, Show } from "solid-js"
2+
import { Dialog } from "@opencode-ai/ui/dialog"
3+
import { useLanguage } from "@/context/language"
4+
import { usePlatform } from "@/context/platform"
5+
import { fetchReleases } from "@/api/releases"
6+
import { ReleaseList } from "@/components/release-list"
7+
8+
export function DialogChangelog() {
9+
const language = useLanguage()
10+
const platform = usePlatform()
11+
const [data] = createResource(() => fetchReleases(platform))
12+
13+
return (
14+
<Dialog size="x-large" transition title="Changelog">
15+
<div class="flex-1 min-h-0 flex flex-col">
16+
<ErrorBoundary
17+
fallback={(e) => (
18+
<p class="text-text-weak p-6">
19+
{e instanceof Error ? e.message : "Failed to load changelog"}
20+
</p>
21+
)}
22+
>
23+
<Suspense fallback={<p class="text-text-weak p-6">{language.t("common.loading")}...</p>}>
24+
<Show
25+
when={(data()?.releases.length ?? 0) > 0}
26+
fallback={<p class="text-text-weak p-6">{language.t("common.noReleasesFound")}</p>}
27+
>
28+
<ReleaseList
29+
releases={data()!.releases}
30+
hasMore={false}
31+
loadingMore={false}
32+
onLoadMore={() => {}}
33+
/>
34+
</Show>
35+
</Suspense>
36+
</ErrorBoundary>
37+
</div>
38+
</Dialog>
39+
)
40+
}

packages/app/src/components/dialog-select-file.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,4 +459,4 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
459459
</List>
460460
</Dialog>
461461
)
462-
}
462+
}

packages/app/src/components/dialog-settings.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,25 @@ import { Component } from "solid-js"
22
import { Dialog } from "@opencode-ai/ui/dialog"
33
import { Tabs } from "@opencode-ai/ui/tabs"
44
import { Icon } from "@opencode-ai/ui/icon"
5+
import { Button } from "@opencode-ai/ui/button"
56
import { useLanguage } from "@/context/language"
67
import { usePlatform } from "@/context/platform"
8+
import { useDialog } from "@opencode-ai/ui/context/dialog"
79
import { SettingsGeneral } from "./settings-general"
810
import { SettingsKeybinds } from "./settings-keybinds"
911
import { SettingsProviders } from "./settings-providers"
1012
import { SettingsModels } from "./settings-models"
1113
import { SettingsArchive } from "./settings-archive"
14+
import { DialogChangelog } from "@/components/dialog-changelog"
1215

1316
export const DialogSettings: Component = () => {
1417
const language = useLanguage()
1518
const platform = usePlatform()
19+
const dialog = useDialog()
20+
21+
function handleShowChangelog() {
22+
dialog.show(() => <DialogChangelog />)
23+
}
1624

1725
return (
1826
<Dialog size="x-large" transition>
@@ -63,6 +71,12 @@ export const DialogSettings: Component = () => {
6371
<div class="flex flex-col gap-1 pl-1 py-1 text-12-medium text-text-weak">
6472
<span>{language.t("app.name.desktop")}</span>
6573
<span class="text-11-regular">v{platform.version}</span>
74+
<button
75+
class="text-11-regular text-text-weak hover:text-text-base self-start"
76+
onClick={handleShowChangelog}
77+
>
78+
Changelog
79+
</button>
6680
</div>
6781
</div>
6882
</Tabs.List>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { Component } from "solid-js"
2+
import { List } from "@opencode-ai/ui/list"
3+
import { Markdown } from "@opencode-ai/ui/markdown"
4+
import { Button } from "@opencode-ai/ui/button"
5+
import { Tag } from "@opencode-ai/ui/tag"
6+
import { useLanguage } from "@/context/language"
7+
import { getRelativeTime } from "@/utils/time"
8+
9+
type Release = {
10+
tag: string
11+
body: string
12+
date: string
13+
}
14+
15+
interface ReleaseListProps {
16+
releases: Release[]
17+
hasMore: boolean
18+
loadingMore: boolean
19+
onLoadMore: () => void
20+
}
21+
22+
export const ReleaseList: Component<ReleaseListProps> = (props) => {
23+
const language = useLanguage()
24+
25+
return (
26+
<List
27+
items={props.releases}
28+
key={(x) => x.tag}
29+
search={false}
30+
emptyMessage="No releases found"
31+
loadingMessage={language.t("common.loading")}
32+
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"
33+
add={{
34+
render: () =>
35+
props.hasMore ? (
36+
<div class="p-4 flex justify-center">
37+
<Button variant="secondary" size="small" onClick={props.onLoadMore} loading={props.loadingMore}>
38+
{language.t("common.loadMore")}
39+
</Button>
40+
</div>
41+
) : null,
42+
}}
43+
>
44+
{(item) => (
45+
<div class="mb-8">
46+
<div class="py-2 pr-3 pl-2 flex items-baseline gap-2 sticky top-0 z-10 bg-surface-raised-stronger-non-alpha">
47+
<span class="text-[20px] font-semibold">{item.tag}</span>
48+
<span class="text-xs text-text-weak">{item.date ? getRelativeTime(item.date, language.t) : ""}</span>
49+
{item.tag === props.releases[0]?.tag && <Tag>{language.t("changelog.tag.latest")}</Tag>}
50+
</div>
51+
<div class="px-2 pb-2">
52+
<Markdown
53+
text={item.body}
54+
class="prose prose-sm max-w-none text-text-base [&_h2]:border-b [&_h2]:border-border-weak-base [&_h2]:pb-1 [&_h2]:mt-8 [&_h2]:mb-3 [&_h2]:text-sm [&_h2]:font-medium [&_h2]:capitalize [&_h2:first-child]:mt-4 [&_a.external-link]:text-text-interactive-base [&_a.external-link]:font-medium"
55+
/>
56+
</div>
57+
</div>
58+
)}
59+
</List>
60+
)
61+
}

packages/app/src/i18n/ar.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,10 @@ export const dict = {
506506
"common.close": "إغلاق",
507507
"common.edit": "تحرير",
508508
"common.loadMore": "تحميل المزيد",
509+
"common.changelog": "التغييرات",
510+
"common.noReleasesFound": "لم يتم العثور على إصدارات",
511+
"changelog.tag.latest": "الأحدث",
512+
509513
"common.key.esc": "ESC",
510514
"sidebar.menu.toggle": "تبديل القائمة",
511515
"sidebar.nav.projectsAndSessions": "المشاريع والجلسات",
@@ -753,4 +757,4 @@ export const dict = {
753757
"common.time.daysAgo.short": "قبل {{count}} ي",
754758
"settings.providers.connected.environmentDescription": "متصل من متغيرات البيئة الخاصة بك",
755759
"settings.providers.custom.description": "أضف مزود متوافق مع OpenAI بواسطة عنوان URL الأساسي.",
756-
}
760+
}

packages/app/src/i18n/br.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,9 @@ export const dict = {
512512
"common.close": "Fechar",
513513
"common.edit": "Editar",
514514
"common.loadMore": "Carregar mais",
515+
"common.changelog": "Novidades",
516+
"common.noReleasesFound": "Nenhuma release encontrada",
517+
"changelog.tag.latest": "Mais recente",
515518
"common.key.esc": "ESC",
516519
"sidebar.menu.toggle": "Alternar menu",
517520
"sidebar.nav.projectsAndSessions": "Projetos e sessões",

packages/app/src/i18n/bs.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,9 @@ export const dict = {
572572
"common.close": "Zatvori",
573573
"common.edit": "Uredi",
574574
"common.loadMore": "Učitaj još",
575+
"common.changelog": "Novosti",
576+
"common.noReleasesFound": "Nema pronađenih verzija",
577+
"changelog.tag.latest": "Najnovije",
575578
"common.key.esc": "ESC",
576579

577580
"sidebar.menu.toggle": "Prikaži/sakrij meni",

packages/app/src/i18n/da.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,9 @@ export const dict = {
568568
"common.close": "Luk",
569569
"common.edit": "Rediger",
570570
"common.loadMore": "Indlæs flere",
571+
"common.changelog": "Nyheder",
572+
"common.noReleasesFound": "Ingen versioner fundet",
573+
"changelog.tag.latest": "Seneste",
571574

572575
"common.key.esc": "ESC",
573576
"sidebar.menu.toggle": "Skift menu",

0 commit comments

Comments
 (0)