fix(versions): install bundled core and separate version cards

- Replace Reinstall with Install Bundled Core using the local archive
- Detect bundled core availability and update installation messages
- Separate version cards and refine spacing for narrow windows
- Update translations and mock behavior with installation regression tests
This commit is contained in:
lzt404 committed 2026-10-04 17:31:52 +08:00
1 parent 5a5eab96e2
commit 4d1afc82ef
8 files changed
+87 -37

No files matched your search

+2 -2
View File
@@ -1639,8 +1639,8 @@ export const jaOverrides = {
'kernel.versions.confirmUpdateTitle': 'コアを停止して更新',
'kernel.versions.stopAndConfirmDescription': 'コアを更新すると実行中のコアプロセスが停止し、バージョン {version} がインストールされます。更新後にコアを再起動できます。続行しますか?',
'kernel.versions.stopAndUpdateVersion': '{version} に停止して更新',
'kernel.versions.reinstallTitle': '現在のバージョンを再インストール',
'kernel.versions.reinstall': '再インストール',
'kernel.versions.installBundledTitle': 'ダウンロードせずに同梱コア {version} をインストール',
'kernel.versions.installBundled': '同梱コアをインストール',
'kernel.versions.noBundled': 'このリリースには組み込みコアが含まれていません',
'kernel.versions.offlineInstall': 'オフラインインストール',
'kernel.versions.downloadSource': 'ダウンロード元を切り替える',
+2 -2
View File
@@ -1639,8 +1639,8 @@ export const en: Record<MessageKey, string> = {
'kernel.versions.confirmUpdateTitle': 'Stop and Update Core',
'kernel.versions.stopAndConfirmDescription': 'Updating the core will stop the currently running core process and install version {version}. You can restart the core after the update. Do you want to continue?',
'kernel.versions.stopAndUpdateVersion': 'Stop and update to {version}',
'kernel.versions.reinstallTitle': 'Reinstall the current version',
'kernel.versions.reinstall': 'Reinstall',
'kernel.versions.installBundledTitle': 'Install bundled core {version} without downloading',
'kernel.versions.installBundled': 'Install Bundled Core',
'kernel.versions.noBundled': 'This release does not include a bundled core',
'kernel.versions.offlineInstall': 'Offline Install',
'kernel.versions.downloadSource': 'Switch Download Source',
+2 -2
View File
@@ -1638,8 +1638,8 @@ export const zhCN = {
'kernel.versions.confirmUpdateTitle': '停止并更新内核',
'kernel.versions.stopAndConfirmDescription': '更新内核将停止正在运行的内核进程,并下载安装版本 {version}。更新完成后需重新启动内核,是否继续?',
'kernel.versions.stopAndUpdateVersion': '停止并更新至 {version}',
'kernel.versions.reinstallTitle': '重新安装当前版本',
'kernel.versions.reinstall': '重新安装',
'kernel.versions.installBundledTitle': '安装随应用附带的内核 {version},无需下载',
'kernel.versions.installBundled': '安装内置内核',
'kernel.versions.noBundled': '当前发行包未包含内置内核',
'kernel.versions.offlineInstall': '离线安装',
'kernel.versions.downloadSource': '切换下载源',
+5 -3
View File
@@ -15,6 +15,7 @@ export type BrowserMockRuntime = {
};
const SCENARIOS = new Set<BrowserMockScenario>(['running', 'stopped', 'empty', 'error']);
const BUNDLED_CORE = { version: '7.3.15', assetName: 'CLIProxyAPI_7.3.15_windows_amd64.zip' };
const PROVIDER_SECTIONS = [
'gemini-api-key',
'codex-api-key',
@@ -931,9 +932,10 @@ export function createBrowserMockRuntime(
case 'get_core_install_task': return clone(state.coreInstallTask);
case 'install_core_version':
case 'install_bundled_core': {
const source = command === 'install_bundled_core' ? BUNDLED_CORE : state.coreLatest;
const result = {
version: readString(payload.version) || state.coreLatest.version,
assetName: state.coreLatest.assetName,
version: command === 'install_bundled_core' ? source.version : readString(payload.version) || source.version,
assetName: source.assetName,
installDir: state.coreStatus.installDir,
binaryPath: 'C:\\EasyCLIProxyAPI\\cpa-core\\cli-proxy-api.exe',
};
@@ -944,7 +946,7 @@ export function createBrowserMockRuntime(
return clone(result);
}
case 'cancel_core_install': return null;
case 'detect_bundled_core': return { version: '7.3.15', assetName: 'CLIProxyAPI_7.3.15_windows_amd64.zip' };
case 'detect_bundled_core': return clone(BUNDLED_CORE);
case 'get_version_source_settings': return clone(state.versionSource);
case 'set_download_source': {
state.versionSource.source = readString(payload.source) || 'github';
+35 -16
View File
@@ -7,7 +7,7 @@ import {
ExternalLink,
Info,
RefreshCw,
RotateCcw,
Package,
Trash2,
X,
} from 'lucide-react';
@@ -27,6 +27,11 @@ export type CoreInstallResult = {
binaryPath: string | null;
};
type BundledCoreInfo = {
version: string;
assetName: string;
};
export type CoreInstallTask = {
running: boolean;
cancellable: boolean;
@@ -99,6 +104,8 @@ export function VersionManagementPage() {
} = useCoreUpdate();
const [installedAppVersion, setInstalledAppVersion] = useState('');
const [bundledCore, setBundledCore] = useState<BundledCoreInfo | null>(null);
const [bundledCoreError, setBundledCoreError] = useState('');
const [installing, setInstalling] = useState(false);
const [progress, setProgress] = useState<CoreInstallTask | null>(null);
@@ -244,7 +251,7 @@ export function VersionManagementPage() {
}
};
const installVersion = async (version: string) => {
const installCore = async (target: { kind: 'bundled' } | { kind: 'release'; version: string }) => {
completedInstallKeyRef.current = '';
manualInstallInProgressRef.current = true;
setInstalling(true);
@@ -252,8 +259,8 @@ export function VersionManagementPage() {
setInstallDialogOpen(true);
setProgress({
running: true,
cancellable: true,
phase: 'Preparing download',
cancellable: target.kind === 'release',
phase: target.kind === 'bundled' ? 'Preparing bundled kernel' : 'Preparing download',
downloaded: 0,
total: null,
percent: null,
@@ -262,8 +269,11 @@ export function VersionManagementPage() {
});
try {
const result = await invoke<CoreInstallResult>('install_core_version', { version });
showInstallCompletedNotice(result, t('kernel.install.completed', { version: result.version }));
const result = target.kind === 'bundled'
? await invoke<CoreInstallResult>('install_bundled_core')
: await invoke<CoreInstallResult>('install_core_version', { version: target.version });
const message = t(target.kind === 'bundled' ? 'kernel.install.bundledCompleted' : 'kernel.install.completed', { version: result.version });
showInstallCompletedNotice(result, message);
manualInstallInProgressRef.current = false;
setProgress({
running: false,
@@ -272,13 +282,12 @@ export function VersionManagementPage() {
downloaded: 1,
total: 1,
percent: 100,
message: t('kernel.install.completed', { version: result.version }),
message,
result,
});
setInstallDialogOpen(false);
setProgress(null);
setCancellingInstall(false);
await refreshStatus();
} catch (error) {
manualInstallInProgressRef.current = false;
const errorMessage = String(error);
@@ -296,6 +305,7 @@ export function VersionManagementPage() {
}));
} finally {
setInstalling(false);
await refreshStatus();
}
};
@@ -382,6 +392,13 @@ export function VersionManagementPage() {
loadInstallTask();
void loadVersionSourceSettings();
void invoke<BundledCoreInfo | null>('detect_bundled_core')
.then((info) => {
if (!disposed) setBundledCore(info);
})
.catch((error) => {
if (!disposed) setBundledCoreError(String(error));
});
void getVersion()
.then((version) => {
@@ -402,7 +419,7 @@ export function VersionManagementPage() {
const coreInstalled = Boolean(coreStatus?.installed);
const coreProcessBusy = Boolean(coreStatus?.starting);
const busy = checkingLatest || installing || coreProcessBusy;
const installDisabled = busy || installing;
const installDisabled = installing || coreProcessBusy;
const resolvedAppVersion = appUpdate?.currentVersion || installedAppVersion;
const currentAppVersion = resolvedAppVersion ? displayAppVersion(resolvedAppVersion) : t('common.detecting');
@@ -511,7 +528,7 @@ export function VersionManagementPage() {
return (
<section className="page management-page version-management-page">
<MessageNotice message={versionSourceError} onDismiss={() => setVersionSourceError('')} />
<section className="panel version-list">
<section className="version-list">
<div className="version-source-row" aria-label={t('kernel.versions.downloadSource')}>
<div className="version-source-copy">
<strong>{t('kernel.versions.downloadSource')}</strong>
@@ -686,12 +703,12 @@ export function VersionManagementPage() {
<button
type="button"
className="secondary-button"
title={t('kernel.versions.reinstallTitle')}
disabled={!currentVersion || installDisabled}
onClick={() => void installVersion(currentVersion)}
title={bundledCoreError || (bundledCore ? t('kernel.versions.installBundledTitle', { version: bundledCore.version }) : t('kernel.versions.noBundled'))}
disabled={!bundledCore || installDisabled}
onClick={() => void installCore({ kind: 'bundled' })}
>
<RotateCcw size={15} aria-hidden="true" />
<span>{t('kernel.versions.reinstall')}</span>
<Package size={15} aria-hidden="true" />
<span>{t('kernel.versions.installBundled')}</span>
</button>
</div>
</article>
@@ -820,7 +837,7 @@ export function VersionManagementPage() {
className="primary-button"
onClick={() => {
setConfirmUpdateOpen(false);
void installVersion(latestVersion);
void installCore({ kind: 'release', version: latestVersion });
}}
>
<Download size={15} aria-hidden="true" />
@@ -905,6 +922,8 @@ function localizeInstallPhase(
'Preparing download': 'kernel.phase.preparingDownload',
Downloading: 'kernel.phase.downloading',
Extracting: 'kernel.phase.extracting',
'Preparing bundled kernel': 'kernel.phase.preparingBundled',
'Verify bundled kernel': 'kernel.phase.preparingBundled',
'Extract bundled kernel': 'kernel.phase.preparingBundled',
'Installation complete': 'kernel.phase.completed',
'Installation failed': 'kernel.phase.failed',
+8 -8
View File
@@ -1135,7 +1135,7 @@ textarea:focus-visible {
.version-list-item,
.app-release-notes {
border: 1px solid var(--theme-e3e1db);
border-radius: 14px;
border-radius: 12px;
background: var(--theme-fffdf8);
box-shadow: var(--ui-panel-shadow);
}
@@ -1146,7 +1146,7 @@ textarea:focus-visible {
min-height: 84px;
align-items: center;
gap: 24px;
padding: 20px 26px;
padding: 20px 24px;
}
.version-source-copy {
@@ -1231,10 +1231,10 @@ textarea:focus-visible {
.version-list-item {
display: grid;
grid-template-rows: minmax(0, 1fr) auto;
min-height: 280px;
min-height: 240px;
align-items: stretch;
gap: 26px;
padding: 26px;
gap: 20px;
padding: 24px;
}
.version-item-content {
@@ -1401,7 +1401,7 @@ textarea:focus-visible {
align-items: center;
justify-content: space-between;
gap: 14px;
padding: 20px 26px;
padding: 20px 24px;
}
.app-release-notes-heading > div {
@@ -1445,7 +1445,7 @@ textarea:focus-visible {
}
.app-release-notes-body {
padding: 0 26px 20px;
padding: 0 24px 20px;
}
.app-release-notes-meta {
@@ -1551,7 +1551,7 @@ textarea:focus-visible {
.app-release-notes-footer {
display: flex;
justify-content: flex-end;
padding: 10px 26px;
padding: 10px 24px;
border-top: 1px solid var(--theme-e8e5dc);
background: var(--theme-faf9f5);
}
+14 -4
View File
@@ -57,10 +57,6 @@
/* The download source is one control group: move the group together, then let
the select shrink while the action retains a readable label. */
.version-management-page > .version-list {
padding: 0;
}
.version-source-row {
display: flex;
flex-wrap: wrap;
@@ -89,6 +85,16 @@
}
@container workspace (max-width: 620px) {
.version-list,
.version-card-grid {
gap: 16px;
}
.version-source-row,
.version-list-item {
padding: 16px;
}
.app-release-notes-heading {
flex-wrap: wrap;
padding: 16px;
@@ -98,6 +104,10 @@
padding: 0 16px 16px;
}
.app-release-notes-footer {
padding: 10px 16px;
}
.app-release-notes-markdown {
gap: 14px 12px;
}
+19
View File
@@ -19,6 +19,25 @@ describe('browser mock options', () => {
});
describe('browser mock runtime', () => {
for (const scenario of ['running', 'empty'] as const) {
test(`installs the bundled version independently of the latest release (${scenario})`, async () => {
const runtime = createBrowserMockRuntime(scenario);
const bundled = await runtime.invoke('detect_bundled_core') as { version: string; assetName: string };
const latest = await runtime.invoke('check_latest_core') as { version: string };
expect(bundled.version).not.toBe(latest.version);
if (scenario === 'running') {
await runtime.invoke('install_core_version', { version: latest.version });
}
expect(await runtime.invoke('install_bundled_core')).toMatchObject(bundled);
expect(await runtime.invoke('get_core_status')).toMatchObject({
installed: true,
currentVersion: bundled.version,
running: scenario === 'running',
});
});
}
test('models the core process lifecycle and emits status events', async () => {
const events: Array<{ event: string; payload: unknown }> = [];
const runtime = createBrowserMockRuntime('stopped', (event, payload) => {