diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2b99e9f..350d94d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/frontend/src/api/system.ts b/frontend/src/api/system.ts
index 372f095..90c51dc 100644
--- a/frontend/src/api/system.ts
+++ b/frontend/src/api/system.ts
@@ -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
}>
diff --git a/frontend/src/components/ModelDetailsModal.tsx b/frontend/src/components/ModelDetailsModal.tsx
index 264a599..f092265 100644
--- a/frontend/src/components/ModelDetailsModal.tsx
+++ b/frontend/src/components/ModelDetailsModal.tsx
@@ -40,19 +40,27 @@ export function ModelDetailsModal({ model, t, onClose }: Props) {
+ )
+}
diff --git a/frontend/src/i18n/messages.ts b/frontend/src/i18n/messages.ts
index 5278527..ae1ed9d 100644
--- a/frontend/src/i18n/messages.ts
+++ b/frontend/src/i18n/messages.ts
@@ -55,73 +55,80 @@ export const messages: Record = {
lastSessionEscape: 'Last escape',
checkUpdates: 'Check updates',
versionUpdate: 'Version update',
- latestVersionHint: 'Updates go directly to the latest stable release.',
+ latestVersionHint: 'Download the latest version first, then apply it when you are ready.',
updateAvailable: 'Update available',
upToDate: 'Up to date',
currentVersion: 'Current version',
latestVersion: 'Latest version',
skippedBetweenLabel: 'Versions in between',
- releaseNotes: 'Release notes',
- updateNoNotes: 'No release notes were provided.',
- updaterReady: 'Host updater connected',
- updaterUnavailable: 'Host updater not connected',
+ releaseNotes: 'What changed',
+ updateNoNotes: 'No notes for this version.',
+ versionHistory: 'Version history',
+ versionHistoryHint: 'Open one version at a time to read what changed. Older versions stay collapsed so the page stays short.',
+ versionCurrentBadge: 'Current',
+ versionLatestBadge: 'Latest',
+ restoreThisVersion: 'Restore this version',
+ restoreVersionTitle: 'Restore {version}?',
+ restoreVersionHint: 'The service will switch back to this version. New requests pause only while it restarts.',
+ updaterReady: 'Updates are ready on this host',
+ updaterUnavailable: 'Updates are not available on this host',
updaterNeedsHost: 'This process cannot replace its own container',
- updaterUnavailableHint: 'Console updates need a host updater next to Docker. This page can check GitHub, but it cannot recreate the container until that daemon is connected.',
+ updaterUnavailableHint: 'This page can check for new versions, but this host cannot apply them until updates are set up.',
updaterNeedInstall: 'Install the host updater from deploy/install-updater.sh or deploy/install-updater.ps1 so it can talk to Docker.',
updaterNeedCompose: 'Linux needs the updater socket mounted into the container. Docker Desktop uses a loopback updater URL instead of a Unix socket.',
updaterNeedRelease: 'The running build must be a published GitHub release, not a local or development image.',
- updateNow: 'Prepare update',
- applyUpdateNow: 'Restart with downloaded image',
+ updateNow: 'Download update',
+ applyUpdateNow: 'Update now',
cancelUpdate: 'Cancel download',
- discardPreparedImage: 'Discard downloaded image',
- updateReadyHint: 'The image is downloaded and ready. Confirm when you want to restart the service; new requests will pause only then.',
- updateReadyTargetHint: 'Restart will switch this host to {version}. New requests pause only during that restart.',
- updateNewerReleaseHint: 'A newer release is available ({latest}). Restart this downloaded image first, or discard it and prepare again.',
+ discardPreparedImage: 'Discard this update',
+ updateReadyHint: 'The update is downloaded. Apply it when you want to restart; new requests pause only then.',
+ updateReadyTargetHint: 'Ready to switch to {version}. New requests pause only during the restart.',
+ updateNewerReleaseHint: 'A newer version is available ({latest}). Update now, or discard this download and start again.',
updateStagedUnavailableHint: 'Update the host updater first to use the staged update flow.',
- updateLegacyOneShotHint: 'This host updater will recreate the container in one step, then replace itself. Later updates can stage the image first.',
- rollbackToVersion: 'Roll back',
- rollbackToVersionHint: 'Recreate the container with one of the three most recent stable releases older than the running version.',
- updateReloadingIn: 'Updating… refresh in {seconds}s',
+ updateLegacyOneShotHint: 'This host will download and apply the update in one step. Later versions can download first, then wait for Update now.',
+ rollbackToVersion: 'Restore an older version',
+ rollbackToVersionHint: 'You can restore one of the three most recent versions before the one running now.',
+ updateReloadingIn: 'Refreshing in {seconds}s',
updateInProgress: 'Updating…',
- updatePreparingImage: 'Downloading image…',
- updatePreparingImageHint: 'The host updater is pulling the new image and the matching host-updater binary. The running service stays on the current version until you confirm the restart. Cancel if the download hangs.',
- updateApplyingHint: 'The host updater is recreating the container, then replacing its own binary and restarting. The console will refresh after the health check.',
+ updatePreparingImage: 'Downloading update…',
+ updatePreparingImageHint: 'Downloading the update in the background. This host stays on the current version until you tap Update now. Cancel if the download hangs.',
+ updateApplyingHint: 'Applying the update. This page will refresh when it is ready.',
updateInProgressHint: 'The service is updating and will refresh automatically when it is ready.',
updateUnavailableHint: 'Updates are temporarily unavailable. Please try again later.',
updateFailedHint: 'The update could not be completed. Please try again.',
updateCheckFailedHint: 'Unable to check for updates. Please try again later.',
- updatePreparingHint: 'Preparing the update: checking the release, pausing new requests, and creating a SQLite backup. Existing connections may be interrupted and retried by the client.',
- updateRunningHint: 'The host updater accepted the job. The service will come back automatically after the health check.',
- updateReloadingHint: 'The new service is healthy. Reloading the console…',
+ updatePreparingHint: 'Getting the update ready: checking the version and saving a backup. Existing connections may be interrupted; clients can retry.',
+ updateRunningHint: 'The update is running. This page will refresh when it is ready.',
+ updateReloadingHint: 'Update complete. Refreshing this page…',
updateElapsed: '{seconds}s elapsed',
sqliteProtection: 'SQLite protection',
- sqliteProtectionHint: 'The named volume remains attached and a verified snapshot is created before the update.',
- sqliteBackupBeforeUpdate: 'Verified SQLite snapshot before replacement',
- sqliteKeepFive: 'Keep the five most recent snapshots',
- sqliteRollbackTogether: 'Restore database and image together on failure',
+ sqliteProtectionHint: 'Your data stays in place, and a verified backup is created before the update.',
+ sqliteBackupBeforeUpdate: 'Verified backup before the update',
+ sqliteKeepFive: 'Keep the five most recent backups',
+ sqliteRollbackTogether: 'Restore the database and previous version together if the update fails',
updateStatus: 'Update status',
noUpdateJob: 'No update job yet',
- confirmUpdate: 'Confirm update',
- confirmUpdateHint: 'New requests pause briefly while SQLite is backed up. Existing connections may be interrupted; clients can retry them.',
+ confirmUpdate: 'Update now',
+ confirmUpdateHint: 'New requests pause briefly while a backup is created. Existing connections may be interrupted; clients can retry them.',
updateState_idle: 'Idle',
updateState_unavailable: 'Unavailable',
- updateState_queued: 'Queued',
- updateState_preparing: 'Preparing',
- updateState_preparing_image: 'Downloading image',
- updateState_checking: 'Checking release',
- updateState_backing_up: 'Backing up SQLite',
- updateState_submitting: 'Submitting update',
- updateState_running: 'Updating service',
- updateState_pulling: 'Pulling image',
- updateState_host_binary: 'Downloading host updater',
- updateState_image_ready: 'Image ready',
- updateState_ready_to_apply: 'Ready to restart',
- updateState_recreating: 'Recreating service',
- updateState_health_checking: 'Health checking',
- updateState_rolling_back: 'Rolling back',
- updateState_succeeded: 'Succeeded',
- updateState_rolled_back: 'Rolled back',
- updateState_failed: 'Failed',
+ updateState_queued: 'Waiting to start',
+ updateState_preparing: 'Getting ready',
+ updateState_preparing_image: 'Downloading update',
+ updateState_checking: 'Checking version',
+ updateState_backing_up: 'Saving a backup',
+ updateState_submitting: 'Starting update',
+ updateState_running: 'Updating',
+ updateState_pulling: 'Downloading update',
+ updateState_host_binary: 'Downloading update helper',
+ updateState_image_ready: 'Download complete',
+ updateState_ready_to_apply: 'Ready to update',
+ updateState_recreating: 'Restarting',
+ updateState_health_checking: 'Checking that it started',
+ updateState_rolling_back: 'Restoring previous version',
+ updateState_succeeded: 'Updated',
+ updateState_rolled_back: 'Restored previous version',
+ updateState_failed: 'Update failed',
checking: 'checking',
refresh: 'Refresh',
apiKey: 'API key',
@@ -146,7 +153,7 @@ export const messages: Record = {
pages: {
home: { title: 'Overview', desc: 'Runtime health, request volume, and ready endpoints.' },
auth: { title: 'Auth', desc: 'Browser device-flow login, PAT login, and session rewarm.' },
- providers: { title: 'Models', desc: 'Models currently exposed by signed-in accounts.' },
+ providers: { title: 'Models', desc: 'Models from signed-in accounts, plus per-model defaults for context and reasoning.' },
access: { title: 'Access', desc: 'Copy Base URL and run a quick Chat Completions check.' },
accounts: { title: 'Accounts', desc: 'One login per account. Browser sign-in first; PAT only when needed.' },
logs: { title: 'Logs', desc: 'Recent proxy requests and process output.' },
@@ -377,25 +384,30 @@ export const messages: Record = {
routedTo: 'Upstream model {model}',
stateCol: 'State',
contextWindowCol: 'Context window',
- contextConfigHint: 'Per-model defaults · explicit API fields take priority',
+ modelDefaultsCol: 'Context and reasoning',
+ contextConfigHint: 'These are defaults. If a request sets Max context or reasoning, that request wins.',
+ qoderContextHint: 'Default context window used when the request omits context length / max input tokens.',
contextInvalid: 'Context window must be an integer between 1,024 and 4,000,000 tokens.',
contextSaved: 'Saved context window for {model}.',
contextReset: 'Restored the default context window for {model}.',
contextMaxOn: 'Enabled Max context for {model}.',
contextMaxOff: 'Disabled Max context for {model}.',
contextMaxUnavailable: 'This model has no Max context switch.',
- maxMode: 'Max',
- catalogWindow: 'Catalog window',
- reasoningLevels: 'Reasoning',
+ maxMode: 'Max context',
+ maxModeHint: 'Trae larger context window, often 200k to 1M. Not a plan or quota. Used only when the request omits is_max_mode; an explicit request still wins. Unsupported models never send this switch.',
+ catalogWindow: 'Catalog size',
+ catalogWindowHint: 'This Trae model has no Max-context switch. The number is the catalog window.',
+ reasoningLevels: 'Reasoning intensity',
+ reasoningHint: 'Default thinking effort when the request omits reasoning_effort and related flags. This does not lock every call to that level, and it does not cap a higher client value. A level the model does not allow falls back to an allowed option.',
reasoningFixed: 'Fixed thinking: {type}',
noReasoningLevels: 'This model has no selectable reasoning levels.',
- reasoningSaved: 'Saved reasoning for {model}: {level}.',
+ reasoningSaved: 'Saved default reasoning for {model}: {level}.',
reasoningLevel_none: 'Off',
reasoningLevel_low: 'Low',
reasoningLevel_medium: 'Medium',
reasoningLevel_high: 'High',
reasoningLevel_xhigh: 'Extra high',
- reasoningLevel_max: 'Max',
+ reasoningLevel_max: 'Max effort',
modelDetails: 'Details',
promptMaxTokens: 'Prompt max',
maxOutputTokens: 'Output max',
@@ -632,72 +644,79 @@ export const messages: Record = {
lastSessionEscape: '最近逃逸',
checkUpdates: '检查更新',
versionUpdate: '版本更新',
- latestVersionHint: '更新会直接升级到最新稳定版本。',
+ latestVersionHint: '先下载最新版本,准备好后再点立即更新。',
updateAvailable: '有可用更新',
upToDate: '已是最新版本',
currentVersion: '当前版本',
latestVersion: '最新版本',
skippedBetweenLabel: '中间经过的版本',
- releaseNotes: '版本说明',
- updateNoNotes: '该版本暂无说明。',
- updaterReady: '宿主机更新器已连接',
- updaterUnavailable: '未连接宿主机更新器',
+ releaseNotes: '更新内容',
+ updateNoNotes: '这个版本还没有说明。',
+ versionHistory: '历史版本',
+ versionHistoryHint: '一次只展开一个版本看更新内容,其他版本收起,页面就不会太长。',
+ versionCurrentBadge: '当前',
+ versionLatestBadge: '最新',
+ restoreThisVersion: '恢复此版本',
+ restoreVersionTitle: '恢复到 {version}?',
+ restoreVersionHint: '服务会切回这个版本。只有重启时才会暂停新请求。',
+ updaterReady: '这台机器可以更新',
+ updaterUnavailable: '这台机器暂时不能更新',
updaterNeedsHost: '当前进程不能替换自己的容器',
- updaterUnavailableHint: '控制台更新需要宿主机上的更新器来操作 Docker。这一页可以检查 GitHub,但更新器没连上时不能重建容器。',
+ updaterUnavailableHint: '这一页可以检查新版本,但还不能在这台机器上安装更新。',
updaterNeedInstall: '用 deploy/install-updater.sh 或 deploy/install-updater.ps1 安装宿主机更新器,让它能调用 Docker。',
updaterNeedCompose: 'Linux 需要把更新器 socket 挂进容器。Docker Desktop 用本机回环地址,而不是 Unix socket。',
updaterNeedRelease: '当前运行的必须是已发布的 GitHub Release,不能是本地或开发镜像。',
- updateNow: '准备更新',
- applyUpdateNow: '使用已下载镜像更新',
+ updateNow: '下载更新',
+ applyUpdateNow: '立即更新',
cancelUpdate: '取消下载',
- discardPreparedImage: '放弃已下载镜像',
- updateReadyHint: '镜像已经拉取完成。确认后才会重启服务;在此之前不会暂停新请求。',
- updateReadyTargetHint: '确认后会把本机切换到 {version}。只有重启时才会暂停新请求。',
- updateNewerReleaseHint: '又有更新的版本({latest})。可以先重启这份已下载镜像,或放弃后重新准备。',
+ discardPreparedImage: '放弃这次更新',
+ updateReadyHint: '更新已经下载好。点立即更新才会重启;在此之前不会暂停新请求。',
+ updateReadyTargetHint: '可以切换到 {version}。只有重启时才会暂停新请求。',
+ updateNewerReleaseHint: '又有更新的版本({latest})。可以先立即更新,或放弃这次下载后重新开始。',
updateStagedUnavailableHint: '请先更新宿主机更新器,才能使用分阶段更新。',
- updateLegacyOneShotHint: '当前宿主机更新器会一次重建容器并替换自己。之后的更新可以先准备镜像再确认重启。',
- rollbackToVersion: '回滚版本',
- rollbackToVersionHint: '可回滚到当前版本之前最近三个稳定版,会重建容器。',
- updateReloadingIn: '更新中… {seconds}s 后刷新',
+ updateLegacyOneShotHint: '这台机器会一次下载并完成更新。之后的版本可以先下载,再点立即更新。',
+ rollbackToVersion: '恢复旧版本',
+ rollbackToVersionHint: '可以恢复到当前版本之前最近的三个版本。',
+ updateReloadingIn: '{seconds}s 后刷新',
updateInProgress: '更新中…',
- updatePreparingImage: '正在下载镜像…',
- updatePreparingImageHint: '宿主机更新器正在拉取新镜像,并下载对应的宿主机更新器二进制。当前服务仍运行旧版本,确认重启后才会切换。下载卡住时可以取消。',
- updateApplyingHint: '宿主机更新器正在重建容器,然后就地替换自己的二进制并退出重启。健康检查通过后控制台会自动刷新。',
+ updatePreparingImage: '正在下载更新…',
+ updatePreparingImageHint: '正在后台下载更新。当前版本会继续运行,下载完成后点立即更新才会切换。下载卡住时可以取消。',
+ updateApplyingHint: '正在安装更新。完成后会自动刷新这个页面。',
updateInProgressHint: '系统正在更新,准备就绪后会自动刷新页面。',
updateUnavailableHint: '暂时无法更新,请稍后重试。',
updateFailedHint: '更新未完成,请重试。',
updateCheckFailedHint: '暂时无法检查更新,请稍后重试。',
- updatePreparingHint: '正在准备更新:检查版本、暂停新请求并创建 SQLite 快照。现有连接可能会被中断,客户端可自行重试。',
- updateRunningHint: '宿主机更新器已接收任务,健康检查完成后服务会自动恢复。',
- updateReloadingHint: '新服务已通过健康检查,正在重新加载控制台…',
+ updatePreparingHint: '正在准备更新:检查版本并保存备份。现有连接可能会被中断,客户端可自行重试。',
+ updateRunningHint: '正在更新。完成后会自动刷新这个页面。',
+ updateReloadingHint: '更新完成,正在刷新页面…',
updateElapsed: '已用时 {seconds}s',
sqliteProtection: 'SQLite 保护',
- sqliteProtectionHint: '保留原数据卷,并在更新前创建经过完整性校验的快照。',
- sqliteBackupBeforeUpdate: '替换前创建并校验 SQLite 快照',
- sqliteKeepFive: '保留最近 5 份快照',
- sqliteRollbackTogether: '失败时同时恢复数据库和旧镜像',
+ sqliteProtectionHint: '数据会留在原处,并在更新前创建经过校验的备份。',
+ sqliteBackupBeforeUpdate: '更新前创建并校验备份',
+ sqliteKeepFive: '保留最近 5 份备份',
+ sqliteRollbackTogether: '失败时同时恢复数据库和上一版本',
updateStatus: '更新状态',
noUpdateJob: '暂无更新任务',
- confirmUpdate: '确认更新',
- confirmUpdateHint: '系统会短暂停止接收新请求并备份 SQLite。现有连接可能会被中断,客户端可自行重试。',
+ confirmUpdate: '立即更新',
+ confirmUpdateHint: '系统会短暂停止接收新请求并保存备份。现有连接可能会被中断,客户端可自行重试。',
updateState_idle: '空闲',
updateState_unavailable: '不可用',
- updateState_queued: '等待执行',
+ updateState_queued: '等待开始',
updateState_preparing: '准备中',
- updateState_preparing_image: '正在下载镜像',
- updateState_checking: '检查版本中',
- updateState_backing_up: '备份 SQLite 中',
- updateState_submitting: '提交更新中',
- updateState_running: '更新服务中',
- updateState_pulling: '拉取镜像中',
- updateState_host_binary: '正在下载宿主机更新器',
- updateState_image_ready: '镜像已就绪',
- updateState_ready_to_apply: '待确认重启',
- updateState_recreating: '重建服务中',
- updateState_health_checking: '健康检查中',
- updateState_rolling_back: '回滚中',
- updateState_succeeded: '更新成功',
- updateState_rolled_back: '已回滚',
+ updateState_preparing_image: '正在下载更新',
+ updateState_checking: '正在检查版本',
+ updateState_backing_up: '正在保存备份',
+ updateState_submitting: '正在开始更新',
+ updateState_running: '正在更新',
+ updateState_pulling: '正在下载更新',
+ updateState_host_binary: '正在下载更新程序',
+ updateState_image_ready: '下载完成',
+ updateState_ready_to_apply: '可以立即更新',
+ updateState_recreating: '正在重启',
+ updateState_health_checking: '正在确认启动',
+ updateState_rolling_back: '正在恢复上一版本',
+ updateState_succeeded: '更新完成',
+ updateState_rolled_back: '已恢复上一版本',
updateState_failed: '更新失败',
checking: '检查中',
refresh: '刷新',
@@ -723,7 +742,7 @@ export const messages: Record = {
pages: {
home: { title: '概览', desc: '查看运行状态、请求量与可用端点。' },
auth: { title: '授权', desc: '浏览器 device-flow 登录、PAT 登录,以及会话 rewarm。' },
- providers: { title: '模型', desc: '当前已登录账号可用的模型列表。' },
+ providers: { title: '模型', desc: '已登录账号可用的模型,以及每个模型的上下文和推理默认值。' },
access: { title: '接入', desc: '复制 Base URL,并快速验证 Chat Completions。' },
accounts: { title: '账号', desc: '一个账号一个登录态。先走浏览器登录;浏览器不方便再用 PAT。' },
logs: { title: '日志', desc: '查看近期代理请求和进程输出。' },
@@ -954,25 +973,30 @@ export const messages: Record = {
routedTo: '上游模型 {model}',
stateCol: '状态',
contextWindowCol: '上下文窗口',
- contextConfigHint: '按模型配置默认值 · API 显式参数优先',
+ modelDefaultsCol: '上下文与推理',
+ contextConfigHint: '这里改的是默认值。请求里如果指定了更大上下文或推理强度,仍以请求为准。',
+ qoderContextHint: '请求没写上下文长度时使用的默认窗口。',
contextInvalid: '上下文窗口必须是 1,024 到 4,000,000 之间的整数。',
contextSaved: '已保存 {model} 的上下文窗口。',
contextReset: '已恢复 {model} 的默认上下文窗口。',
- contextMaxOn: '已开启 {model} 的 Max 上下文。',
- contextMaxOff: '已关闭 {model} 的 Max 上下文。',
- contextMaxUnavailable: '这个模型没有 Max 上下文开关。',
- maxMode: 'Max',
- catalogWindow: '目录窗口',
+ contextMaxOn: '已开启 {model} 的更大上下文。',
+ contextMaxOff: '已关闭 {model} 的更大上下文。',
+ contextMaxUnavailable: '这个模型没有更大上下文开关。',
+ maxMode: '更大上下文',
+ maxModeHint: 'Trae 的更大上下文窗口,常见是 200k 提到 1M,不是套餐也不是额度。只在请求没带 is_max_mode 时使用;请求里写了仍以请求为准。模型不支持时不会发送这个开关。',
+ catalogWindow: '目录默认窗口',
+ catalogWindowHint: '这个 Trae 模型没有更大上下文开关,数字是目录里的默认窗口。',
reasoningLevels: '推理强度',
+ reasoningHint: '请求没指定推理强度时使用的默认档位。选了不会强制每次都走这一档,也不会限制客户端改用其他允许档位。不在列表里的值会回退到可用档位,而不是锁死。',
reasoningFixed: '固定思考:{type}',
noReasoningLevels: '这个模型没有可选的推理档位。',
- reasoningSaved: '已保存 {model} 的推理强度:{level}。',
+ reasoningSaved: '已保存 {model} 的默认推理强度:{level}。',
reasoningLevel_none: '关闭',
reasoningLevel_low: '低',
reasoningLevel_medium: '中',
reasoningLevel_high: '高',
reasoningLevel_xhigh: '极高',
- reasoningLevel_max: '最大',
+ reasoningLevel_max: '最强推理',
modelDetails: '详情',
promptMaxTokens: '输入上限',
maxOutputTokens: '输出上限',
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 4126039..0f12ed5 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -190,6 +190,11 @@ body {
padding-right: 0.15rem;
}
+.release-notes-box--compact {
+ max-height: 10rem;
+ margin-top: 0;
+}
+
.release-notes {
color: var(--foreground);
font-size: 0.75rem;
diff --git a/frontend/src/lib/versionHistory.ts b/frontend/src/lib/versionHistory.ts
new file mode 100644
index 0000000..121ce41
--- /dev/null
+++ b/frontend/src/lib/versionHistory.ts
@@ -0,0 +1,34 @@
+export type VersionHistoryRelease = {
+ tag_name: string
+ name?: string
+ body?: string
+ published_at?: string
+ html_url?: string
+}
+
+function dedupeReleases(releases: VersionHistoryRelease[]) {
+ const seen = new Set()
+ const items: VersionHistoryRelease[] = []
+ for (const release of releases) {
+ const tag = release.tag_name?.trim()
+ if (!tag || seen.has(tag)) continue
+ seen.add(tag)
+ items.push({ ...release, tag_name: tag })
+ }
+ return items
+}
+
+export function buildVersionHistory(
+ currentVersion: string,
+ nextVersion: string | undefined,
+ latest: VersionHistoryRelease | undefined,
+ recent: VersionHistoryRelease[] | undefined,
+ rollback: VersionHistoryRelease[] | undefined,
+) {
+ if (recent?.length) return dedupeReleases(recent)
+ return dedupeReleases([
+ ...(latest ? [latest] : nextVersion ? [{ tag_name: nextVersion }] : []),
+ ...(currentVersion ? [{ tag_name: currentVersion }] : []),
+ ...(rollback || []),
+ ])
+}
diff --git a/frontend/src/pages/ProvidersPage.tsx b/frontend/src/pages/ProvidersPage.tsx
index 720b439..82010ed 100644
--- a/frontend/src/pages/ProvidersPage.tsx
+++ b/frontend/src/pages/ProvidersPage.tsx
@@ -1,5 +1,5 @@
import { useMemo, useState } from 'react'
-import { Button, Card, Chip, Input, Table } from '@heroui/react'
+import { Button, Card, Chip, Input, Table, Tooltip } from '@heroui/react'
import { Cube, ArrowClockwise, ArrowCounterClockwise, FloppyDisk, MagnifyingGlass, Info } from '@phosphor-icons/react'
import { useI18n } from '@/hooks/useI18n'
import { useOverview } from '@/hooks/useOverview'
@@ -43,6 +43,19 @@ function reasoningLabel(t: (key: string, vars?: Record)
type Translate = (key: string, vars?: Record) => string
+function HintLabel({ label, hint }: { label: string; hint: string }) {
+ return (
+
+
+ {label}
+
+
+