Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
93b9e47
changelog v1
alexyaroshuk Feb 4, 2026
62fa5c1
fix styles
alexyaroshuk Feb 4, 2026
991e823
style fixes
alexyaroshuk Feb 4, 2026
1abc228
add timetsamps
alexyaroshuk Feb 4, 2026
4503bde
fix styling, add scrollbar
alexyaroshuk Feb 4, 2026
72eec20
add links
alexyaroshuk Feb 4, 2026
d364c43
style fixes
alexyaroshuk Feb 4, 2026
1587d93
fixes
alexyaroshuk Feb 4, 2026
954d319
fix ui
alexyaroshuk Feb 4, 2026
d8bcfd9
trnslations, couple more fixes
alexyaroshuk Feb 4, 2026
ba5121c
Merge remote-tracking branch 'upstream/dev' into feat/changelog
alexyaroshuk Feb 10, 2026
e514919
changelog refactor
alexyaroshuk Feb 11, 2026
70b5554
refactor, add caching
alexyaroshuk Feb 13, 2026
2090521
Merge branch 'clean-dev' into feat/changelog
alexyaroshuk Feb 26, 2026
b9ca79f
refactor: use createResource + Suspense instead of manual signals, re…
alexyaroshuk Feb 28, 2026
9ea36cc
sync time.ts specific code to fix typecheck
alexyaroshuk Feb 28, 2026
276d60e
fix ar.ts, sync dialog-select-file.tsx to fix typecheck
alexyaroshuk Feb 28, 2026
29d9005
Merge branch 'dev' into feat/changelog
alexyaroshuk Feb 28, 2026
fe0f298
fix i18n
alexyaroshuk Feb 28, 2026
bb2e9ff
Merge branch 'dev' into feat/changelog
alexyaroshuk Mar 3, 2026
1bbd3a7
fix: wrap DialogChangelog with DataProvider
alexyaroshuk Mar 3, 2026
92aab78
Merge branch 'dev' into feat/changelog
alexyaroshuk Mar 3, 2026
f697cce
Merge branch 'dev' into feat/changelog
alexyaroshuk Mar 7, 2026
c7e78c5
Merge branch 'dev' into feat/changelog
alexyaroshuk Mar 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions packages/app/src/api/releases.ts
Original file line number Diff line number Diff line change
@@ -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 }
150 changes: 150 additions & 0 deletions packages/app/src/components/dialog-changelog.css
Original file line number Diff line number Diff line change
@@ -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);
}
43 changes: 43 additions & 0 deletions packages/app/src/components/dialog-changelog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { createResource, Suspense, ErrorBoundary, Show } from "solid-js"
import { Dialog } from "@opencode-ai/ui/dialog"
import { DataProvider } from "@opencode-ai/ui/context"
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 (
<Dialog size="x-large" transition title="Changelog">
<DataProvider data={{ session: [], session_status: {}, session_diff: {}, message: {}, part: {} }} directory="">
<div class="flex-1 min-h-0 flex flex-col">
<ErrorBoundary
fallback={(e) => (
<p class="text-text-weak p-6">
{e instanceof Error ? e.message : "Failed to load changelog"}
</p>
)}
>
<Suspense fallback={<p class="text-text-weak p-6">{language.t("common.loading")}...</p>}>
<Show
when={(data()?.releases.length ?? 0) > 0}
fallback={<p class="text-text-weak p-6">{language.t("common.noReleasesFound")}</p>}
>
<ReleaseList
releases={data()!.releases}
hasMore={false}
loadingMore={false}
onLoadMore={() => {}}
/>
</Show>
</Suspense>
</ErrorBoundary>
</div>
</DataProvider>
</Dialog>
)
}
2 changes: 1 addition & 1 deletion packages/app/src/components/dialog-select-file.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -459,4 +459,4 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
</List>
</Dialog>
)
}
}
14 changes: 14 additions & 0 deletions packages/app/src/components/dialog-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => <DialogChangelog />)
}

return (
<Dialog size="x-large" transition>
Expand Down Expand Up @@ -52,6 +60,12 @@ export const DialogSettings: Component = () => {
<div class="flex flex-col gap-1 pl-1 py-1 text-12-medium text-text-weak">
<span>{language.t("app.name.desktop")}</span>
<span class="text-11-regular">v{platform.version}</span>
<button
class="text-11-regular text-text-weak hover:text-text-base self-start"
onClick={handleShowChangelog}
>
Changelog
</button>
</div>
</div>
</Tabs.List>
Expand Down
61 changes: 61 additions & 0 deletions packages/app/src/components/release-list.tsx
Original file line number Diff line number Diff line change
@@ -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<ReleaseListProps> = (props) => {
const language = useLanguage()

return (
<List
items={props.releases}
key={(x) => 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 ? (
<div class="p-4 flex justify-center">
<Button variant="secondary" size="small" onClick={props.onLoadMore} loading={props.loadingMore}>
{language.t("common.loadMore")}
</Button>
</div>
) : null,
}}
>
{(item) => (
<div class="mb-8">
<div class="py-2 pr-3 pl-2 flex items-baseline gap-2 sticky top-0 z-10 bg-surface-raised-stronger-non-alpha">
<span class="text-[20px] font-semibold">{item.tag}</span>
<span class="text-xs text-text-weak">{item.date ? getRelativeTime(item.date, language.t) : ""}</span>
{item.tag === props.releases[0]?.tag && <Tag>{language.t("changelog.tag.latest")}</Tag>}
</div>
<div class="px-2 pb-2">
<Markdown
text={item.body}
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"
/>
</div>
</div>
)}
</List>
)
}
6 changes: 5 additions & 1 deletion packages/app/src/i18n/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,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": "المشاريع والجلسات",
Expand Down Expand Up @@ -749,4 +753,4 @@ export const dict = {
"common.time.daysAgo.short": "قبل {{count}} ي",
"settings.providers.connected.environmentDescription": "متصل من متغيرات البيئة الخاصة بك",
"settings.providers.custom.description": "أضف مزود متوافق مع OpenAI بواسطة عنوان URL الأساسي.",
}
}
3 changes: 3 additions & 0 deletions packages/app/src/i18n/br.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,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",
Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/i18n/bs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,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",
Expand Down
Loading
Loading