opencode.json
+ {(() => {
+ const value = language.t("dialog.plugins.empty")
+ const file = "opencode.json"
+ const parts = value.split(file)
+ if (parts.length === 1) return value
+ return (
+ <>
+ {parts[0]}
+ {file}
+ {parts.slice(1).join(file)}
+ >
+ )
+ })()}
([^<]+)<\/p>/)
+ const imgMatch = content.match(/ ([^<]+)<\/p>/)
+ const imgMatch = content.match(/ {item.title} {item.description} ({ source, items }))
+}
+
+function parseMarkdown(body: string) {
+ const lines = body.split("\n")
+ const sections: { title: string; items: string[] }[] = []
+ let current: { title: string; items: string[] } | null = null
+ let skip = false
+
+ for (const line of lines) {
+ if (line.startsWith("## ")) {
+ if (current) sections.push(current)
+ const title = line.slice(3).trim()
+ current = { title, items: [] }
+ skip = false
+ } else if (line.startsWith("**Thank you")) {
+ skip = true
+ } else if (line.startsWith("- ") && !skip) {
+ current?.items.push(line.slice(2).trim())
+ }
+ }
+ if (current) sections.push(current)
+
+ const highlights = parseHighlights(body)
+
+ return { sections, highlights }
+}
+
+export async function GET() {
+ const response = await fetch("https://api.github.com/repos/anomalyco/opencode/releases?per_page=20", {
+ headers: {
+ Accept: "application/vnd.github.v3+json",
+ "User-Agent": "OpenCode-Console",
+ },
+ })
+
+ if (!response.ok) {
+ return { releases: [] }
+ }
+
+ const releases = (await response.json()) as Release[]
+
+ return {
+ releases: releases.map((release) => {
+ const parsed = parseMarkdown(release.body || "")
+ return {
+ tag: release.tag_name,
+ name: release.name,
+ date: release.published_at,
+ url: release.html_url,
+ highlights: parsed.highlights,
+ sections: parsed.sections,
+ }
+ }),
+ }
+}
diff --git a/packages/console/app/src/routes/changelog/index.css b/packages/console/app/src/routes/changelog/index.css
index a445c74474..233d85cc0e 100644
--- a/packages/console/app/src/routes/changelog/index.css
+++ b/packages/console/app/src/routes/changelog/index.css
@@ -367,11 +367,18 @@
display: flex;
flex-direction: column;
gap: 4px;
+ position: sticky;
+ top: 80px;
+ align-self: start;
+ background: var(--color-background);
+ padding: 8px 0;
@media (max-width: 50rem) {
+ position: static;
flex-direction: row;
align-items: center;
gap: 12px;
+ padding: 0;
}
[data-slot="version"] {
@@ -402,24 +409,26 @@
[data-component="section"] {
h3 {
- font-size: 14px;
+ font-size: 13px;
font-weight: 600;
color: var(--color-text-strong);
- margin-bottom: 8px;
+ margin-bottom: 6px;
}
ul {
list-style: none;
padding: 0;
margin: 0;
+ padding-left: 16px;
display: flex;
flex-direction: column;
- gap: 6px;
+ gap: 4px;
li {
color: var(--color-text);
+ font-size: 13px;
line-height: 1.5;
- padding-left: 16px;
+ padding-left: 12px;
position: relative;
&::before {
@@ -431,7 +440,7 @@
[data-slot="author"] {
color: var(--color-text-weak);
- font-size: 13px;
+ font-size: 12px;
margin-left: 4px;
text-decoration: none;
@@ -465,6 +474,120 @@
}
}
}
+
+ [data-component="highlights"] {
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+ margin-bottom: 1.5rem;
+ }
+
+ [data-component="collapsible-sections"] {
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+ }
+
+ [data-component="collapsible-section"] {
+ [data-slot="toggle"] {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ background: none;
+ border: none;
+ padding: 6px 0;
+ cursor: pointer;
+ font-family: inherit;
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--color-text-weak);
+
+ &:hover {
+ color: var(--color-text);
+ }
+
+ [data-slot="icon"] {
+ font-size: 10px;
+ }
+ }
+
+ ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ padding-left: 16px;
+ padding-bottom: 8px;
+
+ li {
+ color: var(--color-text);
+ font-size: 13px;
+ line-height: 1.5;
+ padding-left: 12px;
+ position: relative;
+
+ &::before {
+ content: "-";
+ position: absolute;
+ left: 0;
+ color: var(--color-text-weak);
+ }
+
+ [data-slot="author"] {
+ color: var(--color-text-weak);
+ font-size: 12px;
+ margin-left: 4px;
+ text-decoration: none;
+
+ &:hover {
+ text-decoration: underline;
+ text-underline-offset: 2px;
+ text-decoration-thickness: 1px;
+ }
+ }
+ }
+ }
+ }
+
+ [data-component="highlight"] {
+ h4 {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--color-text-strong);
+ margin-bottom: 8px;
+ }
+
+ hr {
+ border: none;
+ border-top: 1px solid var(--color-border-weak);
+ margin-bottom: 16px;
+ }
+
+ [data-slot="highlight-item"] {
+ margin-bottom: 24px;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ p[data-slot="title"] {
+ font-weight: 600;
+ font-size: 16px;
+ margin-bottom: 4px;
+ }
+
+ p {
+ font-size: 14px;
+ margin-bottom: 12px;
+ }
+ }
+
+ img,
+ video {
+ max-width: 100%;
+ height: auto;
+ border-radius: 4px;
+ }
+ }
}
a {
diff --git a/packages/console/app/src/routes/changelog/index.tsx b/packages/console/app/src/routes/changelog/index.tsx
index c1b931fe3e..87e021ec88 100644
--- a/packages/console/app/src/routes/changelog/index.tsx
+++ b/packages/console/app/src/routes/changelog/index.tsx
@@ -5,7 +5,7 @@ import { Header } from "~/component/header"
import { Footer } from "~/component/footer"
import { Legal } from "~/component/legal"
import { config } from "~/config"
-import { For, Show } from "solid-js"
+import { For, Show, createSignal } from "solid-js"
type Release = {
tag_name: string
@@ -40,6 +40,59 @@ function formatDate(dateString: string) {
})
}
+type HighlightMedia = { type: "video"; src: string } | { type: "image"; src: string; width: string; height: string }
+
+type HighlightItem = {
+ title: string
+ description: string
+ shortDescription?: string
+ media: HighlightMedia
+}
+
+type HighlightGroup = {
+ source: string
+ items: HighlightItem[]
+}
+
+function parseHighlights(body: string): HighlightGroup[] {
+ const groups = new Map
([^<]+)<\/h2>/)
+ const pMatch = content.match(/
({ source, items }))
+}
+
function parseMarkdown(body: string) {
const lines = body.split("\n")
const sections: { title: string; items: string[] }[] = []
@@ -60,7 +113,9 @@ function parseMarkdown(body: string) {
}
if (current) sections.push(current)
- return { sections }
+ const highlights = parseHighlights(body)
+
+ return { sections, highlights }
}
function ReleaseItem(props: { item: string }) {
@@ -87,6 +142,60 @@ function ReleaseItem(props: { item: string }) {
)
}
+function HighlightSection(props: { group: HighlightGroup }) {
+ return (
+
{props.group.source}
+
+
+
+
+ {section.title}
-
-
- {section.title}
+
+
+