feat(desktop): add titlebar update control

This commit is contained in:
t4we fan
2026-08-25 11:22:25 +08:00
parent 637017fbb3
commit b1fd39daf8
17 changed files with 366 additions and 20 deletions
@@ -1,18 +1,27 @@
/** Independent Desktop frame portalled above the upstream content viewport. */
import { createPortal } from 'react-dom'
import { RefreshCw } from 'lucide-react'
import { useState } from 'react'
import type {
InjectFace, PropsLocale, PropsRuntime,
} from '@deepseek-ai/dsh-client-ui-slots'
import type { DesktopSettingsApi } from './desktop-settings-api.ts'
import type { DesktopClientEnvironment } from './environment.ts'
import { DesktopNativeActions } from './DesktopNativeActions.tsx'
import { Button } from '../native-ui/components/ui/button.tsx'
import type { DesktopSettingsLocaleKey } from './desktop-settings-locales.ts'
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from '../native-ui/components/ui/hover-card.tsx'
export interface DesktopFrameTitlebarInjected {
readonly environment: DesktopClientEnvironment
readonly api: Pick<
DesktopSettingsApi,
'openTerminal' | 'restart' | 'restartToRecovery' | 'reloadRenderer' | 'toggleDeveloperTools'
'openTerminal' | 'restart' | 'restartToRecovery' | 'reloadRenderer' | 'toggleDeveloperTools' | 'checkForUpdates'
>
}
@@ -20,6 +29,57 @@ export type DesktopFrameTitlebarProps = PropsRuntime<'shell.overlay'>
& PropsLocale<'desktop.settings'>
& InjectFace<DesktopFrameTitlebarInjected>
export function DesktopVersionControl({
version,
checkForUpdates,
t,
}: {
readonly version: string
readonly checkForUpdates: () => Promise<void>
readonly t: (key: DesktopSettingsLocaleKey) => string
}) {
const [checking, setChecking] = useState(false)
const [failed, setFailed] = useState(false)
const runCheck = (): void => {
if (checking) return
setChecking(true)
setFailed(false)
void checkForUpdates()
.catch(() => { setFailed(true) })
.finally(() => { setChecking(false) })
}
const visibleVersion = `v${version}`
return (
<HoverCard>
<HoverCardTrigger
closeDelay={200}
delay={150}
render={<button type="button" className="dshDesktopFrameVersion" />}
aria-label={`${t('currentVersion')} ${visibleVersion}`}
>
{visibleVersion}
</HoverCardTrigger>
<HoverCardContent className="dshDesktopVersionPopover">
<div className="dshDesktopVersionPopoverHeader">
<span>{t('currentVersion')}</span>
<strong>{visibleVersion}</strong>
</div>
<Button
className="dshDesktopVersionCheckButton"
disabled={checking}
size="sm"
variant="outline"
onClick={runCheck}
>
<RefreshCw aria-hidden="true" />
<span>{t(checking ? 'checkingForUpdates' : 'checkForUpdates')}</span>
</Button>
{failed && <span className="dshDesktopVersionCheckError" role="alert">{t('checkForUpdatesError')}</span>}
</HoverCardContent>
</HoverCard>
)
}
/** Horizontal frame surface; the unrelated upstream content starts below it. */
export function DesktopFrameTitlebar({ api, environment, t }: DesktopFrameTitlebarProps) {
return createPortal((
@@ -31,6 +91,7 @@ export function DesktopFrameTitlebar({ api, environment, t }: DesktopFrameTitleb
>
<div className="dshDesktopFrameIdentity">
<span className="dshDesktopFrameProduct">DSH Desktop</span>
<DesktopVersionControl version={environment.version} checkForUpdates={api.checkForUpdates} t={t} />
<span className="dshDesktopFrameMode">
{t(environment.mode === 'compatibility' ? 'compatibilityMode' : 'extendedMode')}
</span>
@@ -36,6 +36,8 @@ export interface DesktopWindowDragRegion {
/** Generation-stable native window geometry exposed to Desktop client plugins. */
export interface DesktopWindowService {
/** Installed Desktop product version for this renderer generation. */
readonly version: string
readonly mode: 'compatibility' | 'extended' | 'advanced'
readonly platform: 'darwin' | 'win32' | 'linux'
/** Capability-gated native material active behind this renderer. */
@@ -10,6 +10,7 @@ const RESTART_PATH = '/api/desktop/restart'
const RECOVERY_RESTART_PATH = '/api/desktop/restart/recovery'
const RENDERER_RELOAD_PATH = '/api/desktop/developer/reload'
const DEVELOPER_TOOLS_TOGGLE_PATH = '/api/desktop/developer/devtools'
const UPDATE_CHECK_PATH = '/api/desktop/updates/check'
const MAX_PROFILES = 256
const MAX_PROFILE_NAME_LENGTH = 255
@@ -57,6 +58,7 @@ export interface DesktopSettingsApi {
restartToRecovery(): Promise<void>
reloadRenderer(): Promise<void>
toggleDeveloperTools(): Promise<void>
checkForUpdates(): Promise<void>
}
type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
@@ -199,6 +201,9 @@ export function createDesktopSettingsApi(fetcher: FetchLike = globalThis.fetch.b
async toggleDeveloperTools() {
parseDesktopActionAcceptance(await readResponse(await post(fetcher, DEVELOPER_TOOLS_TOGGLE_PATH, {})))
},
async checkForUpdates() {
parseDesktopActionAcceptance(await readResponse(await post(fetcher, UPDATE_CHECK_PATH, {})))
},
})
}
@@ -213,4 +218,5 @@ export const desktopSettingsPaths = Object.freeze({
recoveryRestart: RECOVERY_RESTART_PATH,
rendererReload: RENDERER_RELOAD_PATH,
developerToolsToggle: DEVELOPER_TOOLS_TOGGLE_PATH,
updateCheck: UPDATE_CHECK_PATH,
})
@@ -11,6 +11,10 @@ export const zh = {
openTerminal: '打开 DSH 终端',
openingTerminal: '正在打开…',
openTerminalError: '暂时无法打开 DSH 终端。你仍可以从应用菜单中打开。',
currentVersion: '当前版本',
checkForUpdates: '检查更新',
checkingForUpdates: '正在检查…',
checkForUpdatesError: '暂时无法检查更新,请稍后再试。',
restartDesktop: '重启',
restartingDesktop: '正在重启…',
restartOptions: '重启选项',
@@ -89,6 +93,10 @@ export const en: Record<DesktopSettingsLocaleKey, string> = {
openTerminal: 'Open DSH Terminal',
openingTerminal: 'Opening…',
openTerminalError: 'DSH Terminal could not be opened just now. You can still open it from the application menu.',
currentVersion: 'Current version',
checkForUpdates: 'Check for updates',
checkingForUpdates: 'Checking…',
checkForUpdatesError: 'Updates could not be checked just now. Please try again shortly.',
restartDesktop: 'Restart',
restartingDesktop: 'Restarting…',
restartOptions: 'Restart options',
@@ -9,6 +9,8 @@ export type DesktopClientMaterial = 'off' | 'transparent' | 'acrylic' | 'mica'
/** Validated renderer environment supplied by the Electron Host. */
export interface DesktopClientEnvironment {
/** Installed Desktop product version supplied by the Electron Host. */
version: string
/** Active shell mode for this BrowserWindow lifetime. */
mode: DesktopClientMode
/** Electron Host platform used for native spacing and drag regions. */
@@ -22,6 +24,7 @@ export interface DesktopClientEnvironment {
const MODES = new Set<DesktopClientMode>(['compatibility', 'extended', 'advanced'])
const PLATFORMS = new Set<DesktopClientPlatform>(['darwin', 'win32', 'linux'])
const MATERIALS = new Set<DesktopClientMaterial>(['off', 'transparent', 'acrylic', 'mica'])
const VERSION_PATTERN = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u
/**
* Validate the Electron-owned query marker before any desktop client effects run.
@@ -33,6 +36,7 @@ export function parseDesktopClientEnvironment(search: string): DesktopClientEnvi
const mode = params.get('dsh-desktop-mode')
const platform = params.get('dsh-desktop-platform')
const material = params.get('dsh-desktop-material')
const version = params.get('dsh-desktop-version')
if (mode === null && platform === null) return undefined
if (!MODES.has(mode as DesktopClientMode)) {
throw new Error(`dsh-plugin-desktop: invalid or missing dsh-desktop-mode ${JSON.stringify(mode)}`)
@@ -43,6 +47,9 @@ export function parseDesktopClientEnvironment(search: string): DesktopClientEnvi
if (!MATERIALS.has(material as DesktopClientMaterial)) {
throw new Error(`dsh-plugin-desktop: invalid or missing dsh-desktop-material ${JSON.stringify(material)}`)
}
if (version === null || version.length > 64 || !VERSION_PATTERN.test(version)) {
throw new Error(`dsh-plugin-desktop: invalid or missing dsh-desktop-version ${JSON.stringify(version)}`)
}
const micaMarker = params.get('dsh-desktop-mica')
const micaSupported = platform === 'win32'
? micaMarker === '1' ? true : micaMarker === '0' ? false : undefined
@@ -57,6 +64,7 @@ export function parseDesktopClientEnvironment(search: string): DesktopClientEnvi
throw new Error('dsh-plugin-desktop: renderer material is incompatible with its mode or platform')
}
return {
version,
mode: mode as DesktopClientMode,
platform: platform as DesktopClientPlatform,
material: material as DesktopClientMaterial,
@@ -117,6 +117,97 @@ body[data-dsh-desktop-mode="extended"]:not([data-dsh-desktop-material="off"]) {
pointer-events: none;
}
.dshDesktopFrameProduct { font-size: 13px; font-weight: 600; white-space: nowrap; }
.dshDesktopFrameVersion {
min-height: 22px;
padding: 2px 5px;
border: 0;
border-radius: 6px;
background: transparent;
color: var(--dsw-alias-label-secondary);
cursor: default;
font: inherit;
font-size: 11px;
pointer-events: auto;
white-space: nowrap;
-webkit-app-region: no-drag;
}
.dshDesktopFrameVersion:hover,
.dshDesktopFrameVersion[data-popup-open],
.dshDesktopFrameVersion:focus-visible {
background: var(--dsw-alias-interactive-bg-hover);
color: var(--dsw-alias-label-primary);
}
.dshDesktopFrameVersion:focus-visible {
outline: 2px solid var(--dsw-alias-brand-primary);
outline-offset: 1px;
}
.dshShadcnHoverCardPositioner {
z-index: 2147483647;
outline: none;
-webkit-app-region: no-drag;
}
.dshShadcnHoverCardContent {
transform-origin: var(--transform-origin);
transition: opacity 120ms ease, transform 120ms ease;
}
.dshShadcnHoverCardContent[data-starting-style],
.dshShadcnHoverCardContent[data-ending-style] {
opacity: 0;
transform: scale(.98);
}
.dshDesktopVersionPopover {
display: grid;
gap: 9px;
box-sizing: border-box;
width: 210px;
padding: 10px;
border: 1px solid var(--dsw-alias-border-l1);
border-radius: 10px;
background: var(--dsw-alias-bg-layer-1);
box-shadow: 0 12px 32px color-mix(in srgb, #000 28%, transparent);
-webkit-app-region: no-drag;
}
.dshDesktopVersionPopoverHeader {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
color: var(--dsw-alias-label-secondary);
font-size: 11px;
}
.dshDesktopVersionPopoverHeader strong {
color: var(--dsw-alias-label-primary);
font-size: 12px;
font-weight: 600;
}
.dshDesktopVersionCheckButton {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 7px;
min-height: 30px;
padding: 5px 9px;
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 7px;
background: color-mix(in srgb, var(--dsw-alias-bg-base) 48%, transparent);
color: var(--dsw-alias-label-primary);
cursor: pointer;
font: inherit;
font-size: 12px;
-webkit-app-region: no-drag;
}
.dshDesktopVersionCheckButton:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
.dshDesktopVersionCheckButton:focus-visible {
outline: 2px solid var(--dsw-alias-brand-primary);
outline-offset: 1px;
}
.dshDesktopVersionCheckButton:disabled { cursor: default; opacity: .55; }
.dshDesktopVersionCheckButton svg { width: 14px; height: 14px; stroke-width: 1.8; }
.dshDesktopVersionCheckError {
color: var(--dsw-alias-state-error-primary);
font-size: 11px;
line-height: 1.4;
}
.dshDesktopFrameMode {
padding: 2px 8px;
border: 1px solid var(--dsw-alias-border-l2);
@@ -32,6 +32,9 @@ export const DESKTOP_RENDERER_RELOAD_PATH = '/api/desktop/developer/reload'
/** Toggle the mounted window's Developer Tools through the launcher. */
export const DESKTOP_DEVELOPER_TOOLS_TOGGLE_PATH = '/api/desktop/developer/devtools'
/** Run the generation-owned manual update check. */
export const DESKTOP_UPDATE_CHECK_PATH = '/api/desktop/updates/check'
/** Export one local diagnostic archive through the launcher-owned flow. */
export const DESKTOP_DIAGNOSTICS_EXPORT_PATH = '/api/desktop/diagnostics/export'
@@ -148,6 +151,14 @@ export interface DesktopDeveloperToolsToggleResponse {
readonly accepted: true
}
/** Exact empty body accepted by the manual update-check endpoint. */
export type DesktopUpdateCheckRequest = Readonly<Record<string, never>>
/** Successful completion of the interactive update-check flow. */
export interface DesktopUpdateCheckResponse {
readonly accepted: true
}
/** Exact empty body accepted by the diagnostic-export endpoint. */
export type DesktopDiagnosticsExportRequest = Readonly<Record<string, never>>
@@ -420,6 +420,30 @@ export async function handleDesktopDeveloperToolsToggleRequest(
}
}
/** Run the generation-owned interactive update flow from an exact empty request. */
export async function handleDesktopUpdateCheckRequest(
req: IncomingMessage,
res: ServerResponse,
expectedOrigin: string,
checkNow: () => Promise<void>,
reportError: (operation: string, cause: unknown) => void = () => {},
): Promise<void> {
if (req.method !== 'POST') return finishJson(res, 405, error('method not allowed'), 'POST')
if (!isSameOriginLoopbackRequest(req, expectedOrigin, true)) {
return finishJson(res, 403, error('forbidden'))
}
const value = await parsePostBody(req, res)
if (value === INVALID_BODY) return
if (!isEmptyRequest(value)) return finishJson(res, 400, error('invalid update check request'))
try {
await checkNow()
finishJson(res, 200, { accepted: true })
} catch (cause) {
reportError('check for updates', cause)
finishJson(res, 500, error('updates could not be checked'))
}
}
/** Export diagnostics from an exact empty same-origin request. */
export async function handleDesktopDiagnosticsExportRequest(
req: IncomingMessage,
+10 -1
View File
@@ -149,12 +149,14 @@ export function desktopRendererUrl(
port: number,
mode: DesktopShellMode,
platform: Context['desktopRuntime']['platform'],
appVersion: string,
material: DesktopWindowMaterial = 'off',
windowsBuild?: number,
): string {
const url = new URL(`http://127.0.0.1:${String(port)}/`)
url.searchParams.set('dsh-desktop-mode', mode)
url.searchParams.set('dsh-desktop-platform', platform)
url.searchParams.set('dsh-desktop-version', appVersion)
url.searchParams.set('dsh-desktop-material', material)
if (mode === 'extended' || (mode === 'compatibility' && platform !== 'linux')) {
// Body-level plugin portals do not inherit the framed root's geometry.
@@ -345,7 +347,14 @@ export function apply(ctx: Context, config: Config): void {
...config,
material,
...(runtime.windowsBuild === undefined ? {} : { windowsBuild: runtime.windowsBuild }),
url: desktopRendererUrl(ctx.webServer.port, config.mode, runtime.platform, material, runtime.windowsBuild),
url: desktopRendererUrl(
ctx.webServer.port,
config.mode,
runtime.platform,
runtime.updates.currentVersion,
material,
runtime.windowsBuild,
),
productName: 'DSH Desktop',
windowTitle: 'DeepSeek Harness Desktop',
iconPath,
@@ -0,0 +1,45 @@
import { PreviewCard as HoverCardPrimitive } from '@base-ui/react/preview-card'
import { cn } from '../../lib/utils.ts'
function HoverCard(props: HoverCardPrimitive.Root.Props): JSX.Element {
return <HoverCardPrimitive.Root {...props} />
}
function HoverCardTrigger(props: HoverCardPrimitive.Trigger.Props): JSX.Element {
return <HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
}
function HoverCardContent({
align = 'center',
alignOffset = 0,
className,
side = 'bottom',
sideOffset = 6,
...props
}: HoverCardPrimitive.Popup.Props & Pick<
HoverCardPrimitive.Positioner.Props,
'align' | 'alignOffset' | 'side' | 'sideOffset'
>): JSX.Element {
return (
<HoverCardPrimitive.Portal>
<HoverCardPrimitive.Positioner
align={align}
alignOffset={alignOffset}
className="dshShadcnHoverCardPositioner z-50 outline-none"
side={side}
sideOffset={sideOffset}
>
<HoverCardPrimitive.Popup
className={cn(
'dshShadcnHoverCardContent w-64 origin-(--transform-origin) rounded-lg border bg-popover p-4 text-popover-foreground shadow-md outline-none transition-[opacity,transform] data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0',
className,
)}
data-slot="hover-card-content"
{...props}
/>
</HoverCardPrimitive.Positioner>
</HoverCardPrimitive.Portal>
)
}
export { HoverCard, HoverCardContent, HoverCardTrigger }
+7 -1
View File
@@ -36,6 +36,8 @@ export interface DesktopUpdateLifecycleOptions {
/** Lifecycle handle for one generation's update operations. */
export interface DesktopUpdateLifecycle {
/** Run the same interactive update flow exposed by the native tray. */
checkNow(): Promise<void>
dispose(): Promise<void>
}
@@ -81,7 +83,7 @@ class DesktopUpdateLifecycleOwner implements DesktopUpdateLifecycle {
group: 'status',
order: 10,
label: () => this.trayLabel(),
invoke: () => this.runManualCheck(),
invoke: () => this.checkNow(),
})
if (options.adapter.isPackaged && options.policy.enabled) {
this.scheduleBackgroundCheck(options.policy.initialDelayMs)
@@ -103,6 +105,10 @@ class DesktopUpdateLifecycleOwner implements DesktopUpdateLifecycle {
return this.disposeTask
}
checkNow(): Promise<void> {
return this.runManualCheck()
}
private async loadState(): Promise<void> {
try {
const parsed = parseState(await readState(this.options.adapter.statePath))
+24 -2
View File
@@ -2,6 +2,9 @@
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import type {} from '@deepseek-ai/dsh-host-webserver'
import { DESKTOP_UPDATE_CHECK_PATH } from './desktop-settings-contract.ts'
import { handleDesktopUpdateCheckRequest } from './desktop-settings-route.ts'
import type {} from './runtime.ts'
import { startDesktopUpdateLifecycle } from './update-lifecycle.ts'
@@ -9,7 +12,7 @@ import { startDesktopUpdateLifecycle } from './update-lifecycle.ts'
export const name = 'desktop-updates'
/** Native adapter required for network, tray, confirmation, and installer access. */
export const inject = ['desktopRuntime']
export const inject = ['desktopRuntime', 'webServer']
const MAX_TIMER_DELAY_MS = 2_147_483_647
@@ -46,6 +49,25 @@ export function apply(ctx: Context, config: Config): void {
locale: () => ctx.desktopRuntime.locale,
registerTrayItem: item => ctx.desktopRuntime.registerTrayItem(item),
})
return () => lifecycle.dispose()
const rendererOrigin = `http://127.0.0.1:${String(ctx.webServer.port)}`
const unregister = ctx.webServer.register({
kind: 'exact',
path: DESKTOP_UPDATE_CHECK_PATH,
handler: (req, res) => handleDesktopUpdateCheckRequest(
req,
res,
rendererOrigin,
() => lifecycle.checkNow(),
(operation, cause) => {
ctx.logger.error(
`dsh-plugin-desktop: failed to ${operation}: ${cause instanceof Error ? cause.message : String(cause)}`,
)
},
),
})
return async () => {
unregister()
await lifecycle.dispose()
}
}, 'dsh-plugin-desktop: update polling, confirmation, and installer handoff')
}
@@ -7,6 +7,7 @@ import {
DesktopNativeActions,
DesktopRestartMenuItems,
} from '../src/client/DesktopNativeActions.tsx'
import { DesktopVersionControl } from '../src/client/ExtendedTitlebar.tsx'
import { DesktopSettingsSection } from '../src/client/DesktopSettingsSection.tsx'
import { DesktopTerminalSettingsAction } from '../src/client/DesktopTerminalSettingsAction.tsx'
import {
@@ -67,7 +68,8 @@ describe('Desktop settings API', () => {
|| path === desktopSettingsPaths.restart
|| path === desktopSettingsPaths.recoveryRestart
|| path === desktopSettingsPaths.rendererReload
|| path === desktopSettingsPaths.developerToolsToggle) {
|| path === desktopSettingsPaths.developerToolsToggle
|| path === desktopSettingsPaths.updateCheck) {
return json({ accepted: true })
}
return path === desktopSettingsPaths.settings || path === desktopSettingsPaths.profileCreate || path === desktopSettingsPaths.profileDelete
@@ -86,6 +88,7 @@ describe('Desktop settings API', () => {
await expect(api.restartToRecovery()).resolves.toBeUndefined()
await expect(api.reloadRenderer()).resolves.toBeUndefined()
await expect(api.toggleDeveloperTools()).resolves.toBeUndefined()
await expect(api.checkForUpdates()).resolves.toBeUndefined()
expect(fetcher.mock.calls.map(call => call[0])).toEqual([
desktopSettingsPaths.settings,
@@ -98,6 +101,7 @@ describe('Desktop settings API', () => {
desktopSettingsPaths.recoveryRestart,
desktopSettingsPaths.rendererReload,
desktopSettingsPaths.developerToolsToggle,
desktopSettingsPaths.updateCheck,
])
expect(fetcher.mock.calls[1]?.[1]).toMatchObject({
method: 'POST',
@@ -147,6 +151,7 @@ describe('Desktop native action presentation', () => {
restartToRecovery: vi.fn(async () => {}),
reloadRenderer: vi.fn(async () => {}),
toggleDeveloperTools: vi.fn(async () => {}),
checkForUpdates: vi.fn(async () => {}),
}
const t = (key: DesktopSettingsLocaleKey): string => en[key]
@@ -163,6 +168,18 @@ describe('Desktop native action presentation', () => {
expect(markup).toContain('aria-label="Developer options"')
})
it('renders the Host-supplied version through the shadcn hover-card trigger', () => {
const markup = renderToStaticMarkup(createElement(DesktopVersionControl, {
version: '2.0.3',
checkForUpdates: api.checkForUpdates,
t,
}))
expect(markup).toContain('v2.0.3')
expect(markup).toContain('aria-label="Current version v2.0.3"')
expect(markup).toContain('data-slot="hover-card-trigger"')
})
it('keeps explicit text labels in settings', () => {
const markup = renderToStaticMarkup(createElement(DesktopNativeActions, {
api,
@@ -259,6 +276,7 @@ describe('Desktop settings Slot registration', () => {
} as unknown as ClientContext
applyDesktopSettings(ctx, {
version: '2.0.3',
mode: 'compatibility',
platform: 'darwin',
material: 'off',
@@ -38,12 +38,12 @@ describe('desktop client environment', () => {
})
it('accepts the Electron-owned kebab query markers', () => {
expect(parseDesktopClientEnvironment('?dsh-desktop-mode=advanced&dsh-desktop-platform=darwin&dsh-desktop-material=transparent'))
.toEqual({ mode: 'advanced', platform: 'darwin', material: 'transparent', micaSupported: false })
expect(parseDesktopClientEnvironment('?dsh-desktop-platform=win32&dsh-desktop-mode=compatibility&dsh-desktop-material=off&dsh-desktop-mica=0'))
.toEqual({ mode: 'compatibility', platform: 'win32', material: 'off', micaSupported: false })
expect(parseDesktopClientEnvironment('?dsh-desktop-mode=extended&dsh-desktop-platform=win32&dsh-desktop-material=mica&dsh-desktop-mica=1'))
.toEqual({ mode: 'extended', platform: 'win32', material: 'mica', micaSupported: true })
expect(parseDesktopClientEnvironment('?dsh-desktop-mode=advanced&dsh-desktop-platform=darwin&dsh-desktop-version=2.0.3&dsh-desktop-material=transparent'))
.toEqual({ version: '2.0.3', mode: 'advanced', platform: 'darwin', material: 'transparent', micaSupported: false })
expect(parseDesktopClientEnvironment('?dsh-desktop-platform=win32&dsh-desktop-mode=compatibility&dsh-desktop-version=2.0.3&dsh-desktop-material=off&dsh-desktop-mica=0'))
.toEqual({ version: '2.0.3', mode: 'compatibility', platform: 'win32', material: 'off', micaSupported: false })
expect(parseDesktopClientEnvironment('?dsh-desktop-mode=extended&dsh-desktop-platform=win32&dsh-desktop-version=2.0.3&dsh-desktop-material=mica&dsh-desktop-mica=1'))
.toEqual({ version: '2.0.3', mode: 'extended', platform: 'win32', material: 'mica', micaSupported: true })
})
it.each([
@@ -52,7 +52,8 @@ describe('desktop client environment', () => {
['?dsh-desktop-platform=darwin', 'dsh-desktop-mode'],
['?dsh-desktop-mode=advanced&dsh-desktop-platform=android', 'dsh-desktop-platform'],
['?dsh-desktop-mode=advanced&dsh-desktop-platform=darwin', 'dsh-desktop-material'],
['?dsh-desktop-mode=advanced&dsh-desktop-platform=win32&dsh-desktop-material=mica&dsh-desktop-mica=0', 'incompatible'],
['?dsh-desktop-mode=advanced&dsh-desktop-platform=darwin&dsh-desktop-material=off', 'dsh-desktop-version'],
['?dsh-desktop-mode=advanced&dsh-desktop-platform=win32&dsh-desktop-version=2.0.3&dsh-desktop-material=mica&dsh-desktop-mica=0', 'incompatible'],
])('fails loud for malformed marker %s', (search, field) => {
expect(() => parseDesktopClientEnvironment(search)).toThrow(field)
})
@@ -186,6 +187,7 @@ describe('advanced desktop layout', () => {
try {
applyAdvancedShell(ctx, {
version: '2.0.3',
mode: 'advanced',
platform: 'darwin',
material: 'transparent',
@@ -210,8 +212,9 @@ describe('advanced desktop layout', () => {
it('reports generation-stable safe areas and drag geometry to client plugins', () => {
expect(desktopWindowService({
mode: 'compatibility', platform: 'darwin', material: 'off', micaSupported: false,
version: '2.0.3', mode: 'compatibility', platform: 'darwin', material: 'off', micaSupported: false,
})).toEqual({
version: '2.0.3',
mode: 'compatibility',
platform: 'darwin',
material: 'off',
@@ -225,9 +228,10 @@ describe('advanced desktop layout', () => {
},
})
const mac = desktopWindowService({
mode: 'advanced', platform: 'darwin', material: 'transparent', micaSupported: false,
version: '2.0.3', mode: 'advanced', platform: 'darwin', material: 'transparent', micaSupported: false,
})
expect(mac).toEqual({
version: '2.0.3',
mode: 'advanced',
platform: 'darwin',
material: 'transparent',
@@ -244,8 +248,9 @@ describe('advanced desktop layout', () => {
expect(Object.isFrozen(mac.safeAreaInsets)).toBe(true)
expect(Object.isFrozen(mac.dragRegion)).toBe(true)
expect(desktopWindowService({
mode: 'advanced', platform: 'win32', material: 'acrylic', micaSupported: false,
version: '2.0.3', mode: 'advanced', platform: 'win32', material: 'acrylic', micaSupported: false,
})).toEqual({
version: '2.0.3',
mode: 'advanced',
platform: 'win32',
material: 'acrylic',
@@ -259,8 +264,9 @@ describe('advanced desktop layout', () => {
},
})
expect(desktopWindowService({
mode: 'extended', platform: 'win32', material: 'mica', micaSupported: true,
version: '2.0.3', mode: 'extended', platform: 'win32', material: 'mica', micaSupported: true,
})).toEqual({
version: '2.0.3',
mode: 'extended',
platform: 'win32',
material: 'mica',
@@ -427,6 +433,7 @@ describe('independent Desktop frame', () => {
try {
applyExtendedShell(ctx, {
version: '2.0.3',
mode: 'extended',
platform: 'win32',
material: 'acrylic',
@@ -504,6 +511,7 @@ describe('independent Desktop frame', () => {
try {
applyFramedShell(ctx, {
version: '2.0.3',
mode: 'compatibility',
platform: 'darwin',
material: 'transparent',
@@ -18,6 +18,7 @@ import {
handleDesktopRendererReloadRequest,
handleDesktopSettingsRequest,
handleDesktopTerminalOpenRequest,
handleDesktopUpdateCheckRequest,
desktopSettingsRouteConstants,
} from '../src/desktop-settings-route.ts'
import type { DesktopProfileSummary } from '../src/profile-manager.ts'
@@ -533,6 +534,24 @@ describe('desktop settings HTTP boundary', () => {
expect(openTerminal).toHaveBeenCalledOnce()
})
it('runs the shared interactive update flow only for an exact empty request', async () => {
const checkNow = vi.fn(async () => {})
const accepted = response()
await handleDesktopUpdateCheckRequest(jsonRequest({}), accepted, ORIGIN, checkNow)
expect(accepted.statusCode).toBe(200)
expect(JSON.parse(accepted.body)).toEqual({ accepted: true })
expect(checkNow).toHaveBeenCalledOnce()
const rejected = response()
await handleDesktopUpdateCheckRequest(
jsonRequest({ version: '9.9.9' }), rejected, ORIGIN, checkNow,
)
expect(rejected.statusCode).toBe(400)
expect(checkNow).toHaveBeenCalledOnce()
})
it('queues an explicit restart only after an exact request has been acknowledged', async () => {
const scheduleRestart = vi.fn()
const controller = new DesktopSettingsController(bootstrap({ scheduleRestart }))
+6 -2
View File
@@ -207,23 +207,26 @@ describe('desktop Host plugin', () => {
})
it('builds the loopback root with validated renderer mode and platform markers', () => {
const url = new URL(desktopRendererUrl(43120, 'advanced', 'darwin'))
const url = new URL(desktopRendererUrl(43120, 'advanced', 'darwin', '2.0.3'))
expect(url.origin).toBe('http://127.0.0.1:43120')
expect(url.pathname).toBe('/')
expect(Object.fromEntries(url.searchParams)).toEqual({
'dsh-desktop-mode': 'advanced',
'dsh-desktop-platform': 'darwin',
'dsh-desktop-version': '2.0.3',
'dsh-desktop-material': 'off',
})
expect(Object.fromEntries(new URL(desktopRendererUrl(
43120,
'extended',
'win32',
'2.0.3',
'mica',
22_631,
)).searchParams)).toEqual({
'dsh-desktop-mode': 'extended',
'dsh-desktop-platform': 'win32',
'dsh-desktop-version': '2.0.3',
'dsh-desktop-material': 'mica',
'dsh-desktop-titlebar-inset': '36',
'dsh-desktop-mica': '1',
@@ -232,6 +235,7 @@ describe('desktop Host plugin', () => {
43120,
'compatibility',
'linux',
'2.0.3',
)).searchParams)).not.toHaveProperty('dsh-desktop-titlebar-inset')
})
@@ -250,7 +254,7 @@ describe('desktop Host plugin', () => {
expect(loaderAwait).not.toHaveBeenCalled()
expect(harness.shell()).toEqual(expect.objectContaining({
mode: 'compatibility',
url: 'http://127.0.0.1:43120/?dsh-desktop-mode=compatibility&dsh-desktop-platform=darwin&dsh-desktop-material=transparent&dsh-desktop-titlebar-inset=36',
url: 'http://127.0.0.1:43120/?dsh-desktop-mode=compatibility&dsh-desktop-platform=darwin&dsh-desktop-version=2.0.0&dsh-desktop-material=transparent&dsh-desktop-titlebar-inset=36',
productName: 'DSH Desktop',
windowTitle: 'DeepSeek Harness Desktop',
readThemeSource: expect.any(Function),
+5 -1
View File
@@ -82,6 +82,10 @@ async function createHarness(options: {
} as unknown as DesktopRuntime
const ctx = {
desktopRuntime: runtime,
webServer: {
port: 43120,
register: () => () => {},
},
logger: { warn: (...args: unknown[]) => { warnings.push(args) } },
effect: (register: () => (() => void | Promise<void>)) => {
disposer = register()
@@ -111,7 +115,7 @@ afterEach(() => {
describe('desktop update Host plugin', () => {
it('exposes the packaged 60-second and six-hour background policy', () => {
expect(inject).toEqual(['desktopRuntime'])
expect(inject).toEqual(['desktopRuntime', 'webServer'])
expect(Config({} as UpdateConfig)).toEqual({
enabled: true,
initialDelayMs: 60_000,