chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,50 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { ChangelogActions, ChangelogTimeline } from '@/app/(landing)/changelog/components'
import type { ChangelogEntry, GitHubRelease } from '@/app/(landing)/changelog/types'
import { mapReleases, releasesEndpoint } from '@/app/(landing)/changelog/utils'
import { ProseHero, ProseShell } from '@/app/(landing)/components/prose-page'
const logger = createLogger('Changelog')
/**
* Changelog page - reuses the shared prose primitives ({@link ProseShell} +
* {@link ProseHero}) so its headline and column match Terms and Privacy, then
* renders the GitHub-release timeline. The first page of releases is
* fetched here on the server at build/revalidate time; the {@link ChangelogTimeline}
* client leaf paginates the rest. Re-authored from the prior dark changelog onto
* the platform light tokens.
*/
const LEAD =
'Every new feature, improvement, and fix in Sim, the open-source AI workspace, with release notes straight from GitHub.'
async function getInitialEntries(): Promise<ChangelogEntry[]> {
try {
// boundary-raw-fetch: external GitHub Releases API (cross-origin), not a same-origin contract
const res = await fetch(releasesEndpoint(1), {
headers: { Accept: 'application/vnd.github+json' },
next: { revalidate: 3600 },
})
const releases = (await res.json()) as GitHubRelease[]
return mapReleases(releases ?? [])
} catch (error) {
logger.warn('Failed to load initial changelog releases from GitHub', {
error: getErrorMessage(error),
})
return []
}
}
export default async function Changelog() {
const entries = await getInitialEntries()
return (
<ProseShell>
<ProseHero title='Changelog' lead={LEAD} actions={<ChangelogActions />} />
<section id='releases' aria-label='Release history'>
<ChangelogTimeline initialEntries={entries} />
</section>
</ProseShell>
)
}
@@ -0,0 +1,38 @@
'use client'
import { ChipLink } from '@sim/emcn'
import { BookOpen, Github, Rss } from 'lucide-react'
/**
* Changelog hero actions - the GitHub / Documentation / RSS pill links shown
* beneath the changelog headline. A small client leaf because `ChipLink` is a
* Client Component and its `leftIcon` is a component reference that cannot cross
* the server→client boundary as a prop (same pattern as the platform pill CTA).
* GitHub is the primary filled chip; Docs and RSS are the default pills.
*/
export function ChangelogActions() {
return (
<div className='flex flex-wrap items-center gap-1'>
<ChipLink
variant='primary'
href='https://github.com/simstudioai/sim/releases'
target='_blank'
rel='noopener noreferrer'
leftIcon={Github}
>
View on GitHub
</ChipLink>
<ChipLink
href='https://docs.sim.ai'
target='_blank'
rel='noopener noreferrer'
leftIcon={BookOpen}
>
Documentation
</ChipLink>
<ChipLink href='/changelog.xml' leftIcon={Rss}>
RSS Feed
</ChipLink>
</div>
)
}
@@ -0,0 +1 @@
export { ChangelogActions } from './changelog-actions'
@@ -0,0 +1,220 @@
'use client'
import { type ReactNode, useRef, useState } from 'react'
import { Streamdown } from 'streamdown'
import 'streamdown/styles.css'
import { Avatar, AvatarFallback, AvatarImage, Chip, cn } from '@sim/emcn'
import { formatDate } from '@sim/utils/formatting'
import type { ChangelogEntry, GitHubRelease } from '@/app/(landing)/changelog/types'
import { mapReleases, releasesEndpoint } from '@/app/(landing)/changelog/utils'
/**
* The changelog timeline - the single client leaf of the changelog page. Renders
* each GitHub release as a `<section>` (an `<h2>` version tag + contributor
* avatars + cleaned markdown via {@link Streamdown}) and paginates further pages
* from the GitHub Releases API on demand. Re-authored from the prior dark
* timeline onto the platform light tokens; the fetch, markdown cleaning, and
* load-more behavior are preserved.
*/
interface ChangelogTimelineProps {
initialEntries: ChangelogEntry[]
}
function stripContributors(body: string): string {
let output = body
output = output.replace(
/(^|\n)#{1,6}\s*Contributors\s*\n[\s\S]*?(?=\n\s*\n|\n#{1,6}\s|$)/gi,
'\n'
)
output = output.replace(
/(^|\n)\s*(?:\*\*|__)?\s*Contributors\s*(?:\*\*|__)?\s*:?\s*\n[\s\S]*?(?=\n\s*\n|\n#{1,6}\s|$)/gi,
'\n'
)
output = output.replace(
/(^|\n)[-*+]\s*(?:@[A-Za-z0-9-]+(?:\s*,\s*|\s+))+@[A-Za-z0-9-]+\s*(?=\n)/g,
'\n'
)
output = output.replace(
/(^|\n)\s*(?:@[A-Za-z0-9-]+(?:\s*,\s*|\s+))+@[A-Za-z0-9-]+\s*(?=\n)/g,
'\n'
)
return output
}
function stripPrReferences(body: string): string {
return body.replace(/\s*\(\s*\[#\d+\]\([^)]*\)\s*\)/g, '').replace(/\s*\(\s*#\d+\s*\)/g, '')
}
function cleanMarkdown(body: string): string {
return stripPrReferences(stripContributors(body))
}
function isContributorsLabel(children: ReactNode): boolean {
return /^\s*contributors\s*:?\s*$/i.test(String(children))
}
export function ChangelogTimeline({ initialEntries }: ChangelogTimelineProps) {
const [entries, setEntries] = useState<ChangelogEntry[]>(initialEntries)
const [loading, setLoading] = useState<boolean>(false)
const [done, setDone] = useState<boolean>(false)
const pageRef = useRef(1)
const loadMore = async () => {
if (loading || done) return
setLoading(true)
try {
const nextPage = pageRef.current + 1
// boundary-raw-fetch: external GitHub Releases API (cross-origin), not a same-origin contract
const res = await fetch(releasesEndpoint(nextPage), {
headers: { Accept: 'application/vnd.github+json' },
})
const releases = (await res.json()) as GitHubRelease[]
const mapped = mapReleases(releases ?? [])
if (mapped.length === 0) {
setDone(true)
} else {
setEntries((prev) => [...prev, ...mapped])
pageRef.current = nextPage
}
} catch {
setDone(true)
} finally {
setLoading(false)
}
}
return (
<div className='flex flex-col gap-7'>
{entries.map((entry) => {
const headingId = `release-${entry.tag}-heading`
return (
<section key={entry.tag} aria-labelledby={headingId} className='flex flex-col'>
<div className='flex items-center justify-between gap-4'>
<div className='flex items-center gap-2'>
<h2 id={headingId} className='text-[18px] text-[var(--text-primary)] leading-[1.3]'>
{entry.tag}
</h2>
{entry.contributors.length > 0 ? (
<div className='flex'>
{entry.contributors.slice(0, 5).map((contributor, index) => (
<a
key={contributor}
href={`https://github.com/${contributor}`}
target='_blank'
rel='noopener noreferrer'
aria-label={`View @${contributor} on GitHub`}
title={`@${contributor}`}
className={index === 0 ? 'block' : '-ms-2 block'}
>
<Avatar className='size-6 ring-2 ring-[var(--bg)]'>
<AvatarImage
src={`https://avatars.githubusercontent.com/${contributor}`}
alt={`@${contributor}`}
className='hover:z-10'
/>
<AvatarFallback>{contributor.slice(0, 2).toUpperCase()}</AvatarFallback>
</Avatar>
</a>
))}
{entry.contributors.length > 5 ? (
<div className='-ms-2 relative flex size-6 items-center justify-center rounded-full bg-[var(--surface-2)] text-[10px] text-[var(--text-body)] ring-2 ring-[var(--bg)] hover:z-10'>
+{entry.contributors.length - 5}
</div>
) : null}
</div>
) : null}
</div>
<span className='text-[12px] text-[var(--text-muted)]'>
{formatDate(new Date(entry.date))}
</span>
</div>
<div aria-hidden='true' className='mt-[9px] mb-3 h-px bg-[var(--border)]' />
<div className='max-w-none'>
<Streamdown
mode='static'
components={{
h2: ({ children, ...props }) =>
isContributorsLabel(children) ? null : (
<h3
className='mt-5 mb-2 text-[14px] text-[var(--text-primary)] leading-[1.4]'
{...props}
>
{children}
</h3>
),
h3: ({ children, ...props }) =>
isContributorsLabel(children) ? null : (
<h4
className='mt-4 mb-1 text-[14px] text-[var(--text-primary)] leading-[1.4]'
{...props}
>
{children}
</h4>
),
ul: ({ children, ...props }) => (
<ul className='mt-2 mb-3 space-y-1.5' {...props}>
{children}
</ul>
),
li: ({ children, ...props }) =>
isContributorsLabel(children) ? null : (
<li
className='text-[13px] text-[var(--text-body)] leading-relaxed'
{...props}
>
{children}
</li>
),
p: ({ children, ...props }) =>
isContributorsLabel(children) ? null : (
<p
className='mb-3 text-[13px] text-[var(--text-body)] leading-relaxed'
{...props}
>
{children}
</p>
),
strong: ({ children, ...props }) => (
<strong className='text-[var(--text-primary)]' {...props}>
{children}
</strong>
),
inlineCode: ({ children }) => (
<code className='whitespace-normal rounded bg-[var(--surface-2)] px-1 py-0.5 font-mono text-[var(--text-primary)] not-italic'>
{children}
</code>
),
img: () => null,
a: ({ children, className, ...props }) => (
<a
{...props}
className={cn('text-[var(--text-primary)] underline', className)}
target='_blank'
rel='noopener noreferrer'
>
{children}
</a>
),
}}
>
{cleanMarkdown(entry.content)}
</Streamdown>
</div>
</section>
)
})}
{!done ? (
<div>
<Chip type='button' flush onClick={loadMore} disabled={loading}>
{loading ? 'Loading…' : 'Show more'}
</Chip>
</div>
) : null}
</div>
)
}
@@ -0,0 +1 @@
export { ChangelogTimeline } from './changelog-timeline'
@@ -0,0 +1,2 @@
export { ChangelogActions } from './changelog-actions'
export { ChangelogTimeline } from './changelog-timeline'
+18
View File
@@ -0,0 +1,18 @@
import { buildLandingMetadata } from '@/lib/landing/seo'
import Changelog from '@/app/(landing)/changelog/changelog'
export const revalidate = 3600
const TITLE = 'Changelog | Sim, the AI Workspace'
const DESCRIPTION =
'Every new feature, improvement, and fix in Sim, the open-source AI workspace, with release notes straight from GitHub.'
export const metadata = buildLandingMetadata({
title: TITLE,
description: DESCRIPTION,
path: '/changelog',
})
export default function Page() {
return <Changelog />
}
+26
View File
@@ -0,0 +1,26 @@
/**
* Changelog types shared between the server page (initial fetch) and the client
* timeline (load-more). {@link GitHubRelease} is the minimal shape we read from
* the GitHub Releases API; {@link ChangelogEntry} is the normalized entry the UI
* renders.
*/
/** The minimal subset of a GitHub Releases API item that the changelog reads. */
export interface GitHubRelease {
tag_name: string
name: string | null
body: string | null
published_at: string
html_url: string
prerelease: boolean
}
/** A normalized changelog entry rendered in the timeline. */
export interface ChangelogEntry {
tag: string
title: string
content: string
date: string
url: string
contributors: string[]
}
+46
View File
@@ -0,0 +1,46 @@
import type { ChangelogEntry, GitHubRelease } from '@/app/(landing)/changelog/types'
/**
* Changelog helpers shared by the server page (initial page) and the client
* timeline (subsequent pages), so the GitHub-release → entry mapping is defined
* once and both surfaces stay in sync.
*/
/** How many releases to request per GitHub API page. */
export const RELEASES_PER_PAGE = 10
/** Builds the GitHub Releases endpoint for a given 1-based page. */
export function releasesEndpoint(page: number): string {
return `https://api.github.com/repos/simstudioai/sim/releases?per_page=${RELEASES_PER_PAGE}&page=${page}`
}
/** Removes literal `&nbsp` artifacts from release bodies. */
export function sanitizeContent(body: string): string {
return body.replace(/&nbsp/g, '')
}
/** Extracts unique `@handle` GitHub mentions from a release body. */
export function extractMentions(body: string): string[] {
const matches = body.match(/@([A-Za-z0-9-]+)/g) ?? []
return Array.from(new Set(matches.map((mention) => mention.slice(1))))
}
/** Maps non-prerelease GitHub releases to normalized {@link ChangelogEntry} items. */
export function mapReleases(releases: GitHubRelease[]): ChangelogEntry[] {
return releases.reduce<ChangelogEntry[]>((acc, release) => {
if (release.prerelease) {
return acc
}
const body = String(release.body ?? '')
acc.push({
tag: release.tag_name,
title: release.name || release.tag_name,
content: sanitizeContent(body),
date: release.published_at,
url: release.html_url,
contributors: extractMentions(body),
})
return acc
}, [])
}