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:
1 parent
5a5eab96e2
commit
4d1afc82ef
8 files changed
+87
-37
No files matched your search
+2
-2
@@ -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': 'ダウンロード元を切り替える',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': '切换下载源',
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,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) => {
|
||||
|
||||
Reference in new issue
Block a user