mirror of
https://gh-proxy.org/https://github.com/klinsmaya/workdsh.git
synced 2026-11-09 01:47:06 +08:00
feat(desktop): add titlebar update control
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
@@ -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))
|
||||
|
||||
@@ -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 }))
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user