Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,16 @@ Write each change in both `### English` and `### 中文` under `## Unreleased`.

### English

- Show a compact version history on the System page: one release open at a time, with notes and restore for the three previous versions
- Use plain update copy on the System page: download the update, tap Update now, then refresh after a short countdown
- Explain Models-page defaults: Trae Max is a larger context window, and reasoning intensity is a fallback when the request omits it

### 中文

- 系统页用折叠列表展示近期版本:一次只展开一条,可看说明,并可恢复到最近三个旧版本
- 系统页更新文案改为普通说法:先下载更新,再点立即更新,完成后倒计时刷新页面
- 模型页补充说明:Trae 的 Max 是更大上下文,推理强度只是请求未指定时的默认档,不会锁死每次调用

## 0.3.1 - 2026-09-07

### English
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/api/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ export type SystemUpdateInfo = {
rollback_versions?: Array<{
tag_name: string
name?: string
body?: string
published_at?: string
html_url?: string
}>
recent_releases?: Array<{
tag_name: string
name?: string
body?: string
published_at?: string
html_url?: string
}>
Expand Down
22 changes: 15 additions & 7 deletions frontend/src/components/ModelDetailsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,27 @@ export function ModelDetailsModal({ model, t, onClose }: Props) {
<section>
<div className="text-xs font-medium text-muted">{t('contextWindowCol')}</div>
<div className="mt-1 text-sm">{formatTokens(windowDev)}{windowMax && windowMax !== windowDev ? ` → ${formatTokens(windowMax)}` : ''}</div>
{model.supports_max_mode ? <p className="mt-1 text-[11px] leading-5 text-muted">{t('maxModeHint')}</p> : null}
{model.prompt_max_tokens ? <div className="mt-1 text-[11px] text-muted">{t('promptMaxTokens')}: {formatTokens(model.prompt_max_tokens)}</div> : null}
{model.max_output_tokens ? <div className="text-[11px] text-muted">{t('maxOutputTokens')}: {formatTokens(model.max_output_tokens)}</div> : null}
</section>
<section>
<div className="text-xs font-medium text-muted">{t('reasoningLevels')}</div>
{options.length ? (
<div className="mt-2 flex flex-wrap gap-1.5">
{options.map((level) => (
<Chip key={level} size="sm" variant="soft" color={level === (model.reasoning_effort || model.reasoning_default) ? 'success' : 'default'}>
{level}{level === model.reasoning_default ? ` · ${t('defaultValue')}` : ''}{level === model.reasoning_effort && level !== model.reasoning_default ? ` · ${t('custom')}` : ''}
</Chip>
))}
</div>
<>
<p className="mt-1 text-[11px] leading-5 text-muted">{t('reasoningHint')}</p>
<div className="mt-2 flex flex-wrap gap-1.5">
{options.map((level) => {
const key = `reasoningLevel_${level}`
const label = t(key)
return (
<Chip key={level} size="sm" variant="soft" color={level === (model.reasoning_effort || model.reasoning_default) ? 'success' : 'default'}>
{label === key ? level : label}{level === model.reasoning_default ? ` · ${t('defaultValue')}` : ''}{level === model.reasoning_effort && level !== model.reasoning_default ? ` · ${t('custom')}` : ''}
</Chip>
)
})}
</div>
</>
) : (
<div className="mt-1 text-sm text-muted">
{model.reasoning_type ? t('reasoningFixed', { type: model.reasoning_type }) : t('noReasoningLevels')}
Expand Down
87 changes: 87 additions & 0 deletions frontend/src/components/VersionHistory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Accordion, Button, Chip } from '@heroui/react'
import { ReleaseNotes } from '@/components/ReleaseNotes'
import { extractReleaseNotes } from '@/lib/releaseNotes'
import { useI18n } from '@/hooks/useI18n'
import type { Lang } from '@/i18n/messages'
import type { VersionHistoryRelease } from '@/lib/versionHistory'

function formatReleaseDate(value: string | undefined, lang: Lang) {
if (!value) return ''
const date = new Date(value)
if (Number.isNaN(date.getTime())) return ''
return date.toLocaleDateString(lang === 'zh' ? 'zh-CN' : 'en-US', { year: 'numeric', month: 'short', day: 'numeric' })
}

export function VersionHistory({
releases,
currentVersion,
nextVersion,
rollbackTags,
canRollback,
submitting,
onRestore,
}: {
releases: VersionHistoryRelease[]
currentVersion: string
nextVersion?: string
rollbackTags: string[]
canRollback: boolean
submitting: boolean
onRestore: (version: string) => void
}) {
const { t, lang } = useI18n()
if (!releases.length) return null

const featured = nextVersion && nextVersion !== currentVersion ? nextVersion : currentVersion
const defaultExpanded = releases.some((release) => release.tag_name === featured)
? featured
: releases[0]?.tag_name
const rollbackSet = new Set(rollbackTags)

return (
<div className="mt-5">
<div className="mb-2">
<p className="text-xs font-medium text-foreground">{t('versionHistory')}</p>
<p className="mt-1 text-xs leading-5 text-muted">{t('versionHistoryHint')}</p>
</div>
<Accordion className="rounded-lg border border-separator" hideSeparator defaultExpandedKeys={defaultExpanded ? [defaultExpanded] : []}>
{releases.map((release) => {
const tag = release.tag_name
const isCurrent = tag === currentVersion
const isLatest = Boolean(nextVersion && tag === nextVersion && nextVersion !== currentVersion)
const canRestore = canRollback && rollbackSet.has(tag) && !isCurrent
const published = formatReleaseDate(release.published_at, lang)
return (
<Accordion.Item key={tag} id={tag}>
<Accordion.Heading>
<Accordion.Trigger className="items-center gap-3 px-3 py-2.5 text-left">
<span className="flex min-w-0 flex-1 items-center gap-2">
<span className="mono text-sm font-semibold">{tag}</span>
{isCurrent ? <Chip size="sm" variant="soft" color="success">{t('versionCurrentBadge')}</Chip> : null}
{isLatest ? <Chip size="sm" variant="soft" color="warning">{t('versionLatestBadge')}</Chip> : null}
{published ? <span className="truncate text-xs text-muted">{published}</span> : null}
</span>
<Accordion.Indicator />
</Accordion.Trigger>
</Accordion.Heading>
<Accordion.Panel>
<Accordion.Body className="px-3 pb-3">
<div className="release-notes-box release-notes-box--compact">
<ReleaseNotes markdown={extractReleaseNotes(release.body, lang)} emptyLabel={t('updateNoNotes')} />
</div>
{canRestore ? (
<div className="mt-3 flex justify-end">
<Button size="sm" variant="ghost" isDisabled={submitting} onPress={() => onRestore(tag)}>
{t('restoreThisVersion')}
</Button>
</div>
) : null}
</Accordion.Body>
</Accordion.Panel>
</Accordion.Item>
)
})}
</Accordion>
</div>
)
}
Loading
Loading