From 5f91fd3d8dbf30bf09071a5b570380b5e6c5668c Mon Sep 17 00:00:00 2001 From: yaojin Date: Thu, 20 Aug 2026 02:02:37 -0700 Subject: [PATCH] feat: show bundled Harness version --- src/main/index.ts | 39 +++++++++++++++++++++++------- src/main/version-info.ts | 42 ++++++++++++++++++++++++++++++++ test/version-info.test.ts | 45 +++++++++++++++++++++++++++++++++++ test/windows-titlebar.test.ts | 8 +++++++ 4 files changed, 125 insertions(+), 9 deletions(-) create mode 100644 src/main/version-info.ts create mode 100644 test/version-info.test.ts diff --git a/src/main/index.ts b/src/main/index.ts index 1bf6f5b0..2ca78e92 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -46,6 +46,7 @@ import { type DesktopMenuCommand } from '../shared/desktop-menu' import { buildPluginRecoveryViewModel } from './plugin-recovery-view' +import { aboutDetail, bundledHarnessVersion } from './version-info' type PluginRecoveryAction = 'uninstall' | 'show-log' | 'quit' | 'restart' | 'refresh' @@ -505,6 +506,26 @@ function assertTrustedMainWindowEvent(event: IpcMainInvokeEvent): void { } } +async function showAbout(window: BrowserWindow): Promise { + const locale = harnessLocale() + const checkForUpdatesLabel = locale === 'zh' ? '检查更新' : 'Check for Updates' + const result = await dialog.showMessageBox(window, { + type: 'info', + title: 'DSH Desktop', + message: locale === 'zh' ? '关于 DSH Desktop' : 'About DSH Desktop', + detail: aboutDetail( + app.getVersion(), + bundledHarnessVersion(app.getAppPath()), + locale + ), + buttons: [checkForUpdatesLabel, locale === 'zh' ? '关闭' : 'Close'], + defaultId: 1, + cancelId: 1, + noLink: true + }) + if (result.response === 0) await checkForUpdates(true) +} + async function executeDesktopMenuCommand(command: DesktopMenuCommand): Promise { const window = mainWindow if (!window || window.isDestroyed()) return @@ -560,14 +581,7 @@ async function executeDesktopMenuCommand(command: DesktopMenuCommand): Promise { + if (mainWindow && !mainWindow.isDestroyed()) { + void showAbout(mainWindow).catch(showUnexpectedError) + } + } + }, { label: checkForUpdatesLabel, accelerator: 'CmdOrCtrl+U', diff --git a/src/main/version-info.ts b/src/main/version-info.ts new file mode 100644 index 00000000..35dc85d2 --- /dev/null +++ b/src/main/version-info.ts @@ -0,0 +1,42 @@ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' + +interface PackageMetadata { + version?: unknown + dependencies?: Record +} + +function readPackageMetadata(path: string): PackageMetadata | undefined { + try { + return JSON.parse(readFileSync(path, 'utf8')) as PackageMetadata + } catch { + return undefined + } +} + +function validVersion(value: unknown): string | undefined { + return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined +} + +export function bundledHarnessVersion(appPath: string): string | undefined { + const installedMetadata = readPackageMetadata( + join(appPath, 'node_modules', '@deepseek-ai', 'dsh', 'package.json') + ) + const installedVersion = validVersion(installedMetadata?.version) + if (installedVersion) return installedVersion + + const appMetadata = readPackageMetadata(join(appPath, 'package.json')) + return validVersion(appMetadata?.dependencies?.['@deepseek-ai/dsh']) +} + +export function aboutDetail( + desktopVersion: string, + harnessVersion: string | undefined, + locale: 'en' | 'zh' +): string { + const harness = harnessVersion ?? (locale === 'zh' ? '未知' : 'Unknown') + if (locale === 'zh') { + return `DSH Desktop 版本:${desktopVersion}\n内置 Harness 版本:${harness}\n\nHarness 随 DSH Desktop 更新。` + } + return `DSH Desktop version: ${desktopVersion}\nBundled Harness version: ${harness}\n\nHarness is updated with DSH Desktop.` +} diff --git a/test/version-info.test.ts b/test/version-info.test.ts new file mode 100644 index 00000000..715c20bc --- /dev/null +++ b/test/version-info.test.ts @@ -0,0 +1,45 @@ +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { aboutDetail, bundledHarnessVersion } from '../src/main/version-info' + +const temporaryRoots: string[] = [] + +afterEach(async () => { + await Promise.all(temporaryRoots.splice(0).map((root) => rm(root, { recursive: true }))) +}) + +describe('desktop version information', () => { + it('reports the version of the Harness package that is actually bundled', async () => { + const root = await mkdtemp(join(tmpdir(), 'dsh-version-info-')) + temporaryRoots.push(root) + const harnessRoot = join(root, 'node_modules', '@deepseek-ai', 'dsh') + await mkdir(harnessRoot, { recursive: true }) + await writeFile(join(root, 'package.json'), JSON.stringify({ + dependencies: { '@deepseek-ai/dsh': '0.1.0-rc.7' } + })) + await writeFile(join(harnessRoot, 'package.json'), JSON.stringify({ + version: '0.1.0-rc.8' + })) + + expect(bundledHarnessVersion(root)).toBe('0.1.0-rc.8') + }) + + it('falls back to the app dependency when installed package metadata is unavailable', async () => { + const root = await mkdtemp(join(tmpdir(), 'dsh-version-info-')) + temporaryRoots.push(root) + await writeFile(join(root, 'package.json'), JSON.stringify({ + dependencies: { '@deepseek-ai/dsh': '0.1.0-rc.8' } + })) + + expect(bundledHarnessVersion(root)).toBe('0.1.0-rc.8') + }) + + it('explains that Harness updates arrive with Desktop', () => { + expect(aboutDetail('0.1.1', '0.1.0-rc.8', 'zh')).toContain('内置 Harness 版本:0.1.0-rc.8') + expect(aboutDetail('0.1.1', '0.1.0-rc.8', 'en')).toContain( + 'Harness is updated with DSH Desktop.' + ) + }) +}) diff --git a/test/windows-titlebar.test.ts b/test/windows-titlebar.test.ts index 811dbe89..9f93ff32 100644 --- a/test/windows-titlebar.test.ts +++ b/test/windows-titlebar.test.ts @@ -54,6 +54,14 @@ describe('Windows titlebar menu', () => { expect(main).toContain('if (!isDesktopMenuCommand(command))') }) + it('shows the bundled Harness version and offers an update check from About', async () => { + const main = await readFile('src/main/index.ts', 'utf8') + + expect(main).toContain('bundledHarnessVersion(app.getAppPath())') + expect(main).toContain('if (result.response === 0) await checkForUpdates(true)') + expect(main).toContain('void showAbout(mainWindow).catch(showUnexpectedError)') + }) + it('synchronizes the native controls with Harness light and dark themes', async () => { const main = await readFile('src/main/index.ts', 'utf8') const preload = await readFile('src/preload/windows-titlebar.ts', 'utf8')